tgupdate: merge t/misc/no-regex-search into gitweb-additions base
[git/gitweb.git] / gitweb / gitweb.perl
blob30e084ff13ba5f3ad8acc821fe22d8f4d581964f
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;
33 BEGIN {
34 CGI->compile() if $ENV{'MOD_PERL'};
37 our $version = "++GIT_VERSION++";
39 our ($my_url, $my_uri, $base_url, $path_info, $home_link);
40 sub evaluate_uri {
41 our $cgi;
43 our $my_url = $cgi->url();
44 our $my_uri = $cgi->url(-absolute => 1);
46 # Base URL for relative URLs in gitweb ($logo, $favicon, ...),
47 # needed and used only for URLs with nonempty PATH_INFO
48 # This must be an absolute URL (i.e. no scheme, host or port), NOT a full one
49 our $base_url = $my_uri || '/';
51 # When the script is used as DirectoryIndex, the URL does not contain the name
52 # of the script file itself, and $cgi->url() fails to strip PATH_INFO, so we
53 # have to do it ourselves. We make $path_info global because it's also used
54 # later on.
56 # Another issue with the script being the DirectoryIndex is that the resulting
57 # $my_url data is not the full script URL: this is good, because we want
58 # generated links to keep implying the script name if it wasn't explicitly
59 # indicated in the URL we're handling, but it means that $my_url cannot be used
60 # as base URL.
61 # Therefore, if we needed to strip PATH_INFO, then we know that we have
62 # to build the base URL ourselves:
63 our $path_info = decode_utf8($ENV{"PATH_INFO"});
64 if ($path_info) {
65 # $path_info has already been URL-decoded by the web server, but
66 # $my_url and $my_uri have not. URL-decode them so we can properly
67 # strip $path_info.
68 $my_url = unescape($my_url);
69 $my_uri = unescape($my_uri);
70 if ($my_url =~ s,\Q$path_info\E$,, &&
71 $my_uri =~ s,\Q$path_info\E$,, &&
72 defined $ENV{'SCRIPT_NAME'}) {
73 $base_url = $ENV{'SCRIPT_NAME'} || '/';
77 # target of the home link on top of all pages
78 our $home_link = $my_uri || "/";
81 # core git executable to use
82 # this can just be "git" if your webserver has a sensible PATH
83 our $GIT = "++GIT_BINDIR++/git";
85 # absolute fs-path which will be prepended to the project path
86 #our $projectroot = "/pub/scm";
87 our $projectroot = "++GITWEB_PROJECTROOT++";
89 # fs traversing limit for getting project list
90 # the number is relative to the projectroot
91 our $project_maxdepth = "++GITWEB_PROJECT_MAXDEPTH++";
93 # string of the home link on top of all pages
94 our $home_link_str = "++GITWEB_HOME_LINK_STR++";
96 # extra breadcrumbs preceding the home link
97 our @extra_breadcrumbs = ();
99 # name of your site or organization to appear in page titles
100 # replace this with something more descriptive for clearer bookmarks
101 our $site_name = "++GITWEB_SITENAME++"
102 || ($ENV{'SERVER_NAME'} || "Untitled") . " Git";
104 # html snippet to include in the <head> section of each page
105 our $site_html_head_string = "++GITWEB_SITE_HTML_HEAD_STRING++";
106 # filename of html text to include at top of each page
107 our $site_header = "++GITWEB_SITE_HEADER++";
108 # html text to include at home page
109 our $home_text = "++GITWEB_HOMETEXT++";
110 # filename of html text to include at bottom of each page
111 our $site_footer = "++GITWEB_SITE_FOOTER++";
113 # URI of stylesheets
114 our @stylesheets = ("++GITWEB_CSS++");
115 # URI of a single stylesheet, which can be overridden in GITWEB_CONFIG.
116 our $stylesheet = undef;
117 # URI of GIT logo (72x27 size)
118 our $logo = "++GITWEB_LOGO++";
119 # URI of GIT favicon, assumed to be image/png type
120 our $favicon = "++GITWEB_FAVICON++";
121 # URI of gitweb.js (JavaScript code for gitweb)
122 our $javascript = "++GITWEB_JS++";
124 # URI and label (title) of GIT logo link
125 #our $logo_url = "http://www.kernel.org/pub/software/scm/git/docs/";
126 #our $logo_label = "git documentation";
127 our $logo_url = "http://git-scm.com/";
128 our $logo_label = "git homepage";
130 # source of projects list
131 our $projects_list = "++GITWEB_LIST++";
133 # the width (in characters) of the projects list "Description" column
134 our $projects_list_description_width = 25;
136 # group projects by category on the projects list
137 # (enabled if this variable evaluates to true)
138 our $projects_list_group_categories = 0;
140 # default category if none specified
141 # (leave the empty string for no category)
142 our $project_list_default_category = "";
144 # default order of projects list
145 # valid values are none, project, descr, owner, and age
146 our $default_projects_order = "project";
148 # default order of refs list
149 # valid values are age and name
150 our $default_refs_order = "age";
152 # show repository only if this file exists
153 # (only effective if this variable evaluates to true)
154 our $export_ok = "++GITWEB_EXPORT_OK++";
156 # don't generate age column on the projects list page
157 our $omit_age_column = 0;
159 # use contents of this file (in iso, iso-strict or raw format) as
160 # the last activity data if it exists and is a valid date
161 our $lastactivity_file = undef;
163 # don't generate information about owners of repositories
164 our $omit_owner=0;
166 # owner link hook given owner name (full and NOT obfuscated)
167 # should return full URL-escaped link to attach to owner, for example:
168 # sub { return "/showowner.cgi?owner=".CGI::Util::escape($_[0]); }
169 our $owner_link_hook = undef;
171 # show repository only if this subroutine returns true
172 # when given the path to the project, for example:
173 # sub { return -e "$_[0]/git-daemon-export-ok"; }
174 our $export_auth_hook = undef;
176 # only allow viewing of repositories also shown on the overview page
177 our $strict_export = "++GITWEB_STRICT_EXPORT++";
179 # base URL for bundle info link shown on summary page, but only if
180 # this config item is defined AND a 'bundles' subdirectory exists
181 # in the project's repository.
182 # i.e. full URL is "git_base_bundles_url/$project/bundles"
183 our $git_base_bundles_url = undef;
185 ## URL Hints
187 ## Any of the urls in @git_base_url_list, @git_base_mirror_urls or
188 ## @git_base_push_urls may be an array ref instead of a scalar in which
189 ## case ${}[0] is the url and ${}[1] is an html fragment "hint" to display
190 ## right after the URL.
192 # list of git base URLs used for URL to where fetch project from,
193 # i.e. full URL is "$git_base_url/$project"
194 our @git_base_url_list = grep { $_ ne '' } ("++GITWEB_BASE_URL++");
196 ## For push projects (a .nofetch file exists OR gitweb.showpush is true)
197 ## @git_base_url_list entries are shown as "URL" and @git_base_push_urls
198 ## are shown as "push URL" and @git_base_mirror_urls are ignored.
199 ## For non-push projects, @git_base_url_list and @git_base_mirror_urls are shown
200 ## as "URL" and @git_base_push_urls are ignored.
202 # URLs shown for mirrors but not for push projects in addition to base_url_list,
203 # extended by the project name (i.e. full URL is "$git_mirror_url/$project")
204 our @git_base_mirror_urls = ();
206 # URLs designated for pushing new changes, extended by the
207 # project name (i.e. "$git_base_push_url[0]/$project")
208 our @git_base_push_urls = ();
210 # https hint html inserted right after any https push URL (undef for none)
211 # ignored if the url already has its own hint
212 # this is supported for backwards compatibility but is now deprecated in favor
213 # of using an array ref in the @git_base_push_urls list instead
214 our $https_hint_html = undef;
216 # default blob_plain mimetype and default charset for text/plain blob
217 our $default_blob_plain_mimetype = 'application/octet-stream';
218 our $default_text_plain_charset = undef;
220 # file to use for guessing MIME types before trying /etc/mime.types
221 # (relative to the current git repository)
222 our $mimetypes_file = undef;
224 # assume this charset if line contains non-UTF-8 characters;
225 # it should be valid encoding (see Encoding::Supported(3pm) for list),
226 # for which encoding all byte sequences are valid, for example
227 # 'iso-8859-1' aka 'latin1' (it is decoded without checking, so it
228 # could be even 'utf-8' for the old behavior)
229 our $fallback_encoding = 'latin1';
231 # rename detection options for git-diff and git-diff-tree
232 # - default is '-M', with the cost proportional to
233 # (number of removed files) * (number of new files).
234 # - more costly is '-C' (which implies '-M'), with the cost proportional to
235 # (number of changed files + number of removed files) * (number of new files)
236 # - even more costly is '-C', '--find-copies-harder' with cost
237 # (number of files in the original tree) * (number of new files)
238 # - one might want to include '-B' option, e.g. '-B', '-M'
239 our @diff_opts = ('-M'); # taken from git_commit
241 # cache directory (relative to $GIT_DIR) for project-specific html page caches.
242 # the directory must exist and be writable by the process running gitweb.
243 # additionally some actions must be selected for caching in %html_cache_actions
244 # - default is 'htmlcache'
245 our $html_cache_dir = 'htmlcache';
247 # which actions to cache in $html_cache_dir
248 # if $html_cache_dir exists (relative to $GIT_DIR) and is writable by the
249 # process running gitweb, then any actions selected here will have their output
250 # cached and the cache file will be returned instead of regenerating the page
251 # if it exists. For this to be useful, an external process must create the
252 # 'changed' file (if it does not already exist) in the $html_cache_dir whenever
253 # the project information has been changed. Alternatively it may create a
254 # "$action.changed" file (if it does not exist) instead to limit the changes
255 # to just "$action" instead of any action. If 'changed' or "$action.changed"
256 # exist, then the cached version will never be used for "$action" and a new
257 # cache page will be regenerated (and the "changed" files removed as appropriate).
259 # Additionally if $projlist_cache_lifetime is > 0 AND the forks feature has been
260 # enabled ('$feature{'forks'}{'default'} = [1];') then additionally an external
261 # process must create the 'forkchange' file or update its timestamp if it already
262 # exists whenever a fork is added to or removed from the project (as well as
263 # create the 'changed' or "$action.changed" file). Otherwise the "forks"
264 # section on the summary page may remain out-of-date indefinately.
266 # - default is none
267 # currently only caching of the summary page is supported
268 # - to enable caching of the summary page use:
269 # $html_cache_actions{'summary'} = 1;
270 our %html_cache_actions = ();
272 # utility to automatically produce a default README.html if README.html is
273 # enabled and it does not exist or is 0 bytes in length. If this is set to an
274 # executable utility that takes an absolute path to a .git directory as its
275 # first argument and outputs an HTML fragment to use for README.html, then
276 # it will be called when README.html is enabled but empty or missing.
277 our $git_automatic_readme_html = undef;
279 # Disables features that would allow repository owners to inject script into
280 # the gitweb domain.
281 our $prevent_xss = 0;
283 # Path to a POSIX shell. Needed to run $highlight_bin and a snapshot compressor.
284 # Only used when highlight is enabled or snapshots with compressors are enabled.
285 our $posix_shell_bin = "++POSIX_SHELL_BIN++";
287 # Path to the highlight executable to use (must be the one from
288 # http://www.andre-simon.de due to assumptions about parameters and output).
289 # Useful if highlight is not installed on your webserver's PATH.
290 # [Default: highlight]
291 our $highlight_bin = "++HIGHLIGHT_BIN++";
293 # Whether to include project list on the gitweb front page; 0 means yes,
294 # 1 means no list but show tag cloud if enabled (all projects still need
295 # to be scanned, unless the info is cached), 2 means no list and no tag cloud
296 # (very fast)
297 our $frontpage_no_project_list = 0;
299 # projects list cache for busy sites with many projects;
300 # if you set this to non-zero, it will be used as the cached
301 # index lifetime in minutes
303 # the cached list version is stored in $cache_dir/$cache_name and can
304 # be tweaked by other scripts running with the same uid as gitweb -
305 # use this ONLY at secure installations; only single gitweb project
306 # root per system is supported, unless you tweak configuration!
307 our $projlist_cache_lifetime = 0; # in minutes
308 # FHS compliant $cache_dir would be "/var/cache/gitweb"
309 our $cache_dir =
310 (defined $ENV{'TMPDIR'} ? $ENV{'TMPDIR'} : '/tmp').'/gitweb';
311 our $projlist_cache_name = 'gitweb.index.cache';
312 our $cache_grpshared = 0;
314 # information about snapshot formats that gitweb is capable of serving
315 our %known_snapshot_formats = (
316 # name => {
317 # 'display' => display name,
318 # 'type' => mime type,
319 # 'suffix' => filename suffix,
320 # 'format' => --format for git-archive,
321 # 'compressor' => [compressor command and arguments]
322 # (array reference, optional)
323 # 'disabled' => boolean (optional)}
325 'tgz' => {
326 'display' => 'tar.gz',
327 'type' => 'application/x-gzip',
328 'suffix' => '.tar.gz',
329 'format' => 'tar',
330 'compressor' => ['gzip', '-n']},
332 'tbz2' => {
333 'display' => 'tar.bz2',
334 'type' => 'application/x-bzip2',
335 'suffix' => '.tar.bz2',
336 'format' => 'tar',
337 'compressor' => ['bzip2']},
339 'txz' => {
340 'display' => 'tar.xz',
341 'type' => 'application/x-xz',
342 'suffix' => '.tar.xz',
343 'format' => 'tar',
344 'compressor' => ['xz'],
345 'disabled' => 1},
347 'zip' => {
348 'display' => 'zip',
349 'type' => 'application/x-zip',
350 'suffix' => '.zip',
351 'format' => 'zip'},
354 # Aliases so we understand old gitweb.snapshot values in repository
355 # configuration.
356 our %known_snapshot_format_aliases = (
357 'gzip' => 'tgz',
358 'bzip2' => 'tbz2',
359 'xz' => 'txz',
361 # backward compatibility: legacy gitweb config support
362 'x-gzip' => undef, 'gz' => undef,
363 'x-bzip2' => undef, 'bz2' => undef,
364 'x-zip' => undef, '' => undef,
367 # Pixel sizes for icons and avatars. If the default font sizes or lineheights
368 # are changed, it may be appropriate to change these values too via
369 # $GITWEB_CONFIG.
370 our %avatar_size = (
371 'default' => 16,
372 'double' => 32
375 # Used to set the maximum load that we will still respond to gitweb queries.
376 # If server load exceed this value then return "503 server busy" error.
377 # If gitweb cannot determined server load, it is taken to be 0.
378 # Leave it undefined (or set to 'undef') to turn off load checking.
379 our $maxload = 300;
381 # configuration for 'highlight' (http://www.andre-simon.de/)
382 # match by basename
383 our %highlight_basename = (
384 #'Program' => 'py',
385 #'Library' => 'py',
386 'SConstruct' => 'py', # SCons equivalent of Makefile
387 'Makefile' => 'make',
388 'makefile' => 'make',
389 'GNUmakefile' => 'make',
390 'BSDmakefile' => 'make',
392 # match by shebang regex
393 our %highlight_shebang = (
394 # Each entry has a key which is the syntax to use and
395 # a value which is either a qr regex or an array of qr regexs to match
396 # against the first 128 (less if the blob is shorter) BYTES of the blob.
397 # We match /usr/bin/env items separately to require "/usr/bin/env" and
398 # allow a limited subset of NAME=value items to appear.
399 'awk' => [ qr,^#!\s*/(?:\w+/)*(?:[gnm]?awk)(?:\s|$),mo,
400 qr,^#!\s*/usr/bin/env\s+(?:\w+=\w*\s+)*(?:[gnm]?awk)(?:\s|$),mo ],
401 'make' => [ qr,^#!\s*/(?:\w+/)*(?:g?make)(?:\s|$),mo,
402 qr,^#!\s*/usr/bin/env\s+(?:\w+=\w*\s+)*(?:g?make)(?:\s|$),mo ],
403 'php' => [ qr,^#!\s*/(?:\w+/)*(?:php)(?:\s|$),mo,
404 qr,^#!\s*/usr/bin/env\s+(?:\w+=\w*\s+)*(?:php)(?:\s|$),mo ],
405 'pl' => [ qr,^#!\s*/(?:\w+/)*(?:perl)(?:\s|$),mo,
406 qr,^#!\s*/usr/bin/env\s+(?:\w+=\w*\s+)*(?:perl)(?:\s|$),mo ],
407 'py' => [ qr,^#!\s*/(?:\w+/)*(?:python)(?:\s|$),mo,
408 qr,^#!\s*/usr/bin/env\s+(?:\w+=\w*\s+)*(?:python)(?:\s|$),mo ],
409 'sh' => [ qr,^#!\s*/(?:\w+/)*(?:[bd]ash|t?csh|[akz]?sh)(?:\s|$),mo,
410 qr,^#!\s*/usr/bin/env\s+(?:\w+=\w*\s+)*(?:[bd]ash|t?csh|[akz]?sh)(?:\s|$),mo ],
411 'rb' => [ qr,^#!\s*/(?:\w+/)*(?:ruby)(?:\s|$),mo,
412 qr,^#!\s*/usr/bin/env\s+(?:\w+=\w*\s+)*(?:ruby)(?:\s|$),mo ],
414 # match by extension
415 our %highlight_ext = (
416 # main extensions, defining name of syntax;
417 # see files in /usr/share/highlight/langDefs/ directory
418 (map { $_ => $_ } qw(
419 4gl a4c abnf abp ada agda ahk ampl amtrix applescript arc
420 arm as asm asp aspect ats au3 avenue awk bat bb bbcode bib
421 bms bnf boo c cb cfc chl clipper clojure clp cob cs css d
422 diff dot dylan e ebnf erl euphoria exp f90 flx for frink fs
423 go haskell hcl html httpd hx icl icn idl idlang ili
424 inc_luatex ini inp io iss j java js jsp lbn ldif lgt lhs
425 lisp lotos ls lsl lua ly make mel mercury mib miranda ml mo
426 mod2 mod3 mpl ms mssql n nas nbc nice nrx nsi nut nxc oberon
427 objc octave oorexx os oz pas php pike pl pl1 pov pro
428 progress ps ps1 psl pure py pyx q qmake qu r rb rebol rexx
429 rnc s sas sc scala scilab sh sma smalltalk sml sno spec spn
430 sql sybase tcl tcsh tex ttcn3 vala vb verilog vhd xml xpp y
431 yaiff znn)),
432 # alternate extensions, see /etc/highlight/filetypes.conf
433 (map { $_ => '4gl' } qw(informix)),
434 (map { $_ => 'a4c' } qw(ascend)),
435 (map { $_ => 'abp' } qw(abp4)),
436 (map { $_ => 'ada' } qw(a adb ads gnad)),
437 (map { $_ => 'ahk' } qw(autohotkey)),
438 (map { $_ => 'ampl' } qw(dat run)),
439 (map { $_ => 'amtrix' } qw(hnd s4 s4h s4t t4)),
440 (map { $_ => 'as' } qw(actionscript)),
441 (map { $_ => 'asm' } qw(29k 68s 68x a51 assembler x68 x86)),
442 (map { $_ => 'asp' } qw(asa)),
443 (map { $_ => 'aspect' } qw(was wud)),
444 (map { $_ => 'ats' } qw(dats)),
445 (map { $_ => 'au3' } qw(autoit)),
446 (map { $_ => 'bat' } qw(cmd)),
447 (map { $_ => 'bb' } qw(blitzbasic)),
448 (map { $_ => 'bib' } qw(bibtex)),
449 (map { $_ => 'c' } qw(c++ cc cpp cu cxx h hh hpp hxx)),
450 (map { $_ => 'cb' } qw(clearbasic)),
451 (map { $_ => 'cfc' } qw(cfm coldfusion)),
452 (map { $_ => 'chl' } qw(chill)),
453 (map { $_ => 'cob' } qw(cbl cobol)),
454 (map { $_ => 'cs' } qw(csharp)),
455 (map { $_ => 'diff' } qw(patch)),
456 (map { $_ => 'dot' } qw(graphviz)),
457 (map { $_ => 'e' } qw(eiffel se)),
458 (map { $_ => 'erl' } qw(erlang hrl)),
459 (map { $_ => 'euphoria' } qw(eu ew ex exu exw wxu)),
460 (map { $_ => 'exp' } qw(express)),
461 (map { $_ => 'f90' } qw(f95)),
462 (map { $_ => 'flx' } qw(felix)),
463 (map { $_ => 'for' } qw(f f77 ftn)),
464 (map { $_ => 'fs' } qw(fsharp fsx)),
465 (map { $_ => 'haskell' } qw(hs)),
466 (map { $_ => 'html' } qw(htm xhtml)),
467 (map { $_ => 'hx' } qw(haxe)),
468 (map { $_ => 'icl' } qw(clean)),
469 (map { $_ => 'icn' } qw(icon)),
470 (map { $_ => 'ili' } qw(interlis)),
471 (map { $_ => 'inp' } qw(fame)),
472 (map { $_ => 'iss' } qw(innosetup)),
473 (map { $_ => 'j' } qw(jasmin)),
474 (map { $_ => 'java' } qw(groovy grv)),
475 (map { $_ => 'lbn' } qw(luban)),
476 (map { $_ => 'lgt' } qw(logtalk)),
477 (map { $_ => 'lisp' } qw(cl clisp el lsp sbcl scom)),
478 (map { $_ => 'ls' } qw(lotus)),
479 (map { $_ => 'lsl' } qw(lindenscript)),
480 (map { $_ => 'ly' } qw(lilypond)),
481 (map { $_ => 'make' } qw(mak mk kmk)),
482 (map { $_ => 'mel' } qw(maya)),
483 (map { $_ => 'mib' } qw(smi snmp)),
484 (map { $_ => 'ml' } qw(mli ocaml)),
485 (map { $_ => 'mo' } qw(modelica)),
486 (map { $_ => 'mod2' } qw(def mod)),
487 (map { $_ => 'mod3' } qw(i3 m3)),
488 (map { $_ => 'mpl' } qw(maple)),
489 (map { $_ => 'n' } qw(nemerle)),
490 (map { $_ => 'nas' } qw(nasal)),
491 (map { $_ => 'nrx' } qw(netrexx)),
492 (map { $_ => 'nsi' } qw(nsis)),
493 (map { $_ => 'nut' } qw(squirrel)),
494 (map { $_ => 'oberon' } qw(ooc)),
495 (map { $_ => 'objc' } qw(M m mm)),
496 (map { $_ => 'php' } qw(php3 php4 php5 php6)),
497 (map { $_ => 'pike' } qw(pmod)),
498 (map { $_ => 'pl' } qw(perl plex plx pm)),
499 (map { $_ => 'pl1' } qw(bdy ff fp fpp rpp sf sp spb spe spp sps wf wp wpb wpp wps)),
500 (map { $_ => 'progress' } qw(i p w)),
501 (map { $_ => 'py' } qw(python)),
502 (map { $_ => 'pyx' } qw(pyrex)),
503 (map { $_ => 'rb' } qw(pp rjs ruby)),
504 (map { $_ => 'rexx' } qw(rex rx the)),
505 (map { $_ => 'sc' } qw(paradox)),
506 (map { $_ => 'scilab' } qw(sce sci)),
507 (map { $_ => 'sh' } qw(bash ebuild eclass ksh zsh)),
508 (map { $_ => 'sma' } qw(small)),
509 (map { $_ => 'smalltalk' } qw(gst sq st)),
510 (map { $_ => 'sno' } qw(snobal)),
511 (map { $_ => 'sybase' } qw(sp)),
512 (map { $_ => 'tcl' } qw(itcl wish)),
513 (map { $_ => 'tex' } qw(cls sty)),
514 (map { $_ => 'vb' } qw(bas basic bi vbs)),
515 (map { $_ => 'verilog' } qw(v)),
516 (map { $_ => 'xml' } qw(dtd ecf ent hdr hub jnlp nrm plist resx sgm sgml svg tld vxml wml xsd xsl)),
517 (map { $_ => 'y' } qw(bison)),
520 # You define site-wide feature defaults here; override them with
521 # $GITWEB_CONFIG as necessary.
522 our %feature = (
523 # feature => {
524 # 'sub' => feature-sub (subroutine),
525 # 'override' => allow-override (boolean),
526 # 'default' => [ default options...] (array reference)}
528 # if feature is overridable (it means that allow-override has true value),
529 # then feature-sub will be called with default options as parameters;
530 # return value of feature-sub indicates if to enable specified feature
532 # if there is no 'sub' key (no feature-sub), then feature cannot be
533 # overridden
535 # use gitweb_get_feature(<feature>) to retrieve the <feature> value
536 # (an array) or gitweb_check_feature(<feature>) to check if <feature>
537 # is enabled
539 # Enable the 'blame' blob view, showing the last commit that modified
540 # each line in the file. This can be very CPU-intensive.
542 # To enable system wide have in $GITWEB_CONFIG
543 # $feature{'blame'}{'default'} = [1];
544 # To have project specific config enable override in $GITWEB_CONFIG
545 # $feature{'blame'}{'override'} = 1;
546 # and in project config gitweb.blame = 0|1;
547 'blame' => {
548 'sub' => sub { feature_bool('blame', @_) },
549 'override' => 0,
550 'default' => [0]},
552 # Enable the 'incremental blame' blob view, which uses javascript to
553 # incrementally show the revisions of lines as they are discovered
554 # in the history. It is better for large histories, files and slow
555 # servers, but requires javascript in the client and can slow down the
556 # browser on large files.
558 # To enable system wide have in $GITWEB_CONFIG
559 # $feature{'blame_incremental'}{'default'} = [1];
560 # To have project specific config enable override in $GITWEB_CONFIG
561 # $feature{'blame_incremental'}{'override'} = 1;
562 # and in project config gitweb.blame_incremental = 0|1;
563 'blame_incremental' => {
564 'sub' => sub { feature_bool('blame_incremental', @_) },
565 'override' => 0,
566 'default' => [0]},
568 # Enable the 'snapshot' link, providing a compressed archive of any
569 # tree. This can potentially generate high traffic if you have large
570 # project.
572 # Value is a list of formats defined in %known_snapshot_formats that
573 # you wish to offer.
574 # To disable system wide have in $GITWEB_CONFIG
575 # $feature{'snapshot'}{'default'} = [];
576 # To have project specific config enable override in $GITWEB_CONFIG
577 # $feature{'snapshot'}{'override'} = 1;
578 # and in project config, a comma-separated list of formats or "none"
579 # to disable. Example: gitweb.snapshot = tbz2,zip;
580 'snapshot' => {
581 'sub' => \&feature_snapshot,
582 'override' => 0,
583 'default' => ['tgz']},
585 # Enable text search, which will list the commits which match author,
586 # committer or commit text to a given string. Enabled by default.
587 # Project specific override is not supported.
589 # Note that this controls all search features, which means that if
590 # it is disabled, then 'grep' and 'pickaxe' search would also be
591 # disabled.
592 'search' => {
593 'override' => 0,
594 'default' => [1]},
596 # Enable regular expression search. Enabled by default.
597 # Note that you need to have 'search' feature enabled too.
599 # Note that this affects all git search features, which means that if
600 # it is disabled, none of the git search options will allow a regular
601 # expression (the "RE" checkbox) to be used. However, the project
602 # list search is unaffected by this setting (it uses Perl to do the
603 # matching not Git) and will always allow a regular expression to
604 # be used (by checking the box) regardless of this setting.
605 'regexp' => {
606 'sub' => sub { feature_bool('regexp', @_) },
607 'override' => 0,
608 'default' => [1]},
610 # Enable grep search, which will list the files in currently selected
611 # tree containing the given string. Enabled by default. This can be
612 # potentially CPU-intensive, of course.
613 # Note that you need to have 'search' feature enabled too.
615 # To enable system wide have in $GITWEB_CONFIG
616 # $feature{'grep'}{'default'} = [1];
617 # To have project specific config enable override in $GITWEB_CONFIG
618 # $feature{'grep'}{'override'} = 1;
619 # and in project config gitweb.grep = 0|1;
620 'grep' => {
621 'sub' => sub { feature_bool('grep', @_) },
622 'override' => 0,
623 'default' => [1]},
625 # Enable the pickaxe search, which will list the commits that modified
626 # a given string in a file. This can be practical and quite faster
627 # alternative to 'blame', but still potentially CPU-intensive.
628 # Note that you need to have 'search' feature enabled too.
630 # To enable system wide have in $GITWEB_CONFIG
631 # $feature{'pickaxe'}{'default'} = [1];
632 # To have project specific config enable override in $GITWEB_CONFIG
633 # $feature{'pickaxe'}{'override'} = 1;
634 # and in project config gitweb.pickaxe = 0|1;
635 'pickaxe' => {
636 'sub' => sub { feature_bool('pickaxe', @_) },
637 'override' => 0,
638 'default' => [1]},
640 # Enable showing size of blobs in a 'tree' view, in a separate
641 # column, similar to what 'ls -l' does. This cost a bit of IO.
643 # To disable system wide have in $GITWEB_CONFIG
644 # $feature{'show-sizes'}{'default'} = [0];
645 # To have project specific config enable override in $GITWEB_CONFIG
646 # $feature{'show-sizes'}{'override'} = 1;
647 # and in project config gitweb.showsizes = 0|1;
648 'show-sizes' => {
649 'sub' => sub { feature_bool('showsizes', @_) },
650 'override' => 0,
651 'default' => [1]},
653 # Make gitweb use an alternative format of the URLs which can be
654 # more readable and natural-looking: project name is embedded
655 # directly in the path and the query string contains other
656 # auxiliary information. All gitweb installations recognize
657 # URL in either format; this configures in which formats gitweb
658 # generates links.
660 # To enable system wide have in $GITWEB_CONFIG
661 # $feature{'pathinfo'}{'default'} = [1];
662 # Project specific override is not supported.
664 # Note that you will need to change the default location of CSS,
665 # favicon, logo and possibly other files to an absolute URL. Also,
666 # if gitweb.cgi serves as your indexfile, you will need to force
667 # $my_uri to contain the script name in your $GITWEB_CONFIG (and you
668 # will also likely want to set $home_link if you're setting $my_uri).
669 'pathinfo' => {
670 'override' => 0,
671 'default' => [0]},
673 # Make gitweb consider projects in project root subdirectories
674 # to be forks of existing projects. Given project $projname.git,
675 # projects matching $projname/*.git will not be shown in the main
676 # projects list, instead a '+' mark will be added to $projname
677 # there and a 'forks' view will be enabled for the project, listing
678 # all the forks. If project list is taken from a file, forks have
679 # to be listed after the main project.
681 # To enable system wide have in $GITWEB_CONFIG
682 # $feature{'forks'}{'default'} = [1];
683 # Project specific override is not supported.
684 'forks' => {
685 'override' => 0,
686 'default' => [0]},
688 # Insert custom links to the action bar of all project pages.
689 # This enables you mainly to link to third-party scripts integrating
690 # into gitweb; e.g. git-browser for graphical history representation
691 # or custom web-based repository administration interface.
693 # The 'default' value consists of a list of triplets in the form
694 # (label, link, position) where position is the label after which
695 # to insert the link and link is a format string where %n expands
696 # to the project name, %f to the project path within the filesystem,
697 # %h to the current hash (h gitweb parameter) and %b to the current
698 # hash base (hb gitweb parameter); %% expands to %. %e expands to the
699 # project name where all '+' characters have been replaced with '%2B'.
701 # To enable system wide have in $GITWEB_CONFIG e.g.
702 # $feature{'actions'}{'default'} = [('graphiclog',
703 # '/git-browser/by-commit.html?r=%n', 'summary')];
704 # Project specific override is not supported.
705 'actions' => {
706 'override' => 0,
707 'default' => []},
709 # Allow gitweb scan project content tags of project repository,
710 # and display the popular Web 2.0-ish "tag cloud" near the projects
711 # list. Note that this is something COMPLETELY different from the
712 # normal Git tags.
714 # gitweb by itself can show existing tags, but it does not handle
715 # tagging itself; you need to do it externally, outside gitweb.
716 # The format is described in git_get_project_ctags() subroutine.
717 # You may want to install the HTML::TagCloud Perl module to get
718 # a pretty tag cloud instead of just a list of tags.
720 # To enable system wide have in $GITWEB_CONFIG
721 # $feature{'ctags'}{'default'} = [1];
722 # Project specific override is not supported.
724 # A value of 0 means no ctags display or editing. A value of
725 # 1 enables ctags display but never editing. A non-empty value
726 # that is not a string of digits enables ctags display AND the
727 # ability to add tags using a form that uses method POST and
728 # an action value set to the configured 'ctags' value.
729 'ctags' => {
730 'override' => 0,
731 'default' => [0]},
733 # The maximum number of patches in a patchset generated in patch
734 # view. Set this to 0 or undef to disable patch view, or to a
735 # negative number to remove any limit.
737 # To disable system wide have in $GITWEB_CONFIG
738 # $feature{'patches'}{'default'} = [0];
739 # To have project specific config enable override in $GITWEB_CONFIG
740 # $feature{'patches'}{'override'} = 1;
741 # and in project config gitweb.patches = 0|n;
742 # where n is the maximum number of patches allowed in a patchset.
743 'patches' => {
744 'sub' => \&feature_patches,
745 'override' => 0,
746 'default' => [16]},
748 # Avatar support. When this feature is enabled, views such as
749 # shortlog or commit will display an avatar associated with
750 # the email of the committer(s) and/or author(s).
752 # Currently available providers are gravatar and picon.
753 # If an unknown provider is specified, the feature is disabled.
755 # Gravatar depends on Digest::MD5.
756 # Picon currently relies on the indiana.edu database.
758 # To enable system wide have in $GITWEB_CONFIG
759 # $feature{'avatar'}{'default'} = ['<provider>'];
760 # where <provider> is either gravatar or picon.
761 # To have project specific config enable override in $GITWEB_CONFIG
762 # $feature{'avatar'}{'override'} = 1;
763 # and in project config gitweb.avatar = <provider>;
764 'avatar' => {
765 'sub' => \&feature_avatar,
766 'override' => 0,
767 'default' => ['']},
769 # Enable displaying how much time and how many git commands
770 # it took to generate and display page. Disabled by default.
771 # Project specific override is not supported.
772 'timed' => {
773 'override' => 0,
774 'default' => [0]},
776 # Enable turning some links into links to actions which require
777 # JavaScript to run (like 'blame_incremental'). Not enabled by
778 # default. Project specific override is currently not supported.
779 'javascript-actions' => {
780 'override' => 0,
781 'default' => [0]},
783 # Enable and configure ability to change common timezone for dates
784 # in gitweb output via JavaScript. Enabled by default.
785 # Project specific override is not supported.
786 'javascript-timezone' => {
787 'override' => 0,
788 'default' => [
789 'local', # default timezone: 'utc', 'local', or '(-|+)HHMM' format,
790 # or undef to turn off this feature
791 'gitweb_tz', # name of cookie where to store selected timezone
792 'datetime', # CSS class used to mark up dates for manipulation
795 # Syntax highlighting support. This is based on Daniel Svensson's
796 # and Sham Chukoury's work in gitweb-xmms2.git.
797 # It requires the 'highlight' program present in $PATH,
798 # and therefore is disabled by default.
800 # To enable system wide have in $GITWEB_CONFIG
801 # $feature{'highlight'}{'default'} = [1];
803 'highlight' => {
804 'sub' => sub { feature_bool('highlight', @_) },
805 'override' => 0,
806 'default' => [0]},
808 # Enable displaying of remote heads in the heads list
810 # To enable system wide have in $GITWEB_CONFIG
811 # $feature{'remote_heads'}{'default'} = [1];
812 # To have project specific config enable override in $GITWEB_CONFIG
813 # $feature{'remote_heads'}{'override'} = 1;
814 # and in project config gitweb.remoteheads = 0|1;
815 'remote_heads' => {
816 'sub' => sub { feature_bool('remote_heads', @_) },
817 'override' => 0,
818 'default' => [0]},
820 # Enable showing branches under other refs in addition to heads
822 # To set system wide extra branch refs have in $GITWEB_CONFIG
823 # $feature{'extra-branch-refs'}{'default'} = ['dirs', 'of', 'choice'];
824 # To have project specific config enable override in $GITWEB_CONFIG
825 # $feature{'extra-branch-refs'}{'override'} = 1;
826 # and in project config gitweb.extrabranchrefs = dirs of choice
827 # Every directory is separated with whitespace.
829 'extra-branch-refs' => {
830 'sub' => \&feature_extra_branch_refs,
831 'override' => 0,
832 'default' => []},
835 sub gitweb_get_feature {
836 my ($name) = @_;
837 return unless exists $feature{$name};
838 my ($sub, $override, @defaults) = (
839 $feature{$name}{'sub'},
840 $feature{$name}{'override'},
841 @{$feature{$name}{'default'}});
842 # project specific override is possible only if we have project
843 our $git_dir; # global variable, declared later
844 if (!$override || !defined $git_dir) {
845 return @defaults;
847 if (!defined $sub) {
848 warn "feature $name is not overridable";
849 return @defaults;
851 return $sub->(@defaults);
854 # A wrapper to check if a given feature is enabled.
855 # With this, you can say
857 # my $bool_feat = gitweb_check_feature('bool_feat');
858 # gitweb_check_feature('bool_feat') or somecode;
860 # instead of
862 # my ($bool_feat) = gitweb_get_feature('bool_feat');
863 # (gitweb_get_feature('bool_feat'))[0] or somecode;
865 sub gitweb_check_feature {
866 return (gitweb_get_feature(@_))[0];
870 sub feature_bool {
871 my $key = shift;
872 my ($val) = git_get_project_config($key, '--bool');
874 if (!defined $val) {
875 return ($_[0]);
876 } elsif ($val eq 'true') {
877 return (1);
878 } elsif ($val eq 'false') {
879 return (0);
883 sub feature_snapshot {
884 my (@fmts) = @_;
886 my ($val) = git_get_project_config('snapshot');
888 if ($val) {
889 @fmts = ($val eq 'none' ? () : split /\s*[,\s]\s*/, $val);
892 return @fmts;
895 sub feature_patches {
896 my @val = (git_get_project_config('patches', '--int'));
898 if (@val) {
899 return @val;
902 return ($_[0]);
905 sub feature_avatar {
906 my @val = (git_get_project_config('avatar'));
908 return @val ? @val : @_;
911 sub feature_extra_branch_refs {
912 my (@branch_refs) = @_;
913 my $values = git_get_project_config('extrabranchrefs');
915 if ($values) {
916 $values = config_to_multi ($values);
917 @branch_refs = ();
918 foreach my $value (@{$values}) {
919 push @branch_refs, split /\s+/, $value;
923 return @branch_refs;
926 # checking HEAD file with -e is fragile if the repository was
927 # initialized long time ago (i.e. symlink HEAD) and was pack-ref'ed
928 # and then pruned.
929 sub check_head_link {
930 my ($dir) = @_;
931 return 0 unless -d "$dir/objects" && -x _;
932 return 0 unless -d "$dir/refs" && -x _;
933 my $headfile = "$dir/HEAD";
934 return -l $headfile ?
935 readlink($headfile) =~ /^refs\/heads\// : -f $headfile;
938 sub check_export_ok {
939 my ($dir) = @_;
940 return (check_head_link($dir) &&
941 (!$export_ok || -e "$dir/$export_ok") &&
942 (!$export_auth_hook || $export_auth_hook->($dir)));
945 # process alternate names for backward compatibility
946 # filter out unsupported (unknown) snapshot formats
947 sub filter_snapshot_fmts {
948 my @fmts = @_;
950 @fmts = map {
951 exists $known_snapshot_format_aliases{$_} ?
952 $known_snapshot_format_aliases{$_} : $_} @fmts;
953 @fmts = grep {
954 exists $known_snapshot_formats{$_} &&
955 !$known_snapshot_formats{$_}{'disabled'}} @fmts;
958 sub filter_and_validate_refs {
959 my @refs = @_;
960 my %unique_refs = ();
962 foreach my $ref (@refs) {
963 die_error(500, "Invalid ref '$ref' in 'extra-branch-refs' feature") unless (is_valid_ref_format($ref));
964 # 'heads' are added implicitly in get_branch_refs().
965 $unique_refs{$ref} = 1 if ($ref ne 'heads');
967 return sort keys %unique_refs;
970 # If it is set to code reference, it is code that it is to be run once per
971 # request, allowing updating configurations that change with each request,
972 # while running other code in config file only once.
974 # Otherwise, if it is false then gitweb would process config file only once;
975 # if it is true then gitweb config would be run for each request.
976 our $per_request_config = 1;
978 # If true and fileno STDIN is 0 and getsockname succeeds and getpeername fails
979 # with ENOTCONN, then FCGI mode will be activated automatically in just the
980 # same way as though the --fcgi option had been given instead.
981 our $auto_fcgi = 0;
983 # read and parse gitweb config file given by its parameter.
984 # returns true on success, false on recoverable error, allowing
985 # to chain this subroutine, using first file that exists.
986 # dies on errors during parsing config file, as it is unrecoverable.
987 sub read_config_file {
988 my $filename = shift;
989 return unless defined $filename;
990 # die if there are errors parsing config file
991 if (-e $filename) {
992 do $filename;
993 die $@ if $@;
994 return 1;
996 return;
999 our ($GITWEB_CONFIG, $GITWEB_CONFIG_SYSTEM, $GITWEB_CONFIG_COMMON);
1000 sub evaluate_gitweb_config {
1001 our $GITWEB_CONFIG = $ENV{'GITWEB_CONFIG'} || "++GITWEB_CONFIG++";
1002 our $GITWEB_CONFIG_SYSTEM = $ENV{'GITWEB_CONFIG_SYSTEM'} || "++GITWEB_CONFIG_SYSTEM++";
1003 our $GITWEB_CONFIG_COMMON = $ENV{'GITWEB_CONFIG_COMMON'} || "++GITWEB_CONFIG_COMMON++";
1005 # Protect against duplications of file names, to not read config twice.
1006 # Only one of $GITWEB_CONFIG and $GITWEB_CONFIG_SYSTEM is used, so
1007 # there possibility of duplication of filename there doesn't matter.
1008 $GITWEB_CONFIG = "" if ($GITWEB_CONFIG eq $GITWEB_CONFIG_COMMON);
1009 $GITWEB_CONFIG_SYSTEM = "" if ($GITWEB_CONFIG_SYSTEM eq $GITWEB_CONFIG_COMMON);
1011 # Common system-wide settings for convenience.
1012 # Those settings can be ovverriden by GITWEB_CONFIG or GITWEB_CONFIG_SYSTEM.
1013 read_config_file($GITWEB_CONFIG_COMMON);
1015 # Use first config file that exists. This means use the per-instance
1016 # GITWEB_CONFIG if exists, otherwise use GITWEB_SYSTEM_CONFIG.
1017 read_config_file($GITWEB_CONFIG) and return;
1018 read_config_file($GITWEB_CONFIG_SYSTEM);
1021 our $encode_object;
1022 our $to_utf8_pipe_command = '';
1024 sub evaluate_encoding {
1025 my $requested = $fallback_encoding || 'ISO-8859-1';
1026 my $obj = Encode::find_encoding($requested) or
1027 die_error(400, "Requested fallback encoding not found");
1028 if ($obj->name eq 'iso-8859-1') {
1029 # Use Windows-1252 instead as required by the HTML 5 standard
1030 my $altobj = Encode::find_encoding('Windows-1252');
1031 $obj = $altobj if $altobj;
1033 $encode_object = $obj;
1034 my $nm = lc($encode_object->name);
1035 unless ($nm eq 'cp1252' || $nm eq 'ascii' || $nm eq 'utf8' ||
1036 $nm =~ /^utf-8/ || $nm =~ /^iso-8859-/) {
1037 $to_utf8_pipe_command =
1038 quote_command($^X, '-CO', '-MEncode=decode,FB_DEFAULT', '-pse',
1039 '$_ = decode($fe, $_, FB_DEFAULT) if !utf8::decode($_);',
1040 '--', "-fe=$fallback_encoding")." | ";
1044 sub evaluate_email_obfuscate {
1045 # email obfuscation
1046 our $email;
1047 if (!$email && eval { require HTML::Email::Obfuscate; 1 }) {
1048 $email = HTML::Email::Obfuscate->new(lite => 1);
1052 # Get loadavg of system, to compare against $maxload.
1053 # Currently it requires '/proc/loadavg' present to get loadavg;
1054 # if it is not present it returns 0, which means no load checking.
1055 sub get_loadavg {
1056 if( -e '/proc/loadavg' ){
1057 open my $fd, '<', '/proc/loadavg'
1058 or return 0;
1059 my @load = split(/\s+/, scalar <$fd>);
1060 close $fd;
1062 # The first three columns measure CPU and IO utilization of the last one,
1063 # five, and 10 minute periods. The fourth column shows the number of
1064 # currently running processes and the total number of processes in the m/n
1065 # format. The last column displays the last process ID used.
1066 return $load[0] || 0;
1068 # additional checks for load average should go here for things that don't export
1069 # /proc/loadavg
1071 return 0;
1074 # version of the core git binary
1075 our $git_version;
1076 our $git_vernum = "0"; # guaranteed to always match /^\d+(\.\d+)*$/
1077 sub evaluate_git_version {
1078 $git_version = $version; # don't leak system information to attackers
1079 $git_vernum eq "0" or return; # don't run it again
1080 sub cmd_pipe;
1081 my $vers;
1082 if (defined(my $fd = cmd_pipe $GIT, '--version')) {
1083 $vers = <$fd>;
1084 close $fd;
1085 $number_of_git_cmds++;
1087 $git_vernum = $1 if defined($vers) && $vers =~ /git\s+version\s+(\d+(?:\.\d+)*)$/io;
1090 sub check_loadavg {
1091 if (defined $maxload && get_loadavg() > $maxload) {
1092 die_error(503, "The load average on the server is too high");
1096 # ======================================================================
1097 # input validation and dispatch
1099 # input parameters can be collected from a variety of sources (presently, CGI
1100 # and PATH_INFO), so we define an %input_params hash that collects them all
1101 # together during validation: this allows subsequent uses (e.g. href()) to be
1102 # agnostic of the parameter origin
1104 our %input_params = ();
1106 # input parameters are stored with the long parameter name as key. This will
1107 # also be used in the href subroutine to convert parameters to their CGI
1108 # equivalent, and since the href() usage is the most frequent one, we store
1109 # the name -> CGI key mapping here, instead of the reverse.
1111 # XXX: Warning: If you touch this, check the search form for updating,
1112 # too.
1114 our @cgi_param_mapping = (
1115 project => "p",
1116 action => "a",
1117 file_name => "f",
1118 file_parent => "fp",
1119 hash => "h",
1120 hash_parent => "hp",
1121 hash_base => "hb",
1122 hash_parent_base => "hpb",
1123 page => "pg",
1124 order => "o",
1125 searchtext => "s",
1126 searchtype => "st",
1127 snapshot_format => "sf",
1128 ctag_filter => 't',
1129 extra_options => "opt",
1130 search_use_regexp => "sr",
1131 ctag => "by_tag",
1132 diff_style => "ds",
1133 project_filter => "pf",
1134 # this must be last entry (for manipulation from JavaScript)
1135 javascript => "js"
1137 our %cgi_param_mapping = @cgi_param_mapping;
1139 # we will also need to know the possible actions, for validation
1140 our %actions = (
1141 "blame" => \&git_blame,
1142 "blame_incremental" => \&git_blame_incremental,
1143 "blame_data" => \&git_blame_data,
1144 "blobdiff" => \&git_blobdiff,
1145 "blobdiff_plain" => \&git_blobdiff_plain,
1146 "blob" => \&git_blob,
1147 "blob_plain" => \&git_blob_plain,
1148 "commitdiff" => \&git_commitdiff,
1149 "commitdiff_plain" => \&git_commitdiff_plain,
1150 "commit" => \&git_commit,
1151 "forks" => \&git_forks,
1152 "heads" => \&git_heads,
1153 "history" => \&git_history,
1154 "log" => \&git_log,
1155 "patch" => \&git_patch,
1156 "patches" => \&git_patches,
1157 "refs" => \&git_refs,
1158 "remotes" => \&git_remotes,
1159 "rss" => \&git_rss,
1160 "atom" => \&git_atom,
1161 "search" => \&git_search,
1162 "search_help" => \&git_search_help,
1163 "shortlog" => \&git_shortlog,
1164 "summary" => \&git_summary,
1165 "tag" => \&git_tag,
1166 "tags" => \&git_tags,
1167 "tree" => \&git_tree,
1168 "snapshot" => \&git_snapshot,
1169 "object" => \&git_object,
1170 # those below don't need $project
1171 "opml" => \&git_opml,
1172 "frontpage" => \&git_frontpage,
1173 "project_list" => \&git_project_list,
1174 "project_index" => \&git_project_index,
1177 # the only actions we will allow to be cached
1178 my %supported_cache_actions;
1179 BEGIN {%supported_cache_actions = map {( $_ => 1 )} qw(summary)}
1181 # finally, we have the hash of allowed extra_options for the commands that
1182 # allow them
1183 our %allowed_options = (
1184 "--no-merges" => [ qw(rss atom log shortlog history) ],
1187 # fill %input_params with the CGI parameters. All values except for 'opt'
1188 # should be single values, but opt can be an array. We should probably
1189 # build an array of parameters that can be multi-valued, but since for the time
1190 # being it's only this one, we just single it out
1191 sub evaluate_query_params {
1192 our $cgi;
1194 while (my ($name, $symbol) = each %cgi_param_mapping) {
1195 if ($symbol eq 'opt') {
1196 $input_params{$name} = [ map { decode_utf8($_) } $cgi->multi_param($symbol) ];
1197 } else {
1198 $input_params{$name} = decode_utf8($cgi->param($symbol));
1202 # Backwards compatibility - by_tag= <=> t=
1203 if ($input_params{'ctag'}) {
1204 $input_params{'ctag_filter'} = $input_params{'ctag'};
1208 # now read PATH_INFO and update the parameter list for missing parameters
1209 sub evaluate_path_info {
1210 return if defined $input_params{'project'};
1211 return if !$path_info;
1212 $path_info =~ s,^/+,,;
1213 return if !$path_info;
1215 # find which part of PATH_INFO is project
1216 my $project = $path_info;
1217 $project =~ s,/+$,,;
1218 while ($project && !check_head_link("$projectroot/$project")) {
1219 $project =~ s,/*[^/]*$,,;
1221 return unless $project;
1222 $input_params{'project'} = $project;
1224 # do not change any parameters if an action is given using the query string
1225 return if $input_params{'action'};
1226 $path_info =~ s,^\Q$project\E/*,,;
1228 # next, check if we have an action
1229 my $action = $path_info;
1230 $action =~ s,/.*$,,;
1231 if (exists $actions{$action}) {
1232 $path_info =~ s,^$action/*,,;
1233 $input_params{'action'} = $action;
1236 # list of actions that want hash_base instead of hash, but can have no
1237 # pathname (f) parameter
1238 my @wants_base = (
1239 'tree',
1240 'history',
1243 # we want to catch, among others
1244 # [$hash_parent_base[:$file_parent]..]$hash_parent[:$file_name]
1245 my ($parentrefname, $parentpathname, $refname, $pathname) =
1246 ($path_info =~ /^(?:(.+?)(?::(.+))?\.\.)?([^:]+?)?(?::(.+))?$/);
1248 # first, analyze the 'current' part
1249 if (defined $pathname) {
1250 # we got "branch:filename" or "branch:dir/"
1251 # we could use git_get_type(branch:pathname), but:
1252 # - it needs $git_dir
1253 # - it does a git() call
1254 # - the convention of terminating directories with a slash
1255 # makes it superfluous
1256 # - embedding the action in the PATH_INFO would make it even
1257 # more superfluous
1258 $pathname =~ s,^/+,,;
1259 if (!$pathname || substr($pathname, -1) eq "/") {
1260 $input_params{'action'} ||= "tree";
1261 $pathname =~ s,/$,,;
1262 } else {
1263 # the default action depends on whether we had parent info
1264 # or not
1265 if ($parentrefname) {
1266 $input_params{'action'} ||= "blobdiff_plain";
1267 } else {
1268 $input_params{'action'} ||= "blob_plain";
1271 $input_params{'hash_base'} ||= $refname;
1272 $input_params{'file_name'} ||= $pathname;
1273 } elsif (defined $refname) {
1274 # we got "branch". In this case we have to choose if we have to
1275 # set hash or hash_base.
1277 # Most of the actions without a pathname only want hash to be
1278 # set, except for the ones specified in @wants_base that want
1279 # hash_base instead. It should also be noted that hand-crafted
1280 # links having 'history' as an action and no pathname or hash
1281 # set will fail, but that happens regardless of PATH_INFO.
1282 if (defined $parentrefname) {
1283 # if there is parent let the default be 'shortlog' action
1284 # (for http://git.example.com/repo.git/A..B links); if there
1285 # is no parent, dispatch will detect type of object and set
1286 # action appropriately if required (if action is not set)
1287 $input_params{'action'} ||= "shortlog";
1289 if ($input_params{'action'} &&
1290 grep { $_ eq $input_params{'action'} } @wants_base) {
1291 $input_params{'hash_base'} ||= $refname;
1292 } else {
1293 $input_params{'hash'} ||= $refname;
1297 # next, handle the 'parent' part, if present
1298 if (defined $parentrefname) {
1299 # a missing pathspec defaults to the 'current' filename, allowing e.g.
1300 # someproject/blobdiff/oldrev..newrev:/filename
1301 if ($parentpathname) {
1302 $parentpathname =~ s,^/+,,;
1303 $parentpathname =~ s,/$,,;
1304 $input_params{'file_parent'} ||= $parentpathname;
1305 } else {
1306 $input_params{'file_parent'} ||= $input_params{'file_name'};
1308 # we assume that hash_parent_base is wanted if a path was specified,
1309 # or if the action wants hash_base instead of hash
1310 if (defined $input_params{'file_parent'} ||
1311 grep { $_ eq $input_params{'action'} } @wants_base) {
1312 $input_params{'hash_parent_base'} ||= $parentrefname;
1313 } else {
1314 $input_params{'hash_parent'} ||= $parentrefname;
1318 # for the snapshot action, we allow URLs in the form
1319 # $project/snapshot/$hash.ext
1320 # where .ext determines the snapshot and gets removed from the
1321 # passed $refname to provide the $hash.
1323 # To be able to tell that $refname includes the format extension, we
1324 # require the following two conditions to be satisfied:
1325 # - the hash input parameter MUST have been set from the $refname part
1326 # of the URL (i.e. they must be equal)
1327 # - the snapshot format MUST NOT have been defined already (e.g. from
1328 # CGI parameter sf)
1329 # It's also useless to try any matching unless $refname has a dot,
1330 # so we check for that too
1331 if (defined $input_params{'action'} &&
1332 $input_params{'action'} eq 'snapshot' &&
1333 defined $refname && index($refname, '.') != -1 &&
1334 $refname eq $input_params{'hash'} &&
1335 !defined $input_params{'snapshot_format'}) {
1336 # We loop over the known snapshot formats, checking for
1337 # extensions. Allowed extensions are both the defined suffix
1338 # (which includes the initial dot already) and the snapshot
1339 # format key itself, with a prepended dot
1340 while (my ($fmt, $opt) = each %known_snapshot_formats) {
1341 my $hash = $refname;
1342 unless ($hash =~ s/(\Q$opt->{'suffix'}\E|\Q.$fmt\E)$//) {
1343 next;
1345 my $sfx = $1;
1346 # a valid suffix was found, so set the snapshot format
1347 # and reset the hash parameter
1348 $input_params{'snapshot_format'} = $fmt;
1349 $input_params{'hash'} = $hash;
1350 # we also set the format suffix to the one requested
1351 # in the URL: this way a request for e.g. .tgz returns
1352 # a .tgz instead of a .tar.gz
1353 $known_snapshot_formats{$fmt}{'suffix'} = $sfx;
1354 last;
1359 our ($action, $project, $file_name, $file_parent, $hash, $hash_parent, $hash_base,
1360 $hash_parent_base, @extra_options, $page, $searchtype, $search_use_regexp,
1361 $searchtext, $search_regexp, $project_filter);
1362 sub evaluate_and_validate_params {
1363 our $action = $input_params{'action'};
1364 if (defined $action) {
1365 if (!is_valid_action($action)) {
1366 die_error(400, "Invalid action parameter");
1370 # parameters which are pathnames
1371 our $project = $input_params{'project'};
1372 if (defined $project) {
1373 if (!is_valid_project($project)) {
1374 undef $project;
1375 die_error(404, "No such project");
1379 our $project_filter = $input_params{'project_filter'};
1380 if (defined $project_filter) {
1381 if (!is_valid_pathname($project_filter)) {
1382 die_error(404, "Invalid project_filter parameter");
1386 our $file_name = $input_params{'file_name'};
1387 if (defined $file_name) {
1388 if (!is_valid_pathname($file_name)) {
1389 die_error(400, "Invalid file parameter");
1393 our $file_parent = $input_params{'file_parent'};
1394 if (defined $file_parent) {
1395 if (!is_valid_pathname($file_parent)) {
1396 die_error(400, "Invalid file parent parameter");
1400 # parameters which are refnames
1401 our $hash = $input_params{'hash'};
1402 if (defined $hash) {
1403 if (!is_valid_refname($hash)) {
1404 die_error(400, "Invalid hash parameter");
1408 our $hash_parent = $input_params{'hash_parent'};
1409 if (defined $hash_parent) {
1410 if (!is_valid_refname($hash_parent)) {
1411 die_error(400, "Invalid hash parent parameter");
1415 our $hash_base = $input_params{'hash_base'};
1416 if (defined $hash_base) {
1417 if (!is_valid_refname($hash_base)) {
1418 die_error(400, "Invalid hash base parameter");
1422 our @extra_options = @{$input_params{'extra_options'}};
1423 # @extra_options is always defined, since it can only be (currently) set from
1424 # CGI, and $cgi->param() returns the empty array in array context if the param
1425 # is not set
1426 foreach my $opt (@extra_options) {
1427 if (not exists $allowed_options{$opt}) {
1428 die_error(400, "Invalid option parameter");
1430 if (not grep(/^$action$/, @{$allowed_options{$opt}})) {
1431 die_error(400, "Invalid option parameter for this action");
1435 our $hash_parent_base = $input_params{'hash_parent_base'};
1436 if (defined $hash_parent_base) {
1437 if (!is_valid_refname($hash_parent_base)) {
1438 die_error(400, "Invalid hash parent base parameter");
1442 # other parameters
1443 our $page = $input_params{'page'};
1444 if (defined $page) {
1445 if ($page =~ m/[^0-9]/) {
1446 die_error(400, "Invalid page parameter");
1450 our $searchtype = $input_params{'searchtype'};
1451 if (defined $searchtype) {
1452 if ($searchtype =~ m/[^a-z]/) {
1453 die_error(400, "Invalid searchtype parameter");
1457 our $search_use_regexp = $input_params{'search_use_regexp'};
1459 our $searchtext = $input_params{'searchtext'};
1460 our $search_regexp = undef;
1461 if (defined $searchtext) {
1462 if (length($searchtext) < 2) {
1463 die_error(403, "At least two characters are required for search parameter");
1465 if ($search_use_regexp) {
1466 $search_regexp = $searchtext;
1467 if (!eval { qr/$search_regexp/; 1; }) {
1468 (my $error = $@) =~ s/ at \S+ line \d+.*\n?//;
1469 die_error(400, "Invalid search regexp '$search_regexp'",
1470 esc_html($error));
1472 } else {
1473 $search_regexp = quotemeta $searchtext;
1478 # path to the current git repository
1479 our $git_dir;
1480 sub evaluate_git_dir {
1481 our $git_dir = $project ? "$projectroot/$project" : undef;
1484 our (@snapshot_fmts, $git_avatar, @extra_branch_refs);
1485 sub configure_gitweb_features {
1486 # list of supported snapshot formats
1487 our @snapshot_fmts = gitweb_get_feature('snapshot');
1488 @snapshot_fmts = filter_snapshot_fmts(@snapshot_fmts);
1490 # check that the avatar feature is set to a known provider name,
1491 # and for each provider check if the dependencies are satisfied.
1492 # if the provider name is invalid or the dependencies are not met,
1493 # reset $git_avatar to the empty string.
1494 our ($git_avatar) = gitweb_get_feature('avatar');
1495 if ($git_avatar eq 'gravatar') {
1496 $git_avatar = '' unless (eval { require Digest::MD5; 1; });
1497 } elsif ($git_avatar eq 'picon') {
1498 # no dependencies
1499 } else {
1500 $git_avatar = '';
1503 our @extra_branch_refs = gitweb_get_feature('extra-branch-refs');
1504 @extra_branch_refs = filter_and_validate_refs (@extra_branch_refs);
1507 sub get_branch_refs {
1508 return ('heads', @extra_branch_refs);
1511 # custom error handler: 'die <message>' is Internal Server Error
1512 sub handle_errors_html {
1513 my $msg = shift; # it is already HTML escaped
1515 # to avoid infinite loop where error occurs in die_error,
1516 # change handler to default handler, disabling handle_errors_html
1517 set_message("Error occurred when inside die_error:\n$msg");
1519 # you cannot jump out of die_error when called as error handler;
1520 # the subroutine set via CGI::Carp::set_message is called _after_
1521 # HTTP headers are already written, so it cannot write them itself
1522 die_error(undef, undef, $msg, -error_handler => 1, -no_http_header => 1);
1524 set_message(\&handle_errors_html);
1526 our $shown_stale_message = 0;
1527 our $cache_dump = undef;
1528 our $cache_dump_mtime = undef;
1530 # dispatch
1531 my $cache_mode_active;
1532 sub dispatch {
1533 if (!defined $action) {
1534 if (defined $hash) {
1535 $action = git_get_type($hash);
1536 $action or die_error(404, "Object does not exist");
1537 } elsif (defined $hash_base && defined $file_name) {
1538 $action = git_get_type("$hash_base:$file_name");
1539 $action or die_error(404, "File or directory does not exist");
1540 } elsif (defined $project) {
1541 $action = 'summary';
1542 } else {
1543 $action = 'frontpage';
1546 if (!defined($actions{$action})) {
1547 die_error(400, "Unknown action");
1549 if ($action !~ m/^(?:opml|frontpage|project_list|project_index)$/ &&
1550 !$project) {
1551 die_error(400, "Project needed");
1554 my $cached_page = $supported_cache_actions{$action}
1555 ? cached_action_page($action)
1556 : undef;
1557 goto DUMPCACHE if $cached_page;
1558 local *SAVEOUT = *STDOUT;
1559 $cache_mode_active = $supported_cache_actions{$action}
1560 ? cached_action_start($action)
1561 : undef;
1563 configure_gitweb_features();
1564 $actions{$action}->();
1566 return unless $cache_mode_active;
1568 $cached_page = cached_action_finish($action);
1569 *STDOUT = *SAVEOUT;
1571 DUMPCACHE:
1573 $cache_mode_active = 0;
1574 # Avoid any extra unwanted encoding steps as $cached_page is raw bytes
1575 binmode STDOUT, ':raw';
1576 our $fcgi_raw_mode = 1;
1577 print expand_gitweb_pi($cached_page, time);
1578 binmode STDOUT, ':utf8'; # as set at the beginning of gitweb.cgi
1579 $fcgi_raw_mode = 0;
1582 sub reset_timer {
1583 our $t0 = [ gettimeofday() ]
1584 if defined $t0;
1585 our $number_of_git_cmds = 0;
1588 our $first_request = 1;
1589 our $evaluate_uri_force = undef;
1590 sub run_request {
1591 reset_timer();
1593 # Only allow GET and HEAD methods
1594 if (!$ENV{'REQUEST_METHOD'} || ($ENV{'REQUEST_METHOD'} ne 'GET' && $ENV{'REQUEST_METHOD'} ne 'HEAD')) {
1595 print <<EOT;
1596 Status: 405 Method Not Allowed
1597 Content-Type: text/plain
1598 Allow: GET,HEAD
1600 405 Method Not Allowed
1602 return;
1605 evaluate_uri();
1606 &$evaluate_uri_force() if $evaluate_uri_force;
1607 if ($per_request_config) {
1608 if (ref($per_request_config) eq 'CODE') {
1609 $per_request_config->();
1610 } elsif (!$first_request) {
1611 evaluate_gitweb_config();
1612 evaluate_email_obfuscate();
1615 check_loadavg();
1617 # $projectroot and $projects_list might be set in gitweb config file
1618 $projects_list ||= $projectroot;
1620 evaluate_query_params();
1621 evaluate_path_info();
1622 evaluate_and_validate_params();
1623 evaluate_git_dir();
1625 dispatch();
1628 our $is_last_request = sub { 1 };
1629 our ($pre_dispatch_hook, $post_dispatch_hook, $pre_listen_hook);
1630 our $CGI = 'CGI';
1631 our $cgi;
1632 our $fcgi_mode = 0;
1633 our $fcgi_nproc_active = 0;
1634 our $fcgi_raw_mode = 0;
1635 sub is_fcgi {
1636 use Errno;
1637 my $stdinfno = fileno STDIN;
1638 return 0 unless defined $stdinfno && $stdinfno == 0;
1639 return 0 unless getsockname STDIN;
1640 return 0 if getpeername STDIN;
1641 return $!{ENOTCONN}?1:0;
1643 sub configure_as_fcgi {
1644 return if $fcgi_mode;
1646 require FCGI;
1647 require CGI::Fast;
1649 # We have gone to great effort to make sure that all incoming data has
1650 # been converted from whatever format it was in into UTF-8. We have
1651 # even taken care to make sure the output handle is in ':utf8' mode.
1652 # Now along comes FCGI and blows it with:
1654 # Use of wide characters in FCGI::Stream::PRINT is deprecated
1655 # and will stop wprking[sic] in a future version of FCGI
1657 # To fix this we replace FCGI::Stream::PRINT with our own routine that
1658 # first encodes everything and then calls the original routine, but
1659 # not if $fcgi_raw_mode is true (then we just call the original routine).
1661 # Note that we could do this by using utf8::is_utf8 to check instead
1662 # of having a $fcgi_raw_mode global, but that would be slower to run
1663 # the test on each element and much slower than skipping the conversion
1664 # entirely when we know we're outputting raw bytes.
1665 my $orig = \&FCGI::Stream::PRINT;
1666 undef *FCGI::Stream::PRINT;
1667 *FCGI::Stream::PRINT = sub {
1668 @_ = (shift, map {my $x=$_; utf8::encode($x); $x} @_)
1669 unless $fcgi_raw_mode;
1670 goto $orig;
1673 our $CGI = 'CGI::Fast';
1675 $fcgi_mode = 1;
1676 $first_request = 0;
1677 my $request_number = 0;
1678 # let each child service 100 requests
1679 our $is_last_request = sub { ++$request_number >= 100 };
1681 sub evaluate_argv {
1682 my $script_name = $ENV{'SCRIPT_NAME'} || $ENV{'SCRIPT_FILENAME'} || __FILE__;
1683 configure_as_fcgi()
1684 if $script_name =~ /\.fcgi$/ || ($auto_fcgi && is_fcgi());
1686 my $nproc_sub = sub {
1687 my ($arg, $val) = @_;
1688 return unless eval { require FCGI::ProcManager; 1; };
1689 $fcgi_nproc_active = 1;
1690 my $proc_manager = FCGI::ProcManager->new({
1691 n_processes => $val,
1693 our $pre_listen_hook = sub { $proc_manager->pm_manage() };
1694 our $pre_dispatch_hook = sub { $proc_manager->pm_pre_dispatch() };
1695 our $post_dispatch_hook = sub { $proc_manager->pm_post_dispatch() };
1697 if (@ARGV) {
1698 require Getopt::Long;
1699 Getopt::Long::GetOptions(
1700 'fastcgi|fcgi|f' => \&configure_as_fcgi,
1701 'nproc|n=i' => $nproc_sub,
1704 if (!$fcgi_nproc_active && defined $ENV{'GITWEB_FCGI_NPROC'} && $ENV{'GITWEB_FCGI_NPROC'} =~ /^\d+$/) {
1705 &$nproc_sub('nproc', $ENV{'GITWEB_FCGI_NPROC'});
1709 # Any "our" variable that could possibly influence correct handling of
1710 # a CGI request MUST be reset in this subroutine
1711 sub _reset_globals {
1712 # Note that $t0 and $number_of_git_commands are handled by reset_timer
1713 our %input_params = ();
1714 our ($action, $project, $file_name, $file_parent, $hash, $hash_parent, $hash_base,
1715 $hash_parent_base, @extra_options, $page, $searchtype, $search_use_regexp,
1716 $searchtext, $search_regexp, $project_filter) = ();
1717 our $git_dir = undef;
1718 our (@snapshot_fmts, $git_avatar, @extra_branch_refs) = ();
1719 our %avatar_cache = ();
1720 our $config_file = '';
1721 our %config = ();
1722 our $gitweb_project_owner = undef;
1723 our $shown_stale_message = 0;
1724 our $fcgi_raw_mode = 0;
1725 keys %known_snapshot_formats; # reset 'each' iterator
1728 sub run {
1729 evaluate_gitweb_config();
1730 evaluate_encoding();
1731 evaluate_email_obfuscate();
1732 evaluate_git_version();
1733 my ($ml, $mi, $bu, $hl, $subroutine) = ($my_url, $my_uri, $base_url, $home_link, '');
1734 $subroutine .= '$my_url = $ml;' if defined $my_url && $my_url ne '';
1735 $subroutine .= '$my_uri = $mi;' if defined $my_uri; # this one can be ""
1736 $subroutine .= '$base_url = $bu;' if defined $base_url && $base_url ne '';
1737 $subroutine .= '$home_link = $hl;' if defined $home_link && $home_link ne '';
1738 $evaluate_uri_force = eval "sub {$subroutine}" if $subroutine;
1739 $first_request = 1;
1740 evaluate_argv();
1742 $pre_listen_hook->()
1743 if $pre_listen_hook;
1745 REQUEST:
1746 while ($cgi = $CGI->new()) {
1747 $pre_dispatch_hook->()
1748 if $pre_dispatch_hook;
1750 # most globals can simply be reset
1751 _reset_globals;
1753 # evaluate_path_info corrupts %known_snapshot_formats
1754 # so we need a deepish copy of it -- note that
1755 # _reset_globals already took care of resetting its
1756 # hash iterator that evaluate_path_info also leaves
1757 # in an indeterminate state
1758 my %formats = ();
1759 while (my ($k,$v) = each(%known_snapshot_formats)) {
1760 $formats{$k} = {%{$known_snapshot_formats{$k}}};
1762 local *known_snapshot_formats = \%formats;
1764 eval {run_request()};
1766 $post_dispatch_hook->()
1767 if $post_dispatch_hook;
1768 $first_request = 0;
1770 last REQUEST if ($is_last_request->());
1776 run();
1778 if (defined caller) {
1779 # wrapped in a subroutine processing requests,
1780 # e.g. mod_perl with ModPerl::Registry, or PSGI with Plack::App::WrapCGI
1781 return;
1782 } else {
1783 # pure CGI script, serving single request
1784 exit;
1787 ## ======================================================================
1788 ## action links
1790 # possible values of extra options
1791 # -full => 0|1 - use absolute/full URL ($my_uri/$my_url as base)
1792 # -replay => 1 - start from a current view (replay with modifications)
1793 # -path_info => 0|1 - don't use/use path_info URL (if possible)
1794 # -anchor => ANCHOR - add #ANCHOR to end of URL, implies -replay if used alone
1795 sub href {
1796 my %params = @_;
1797 # default is to use -absolute url() i.e. $my_uri
1798 my $href = $params{-full} ? $my_url : $my_uri;
1800 # implicit -replay, must be first of implicit params
1801 $params{-replay} = 1 if (keys %params == 1 && $params{-anchor});
1803 $params{'project'} = $project unless exists $params{'project'};
1805 if ($params{-replay}) {
1806 while (my ($name, $symbol) = each %cgi_param_mapping) {
1807 if (!exists $params{$name}) {
1808 $params{$name} = $input_params{$name};
1813 my $use_pathinfo = gitweb_check_feature('pathinfo');
1814 if (defined $params{'project'} &&
1815 (exists $params{-path_info} ? $params{-path_info} : $use_pathinfo)) {
1816 # try to put as many parameters as possible in PATH_INFO:
1817 # - project name
1818 # - action
1819 # - hash_parent or hash_parent_base:/file_parent
1820 # - hash or hash_base:/filename
1821 # - the snapshot_format as an appropriate suffix
1823 # When the script is the root DirectoryIndex for the domain,
1824 # $href here would be something like http://gitweb.example.com/
1825 # Thus, we strip any trailing / from $href, to spare us double
1826 # slashes in the final URL
1827 $href =~ s,/$,,;
1829 # Then add the project name, if present
1830 $href .= "/".esc_path_info($params{'project'});
1831 delete $params{'project'};
1833 # since we destructively absorb parameters, we keep this
1834 # boolean that remembers if we're handling a snapshot
1835 my $is_snapshot = $params{'action'} eq 'snapshot';
1837 # Summary just uses the project path URL, any other action is
1838 # added to the URL
1839 if (defined $params{'action'}) {
1840 $href .= "/".esc_path_info($params{'action'})
1841 unless $params{'action'} eq 'summary';
1842 delete $params{'action'};
1845 # Next, we put hash_parent_base:/file_parent..hash_base:/file_name,
1846 # stripping nonexistent or useless pieces
1847 $href .= "/" if ($params{'hash_base'} || $params{'hash_parent_base'}
1848 || $params{'hash_parent'} || $params{'hash'});
1849 if (defined $params{'hash_base'}) {
1850 if (defined $params{'hash_parent_base'}) {
1851 $href .= esc_path_info($params{'hash_parent_base'});
1852 # skip the file_parent if it's the same as the file_name
1853 if (defined $params{'file_parent'}) {
1854 if (defined $params{'file_name'} && $params{'file_parent'} eq $params{'file_name'}) {
1855 delete $params{'file_parent'};
1856 } elsif ($params{'file_parent'} !~ /\.\./) {
1857 $href .= ":/".esc_path_info($params{'file_parent'});
1858 delete $params{'file_parent'};
1861 $href .= "..";
1862 delete $params{'hash_parent'};
1863 delete $params{'hash_parent_base'};
1864 } elsif (defined $params{'hash_parent'}) {
1865 $href .= esc_path_info($params{'hash_parent'}). "..";
1866 delete $params{'hash_parent'};
1869 $href .= esc_path_info($params{'hash_base'});
1870 if (defined $params{'file_name'} && $params{'file_name'} !~ /\.\./) {
1871 $href .= ":/".esc_path_info($params{'file_name'});
1872 delete $params{'file_name'};
1874 delete $params{'hash'};
1875 delete $params{'hash_base'};
1876 } elsif (defined $params{'hash'}) {
1877 $href .= esc_path_info($params{'hash'});
1878 delete $params{'hash'};
1881 # If the action was a snapshot, we can absorb the
1882 # snapshot_format parameter too
1883 if ($is_snapshot) {
1884 my $fmt = $params{'snapshot_format'};
1885 # snapshot_format should always be defined when href()
1886 # is called, but just in case some code forgets, we
1887 # fall back to the default
1888 $fmt ||= $snapshot_fmts[0];
1889 $href .= $known_snapshot_formats{$fmt}{'suffix'};
1890 delete $params{'snapshot_format'};
1894 # now encode the parameters explicitly
1895 my @result = ();
1896 for (my $i = 0; $i < @cgi_param_mapping; $i += 2) {
1897 my ($name, $symbol) = ($cgi_param_mapping[$i], $cgi_param_mapping[$i+1]);
1898 if (defined $params{$name}) {
1899 if (ref($params{$name}) eq "ARRAY") {
1900 foreach my $par (@{$params{$name}}) {
1901 push @result, $symbol . "=" . esc_param($par);
1903 } else {
1904 push @result, $symbol . "=" . esc_param($params{$name});
1908 $href .= "?" . join(';', @result) if scalar @result;
1910 # final transformation: trailing spaces must be escaped (URI-encoded)
1911 $href =~ s/(\s+)$/CGI::escape($1)/e;
1913 if ($params{-anchor}) {
1914 $href .= "#".esc_param($params{-anchor});
1917 return $href;
1921 ## ======================================================================
1922 ## validation, quoting/unquoting and escaping
1924 sub is_valid_action {
1925 my $input = shift;
1926 return undef unless exists $actions{$input};
1927 return 1;
1930 sub is_valid_project {
1931 my $input = shift;
1933 return unless defined $input;
1934 if (!is_valid_pathname($input) ||
1935 !(-d "$projectroot/$input") ||
1936 !check_export_ok("$projectroot/$input") ||
1937 ($strict_export && !project_in_list($input))) {
1938 return undef;
1939 } else {
1940 return 1;
1944 sub is_valid_pathname {
1945 my $input = shift;
1947 return undef unless defined $input;
1948 # no '.' or '..' as elements of path, i.e. no '.' or '..'
1949 # at the beginning, at the end, and between slashes.
1950 # also this catches doubled slashes
1951 if ($input =~ m!(^|/)(|\.|\.\.)(/|$)!) {
1952 return undef;
1954 # no null characters
1955 if ($input =~ m!\0!) {
1956 return undef;
1958 return 1;
1961 sub is_valid_ref_format {
1962 my $input = shift;
1964 return undef unless defined $input;
1965 # restrictions on ref name according to git-check-ref-format
1966 if ($input =~ m!(/\.|\.\.|[\000-\040\177 ~^:?*\[]|/$)!) {
1967 return undef;
1969 return 1;
1972 sub is_valid_refname {
1973 my $input = shift;
1975 return undef unless defined $input;
1976 # textual hashes are O.K.
1977 if ($input =~ m/^[0-9a-fA-F]{40}$/) {
1978 return 1;
1980 # allow repeated trailing '[~^]n*' suffix(es)
1981 $input =~ s/^([^~^]+)(?:[~^]\d*)+$/$1/;
1982 # it must be correct pathname
1983 is_valid_pathname($input) or return undef;
1984 # check git-check-ref-format restrictions
1985 is_valid_ref_format($input) or return undef;
1986 return 1;
1989 # decode sequences of octets in utf8 into Perl's internal form,
1990 # which is utf-8 with utf8 flag set if needed. gitweb writes out
1991 # in utf-8 thanks to "binmode STDOUT, ':utf8'" at beginning
1992 sub to_utf8 {
1993 my $str = shift;
1994 return undef unless defined $str;
1996 if (utf8::is_utf8($str) || utf8::decode($str)) {
1997 return $str;
1998 } else {
1999 return $encode_object->decode($str, Encode::FB_DEFAULT);
2003 # quote unsafe chars, but keep the slash, even when it's not
2004 # correct, but quoted slashes look too horrible in bookmarks
2005 sub esc_param {
2006 my $str = shift;
2007 return undef unless defined $str;
2008 $str =~ s/([^A-Za-z0-9\-_.~()\/:@ ]+)/CGI::escape($1)/eg;
2009 $str =~ s/ /\+/g;
2010 return $str;
2013 # the quoting rules for path_info fragment are slightly different
2014 sub esc_path_info {
2015 my $str = shift;
2016 return undef unless defined $str;
2018 # path_info doesn't treat '+' as space (specially), but '?' must be escaped
2019 $str =~ s/([^A-Za-z0-9\-_.~();\/;:@&= +]+)/CGI::escape($1)/eg;
2021 return $str;
2024 # quote unsafe chars in whole URL, so some characters cannot be quoted
2025 sub esc_url {
2026 my $str = shift;
2027 return undef unless defined $str;
2028 $str =~ s/([^A-Za-z0-9\-_.~();\/;?:@&= ]+)/CGI::escape($1)/eg;
2029 $str =~ s/ /\+/g;
2030 return $str;
2033 # quote unsafe characters in HTML attributes
2034 sub esc_attr {
2036 # for XHTML conformance escaping '"' to '&quot;' is not enough
2037 return esc_html(@_);
2040 # replace invalid utf8 character with SUBSTITUTION sequence
2041 sub esc_html {
2042 my $str = shift;
2043 my %opts = @_;
2045 return undef unless defined $str;
2047 $str = to_utf8($str);
2048 $str = $cgi->escapeHTML($str);
2049 if ($opts{'-nbsp'}) {
2050 $str =~ s/ /&#160;/g;
2052 use bytes;
2053 $str =~ s|([[:cntrl:]])|(($1 ne "\t") ? quot_cec($1) : $1)|eg;
2054 return $str;
2057 # quote control characters and escape filename to HTML
2058 sub esc_path {
2059 my $str = shift;
2060 my %opts = @_;
2062 return undef unless defined $str;
2064 $str = to_utf8($str);
2065 $str = $cgi->escapeHTML($str);
2066 if ($opts{'-nbsp'}) {
2067 $str =~ s/ /&#160;/g;
2069 use bytes;
2070 $str =~ s|([[:cntrl:]])|quot_cec($1)|eg;
2071 return $str;
2074 # Sanitize for use in XHTML + application/xml+xhtml (valid XML 1.0)
2075 sub sanitize {
2076 my $str = shift;
2078 return undef unless defined $str;
2080 $str = to_utf8($str);
2081 use bytes;
2082 $str =~ s|([[:cntrl:]])|(index("\t\n\r", $1) != -1 ? $1 : quot_cec($1))|eg;
2083 return $str;
2086 # Make control characters "printable", using character escape codes (CEC)
2087 sub quot_cec {
2088 my $cntrl = shift;
2089 my %opts = @_;
2090 my %es = ( # character escape codes, aka escape sequences
2091 "\t" => '\t', # tab (HT)
2092 "\n" => '\n', # line feed (LF)
2093 "\r" => '\r', # carrige return (CR)
2094 "\f" => '\f', # form feed (FF)
2095 "\b" => '\b', # backspace (BS)
2096 "\a" => '\a', # alarm (bell) (BEL)
2097 "\e" => '\e', # escape (ESC)
2098 "\013" => '\v', # vertical tab (VT)
2099 "\000" => '\0', # nul character (NUL)
2101 my $chr = ( (exists $es{$cntrl})
2102 ? $es{$cntrl}
2103 : sprintf('\x%02x', ord($cntrl)) );
2104 if ($opts{-nohtml}) {
2105 return $chr;
2106 } else {
2107 return "<span class=\"cntrl\">$chr</span>";
2111 # Alternatively use unicode control pictures codepoints,
2112 # Unicode "printable representation" (PR)
2113 sub quot_upr {
2114 my $cntrl = shift;
2115 my %opts = @_;
2117 my $chr = sprintf('&#%04d;', 0x2400+ord($cntrl));
2118 if ($opts{-nohtml}) {
2119 return $chr;
2120 } else {
2121 return "<span class=\"cntrl\">$chr</span>";
2125 # git may return quoted and escaped filenames
2126 sub unquote {
2127 my $str = shift;
2129 sub unq {
2130 my $seq = shift;
2131 my %es = ( # character escape codes, aka escape sequences
2132 't' => "\t", # tab (HT, TAB)
2133 'n' => "\n", # newline (NL)
2134 'r' => "\r", # return (CR)
2135 'f' => "\f", # form feed (FF)
2136 'b' => "\b", # backspace (BS)
2137 'a' => "\a", # alarm (bell) (BEL)
2138 'e' => "\e", # escape (ESC)
2139 'v' => "\013", # vertical tab (VT)
2142 if ($seq =~ m/^[0-7]{1,3}$/) {
2143 # octal char sequence
2144 return chr(oct($seq));
2145 } elsif (exists $es{$seq}) {
2146 # C escape sequence, aka character escape code
2147 return $es{$seq};
2149 # quoted ordinary character
2150 return $seq;
2153 if ($str =~ m/^"(.*)"$/) {
2154 # needs unquoting
2155 $str = $1;
2156 $str =~ s/\\([^0-7]|[0-7]{1,3})/unq($1)/eg;
2158 return $str;
2161 # escape tabs (convert tabs to spaces)
2162 sub untabify {
2163 my $line = shift;
2165 while ((my $pos = index($line, "\t")) != -1) {
2166 if (my $count = (8 - ($pos % 8))) {
2167 my $spaces = ' ' x $count;
2168 $line =~ s/\t/$spaces/;
2172 return $line;
2175 sub project_in_list {
2176 my $project = shift;
2177 my @list = git_get_projects_list();
2178 return @list && scalar(grep { $_->{'path'} eq $project } @list);
2181 sub cached_page_precondition_check {
2182 my $action = shift;
2183 return 1 unless
2184 $action eq 'summary' &&
2185 $projlist_cache_lifetime > 0 &&
2186 gitweb_check_feature('forks');
2188 # Note that ALL the 'forkchange' logic is in this function.
2189 # It does NOT belong in cached_action_page NOR in cached_action_start
2190 # NOR in cached_action_finish. None of those functions should know anything
2191 # about nor do anything to any of the 'forkchange'/"$action.forkchange" files.
2193 # besides the basic 'changed' "$action.changed" check, we may only use
2194 # a summary cache if:
2196 # 1) we are not using a project list cache file
2197 # -OR-
2198 # 2) we are not using the 'forks' feature
2199 # -OR-
2200 # 3) there is no 'forkchange' nor "$action.forkchange" file in $html_cache_dir
2201 # -OR-
2202 # 4) there is no cache file ("$cache_dir/$projlist_cache_name")
2203 # -OR-
2204 # 5) the OLDER of 'forkchange'/"$action.forkchange" is NEWER than the cache file
2206 # Otherwise we must re-generate the cache because we've had a fork change
2207 # (either a fork was added or a fork was removed) AND the change has been
2208 # picked up in the cache file AND we've not got that in our cached copy
2210 # For (5) regenerating the cached page wouldn't get us anything if the project
2211 # cache file is older than the 'forkchange'/"$action.forkchange" because the
2212 # forks information comes from the project cache file and it's clearly not
2213 # picked up the changes yet so we may continue to use a cached page until it does.
2215 my $htmlcd = "$projectroot/$project/$html_cache_dir";
2216 my $fc_mt = (stat("$htmlcd/forkchange"))[9];
2217 my $afc_mt = (stat("$htmlcd/$action.forkchange"))[9];
2218 return 1 unless defined($fc_mt) || defined($afc_mt);
2219 my $prj_mt = (stat("$cache_dir/$projlist_cache_name"))[9];
2220 return 1 unless $prj_mt;
2221 my $old_mt = $fc_mt;
2222 $old_mt = $afc_mt if !defined($old_mt) || (defined($afc_mt) && $afc_mt < $old_mt);
2223 return 1 if $old_mt > $prj_mt;
2225 # We're going to regenerate the cached page because we know the project cache
2226 # has new fork information that we cannot possibly have in our cached copy.
2228 # However, if both 'forkchange' and "$action.forkchange" exist and one of
2229 # them is older than the project cache and one of them is newer, we still
2230 # need to regenerate the page cache, but we will also need to do it again
2231 # in the future because there's yet another fork update not yet in the cache.
2233 # So we make sure to touch "$action.changed" to force a cache regeneration
2234 # and then we remove either or both of 'forkchange'/"$action.forkchange" if
2235 # they're older than the project cache (they've served their purpose, we're
2236 # forcing a page regeneration by touching "$action.changed" but the project
2237 # cache was rebuilt since then so there are no more pending fork updates to
2238 # pick up in the future and they need to go).
2240 # For best results, the external code that touches 'forkchange' should always
2241 # touch 'forkchange' and additionally touch 'summary.forkchange' but only
2242 # if it does not already exist. That way the cached page will be regenerated
2243 # each time it's requested and ANY fork updates are available in the proj
2244 # cache rather than waiting until they all are before updating.
2246 # Note that we take a shortcut here and will zap 'forkchange' since we know
2247 # that it only affects the 'summary' cache. If, in the future, it affects
2248 # other cache types, it will first need to be propogated down to
2249 # "$action.forkchange" for those types before we zap it.
2251 my $fd;
2252 open $fd, '>', "$htmlcd/$action.changed" and close $fd;
2253 $fc_mt=undef, unlink "$htmlcd/forkchange" if defined $fc_mt && $fc_mt < $prj_mt;
2254 $afc_mt=undef, unlink "$htmlcd/$action.forkchange" if defined $afc_mt && $afc_mt < $prj_mt;
2256 # Now we propagate 'forkchange' to "$action.forkchange" if we have the
2257 # one and not the other.
2259 if (defined $fc_mt && ! defined $afc_mt) {
2260 open $fd, '>', "$htmlcd/$action.forkchange" and close $fd;
2261 -e "$htmlcd/$action.forkchange" and
2262 utime($fc_mt, $fc_mt, "$htmlcd/$action.forkchange") and
2263 unlink "$htmlcd/forkchange";
2266 return 0;
2269 sub cached_action_page {
2270 my $action = shift;
2272 return undef unless $action && $html_cache_actions{$action} && $html_cache_dir;
2273 my $htmlcd = "$projectroot/$project/$html_cache_dir";
2274 return undef if -e "$htmlcd/changed" || -e "$htmlcd/$action.changed";
2275 return undef unless cached_page_precondition_check($action);
2276 open my $fd, '<', "$htmlcd/$action" or return undef;
2277 binmode $fd;
2278 local $/;
2279 my $cached_page = <$fd>;
2280 close $fd or return undef;
2281 return $cached_page;
2284 package Git::Gitweb::CacheFile;
2286 sub TIEHANDLE {
2287 use POSIX qw(:fcntl_h);
2288 my $class = shift;
2289 my $cachefile = shift;
2291 sysopen(my $self, $cachefile, O_WRONLY|O_CREAT|O_EXCL, 0664)
2292 or return undef;
2293 $$self->{'cachefile'} = $cachefile;
2294 $$self->{'opened'} = 1;
2295 $$self->{'contents'} = '';
2296 return bless $self, $class;
2299 sub CLOSE {
2300 my $self = shift;
2301 if ($$self->{'opened'}) {
2302 $$self->{'opened'} = 0;
2303 my $result = close $self;
2304 unlink $$self->{'cachefile'} unless $result;
2305 return $result;
2307 return 0;
2310 sub DESTROY {
2311 my $self = shift;
2312 if ($$self->{'opened'}) {
2313 $self->CLOSE() and unlink $$self->{'cachefile'};
2317 sub PRINT {
2318 my $self = shift;
2319 @_ = (map {my $x=$_; utf8::encode($x); $x} @_) unless $fcgi_raw_mode;
2320 print $self @_ if $$self->{'opened'};
2321 $$self->{'contents'} .= join('', @_);
2322 return 1;
2325 sub PRINTF {
2326 my $self = shift;
2327 my $template = shift;
2328 return $self->PRINT(sprintf $template, @_);
2331 sub contents {
2332 my $self = shift;
2333 return $$self->{'contents'};
2336 package main;
2338 # Caller is responsible for preserving STDOUT beforehand if needed
2339 sub cached_action_start {
2340 my $action = shift;
2342 return undef unless $html_cache_actions{$action} && $html_cache_dir;
2343 my $htmlcd = "$projectroot/$project/$html_cache_dir";
2344 return undef unless -d $htmlcd;
2345 if (-e "$htmlcd/changed") {
2346 foreach my $cacheable (keys(%html_cache_actions)) {
2347 next unless $supported_cache_actions{$cacheable} &&
2348 $html_cache_actions{$cacheable};
2349 my $fd;
2350 open $fd, '>', "$htmlcd/$cacheable.changed"
2351 and close $fd;
2353 unlink "$htmlcd/changed";
2355 local *CACHEFILE;
2356 tie *CACHEFILE, 'Git::Gitweb::CacheFile', "$htmlcd/$action.lock" or return undef;
2357 *STDOUT = *CACHEFILE;
2358 unlink "$htmlcd/$action", "$htmlcd/$action.changed";
2359 return 1;
2362 # Caller is responsible for restoring STDOUT afterward if needed
2363 sub cached_action_finish {
2364 my $action = shift;
2366 use File::Spec;
2368 my $obj = tied *STDOUT;
2369 return undef unless ref($obj) eq 'Git::Gitweb::CacheFile';
2370 my $cached_page = $obj->contents;
2371 (my $result = close(STDOUT)) or warn "couldn't close cache file on STDOUT: $!";
2372 # Do not leave STDOUT file descriptor invalid!
2373 local *NULL;
2374 open(NULL, '>', File::Spec->devnull) or die "couldn't open NULL to devnull: $!";
2375 *STDOUT = *NULL;
2376 return $cached_page unless $result;
2377 my $htmlcd = "$projectroot/$project/$html_cache_dir";
2378 return $cached_page unless -d $htmlcd;
2379 unlink "$htmlcd/$action.lock" unless rename "$htmlcd/$action.lock", "$htmlcd/$action";
2380 return $cached_page;
2383 my %expand_pi_subs;
2384 BEGIN {%expand_pi_subs = (
2385 'age_string' => \&age_string,
2386 'age_string_date' => \&age_string_date,
2387 'age_string_age' => \&age_string_age,
2388 'compute_timed_interval' => \&compute_timed_interval,
2389 'compute_commands_count' => \&compute_commands_count,
2390 'format_lastrefresh_row' => \&format_lastrefresh_row,
2393 # Expands any <?gitweb...> processing instructions and returns the result
2394 sub expand_gitweb_pi {
2395 my $page = shift;
2396 $page .= '';
2397 my @time_now = gettimeofday();
2398 $page =~ s{<\?gitweb(?:\s+([^\s>]+)([^>]*))?\s*\?>}
2399 {defined($1) ?
2400 (ref($expand_pi_subs{$1}) eq 'CODE' ?
2401 $expand_pi_subs{$1}->(split(' ',$2), @time_now) :
2402 '') :
2403 '' }goes;
2404 return $page;
2407 ## ----------------------------------------------------------------------
2408 ## HTML aware string manipulation
2410 # Try to chop given string on a word boundary between position
2411 # $len and $len+$add_len. If there is no word boundary there,
2412 # chop at $len+$add_len. Do not chop if chopped part plus ellipsis
2413 # (marking chopped part) would be longer than given string.
2414 sub chop_str {
2415 my $str = shift;
2416 my $len = shift;
2417 my $add_len = shift || 10;
2418 my $where = shift || 'right'; # 'left' | 'center' | 'right'
2420 # Make sure perl knows it is utf8 encoded so we don't
2421 # cut in the middle of a utf8 multibyte char.
2422 $str = to_utf8($str);
2424 # allow only $len chars, but don't cut a word if it would fit in $add_len
2425 # if it doesn't fit, cut it if it's still longer than the dots we would add
2426 # remove chopped character entities entirely
2428 # when chopping in the middle, distribute $len into left and right part
2429 # return early if chopping wouldn't make string shorter
2430 if ($where eq 'center') {
2431 return $str if ($len + 5 >= length($str)); # filler is length 5
2432 $len = int($len/2);
2433 } else {
2434 return $str if ($len + 4 >= length($str)); # filler is length 4
2437 # regexps: ending and beginning with word part up to $add_len
2438 my $endre = qr/.{$len}\w{0,$add_len}/;
2439 my $begre = qr/\w{0,$add_len}.{$len}/;
2441 if ($where eq 'left') {
2442 $str =~ m/^(.*?)($begre)$/;
2443 my ($lead, $body) = ($1, $2);
2444 if (length($lead) > 4) {
2445 $lead = " ...";
2447 return "$lead$body";
2449 } elsif ($where eq 'center') {
2450 $str =~ m/^($endre)(.*)$/;
2451 my ($left, $str) = ($1, $2);
2452 $str =~ m/^(.*?)($begre)$/;
2453 my ($mid, $right) = ($1, $2);
2454 if (length($mid) > 5) {
2455 $mid = " ... ";
2457 return "$left$mid$right";
2459 } else {
2460 $str =~ m/^($endre)(.*)$/;
2461 my $body = $1;
2462 my $tail = $2;
2463 if (length($tail) > 4) {
2464 $tail = "... ";
2466 return "$body$tail";
2470 # pass-through email filter, obfuscating it when possible
2471 sub email_obfuscate {
2472 our $email;
2473 my ($str) = @_;
2474 if ($email) {
2475 $str = $email->escape_html($str);
2476 # Stock HTML::Email::Obfuscate version likes to produce
2477 # invalid XHTML...
2478 $str =~ s#<(/?)B>#<$1b>#g;
2479 return $str;
2480 } else {
2481 $str = esc_html($str);
2482 $str =~ s/@/&#x40;/;
2483 return $str;
2487 # takes the same arguments as chop_str, but also wraps a <span> around the
2488 # result with a title attribute if it does get chopped. Additionally, the
2489 # string is HTML-escaped.
2490 sub chop_and_escape_str {
2491 my ($str) = @_;
2493 my $chopped = chop_str(@_);
2494 $str = to_utf8($str);
2495 if ($chopped eq $str) {
2496 return email_obfuscate($chopped);
2497 } else {
2498 use bytes;
2499 $str =~ s/[[:cntrl:]]/?/g;
2500 return $cgi->span({-title=>$str}, email_obfuscate($chopped));
2504 # Highlight selected fragments of string, using given CSS class,
2505 # and escape HTML. It is assumed that fragments do not overlap.
2506 # Regions are passed as list of pairs (array references).
2508 # Example: esc_html_hl_regions("foobar", "mark", [ 0, 3 ]) returns
2509 # '<span class="mark">foo</span>bar'
2510 sub esc_html_hl_regions {
2511 my ($str, $css_class, @sel) = @_;
2512 my %opts = grep { ref($_) ne 'ARRAY' } @sel;
2513 @sel = grep { ref($_) eq 'ARRAY' } @sel;
2514 return esc_html($str, %opts) unless @sel;
2516 my $out = '';
2517 my $pos = 0;
2519 for my $s (@sel) {
2520 my ($begin, $end) = @$s;
2522 # Don't create empty <span> elements.
2523 next if $end <= $begin;
2525 my $escaped = esc_html(substr($str, $begin, $end - $begin),
2526 %opts);
2528 $out .= esc_html(substr($str, $pos, $begin - $pos), %opts)
2529 if ($begin - $pos > 0);
2530 $out .= $cgi->span({-class => $css_class}, $escaped);
2532 $pos = $end;
2534 $out .= esc_html(substr($str, $pos), %opts)
2535 if ($pos < length($str));
2537 return $out;
2540 # return positions of beginning and end of each match
2541 sub matchpos_list {
2542 my ($str, $regexp) = @_;
2543 return unless (defined $str && defined $regexp);
2545 my @matches;
2546 while ($str =~ /$regexp/g) {
2547 push @matches, [$-[0], $+[0]];
2549 return @matches;
2552 # highlight match (if any), and escape HTML
2553 sub esc_html_match_hl {
2554 my ($str, $regexp) = @_;
2555 return esc_html($str) unless defined $regexp;
2557 my @matches = matchpos_list($str, $regexp);
2558 return esc_html($str) unless @matches;
2560 return esc_html_hl_regions($str, 'match', @matches);
2564 # highlight match (if any) of shortened string, and escape HTML
2565 sub esc_html_match_hl_chopped {
2566 my ($str, $chopped, $regexp) = @_;
2567 return esc_html_match_hl($str, $regexp) unless defined $chopped;
2569 my @matches = matchpos_list($str, $regexp);
2570 return esc_html($chopped) unless @matches;
2572 # filter matches so that we mark chopped string
2573 my $tail = "... "; # see chop_str
2574 unless ($chopped =~ s/\Q$tail\E$//) {
2575 $tail = '';
2577 my $chop_len = length($chopped);
2578 my $tail_len = length($tail);
2579 my @filtered;
2581 for my $m (@matches) {
2582 if ($m->[0] > $chop_len) {
2583 push @filtered, [ $chop_len, $chop_len + $tail_len ] if ($tail_len > 0);
2584 last;
2585 } elsif ($m->[1] > $chop_len) {
2586 push @filtered, [ $m->[0], $chop_len + $tail_len ];
2587 last;
2589 push @filtered, $m;
2592 return esc_html_hl_regions($chopped . $tail, 'match', @filtered);
2595 ## ----------------------------------------------------------------------
2596 ## functions returning short strings
2598 # CSS class for given age epoch value (in seconds)
2599 # and reference time (optional, defaults to now) as second value
2600 sub age_class {
2601 my ($age_epoch, $time_now) = @_;
2602 return "noage" unless defined $age_epoch;
2603 defined $time_now or $time_now = time;
2604 my $age = $time_now - $age_epoch;
2606 if ($age < 60*60*2) {
2607 return "age0";
2608 } elsif ($age < 60*60*24*2) {
2609 return "age1";
2610 } else {
2611 return "age2";
2615 # convert age epoch in seconds to "nn units ago" string
2616 # reference time used is now unless second argument passed in
2617 # to get the old behavior, pass 0 as the first argument and
2618 # the time in seconds as the second
2619 sub age_string {
2620 my ($age_epoch, $time_now) = @_;
2621 return "unknown" unless defined $age_epoch;
2622 return "<?gitweb age_string $age_epoch?>" if $cache_mode_active;
2623 defined $time_now or $time_now = time;
2624 my $age = $time_now - $age_epoch;
2625 my $age_str;
2627 if ($age > 60*60*24*365*2) {
2628 $age_str = (int $age/60/60/24/365);
2629 $age_str .= " years ago";
2630 } elsif ($age > 60*60*24*(365/12)*2) {
2631 $age_str = int $age/60/60/24/(365/12);
2632 $age_str .= " months ago";
2633 } elsif ($age > 60*60*24*7*2) {
2634 $age_str = int $age/60/60/24/7;
2635 $age_str .= " weeks ago";
2636 } elsif ($age > 60*60*24*2) {
2637 $age_str = int $age/60/60/24;
2638 $age_str .= " days ago";
2639 } elsif ($age > 60*60*2) {
2640 $age_str = int $age/60/60;
2641 $age_str .= " hours ago";
2642 } elsif ($age > 60*2) {
2643 $age_str = int $age/60;
2644 $age_str .= " min ago";
2645 } elsif ($age > 2) {
2646 $age_str = int $age;
2647 $age_str .= " sec ago";
2648 } else {
2649 $age_str .= " right now";
2651 return $age_str;
2654 # returns age_string if the age is <= 2 weeks otherwise an absolute date
2655 # this is typically shown to the user directly with the age_string_age as a title
2656 sub age_string_date {
2657 my ($age_epoch, $time_now) = @_;
2658 return "unknown" unless defined $age_epoch;
2659 return "<?gitweb age_string_date $age_epoch?>" if $cache_mode_active;
2660 defined $time_now or $time_now = time;
2661 my $age = $time_now - $age_epoch;
2663 if ($age > 60*60*24*7*2) {
2664 my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($age_epoch);
2665 return sprintf "%4i-%02u-%02i", 1900 + $year, $mon+1, $mday;
2666 } else {
2667 return age_string($age_epoch, $time_now);
2671 # returns an absolute date if the age is <= 2 weeks otherwise age_string
2672 # this is typically used for the 'title' attribute so it will show as a tooltip
2673 sub age_string_age {
2674 my ($age_epoch, $time_now) = @_;
2675 return "unknown" unless defined $age_epoch;
2676 return "<?gitweb age_string_age $age_epoch?>" if $cache_mode_active;
2677 defined $time_now or $time_now = time;
2678 my $age = $time_now - $age_epoch;
2680 if ($age > 60*60*24*7*2) {
2681 return age_string($age_epoch, $time_now);
2682 } else {
2683 my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($age_epoch);
2684 return sprintf "%4i-%02u-%02i", 1900 + $year, $mon+1, $mday;
2688 use constant {
2689 S_IFINVALID => 0030000,
2690 S_IFGITLINK => 0160000,
2693 # submodule/subproject, a commit object reference
2694 sub S_ISGITLINK {
2695 my $mode = shift;
2697 return (($mode & S_IFMT) == S_IFGITLINK)
2700 # convert file mode in octal to symbolic file mode string
2701 sub mode_str {
2702 my $mode = oct shift;
2704 if (S_ISGITLINK($mode)) {
2705 return 'm---------';
2706 } elsif (S_ISDIR($mode & S_IFMT)) {
2707 return 'drwxr-xr-x';
2708 } elsif (S_ISLNK($mode)) {
2709 return 'lrwxrwxrwx';
2710 } elsif (S_ISREG($mode)) {
2711 # git cares only about the executable bit
2712 if ($mode & S_IXUSR) {
2713 return '-rwxr-xr-x';
2714 } else {
2715 return '-rw-r--r--';
2717 } else {
2718 return '----------';
2722 # convert file mode in octal to file type string
2723 sub file_type {
2724 my $mode = shift;
2726 if ($mode !~ m/^[0-7]+$/) {
2727 return $mode;
2728 } else {
2729 $mode = oct $mode;
2732 if (S_ISGITLINK($mode)) {
2733 return "submodule";
2734 } elsif (S_ISDIR($mode & S_IFMT)) {
2735 return "directory";
2736 } elsif (S_ISLNK($mode)) {
2737 return "symlink";
2738 } elsif (S_ISREG($mode)) {
2739 return "file";
2740 } else {
2741 return "unknown";
2745 # convert file mode in octal to file type description string
2746 sub file_type_long {
2747 my $mode = shift;
2749 if ($mode !~ m/^[0-7]+$/) {
2750 return $mode;
2751 } else {
2752 $mode = oct $mode;
2755 if (S_ISGITLINK($mode)) {
2756 return "submodule";
2757 } elsif (S_ISDIR($mode & S_IFMT)) {
2758 return "directory";
2759 } elsif (S_ISLNK($mode)) {
2760 return "symlink";
2761 } elsif (S_ISREG($mode)) {
2762 if ($mode & S_IXUSR) {
2763 return "executable";
2764 } else {
2765 return "file";
2767 } else {
2768 return "unknown";
2773 ## ----------------------------------------------------------------------
2774 ## functions returning short HTML fragments, or transforming HTML fragments
2775 ## which don't belong to other sections
2777 # format line of commit message.
2778 sub format_log_line_html {
2779 my $line = shift;
2781 $line = esc_html($line, -nbsp=>1);
2782 $line =~ s{
2785 # The output of "git describe", e.g. v2.10.0-297-gf6727b0
2786 # or hadoop-20160921-113441-20-g094fb7d
2787 (?<!-) # see strbuf_check_tag_ref(). Tags can't start with -
2788 [A-Za-z0-9.-]+
2789 (?!\.) # refs can't end with ".", see check_refname_format()
2790 -g[0-9a-fA-F]{7,40}
2792 # Just a normal looking Git SHA1
2793 [0-9a-fA-F]{7,40}
2797 $cgi->a({-href => href(action=>"object", hash=>$1),
2798 -class => "text"}, $1);
2799 }egx unless $line =~ /^\s*git-svn-id:/;
2801 return $line;
2804 # format marker of refs pointing to given object
2806 # the destination action is chosen based on object type and current context:
2807 # - for annotated tags, we choose the tag view unless it's the current view
2808 # already, in which case we go to shortlog view
2809 # - for other refs, we keep the current view if we're in history, shortlog or
2810 # log view, and select shortlog otherwise
2811 sub format_ref_marker {
2812 my ($refs, $id) = @_;
2813 my $markers = '';
2815 if (defined $refs->{$id}) {
2816 foreach my $ref (@{$refs->{$id}}) {
2817 # this code exploits the fact that non-lightweight tags are the
2818 # only indirect objects, and that they are the only objects for which
2819 # we want to use tag instead of shortlog as action
2820 my ($type, $name) = qw();
2821 my $indirect = ($ref =~ s/\^\{\}$//);
2822 # e.g. tags/v2.6.11 or heads/next
2823 if ($ref =~ m!^(.*?)s?/(.*)$!) {
2824 $type = $1;
2825 $name = $2;
2826 } else {
2827 $type = "ref";
2828 $name = $ref;
2831 my $class = $type;
2832 $class .= " indirect" if $indirect;
2834 my $dest_action = "shortlog";
2836 if ($indirect) {
2837 $dest_action = "tag" unless $action eq "tag";
2838 } elsif ($action =~ /^(history|(short)?log)$/) {
2839 $dest_action = $action;
2842 my $dest = "";
2843 $dest .= "refs/" unless $ref =~ m!^refs/!;
2844 $dest .= $ref;
2846 my $link = $cgi->a({
2847 -href => href(
2848 action=>$dest_action,
2849 hash=>$dest
2850 )}, esc_html($name));
2852 $markers .= "<span class=\"".esc_attr($class)."\" title=\"".esc_attr($ref)."\">" .
2853 $link . "</span>";
2857 if ($markers) {
2858 return '<span class="refs">'. $markers . '</span>';
2859 } else {
2860 return "";
2864 # format, perhaps shortened and with markers, title line
2865 sub format_subject_html {
2866 my ($long, $short, $href, $extra) = @_;
2867 $extra = '' unless defined($extra);
2869 if (length($short) < length($long)) {
2870 use bytes;
2871 $long =~ s/[[:cntrl:]]/?/g;
2872 return $cgi->a({-href => $href, -class => "list subject",
2873 -title => to_utf8($long)},
2874 esc_html($short)) . $extra;
2875 } else {
2876 return $cgi->a({-href => $href, -class => "list subject"},
2877 esc_html($long)) . $extra;
2881 # Rather than recomputing the url for an email multiple times, we cache it
2882 # after the first hit. This gives a visible benefit in views where the avatar
2883 # for the same email is used repeatedly (e.g. shortlog).
2884 # The cache is shared by all avatar engines (currently gravatar only), which
2885 # are free to use it as preferred. Since only one avatar engine is used for any
2886 # given page, there's no risk for cache conflicts.
2887 our %avatar_cache = ();
2889 # Compute the picon url for a given email, by using the picon search service over at
2890 # http://www.cs.indiana.edu/picons/search.html
2891 sub picon_url {
2892 my $email = lc shift;
2893 if (!$avatar_cache{$email}) {
2894 my ($user, $domain) = split('@', $email);
2895 $avatar_cache{$email} =
2896 "//www.cs.indiana.edu/cgi-pub/kinzler/piconsearch.cgi/" .
2897 "$domain/$user/" .
2898 "users+domains+unknown/up/single";
2900 return $avatar_cache{$email};
2903 # Compute the gravatar url for a given email, if it's not in the cache already.
2904 # Gravatar stores only the part of the URL before the size, since that's the
2905 # one computationally more expensive. This also allows reuse of the cache for
2906 # different sizes (for this particular engine).
2907 sub gravatar_url {
2908 my $email = lc shift;
2909 my $size = shift;
2910 $avatar_cache{$email} ||=
2911 "//www.gravatar.com/avatar/" .
2912 Digest::MD5::md5_hex($email) . "?s=";
2913 return $avatar_cache{$email} . $size;
2916 # Insert an avatar for the given $email at the given $size if the feature
2917 # is enabled.
2918 sub git_get_avatar {
2919 my ($email, %opts) = @_;
2920 my $pre_white = ($opts{-pad_before} ? "&#160;" : "");
2921 my $post_white = ($opts{-pad_after} ? "&#160;" : "");
2922 $opts{-size} ||= 'default';
2923 my $size = $avatar_size{$opts{-size}} || $avatar_size{'default'};
2924 my $url = "";
2925 if ($git_avatar eq 'gravatar') {
2926 $url = gravatar_url($email, $size);
2927 } elsif ($git_avatar eq 'picon') {
2928 $url = picon_url($email);
2930 # Other providers can be added by extending the if chain, defining $url
2931 # as needed. If no variant puts something in $url, we assume avatars
2932 # are completely disabled/unavailable.
2933 if ($url) {
2934 return $pre_white .
2935 "<img width=\"$size\" " .
2936 "class=\"avatar\" " .
2937 "src=\"".esc_url($url)."\" " .
2938 "alt=\"\" " .
2939 "/>" . $post_white;
2940 } else {
2941 return "";
2945 sub format_search_author {
2946 my ($author, $searchtype, $displaytext) = @_;
2947 my $have_search = gitweb_check_feature('search');
2949 if ($have_search) {
2950 my $performed = "";
2951 if ($searchtype eq 'author') {
2952 $performed = "authored";
2953 } elsif ($searchtype eq 'committer') {
2954 $performed = "committed";
2957 return $cgi->a({-href => href(action=>"search", hash=>$hash,
2958 searchtext=>$author,
2959 searchtype=>$searchtype), class=>"list",
2960 title=>"Search for commits $performed by $author"},
2961 $displaytext);
2963 } else {
2964 return $displaytext;
2968 # format the author name of the given commit with the given tag
2969 # the author name is chopped and escaped according to the other
2970 # optional parameters (see chop_str).
2971 sub format_author_html {
2972 my $tag = shift;
2973 my $co = shift;
2974 my $author = chop_and_escape_str($co->{'author_name'}, @_);
2975 return "<$tag class=\"author\">" .
2976 format_search_author($co->{'author_name'}, "author",
2977 git_get_avatar($co->{'author_email'}, -pad_after => 1) .
2978 $author) .
2979 "</$tag>";
2982 # format git diff header line, i.e. "diff --(git|combined|cc) ..."
2983 sub format_git_diff_header_line {
2984 my $line = shift;
2985 my $diffinfo = shift;
2986 my ($from, $to) = @_;
2988 if ($diffinfo->{'nparents'}) {
2989 # combined diff
2990 $line =~ s!^(diff (.*?) )"?.*$!$1!;
2991 if ($to->{'href'}) {
2992 $line .= $cgi->a({-href => $to->{'href'}, -class => "path"},
2993 esc_path($to->{'file'}));
2994 } else { # file was deleted (no href)
2995 $line .= esc_path($to->{'file'});
2997 } else {
2998 # "ordinary" diff
2999 $line =~ s!^(diff (.*?) )"?a/.*$!$1!;
3000 if ($from->{'href'}) {
3001 $line .= $cgi->a({-href => $from->{'href'}, -class => "path"},
3002 'a/' . esc_path($from->{'file'}));
3003 } else { # file was added (no href)
3004 $line .= 'a/' . esc_path($from->{'file'});
3006 $line .= ' ';
3007 if ($to->{'href'}) {
3008 $line .= $cgi->a({-href => $to->{'href'}, -class => "path"},
3009 'b/' . esc_path($to->{'file'}));
3010 } else { # file was deleted
3011 $line .= 'b/' . esc_path($to->{'file'});
3015 return "<div class=\"diff header\">$line</div>\n";
3018 # format extended diff header line, before patch itself
3019 sub format_extended_diff_header_line {
3020 my $line = shift;
3021 my $diffinfo = shift;
3022 my ($from, $to) = @_;
3024 # match <path>
3025 if ($line =~ s!^((copy|rename) from ).*$!$1! && $from->{'href'}) {
3026 $line .= $cgi->a({-href=>$from->{'href'}, -class=>"path"},
3027 esc_path($from->{'file'}));
3029 if ($line =~ s!^((copy|rename) to ).*$!$1! && $to->{'href'}) {
3030 $line .= $cgi->a({-href=>$to->{'href'}, -class=>"path"},
3031 esc_path($to->{'file'}));
3033 # match single <mode>
3034 if ($line =~ m/\s(\d{6})$/) {
3035 $line .= '<span class="info"> (' .
3036 file_type_long($1) .
3037 ')</span>';
3039 # match <hash>
3040 if ($line =~ m/^index [0-9a-fA-F]{40},[0-9a-fA-F]{40}/) {
3041 # can match only for combined diff
3042 $line = 'index ';
3043 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
3044 if ($from->{'href'}[$i]) {
3045 $line .= $cgi->a({-href=>$from->{'href'}[$i],
3046 -class=>"hash"},
3047 substr($diffinfo->{'from_id'}[$i],0,7));
3048 } else {
3049 $line .= '0' x 7;
3051 # separator
3052 $line .= ',' if ($i < $diffinfo->{'nparents'} - 1);
3054 $line .= '..';
3055 if ($to->{'href'}) {
3056 $line .= $cgi->a({-href=>$to->{'href'}, -class=>"hash"},
3057 substr($diffinfo->{'to_id'},0,7));
3058 } else {
3059 $line .= '0' x 7;
3062 } elsif ($line =~ m/^index [0-9a-fA-F]{40}..[0-9a-fA-F]{40}/) {
3063 # can match only for ordinary diff
3064 my ($from_link, $to_link);
3065 if ($from->{'href'}) {
3066 $from_link = $cgi->a({-href=>$from->{'href'}, -class=>"hash"},
3067 substr($diffinfo->{'from_id'},0,7));
3068 } else {
3069 $from_link = '0' x 7;
3071 if ($to->{'href'}) {
3072 $to_link = $cgi->a({-href=>$to->{'href'}, -class=>"hash"},
3073 substr($diffinfo->{'to_id'},0,7));
3074 } else {
3075 $to_link = '0' x 7;
3077 my ($from_id, $to_id) = ($diffinfo->{'from_id'}, $diffinfo->{'to_id'});
3078 $line =~ s!$from_id\.\.$to_id!$from_link..$to_link!;
3081 return $line . "<br/>\n";
3084 # format from-file/to-file diff header
3085 sub format_diff_from_to_header {
3086 my ($from_line, $to_line, $diffinfo, $from, $to, @parents) = @_;
3087 my $line;
3088 my $result = '';
3090 $line = $from_line;
3091 #assert($line =~ m/^---/) if DEBUG;
3092 # no extra formatting for "^--- /dev/null"
3093 if (! $diffinfo->{'nparents'}) {
3094 # ordinary (single parent) diff
3095 if ($line =~ m!^--- "?a/!) {
3096 if ($from->{'href'}) {
3097 $line = '--- a/' .
3098 $cgi->a({-href=>$from->{'href'}, -class=>"path"},
3099 esc_path($from->{'file'}));
3100 } else {
3101 $line = '--- a/' .
3102 esc_path($from->{'file'});
3105 $result .= qq!<div class="diff from_file">$line</div>\n!;
3107 } else {
3108 # combined diff (merge commit)
3109 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
3110 if ($from->{'href'}[$i]) {
3111 $line = '--- ' .
3112 $cgi->a({-href=>href(action=>"blobdiff",
3113 hash_parent=>$diffinfo->{'from_id'}[$i],
3114 hash_parent_base=>$parents[$i],
3115 file_parent=>$from->{'file'}[$i],
3116 hash=>$diffinfo->{'to_id'},
3117 hash_base=>$hash,
3118 file_name=>$to->{'file'}),
3119 -class=>"path",
3120 -title=>"diff" . ($i+1)},
3121 $i+1) .
3122 '/' .
3123 $cgi->a({-href=>$from->{'href'}[$i], -class=>"path"},
3124 esc_path($from->{'file'}[$i]));
3125 } else {
3126 $line = '--- /dev/null';
3128 $result .= qq!<div class="diff from_file">$line</div>\n!;
3132 $line = $to_line;
3133 #assert($line =~ m/^\+\+\+/) if DEBUG;
3134 # no extra formatting for "^+++ /dev/null"
3135 if ($line =~ m!^\+\+\+ "?b/!) {
3136 if ($to->{'href'}) {
3137 $line = '+++ b/' .
3138 $cgi->a({-href=>$to->{'href'}, -class=>"path"},
3139 esc_path($to->{'file'}));
3140 } else {
3141 $line = '+++ b/' .
3142 esc_path($to->{'file'});
3145 $result .= qq!<div class="diff to_file">$line</div>\n!;
3147 return $result;
3150 # create note for patch simplified by combined diff
3151 sub format_diff_cc_simplified {
3152 my ($diffinfo, @parents) = @_;
3153 my $result = '';
3155 $result .= "<div class=\"diff header\">" .
3156 "diff --cc ";
3157 if (!is_deleted($diffinfo)) {
3158 $result .= $cgi->a({-href => href(action=>"blob",
3159 hash_base=>$hash,
3160 hash=>$diffinfo->{'to_id'},
3161 file_name=>$diffinfo->{'to_file'}),
3162 -class => "path"},
3163 esc_path($diffinfo->{'to_file'}));
3164 } else {
3165 $result .= esc_path($diffinfo->{'to_file'});
3167 $result .= "</div>\n" . # class="diff header"
3168 "<div class=\"diff nodifferences\">" .
3169 "Simple merge" .
3170 "</div>\n"; # class="diff nodifferences"
3172 return $result;
3175 sub diff_line_class {
3176 my ($line, $from, $to) = @_;
3178 # ordinary diff
3179 my $num_sign = 1;
3180 # combined diff
3181 if ($from && $to && ref($from->{'href'}) eq "ARRAY") {
3182 $num_sign = scalar @{$from->{'href'}};
3185 my @diff_line_classifier = (
3186 { regexp => qr/^\@\@{$num_sign} /, class => "chunk_header"},
3187 { regexp => qr/^\\/, class => "incomplete" },
3188 { regexp => qr/^ {$num_sign}/, class => "ctx" },
3189 # classifier for context must come before classifier add/rem,
3190 # or we would have to use more complicated regexp, for example
3191 # qr/(?= {0,$m}\+)[+ ]{$num_sign}/, where $m = $num_sign - 1;
3192 { regexp => qr/^[+ ]{$num_sign}/, class => "add" },
3193 { regexp => qr/^[- ]{$num_sign}/, class => "rem" },
3195 for my $clsfy (@diff_line_classifier) {
3196 return $clsfy->{'class'}
3197 if ($line =~ $clsfy->{'regexp'});
3200 # fallback
3201 return "";
3204 # assumes that $from and $to are defined and correctly filled,
3205 # and that $line holds a line of chunk header for unified diff
3206 sub format_unidiff_chunk_header {
3207 my ($line, $from, $to) = @_;
3209 my ($from_text, $from_start, $from_lines, $to_text, $to_start, $to_lines, $section) =
3210 $line =~ m/^\@{2} (-(\d+)(?:,(\d+))?) (\+(\d+)(?:,(\d+))?) \@{2}(.*)$/;
3212 $from_lines = 0 unless defined $from_lines;
3213 $to_lines = 0 unless defined $to_lines;
3215 if ($from->{'href'}) {
3216 $from_text = $cgi->a({-href=>"$from->{'href'}#l$from_start",
3217 -class=>"list"}, $from_text);
3219 if ($to->{'href'}) {
3220 $to_text = $cgi->a({-href=>"$to->{'href'}#l$to_start",
3221 -class=>"list"}, $to_text);
3223 $line = "<span class=\"chunk_info\">@@ $from_text $to_text @@</span>" .
3224 "<span class=\"section\">" . esc_html($section, -nbsp=>1) . "</span>";
3225 return $line;
3228 # assumes that $from and $to are defined and correctly filled,
3229 # and that $line holds a line of chunk header for combined diff
3230 sub format_cc_diff_chunk_header {
3231 my ($line, $from, $to) = @_;
3233 my ($prefix, $ranges, $section) = $line =~ m/^(\@+) (.*?) \@+(.*)$/;
3234 my (@from_text, @from_start, @from_nlines, $to_text, $to_start, $to_nlines);
3236 @from_text = split(' ', $ranges);
3237 for (my $i = 0; $i < @from_text; ++$i) {
3238 ($from_start[$i], $from_nlines[$i]) =
3239 (split(',', substr($from_text[$i], 1)), 0);
3242 $to_text = pop @from_text;
3243 $to_start = pop @from_start;
3244 $to_nlines = pop @from_nlines;
3246 $line = "<span class=\"chunk_info\">$prefix ";
3247 for (my $i = 0; $i < @from_text; ++$i) {
3248 if ($from->{'href'}[$i]) {
3249 $line .= $cgi->a({-href=>"$from->{'href'}[$i]#l$from_start[$i]",
3250 -class=>"list"}, $from_text[$i]);
3251 } else {
3252 $line .= $from_text[$i];
3254 $line .= " ";
3256 if ($to->{'href'}) {
3257 $line .= $cgi->a({-href=>"$to->{'href'}#l$to_start",
3258 -class=>"list"}, $to_text);
3259 } else {
3260 $line .= $to_text;
3262 $line .= " $prefix</span>" .
3263 "<span class=\"section\">" . esc_html($section, -nbsp=>1) . "</span>";
3264 return $line;
3267 # process patch (diff) line (not to be used for diff headers),
3268 # returning HTML-formatted (but not wrapped) line.
3269 # If the line is passed as a reference, it is treated as HTML and not
3270 # esc_html()'ed.
3271 sub format_diff_line {
3272 my ($line, $diff_class, $from, $to) = @_;
3274 if (ref($line)) {
3275 $line = $$line;
3276 } else {
3277 chomp $line;
3278 $line = untabify($line);
3280 if ($from && $to && $line =~ m/^\@{2} /) {
3281 $line = format_unidiff_chunk_header($line, $from, $to);
3282 } elsif ($from && $to && $line =~ m/^\@{3}/) {
3283 $line = format_cc_diff_chunk_header($line, $from, $to);
3284 } else {
3285 $line = esc_html($line, -nbsp=>1);
3289 my $diff_classes = "diff diff_body";
3290 $diff_classes .= " $diff_class" if ($diff_class);
3291 $line = "<div class=\"$diff_classes\">$line</div>\n";
3293 return $line;
3296 # Generates undef or something like "_snapshot_" or "snapshot (_tbz2_ _zip_)",
3297 # linked. Pass the hash of the tree/commit to snapshot.
3298 sub format_snapshot_links {
3299 my ($hash) = @_;
3300 my $num_fmts = @snapshot_fmts;
3301 if ($num_fmts > 1) {
3302 # A parenthesized list of links bearing format names.
3303 # e.g. "snapshot (_tar.gz_ _zip_)"
3304 return "snapshot (" . join(' ', map
3305 $cgi->a({
3306 -href => href(
3307 action=>"snapshot",
3308 hash=>$hash,
3309 snapshot_format=>$_
3311 }, $known_snapshot_formats{$_}{'display'})
3312 , @snapshot_fmts) . ")";
3313 } elsif ($num_fmts == 1) {
3314 # A single "snapshot" link whose tooltip bears the format name.
3315 # i.e. "_snapshot_"
3316 my ($fmt) = @snapshot_fmts;
3317 return
3318 $cgi->a({
3319 -href => href(
3320 action=>"snapshot",
3321 hash=>$hash,
3322 snapshot_format=>$fmt
3324 -title => "in format: $known_snapshot_formats{$fmt}{'display'}"
3325 }, "snapshot");
3326 } else { # $num_fmts == 0
3327 return undef;
3331 ## ......................................................................
3332 ## functions returning values to be passed, perhaps after some
3333 ## transformation, to other functions; e.g. returning arguments to href()
3335 # returns hash to be passed to href to generate gitweb URL
3336 # in -title key it returns description of link
3337 sub get_feed_info {
3338 my $format = shift || 'Atom';
3339 my %res = (action => lc($format));
3340 my $matched_ref = 0;
3342 # feed links are possible only for project views
3343 return unless (defined $project);
3344 # some views should link to OPML, or to generic project feed,
3345 # or don't have specific feed yet (so they should use generic)
3346 return if (!$action || $action =~ /^(?:tags|heads|forks|tag|search)$/x);
3348 my $branch = undef;
3349 # branches refs uses 'refs/' + $get_branch_refs()[x] + '/' prefix
3350 # (fullname) to differentiate from tag links; this also makes
3351 # possible to detect branch links
3352 for my $ref (get_branch_refs()) {
3353 if ((defined $hash_base && $hash_base =~ m!^refs/\Q$ref\E/(.*)$!) ||
3354 (defined $hash && $hash =~ m!^refs/\Q$ref\E/(.*)$!)) {
3355 $branch = $1;
3356 $matched_ref = $ref;
3357 last;
3360 # find log type for feed description (title)
3361 my $type = 'log';
3362 if (defined $file_name) {
3363 $type = "history of $file_name";
3364 $type .= "/" if ($action eq 'tree');
3365 $type .= " on '$branch'" if (defined $branch);
3366 } else {
3367 $type = "log of $branch" if (defined $branch);
3370 $res{-title} = $type;
3371 $res{'hash'} = (defined $branch ? "refs/$matched_ref/$branch" : undef);
3372 $res{'file_name'} = $file_name;
3374 return %res;
3377 ## ----------------------------------------------------------------------
3378 ## git utility subroutines, invoking git commands
3380 # returns path to the core git executable and the --git-dir parameter as list
3381 sub git_cmd {
3382 $number_of_git_cmds++;
3383 return $GIT, '--git-dir='.$git_dir;
3386 # opens a "-|" cmd pipe handle with 2>/dev/null and returns it
3387 sub cmd_pipe {
3389 # In order to be compatible with FCGI mode we must use POSIX
3390 # and access the STDERR_FILENO file descriptor directly
3392 use POSIX qw(STDERR_FILENO dup dup2);
3394 open(my $null, '>', File::Spec->devnull) or die "couldn't open devnull: $!";
3395 (my $saveerr = dup(STDERR_FILENO)) or die "couldn't dup STDERR: $!";
3396 my $dup2ok = dup2(fileno($null), STDERR_FILENO);
3397 close($null) or !$dup2ok or die "couldn't close NULL: $!";
3398 $dup2ok or POSIX::close($saveerr), die "couldn't dup NULL to STDERR: $!";
3399 my $result = open(my $fd, "-|", @_);
3400 $dup2ok = dup2($saveerr, STDERR_FILENO);
3401 POSIX::close($saveerr) or !$dup2ok or die "couldn't close SAVEERR: $!";
3402 $dup2ok or die "couldn't dup SAVERR to STDERR: $!";
3404 return $result ? $fd : undef;
3407 # opens a "-|" git_cmd pipe handle with 2>/dev/null and returns it
3408 sub git_cmd_pipe {
3409 return cmd_pipe git_cmd(), @_;
3412 # quote the given arguments for passing them to the shell
3413 # quote_command("command", "arg 1", "arg with ' and ! characters")
3414 # => "'command' 'arg 1' 'arg with '\'' and '\!' characters'"
3415 # Try to avoid using this function wherever possible.
3416 sub quote_command {
3417 return join(' ',
3418 map { my $a = $_; $a =~ s/(['!])/'\\$1'/g; "'$a'" } @_ );
3421 # get HEAD ref of given project as hash
3422 sub git_get_head_hash {
3423 return git_get_full_hash(shift, 'HEAD');
3426 sub git_get_full_hash {
3427 return git_get_hash(@_);
3430 sub git_get_short_hash {
3431 return git_get_hash(@_, '--short=7');
3434 sub git_get_hash {
3435 my ($project, $hash, @options) = @_;
3436 my $o_git_dir = $git_dir;
3437 my $retval = undef;
3438 $git_dir = "$projectroot/$project";
3439 if (defined(my $fd = git_cmd_pipe 'rev-parse',
3440 '--verify', '-q', @options, $hash)) {
3441 $retval = <$fd>;
3442 chomp $retval if defined $retval;
3443 close $fd;
3445 if (defined $o_git_dir) {
3446 $git_dir = $o_git_dir;
3448 return $retval;
3451 # get type of given object
3452 sub git_get_type {
3453 my $hash = shift;
3455 defined(my $fd = git_cmd_pipe "cat-file", '-t', $hash) or return;
3456 my $type = <$fd>;
3457 close $fd or return;
3458 chomp $type;
3459 return $type;
3462 # repository configuration
3463 our $config_file = '';
3464 our %config;
3466 # store multiple values for single key as anonymous array reference
3467 # single values stored directly in the hash, not as [ <value> ]
3468 sub hash_set_multi {
3469 my ($hash, $key, $value) = @_;
3471 if (!exists $hash->{$key}) {
3472 $hash->{$key} = $value;
3473 } elsif (!ref $hash->{$key}) {
3474 $hash->{$key} = [ $hash->{$key}, $value ];
3475 } else {
3476 push @{$hash->{$key}}, $value;
3480 # return hash of git project configuration
3481 # optionally limited to some section, e.g. 'gitweb'
3482 sub git_parse_project_config {
3483 my $section_regexp = shift;
3484 my %config;
3486 local $/ = "\0";
3488 defined(my $fh = git_cmd_pipe "config", '-z', '-l')
3489 or return;
3491 while (my $keyval = to_utf8(scalar <$fh>)) {
3492 chomp $keyval;
3493 my ($key, $value) = split(/\n/, $keyval, 2);
3495 hash_set_multi(\%config, $key, $value)
3496 if (!defined $section_regexp || $key =~ /^(?:$section_regexp)\./o);
3498 close $fh;
3500 return %config;
3503 # convert config value to boolean: 'true' or 'false'
3504 # no value, number > 0, 'true' and 'yes' values are true
3505 # rest of values are treated as false (never as error)
3506 sub config_to_bool {
3507 my $val = shift;
3509 return 1 if !defined $val; # section.key
3511 # strip leading and trailing whitespace
3512 $val =~ s/^\s+//;
3513 $val =~ s/\s+$//;
3515 return (($val =~ /^\d+$/ && $val) || # section.key = 1
3516 ($val =~ /^(?:true|yes)$/i)); # section.key = true
3519 # convert config value to simple decimal number
3520 # an optional value suffix of 'k', 'm', or 'g' will cause the value
3521 # to be multiplied by 1024, 1048576, or 1073741824
3522 sub config_to_int {
3523 my $val = shift;
3525 # strip leading and trailing whitespace
3526 $val =~ s/^\s+//;
3527 $val =~ s/\s+$//;
3529 if (my ($num, $unit) = ($val =~ /^([0-9]*)([kmg])$/i)) {
3530 $unit = lc($unit);
3531 # unknown unit is treated as 1
3532 return $num * ($unit eq 'g' ? 1073741824 :
3533 $unit eq 'm' ? 1048576 :
3534 $unit eq 'k' ? 1024 : 1);
3536 return $val;
3539 # convert config value to array reference, if needed
3540 sub config_to_multi {
3541 my $val = shift;
3543 return ref($val) ? $val : (defined($val) ? [ $val ] : []);
3546 sub git_get_project_config {
3547 my ($key, $type) = @_;
3549 return unless defined $git_dir;
3551 # key sanity check
3552 return unless ($key);
3553 # only subsection, if exists, is case sensitive,
3554 # and not lowercased by 'git config -z -l'
3555 if (my ($hi, $mi, $lo) = ($key =~ /^([^.]*)\.(.*)\.([^.]*)$/)) {
3556 $lo =~ s/_//g;
3557 $key = join(".", lc($hi), $mi, lc($lo));
3558 return if ($lo =~ /\W/ || $hi =~ /\W/);
3559 } else {
3560 $key = lc($key);
3561 $key =~ s/_//g;
3562 return if ($key =~ /\W/);
3564 $key =~ s/^gitweb\.//;
3566 # type sanity check
3567 if (defined $type) {
3568 $type =~ s/^--//;
3569 $type = undef
3570 unless ($type eq 'bool' || $type eq 'int');
3573 # get config
3574 if (!defined $config_file ||
3575 $config_file ne "$git_dir/config") {
3576 %config = git_parse_project_config('gitweb');
3577 $config_file = "$git_dir/config";
3580 # check if config variable (key) exists
3581 return unless exists $config{"gitweb.$key"};
3583 # ensure given type
3584 if (!defined $type) {
3585 return $config{"gitweb.$key"};
3586 } elsif ($type eq 'bool') {
3587 # backward compatibility: 'git config --bool' returns true/false
3588 return config_to_bool($config{"gitweb.$key"}) ? 'true' : 'false';
3589 } elsif ($type eq 'int') {
3590 return config_to_int($config{"gitweb.$key"});
3592 return $config{"gitweb.$key"};
3595 # get hash of given path at given ref
3596 sub git_get_hash_by_path {
3597 my $base = shift;
3598 my $path = shift || return undef;
3599 my $type = shift;
3601 $path =~ s,/+$,,;
3603 defined(my $fd = git_cmd_pipe "ls-tree", $base, "--", $path)
3604 or die_error(500, "Open git-ls-tree failed");
3605 my $line = to_utf8(scalar <$fd>);
3606 close $fd or return undef;
3608 if (!defined $line) {
3609 # there is no tree or hash given by $path at $base
3610 return undef;
3613 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'
3614 $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t/;
3615 if (defined $type && $type ne $2) {
3616 # type doesn't match
3617 return undef;
3619 return $3;
3622 # get path of entry with given hash at given tree-ish (ref)
3623 # used to get 'from' filename for combined diff (merge commit) for renames
3624 sub git_get_path_by_hash {
3625 my $base = shift || return;
3626 my $hash = shift || return;
3628 local $/ = "\0";
3630 defined(my $fd = git_cmd_pipe "ls-tree", '-r', '-t', '-z', $base)
3631 or return undef;
3632 while (my $line = to_utf8(scalar <$fd>)) {
3633 chomp $line;
3635 #'040000 tree 595596a6a9117ddba9fe379b6b012b558bac8423 gitweb'
3636 #'100644 blob e02e90f0429be0d2a69b76571101f20b8f75530f gitweb/README'
3637 if ($line =~ m/(?:[0-9]+) (?:.+) $hash\t(.+)$/) {
3638 close $fd;
3639 return $1;
3642 close $fd;
3643 return undef;
3646 ## ......................................................................
3647 ## git utility functions, directly accessing git repository
3649 # get the value of config variable either from file named as the variable
3650 # itself in the repository ($GIT_DIR/$name file), or from gitweb.$name
3651 # configuration variable in the repository config file.
3652 sub git_get_file_or_project_config {
3653 my ($path, $name) = @_;
3655 $git_dir = "$projectroot/$path";
3656 open my $fd, '<', "$git_dir/$name"
3657 or return git_get_project_config($name);
3658 my $conf = to_utf8(scalar <$fd>);
3659 close $fd;
3660 if (defined $conf) {
3661 chomp $conf;
3663 return $conf;
3666 sub git_get_project_description {
3667 my $path = shift;
3668 return git_get_file_or_project_config($path, 'description');
3671 sub git_get_project_category {
3672 my $path = shift;
3673 return git_get_file_or_project_config($path, 'category');
3677 # supported formats:
3678 # * $GIT_DIR/ctags/<tagname> file (in 'ctags' subdirectory)
3679 # - if its contents is a number, use it as tag weight,
3680 # - otherwise add a tag with weight 1
3681 # * $GIT_DIR/ctags file, each line is a tag (with weight 1)
3682 # the same value multiple times increases tag weight
3683 # * `gitweb.ctag' multi-valued repo config variable
3684 sub git_get_project_ctags {
3685 my $project = shift;
3686 my $ctags = {};
3688 $git_dir = "$projectroot/$project";
3689 if (opendir my $dh, "$git_dir/ctags") {
3690 my @files = grep { -f $_ } map { "$git_dir/ctags/$_" } readdir($dh);
3691 foreach my $tagfile (@files) {
3692 open my $ct, '<', $tagfile
3693 or next;
3694 my $val = <$ct>;
3695 chomp $val if $val;
3696 close $ct;
3698 (my $ctag = $tagfile) =~ s#.*/##;
3699 $ctag = to_utf8($ctag);
3700 if ($val =~ /^\d+$/) {
3701 $ctags->{$ctag} = $val;
3702 } else {
3703 $ctags->{$ctag} = 1;
3706 closedir $dh;
3708 } elsif (open my $fh, '<', "$git_dir/ctags") {
3709 while (my $line = to_utf8(scalar <$fh>)) {
3710 chomp $line;
3711 $ctags->{$line}++ if $line;
3713 close $fh;
3715 } else {
3716 my $taglist = config_to_multi(git_get_project_config('ctag'));
3717 foreach my $tag (@$taglist) {
3718 $ctags->{$tag}++;
3722 return $ctags;
3725 # return hash, where keys are content tags ('ctags'),
3726 # and values are sum of weights of given tag in every project
3727 sub git_gather_all_ctags {
3728 my $projects = shift;
3729 my $ctags = {};
3731 foreach my $p (@$projects) {
3732 foreach my $ct (keys %{$p->{'ctags'}}) {
3733 $ctags->{$ct} += $p->{'ctags'}->{$ct};
3737 return $ctags;
3740 sub git_populate_project_tagcloud {
3741 my ($ctags, $action) = @_;
3743 # First, merge different-cased tags; tags vote on casing
3744 my %ctags_lc;
3745 foreach (keys %$ctags) {
3746 $ctags_lc{lc $_}->{count} += $ctags->{$_};
3747 if (not $ctags_lc{lc $_}->{topcount}
3748 or $ctags_lc{lc $_}->{topcount} < $ctags->{$_}) {
3749 $ctags_lc{lc $_}->{topcount} = $ctags->{$_};
3750 $ctags_lc{lc $_}->{topname} = $_;
3754 my $cloud;
3755 my $matched = $input_params{'ctag_filter'};
3756 if (eval { require HTML::TagCloud; 1; }) {
3757 $cloud = HTML::TagCloud->new;
3758 foreach my $ctag (sort keys %ctags_lc) {
3759 # Pad the title with spaces so that the cloud looks
3760 # less crammed.
3761 my $title = esc_html($ctags_lc{$ctag}->{topname});
3762 $title =~ s/ /&#160;/g;
3763 $title =~ s/^/&#160;/g;
3764 $title =~ s/$/&#160;/g;
3765 if (defined $matched && $matched eq $ctag) {
3766 $title = qq(<span class="match">$title</span>);
3768 $cloud->add($title, href(-replay=>1, action=>$action, ctag_filter=>$ctag),
3769 $ctags_lc{$ctag}->{count});
3771 } else {
3772 $cloud = {};
3773 foreach my $ctag (keys %ctags_lc) {
3774 my $title = esc_html($ctags_lc{$ctag}->{topname}, -nbsp=>1);
3775 if (defined $matched && $matched eq $ctag) {
3776 $title = qq(<span class="match">$title</span>);
3778 $cloud->{$ctag}{count} = $ctags_lc{$ctag}->{count};
3779 $cloud->{$ctag}{ctag} =
3780 $cgi->a({-href=>href(-replay=>1, action=>$action, ctag_filter=>$ctag)}, $title);
3783 return $cloud;
3786 sub git_show_project_tagcloud {
3787 my ($cloud, $count) = @_;
3788 if (ref $cloud eq 'HTML::TagCloud') {
3789 return $cloud->html_and_css($count);
3790 } else {
3791 my @tags = sort { $cloud->{$a}->{'count'} <=> $cloud->{$b}->{'count'} } keys %$cloud;
3792 return
3793 '<div id="htmltagcloud"'.($project ? '' : ' align="center"').'>' .
3794 join (', ', map {
3795 $cloud->{$_}->{'ctag'}
3796 } splice(@tags, 0, $count)) .
3797 '</div>';
3801 sub git_get_project_url_list {
3802 my $path = shift;
3804 $git_dir = "$projectroot/$path";
3805 open my $fd, '<', "$git_dir/cloneurl"
3806 or return wantarray ?
3807 @{ config_to_multi(git_get_project_config('url')) } :
3808 config_to_multi(git_get_project_config('url'));
3809 my @git_project_url_list = map { chomp; to_utf8($_) } <$fd>;
3810 close $fd;
3812 return wantarray ? @git_project_url_list : \@git_project_url_list;
3815 sub git_get_projects_list {
3816 my $filter = shift || '';
3817 my $paranoid = shift;
3818 my @list;
3820 if (-d $projects_list) {
3821 # search in directory
3822 my $dir = $projects_list;
3823 # remove the trailing "/"
3824 $dir =~ s!/+$!!;
3825 my $pfxlen = length("$dir");
3826 my $pfxdepth = ($dir =~ tr!/!!);
3827 # when filtering, search only given subdirectory
3828 if ($filter && !$paranoid) {
3829 $dir .= "/$filter";
3830 $dir =~ s!/+$!!;
3833 File::Find::find({
3834 follow_fast => 1, # follow symbolic links
3835 follow_skip => 2, # ignore duplicates
3836 dangling_symlinks => 0, # ignore dangling symlinks, silently
3837 wanted => sub {
3838 # global variables
3839 our $project_maxdepth;
3840 our $projectroot;
3841 # skip project-list toplevel, if we get it.
3842 return if (m!^[/.]$!);
3843 # only directories can be git repositories
3844 return unless (-d $_);
3845 # don't traverse too deep (Find is super slow on os x)
3846 # $project_maxdepth excludes depth of $projectroot
3847 if (($File::Find::name =~ tr!/!!) - $pfxdepth > $project_maxdepth) {
3848 $File::Find::prune = 1;
3849 return;
3852 my $path = substr($File::Find::name, $pfxlen + 1);
3853 # paranoidly only filter here
3854 if ($paranoid && $filter && $path !~ m!^\Q$filter\E/!) {
3855 next;
3857 # we check related file in $projectroot
3858 if (check_export_ok("$projectroot/$path")) {
3859 push @list, { path => $path };
3860 $File::Find::prune = 1;
3863 }, "$dir");
3865 } elsif (-f $projects_list) {
3866 # read from file(url-encoded):
3867 # 'git%2Fgit.git Linus+Torvalds'
3868 # 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin'
3869 # 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman'
3870 open my $fd, '<', $projects_list or return;
3871 PROJECT:
3872 while (my $line = <$fd>) {
3873 chomp $line;
3874 my ($path, $owner) = split ' ', $line;
3875 $path = unescape($path);
3876 $owner = unescape($owner);
3877 if (!defined $path) {
3878 next;
3880 # if $filter is rpovided, check if $path begins with $filter
3881 if ($filter && $path !~ m!^\Q$filter\E/!) {
3882 next;
3884 if (check_export_ok("$projectroot/$path")) {
3885 my $pr = {
3886 path => $path
3888 if ($owner) {
3889 $pr->{'owner'} = to_utf8($owner);
3891 push @list, $pr;
3894 close $fd;
3896 return @list;
3899 # written with help of Tree::Trie module (Perl Artistic License, GPL compatibile)
3900 # as side effects it sets 'forks' field to list of forks for forked projects
3901 sub filter_forks_from_projects_list {
3902 my $projects = shift;
3904 my %trie; # prefix tree of directories (path components)
3905 # generate trie out of those directories that might contain forks
3906 foreach my $pr (@$projects) {
3907 my $path = $pr->{'path'};
3908 $path =~ s/\.git$//; # forks of 'repo.git' are in 'repo/' directory
3909 next if ($path =~ m!/$!); # skip non-bare repositories, e.g. 'repo/.git'
3910 next unless ($path); # skip '.git' repository: tests, git-instaweb
3911 next unless (-d "$projectroot/$path"); # containing directory exists
3912 $pr->{'forks'} = []; # there can be 0 or more forks of project
3914 # add to trie
3915 my @dirs = split('/', $path);
3916 # walk the trie, until either runs out of components or out of trie
3917 my $ref = \%trie;
3918 while (scalar @dirs &&
3919 exists($ref->{$dirs[0]})) {
3920 $ref = $ref->{shift @dirs};
3922 # create rest of trie structure from rest of components
3923 foreach my $dir (@dirs) {
3924 $ref = $ref->{$dir} = {};
3926 # create end marker, store $pr as a data
3927 $ref->{''} = $pr if (!exists $ref->{''});
3930 # filter out forks, by finding shortest prefix match for paths
3931 my @filtered;
3932 PROJECT:
3933 foreach my $pr (@$projects) {
3934 # trie lookup
3935 my $ref = \%trie;
3936 DIR:
3937 foreach my $dir (split('/', $pr->{'path'})) {
3938 if (exists $ref->{''}) {
3939 # found [shortest] prefix, is a fork - skip it
3940 push @{$ref->{''}{'forks'}}, $pr;
3941 next PROJECT;
3943 if (!exists $ref->{$dir}) {
3944 # not in trie, cannot have prefix, not a fork
3945 push @filtered, $pr;
3946 next PROJECT;
3948 # If the dir is there, we just walk one step down the trie.
3949 $ref = $ref->{$dir};
3951 # we ran out of trie
3952 # (shouldn't happen: it's either no match, or end marker)
3953 push @filtered, $pr;
3956 return @filtered;
3959 # note: fill_project_list_info must be run first,
3960 # for 'descr_long' and 'ctags' to be filled
3961 sub search_projects_list {
3962 my ($projlist, %opts) = @_;
3963 my $tagfilter = $opts{'tagfilter'};
3964 my $search_re = $opts{'search_regexp'};
3966 return @$projlist
3967 unless ($tagfilter || $search_re);
3969 # searching projects require filling to be run before it;
3970 fill_project_list_info($projlist,
3971 $tagfilter ? 'ctags' : (),
3972 $search_re ? ('path', 'descr') : ());
3973 my @projects;
3974 PROJECT:
3975 foreach my $pr (@$projlist) {
3977 if ($tagfilter) {
3978 next unless ref($pr->{'ctags'}) eq 'HASH';
3979 next unless
3980 grep { lc($_) eq lc($tagfilter) } keys %{$pr->{'ctags'}};
3983 if ($search_re) {
3984 my $path = $pr->{'path'};
3985 $path =~ s/\.git$//; # should not be included in search
3986 next unless
3987 $path =~ /$search_re/ ||
3988 $pr->{'descr_long'} =~ /$search_re/;
3991 push @projects, $pr;
3994 return @projects;
3997 our $gitweb_project_owner = undef;
3998 sub git_get_project_list_from_file {
4000 return if (defined $gitweb_project_owner);
4002 $gitweb_project_owner = {};
4003 # read from file (url-encoded):
4004 # 'git%2Fgit.git Linus+Torvalds'
4005 # 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin'
4006 # 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman'
4007 if (-f $projects_list) {
4008 open(my $fd, '<', $projects_list);
4009 while (my $line = <$fd>) {
4010 chomp $line;
4011 my ($pr, $ow) = split ' ', $line;
4012 $pr = unescape($pr);
4013 $ow = unescape($ow);
4014 $gitweb_project_owner->{$pr} = to_utf8($ow);
4016 close $fd;
4020 sub git_get_project_owner {
4021 my $proj = shift;
4022 my $owner;
4024 return undef unless $proj;
4025 $git_dir = "$projectroot/$proj";
4027 if (defined $project && $proj eq $project) {
4028 $owner = git_get_project_config('owner');
4030 if (!defined $owner && !defined $gitweb_project_owner) {
4031 git_get_project_list_from_file();
4033 if (!defined $owner && exists $gitweb_project_owner->{$proj}) {
4034 $owner = $gitweb_project_owner->{$proj};
4036 if (!defined $owner && (!defined $project || $proj ne $project)) {
4037 $owner = git_get_project_config('owner');
4039 if (!defined $owner) {
4040 $owner = get_file_owner("$git_dir");
4043 return $owner;
4046 sub parse_activity_date {
4047 my $dstr = shift;
4049 if ($dstr =~ /^\s*([-+]?\d+)(?:\s+([-+]\d{4}))?\s*$/) {
4050 # Unix timestamp
4051 return 0 + $1;
4053 if ($dstr =~ /^\s*(\d{4})-(\d{2})-(\d{2})[Tt _](\d{1,2}):(\d{2}):(\d{2})(?:[ _]?([Zz]|(?:[-+]\d{1,2}:?\d{2})))?\s*$/) {
4054 my ($Y,$m,$d,$H,$M,$S,$z) = ($1,$2,$3,$4,$5,$6,$7||'');
4055 my $seconds = timegm(0+$S, 0+$M, 0+$H, 0+$d, $m-1, $Y-1900);
4056 defined($z) && $z ne '' or $z = 'Z';
4057 $z =~ s/://;
4058 substr($z,1,0) = '0' if length($z) == 4;
4059 my $off = 0;
4060 if (uc($z) ne 'Z') {
4061 $off = 60 * (60 * (0+substr($z,1,2)) + (0+substr($z,3,2)));
4062 $off = -$off if substr($z,0,1) eq '-';
4064 return $seconds - $off;
4066 return undef;
4069 # If $quick is true only look at $lastactivity_file
4070 sub git_get_last_activity {
4071 my ($path, $quick) = @_;
4072 my $fd;
4074 $git_dir = "$projectroot/$path";
4075 if ($lastactivity_file && open($fd, "<", "$git_dir/$lastactivity_file")) {
4076 my $activity = <$fd>;
4077 close $fd;
4078 return (undef) unless defined $activity;
4079 chomp $activity;
4080 return (undef) if $activity eq '';
4081 if (my $timestamp = parse_activity_date($activity)) {
4082 return ($timestamp);
4085 return (undef) if $quick;
4086 defined($fd = git_cmd_pipe 'for-each-ref',
4087 '--format=%(committer)',
4088 '--sort=-committerdate',
4089 '--count=1',
4090 map { "refs/$_" } get_branch_refs ()) or return;
4091 my $most_recent = <$fd>;
4092 close $fd or return (undef);
4093 if (defined $most_recent &&
4094 $most_recent =~ / (\d+) [-+][01]\d\d\d$/) {
4095 my $timestamp = $1;
4096 return ($timestamp);
4098 return (undef);
4101 # Implementation note: when a single remote is wanted, we cannot use 'git
4102 # remote show -n' because that command always work (assuming it's a remote URL
4103 # if it's not defined), and we cannot use 'git remote show' because that would
4104 # try to make a network roundtrip. So the only way to find if that particular
4105 # remote is defined is to walk the list provided by 'git remote -v' and stop if
4106 # and when we find what we want.
4107 sub git_get_remotes_list {
4108 my $wanted = shift;
4109 my %remotes = ();
4111 my $fd = git_cmd_pipe 'remote', '-v';
4112 return unless $fd;
4113 while (my $remote = to_utf8(scalar <$fd>)) {
4114 chomp $remote;
4115 $remote =~ s!\t(.*?)\s+\((\w+)\)$!!;
4116 next if $wanted and not $remote eq $wanted;
4117 my ($url, $key) = ($1, $2);
4119 $remotes{$remote} ||= { 'heads' => [] };
4120 $remotes{$remote}{$key} = $url;
4122 close $fd or return;
4123 return wantarray ? %remotes : \%remotes;
4126 # Takes a hash of remotes as first parameter and fills it by adding the
4127 # available remote heads for each of the indicated remotes.
4128 sub fill_remote_heads {
4129 my $remotes = shift;
4130 my @heads = map { "remotes/$_" } keys %$remotes;
4131 my @remoteheads = git_get_heads_list(undef, @heads);
4132 foreach my $remote (keys %$remotes) {
4133 $remotes->{$remote}{'heads'} = [ grep {
4134 $_->{'name'} =~ s!^$remote/!!
4135 } @remoteheads ];
4139 sub git_get_references {
4140 my $type = shift || "";
4141 my %refs;
4142 # 5dc01c595e6c6ec9ccda4f6f69c131c0dd945f8c refs/tags/v2.6.11
4143 # c39ae07f393806ccf406ef966e9a15afc43cc36a refs/tags/v2.6.11^{}
4144 defined(my $fd = git_cmd_pipe "show-ref", "--dereference",
4145 ($type ? ("--", "refs/$type") : ())) # use -- <pattern> if $type
4146 or return;
4148 while (my $line = to_utf8(scalar <$fd>)) {
4149 chomp $line;
4150 if ($line =~ m!^([0-9a-fA-F]{40})\srefs/($type.*)$!) {
4151 if (defined $refs{$1}) {
4152 push @{$refs{$1}}, $2;
4153 } else {
4154 $refs{$1} = [ $2 ];
4158 close $fd or return;
4159 return \%refs;
4162 sub git_get_rev_name_tags {
4163 my $hash = shift || return undef;
4165 defined(my $fd = git_cmd_pipe "name-rev", "--tags", $hash)
4166 or return;
4167 my $name_rev = to_utf8(scalar <$fd>);
4168 close $fd;
4170 if ($name_rev =~ m|^$hash tags/(.*)$|) {
4171 return $1;
4172 } else {
4173 # catches also '$hash undefined' output
4174 return undef;
4178 ## ----------------------------------------------------------------------
4179 ## parse to hash functions
4181 sub parse_date {
4182 my $epoch = shift;
4183 my $tz = shift || "-0000";
4185 my %date;
4186 my @months = ("Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec");
4187 my @days = ("Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat");
4188 my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($epoch);
4189 $date{'hour'} = $hour;
4190 $date{'minute'} = $min;
4191 $date{'mday'} = $mday;
4192 $date{'day'} = $days[$wday];
4193 $date{'month'} = $months[$mon];
4194 $date{'rfc2822'} = sprintf "%s, %d %s %4d %02d:%02d:%02d +0000",
4195 $days[$wday], $mday, $months[$mon], 1900+$year, $hour ,$min, $sec;
4196 $date{'mday-time'} = sprintf "%d %s %02d:%02d",
4197 $mday, $months[$mon], $hour ,$min;
4198 $date{'iso-8601'} = sprintf "%04d-%02d-%02dT%02d:%02d:%02dZ",
4199 1900+$year, 1+$mon, $mday, $hour ,$min, $sec;
4201 my ($tz_sign, $tz_hour, $tz_min) =
4202 ($tz =~ m/^([-+])(\d\d)(\d\d)$/);
4203 $tz_sign = ($tz_sign eq '-' ? -1 : +1);
4204 my $local = $epoch + $tz_sign*((($tz_hour*60) + $tz_min)*60);
4205 ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($local);
4206 $date{'hour_local'} = $hour;
4207 $date{'minute_local'} = $min;
4208 $date{'mday_local'} = $mday;
4209 $date{'tz_local'} = $tz;
4210 $date{'iso-tz'} = sprintf("%04d-%02d-%02d %02d:%02d:%02d %s",
4211 1900+$year, $mon+1, $mday,
4212 $hour, $min, $sec, $tz);
4213 return %date;
4216 sub parse_file_date {
4217 my $file = shift;
4218 my $mtime = (stat("$projectroot/$project/$file"))[9];
4219 return () unless defined $mtime;
4220 my $tzoffset = timegm((localtime($mtime))[0..5]) - $mtime;
4221 my $tzstring = '+';
4222 if ($tzoffset <= 0) {
4223 $tzstring = '-';
4224 $tzoffset *= -1;
4226 $tzoffset = int($tzoffset/60);
4227 $tzstring .= sprintf("%02d%02d", int($tzoffset/60), $tzoffset%60);
4228 return parse_date($mtime, $tzstring);
4231 sub parse_tag {
4232 my $tag_id = shift;
4233 my %tag;
4234 my @comment;
4236 defined(my $fd = git_cmd_pipe "cat-file", "tag", $tag_id) or return;
4237 $tag{'id'} = $tag_id;
4238 while (my $line = to_utf8(scalar <$fd>)) {
4239 chomp $line;
4240 if ($line =~ m/^object ([0-9a-fA-F]{40})$/) {
4241 $tag{'object'} = $1;
4242 } elsif ($line =~ m/^type (.+)$/) {
4243 $tag{'type'} = $1;
4244 } elsif ($line =~ m/^tag (.+)$/) {
4245 $tag{'name'} = $1;
4246 } elsif ($line =~ m/^tagger (.*) ([0-9]+) (.*)$/) {
4247 $tag{'author'} = $1;
4248 $tag{'author_epoch'} = $2;
4249 $tag{'author_tz'} = $3;
4250 if ($tag{'author'} =~ m/^([^<]+) <([^>]*)>/) {
4251 $tag{'author_name'} = $1;
4252 $tag{'author_email'} = $2;
4253 } else {
4254 $tag{'author_name'} = $tag{'author'};
4256 } elsif ($line =~ m/--BEGIN/) {
4257 push @comment, $line;
4258 last;
4259 } elsif ($line eq "") {
4260 last;
4263 push @comment, map(to_utf8($_), <$fd>);
4264 $tag{'comment'} = \@comment;
4265 close $fd or return;
4266 if (!defined $tag{'name'}) {
4267 return
4269 return %tag
4272 sub parse_commit_text {
4273 my ($commit_text, $withparents) = @_;
4274 my @commit_lines = split '\n', $commit_text;
4275 my %co;
4277 pop @commit_lines; # Remove '\0'
4279 if (! @commit_lines) {
4280 return;
4283 my $header = shift @commit_lines;
4284 if ($header !~ m/^[0-9a-fA-F]{40}/) {
4285 return;
4287 ($co{'id'}, my @parents) = split ' ', $header;
4288 while (my $line = shift @commit_lines) {
4289 last if $line eq "\n";
4290 if ($line =~ m/^tree ([0-9a-fA-F]{40})$/) {
4291 $co{'tree'} = $1;
4292 } elsif ((!defined $withparents) && ($line =~ m/^parent ([0-9a-fA-F]{40})$/)) {
4293 push @parents, $1;
4294 } elsif ($line =~ m/^author (.*) ([0-9]+) (.*)$/) {
4295 $co{'author'} = to_utf8($1);
4296 $co{'author_epoch'} = $2;
4297 $co{'author_tz'} = $3;
4298 if ($co{'author'} =~ m/^([^<]+) <([^>]*)>/) {
4299 $co{'author_name'} = $1;
4300 $co{'author_email'} = $2;
4301 } else {
4302 $co{'author_name'} = $co{'author'};
4304 } elsif ($line =~ m/^committer (.*) ([0-9]+) (.*)$/) {
4305 $co{'committer'} = to_utf8($1);
4306 $co{'committer_epoch'} = $2;
4307 $co{'committer_tz'} = $3;
4308 if ($co{'committer'} =~ m/^([^<]+) <([^>]*)>/) {
4309 $co{'committer_name'} = $1;
4310 $co{'committer_email'} = $2;
4311 } else {
4312 $co{'committer_name'} = $co{'committer'};
4316 if (!defined $co{'tree'}) {
4317 return;
4319 $co{'parents'} = \@parents;
4320 $co{'parent'} = $parents[0];
4322 @commit_lines = map to_utf8($_), @commit_lines;
4323 foreach my $title (@commit_lines) {
4324 $title =~ s/^ //;
4325 if ($title ne "") {
4326 $co{'title'} = chop_str($title, 80, 5);
4327 # remove leading stuff of merges to make the interesting part visible
4328 if (length($title) > 50) {
4329 $title =~ s/^Automatic //;
4330 $title =~ s/^merge (of|with) /Merge ... /i;
4331 if (length($title) > 50) {
4332 $title =~ s/(http|rsync):\/\///;
4334 if (length($title) > 50) {
4335 $title =~ s/(master|www|rsync)\.//;
4337 if (length($title) > 50) {
4338 $title =~ s/kernel.org:?//;
4340 if (length($title) > 50) {
4341 $title =~ s/\/pub\/scm//;
4344 $co{'title_short'} = chop_str($title, 50, 5);
4345 last;
4348 if (! defined $co{'title'} || $co{'title'} eq "") {
4349 $co{'title'} = $co{'title_short'} = '(no commit message)';
4351 # remove added spaces
4352 foreach my $line (@commit_lines) {
4353 $line =~ s/^ //;
4355 $co{'comment'} = \@commit_lines;
4357 my $age_epoch = $co{'committer_epoch'};
4358 $co{'age_epoch'} = $age_epoch;
4359 my $time_now = time;
4360 $co{'age_string'} = age_string($age_epoch, $time_now);
4361 $co{'age_string_date'} = age_string_date($age_epoch, $time_now);
4362 $co{'age_string_age'} = age_string_age($age_epoch, $time_now);
4363 return %co;
4366 sub parse_commit {
4367 my ($commit_id) = @_;
4368 my %co;
4370 local $/ = "\0";
4372 defined(my $fd = git_cmd_pipe "rev-list",
4373 "--parents",
4374 "--header",
4375 "--max-count=1",
4376 $commit_id,
4377 "--")
4378 or die_error(500, "Open git-rev-list failed");
4379 %co = parse_commit_text(<$fd>, 1);
4380 close $fd;
4382 return %co;
4385 sub parse_commits {
4386 my ($commit_id, $maxcount, $skip, $filename, @args) = @_;
4387 my @cos;
4389 $maxcount ||= 1;
4390 $skip ||= 0;
4392 local $/ = "\0";
4394 defined(my $fd = git_cmd_pipe "rev-list",
4395 "--header",
4396 @args,
4397 ("--max-count=" . $maxcount),
4398 ("--skip=" . $skip),
4399 @extra_options,
4400 $commit_id,
4401 "--",
4402 ($filename ? ($filename) : ()))
4403 or die_error(500, "Open git-rev-list failed");
4404 while (my $line = <$fd>) {
4405 my %co = parse_commit_text($line);
4406 push @cos, \%co;
4408 close $fd;
4410 return wantarray ? @cos : \@cos;
4413 # parse line of git-diff-tree "raw" output
4414 sub parse_difftree_raw_line {
4415 my $line = shift;
4416 my %res;
4418 # ':100644 100644 03b218260e99b78c6df0ed378e59ed9205ccc96d 3b93d5e7cc7f7dd4ebed13a5cc1a4ad976fc94d8 M ls-files.c'
4419 # ':100644 100644 7f9281985086971d3877aca27704f2aaf9c448ce bc190ebc71bbd923f2b728e505408f5e54bd073a M rev-tree.c'
4420 if ($line =~ m/^:([0-7]{6}) ([0-7]{6}) ([0-9a-fA-F]{40}) ([0-9a-fA-F]{40}) (.)([0-9]{0,3})\t(.*)$/) {
4421 $res{'from_mode'} = $1;
4422 $res{'to_mode'} = $2;
4423 $res{'from_id'} = $3;
4424 $res{'to_id'} = $4;
4425 $res{'status'} = $5;
4426 $res{'similarity'} = $6;
4427 if ($res{'status'} eq 'R' || $res{'status'} eq 'C') { # renamed or copied
4428 ($res{'from_file'}, $res{'to_file'}) = map { unquote($_) } split("\t", $7);
4429 } else {
4430 $res{'from_file'} = $res{'to_file'} = $res{'file'} = unquote($7);
4433 # '::100755 100755 100755 60e79ca1b01bc8b057abe17ddab484699a7f5fdb 94067cc5f73388f33722d52ae02f44692bc07490 94067cc5f73388f33722d52ae02f44692bc07490 MR git-gui/git-gui.sh'
4434 # combined diff (for merge commit)
4435 elsif ($line =~ s/^(::+)((?:[0-7]{6} )+)((?:[0-9a-fA-F]{40} )+)([a-zA-Z]+)\t(.*)$//) {
4436 $res{'nparents'} = length($1);
4437 $res{'from_mode'} = [ split(' ', $2) ];
4438 $res{'to_mode'} = pop @{$res{'from_mode'}};
4439 $res{'from_id'} = [ split(' ', $3) ];
4440 $res{'to_id'} = pop @{$res{'from_id'}};
4441 $res{'status'} = [ split('', $4) ];
4442 $res{'to_file'} = unquote($5);
4444 # 'c512b523472485aef4fff9e57b229d9d243c967f'
4445 elsif ($line =~ m/^([0-9a-fA-F]{40})$/) {
4446 $res{'commit'} = $1;
4449 return wantarray ? %res : \%res;
4452 # wrapper: return parsed line of git-diff-tree "raw" output
4453 # (the argument might be raw line, or parsed info)
4454 sub parsed_difftree_line {
4455 my $line_or_ref = shift;
4457 if (ref($line_or_ref) eq "HASH") {
4458 # pre-parsed (or generated by hand)
4459 return $line_or_ref;
4460 } else {
4461 return parse_difftree_raw_line($line_or_ref);
4465 # parse line of git-ls-tree output
4466 sub parse_ls_tree_line {
4467 my $line = shift;
4468 my %opts = @_;
4469 my %res;
4471 if ($opts{'-l'}) {
4472 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa 16717 panic.c'
4473 $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40}) +(-|[0-9]+)\t(.+)$/s;
4475 $res{'mode'} = $1;
4476 $res{'type'} = $2;
4477 $res{'hash'} = $3;
4478 $res{'size'} = $4;
4479 if ($opts{'-z'}) {
4480 $res{'name'} = $5;
4481 } else {
4482 $res{'name'} = unquote($5);
4484 } else {
4485 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'
4486 $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t(.+)$/s;
4488 $res{'mode'} = $1;
4489 $res{'type'} = $2;
4490 $res{'hash'} = $3;
4491 if ($opts{'-z'}) {
4492 $res{'name'} = $4;
4493 } else {
4494 $res{'name'} = unquote($4);
4498 return wantarray ? %res : \%res;
4501 # generates _two_ hashes, references to which are passed as 2 and 3 argument
4502 sub parse_from_to_diffinfo {
4503 my ($diffinfo, $from, $to, @parents) = @_;
4505 if ($diffinfo->{'nparents'}) {
4506 # combined diff
4507 $from->{'file'} = [];
4508 $from->{'href'} = [];
4509 fill_from_file_info($diffinfo, @parents)
4510 unless exists $diffinfo->{'from_file'};
4511 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
4512 $from->{'file'}[$i] =
4513 defined $diffinfo->{'from_file'}[$i] ?
4514 $diffinfo->{'from_file'}[$i] :
4515 $diffinfo->{'to_file'};
4516 if ($diffinfo->{'status'}[$i] ne "A") { # not new (added) file
4517 $from->{'href'}[$i] = href(action=>"blob",
4518 hash_base=>$parents[$i],
4519 hash=>$diffinfo->{'from_id'}[$i],
4520 file_name=>$from->{'file'}[$i]);
4521 } else {
4522 $from->{'href'}[$i] = undef;
4525 } else {
4526 # ordinary (not combined) diff
4527 $from->{'file'} = $diffinfo->{'from_file'};
4528 if ($diffinfo->{'status'} ne "A") { # not new (added) file
4529 $from->{'href'} = href(action=>"blob", hash_base=>$hash_parent,
4530 hash=>$diffinfo->{'from_id'},
4531 file_name=>$from->{'file'});
4532 } else {
4533 delete $from->{'href'};
4537 $to->{'file'} = $diffinfo->{'to_file'};
4538 if (!is_deleted($diffinfo)) { # file exists in result
4539 $to->{'href'} = href(action=>"blob", hash_base=>$hash,
4540 hash=>$diffinfo->{'to_id'},
4541 file_name=>$to->{'file'});
4542 } else {
4543 delete $to->{'href'};
4547 ## ......................................................................
4548 ## parse to array of hashes functions
4550 sub git_get_heads_list {
4551 my ($limit, @classes) = @_;
4552 @classes = get_branch_refs() unless @classes;
4553 my @patterns = map { "refs/$_" } @classes;
4554 my @headslist;
4556 defined(my $fd = git_cmd_pipe 'for-each-ref',
4557 ($limit ? '--count='.($limit+1) : ()), '--sort=-committerdate',
4558 '--format=%(objectname) %(refname) %(subject)%00%(committer)',
4559 @patterns)
4560 or return;
4561 while (my $line = to_utf8(scalar <$fd>)) {
4562 my %ref_item;
4564 chomp $line;
4565 my ($refinfo, $committerinfo) = split(/\0/, $line);
4566 my ($hash, $name, $title) = split(' ', $refinfo, 3);
4567 my ($committer, $epoch, $tz) =
4568 ($committerinfo =~ /^(.*) ([0-9]+) (.*)$/);
4569 $ref_item{'fullname'} = $name;
4570 my $strip_refs = join '|', map { quotemeta } get_branch_refs();
4571 $name =~ s!^refs/($strip_refs|remotes)/!!;
4572 $ref_item{'name'} = $name;
4573 # for refs neither in 'heads' nor 'remotes' we want to
4574 # show their ref dir
4575 my $ref_dir = (defined $1) ? $1 : '';
4576 if ($ref_dir ne '' and $ref_dir ne 'heads' and $ref_dir ne 'remotes') {
4577 $ref_item{'name'} .= ' (' . $ref_dir . ')';
4580 $ref_item{'id'} = $hash;
4581 $ref_item{'title'} = $title || '(no commit message)';
4582 $ref_item{'epoch'} = $epoch;
4583 if ($epoch) {
4584 $ref_item{'age'} = age_string($ref_item{'epoch'});
4585 } else {
4586 $ref_item{'age'} = "unknown";
4589 push @headslist, \%ref_item;
4591 close $fd;
4593 return wantarray ? @headslist : \@headslist;
4596 sub git_get_tags_list {
4597 my $limit = shift;
4598 my @tagslist;
4599 my $all = shift || 0;
4600 my $order = shift || $default_refs_order;
4601 my $sortkey = $all && $order eq 'name' ? 'refname' : '-creatordate';
4603 defined(my $fd = git_cmd_pipe 'for-each-ref',
4604 ($limit ? '--count='.($limit+1) : ()), "--sort=$sortkey",
4605 '--format=%(objectname) %(objecttype) %(refname) '.
4606 '%(*objectname) %(*objecttype) %(subject)%00%(creator)',
4607 ($all ? 'refs' : 'refs/tags'))
4608 or return;
4609 while (my $line = to_utf8(scalar <$fd>)) {
4610 my %ref_item;
4612 chomp $line;
4613 my ($refinfo, $creatorinfo) = split(/\0/, $line);
4614 my ($id, $type, $name, $refid, $reftype, $title) = split(' ', $refinfo, 6);
4615 my ($creator, $epoch, $tz) =
4616 ($creatorinfo =~ /^(.*) ([0-9]+) (.*)$/);
4617 $ref_item{'fullname'} = $name;
4618 $name =~ s!^refs/!! if $all;
4619 $name =~ s!^refs/tags/!! unless $all;
4621 $ref_item{'type'} = $type;
4622 $ref_item{'id'} = $id;
4623 $ref_item{'name'} = $name;
4624 if ($type eq "tag") {
4625 $ref_item{'subject'} = $title;
4626 $ref_item{'reftype'} = $reftype;
4627 $ref_item{'refid'} = $refid;
4628 } else {
4629 $ref_item{'reftype'} = $type;
4630 $ref_item{'refid'} = $id;
4633 if ($type eq "tag" || $type eq "commit") {
4634 $ref_item{'epoch'} = $epoch;
4635 if ($epoch) {
4636 $ref_item{'age'} = age_string($ref_item{'epoch'});
4637 } else {
4638 $ref_item{'age'} = "unknown";
4642 push @tagslist, \%ref_item;
4644 close $fd;
4646 return wantarray ? @tagslist : \@tagslist;
4649 ## ----------------------------------------------------------------------
4650 ## filesystem-related functions
4652 sub get_file_owner {
4653 my $path = shift;
4655 my ($dev, $ino, $mode, $nlink, $st_uid, $st_gid, $rdev, $size) = stat($path);
4656 my ($name, $passwd, $uid, $gid, $quota, $comment, $gcos, $dir, $shell) = getpwuid($st_uid);
4657 if (!defined $gcos) {
4658 return undef;
4660 my $owner = $gcos;
4661 $owner =~ s/[,;].*$//;
4662 return to_utf8($owner);
4665 # assume that file exists
4666 sub insert_file {
4667 my $filename = shift;
4669 open my $fd, '<', $filename;
4670 while (<$fd>) {
4671 print to_utf8($_);
4673 close $fd;
4676 # return undef on failure
4677 sub collect_output {
4678 defined(my $fd = cmd_pipe @_) or return undef;
4679 if (eof $fd) {
4680 close $fd;
4681 return undef;
4683 my $result = join('', map({ to_utf8($_) } <$fd>));
4684 close $fd or return undef;
4685 return $result;
4688 # return undef on failure
4689 # return '' if only comments
4690 sub collect_html_file {
4691 my $filename = shift;
4693 open my $fd, '<', $filename or return undef;
4694 my $result = join('', map({ to_utf8($_) } <$fd>));
4695 close $fd or return undef;
4696 return undef unless defined($result);
4697 my $test = $result;
4698 $test =~ s/<!--(?:[^-]|(?:-(?!-)))*-->//gs;
4699 $test =~ s/\s+//s;
4700 return $test eq '' ? '' : $result;
4703 ## ......................................................................
4704 ## mimetype related functions
4706 sub mimetype_guess_file {
4707 my $filename = shift;
4708 my $mimemap = shift;
4709 my $rawmode = shift;
4710 -r $mimemap or return undef;
4712 my %mimemap;
4713 open(my $mh, '<', $mimemap) or return undef;
4714 while (<$mh>) {
4715 next if m/^#/; # skip comments
4716 my ($mimetype, @exts) = split(/\s+/);
4717 foreach my $ext (@exts) {
4718 $mimemap{$ext} = $mimetype;
4721 close($mh);
4723 my ($ext, $ans);
4724 $ext = $1 if $filename =~ /\.([^.]*)$/;
4725 $ans = $mimemap{$ext} if $ext;
4726 if (defined $ans) {
4727 my $l = lc($ans);
4728 $ans = 'text/html' if $l eq 'application/xhtml+xml';
4729 if (!$rawmode) {
4730 $ans = 'text/xml' if $l =~ m!^application/[^\s:;,=]+\+xml$! ||
4731 $l eq 'image/svg+xml' ||
4732 $l eq 'application/xml-dtd' ||
4733 $l eq 'application/xml-external-parsed-entity';
4736 return $ans;
4739 sub mimetype_guess {
4740 my $filename = shift;
4741 my $rawmode = shift;
4742 my $mime;
4743 $filename =~ /\./ or return undef;
4745 if ($mimetypes_file) {
4746 my $file = $mimetypes_file;
4747 if ($file !~ m!^/!) { # if it is relative path
4748 # it is relative to project
4749 $file = "$projectroot/$project/$file";
4751 $mime = mimetype_guess_file($filename, $file, $rawmode);
4753 $mime ||= mimetype_guess_file($filename, '/etc/mime.types', $rawmode);
4754 return $mime;
4757 sub blob_mimetype {
4758 my $fd = shift;
4759 my $filename = shift;
4760 my $rawmode = shift;
4761 my $mime;
4763 # The -T/-B file operators produce the wrong result unless a perlio
4764 # layer is present when the file handle is a pipe that delivers less
4765 # than 512 bytes of data before reaching EOF.
4767 # If we are running in a Perl that uses the stdio layer rather than the
4768 # unix+perlio layers we will end up adding a perlio layer on top of the
4769 # stdio layer and get a second level of buffering. This is harmless
4770 # and it makes the -T/-B file operators work properly in all cases.
4772 binmode $fd, ":perlio" or die_error(500, "Adding perlio layer failed")
4773 unless grep /^perlio$/, PerlIO::get_layers($fd);
4775 $mime = mimetype_guess($filename, $rawmode) if defined $filename;
4777 if (!$mime && $filename) {
4778 if ($filename =~ m/\.html?$/i) {
4779 $mime = 'text/html';
4780 } elsif ($filename =~ m/\.xht(?:ml)?$/i) {
4781 $mime = 'text/html';
4782 } elsif ($filename =~ m/\.te?xt?$/i) {
4783 $mime = 'text/plain';
4784 } elsif ($filename =~ m/\.(?:markdown|md)$/i) {
4785 $mime = 'text/plain';
4786 } elsif ($filename =~ m/\.png$/i) {
4787 $mime = 'image/png';
4788 } elsif ($filename =~ m/\.gif$/i) {
4789 $mime = 'image/gif';
4790 } elsif ($filename =~ m/\.jpe?g$/i) {
4791 $mime = 'image/jpeg';
4792 } elsif ($filename =~ m/\.svgz?$/i) {
4793 $mime = 'image/svg+xml';
4797 # just in case
4798 return $default_blob_plain_mimetype || 'application/octet-stream' unless $fd || $mime;
4800 $mime = -T $fd ? 'text/plain' : 'application/octet-stream' unless $mime;
4802 return $mime;
4805 sub is_ascii {
4806 use bytes;
4807 my $data = shift;
4808 return scalar($data =~ /^[\x00-\x7f]*$/);
4811 sub is_valid_utf8 {
4812 my $data = shift;
4813 return utf8::decode($data);
4816 sub extract_html_charset {
4817 return undef unless $_[0] && "$_[0]</head>" =~ m#<head(?:\s+[^>]*)?(?<!/)>(.*?)</head\s*>#is;
4818 my $head = $1;
4819 return $2 if $head =~ m#<meta\s+charset\s*=\s*(['"])\s*([a-z0-9(:)_.+-]+)\s*\1\s*/?>#is;
4820 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) {
4821 my %kv = (lc($1) => $3, lc($4) => $6);
4822 my ($he, $c) = (lc($kv{'http-equiv'}), $kv{'content'});
4823 return $1 if $he && $c && $he eq 'content-type' &&
4824 $c =~ m!\s*text/html\s*;\s*charset\s*=\s*([a-z0-9(:)_.+-]+)\s*$!is;
4826 return undef;
4829 sub blob_contenttype {
4830 my ($fd, $file_name, $type) = @_;
4832 $type ||= blob_mimetype($fd, $file_name, 1);
4833 return $type unless $type =~ m!^text/.+!i;
4834 my ($leader, $charset, $htmlcharset);
4835 if ($fd && read($fd, $leader, 32768)) {{
4836 $charset='US-ASCII' if is_ascii($leader);
4837 return ("$type; charset=UTF-8", $leader) if !$charset && is_valid_utf8($leader);
4838 $charset='ISO-8859-1' unless $charset;
4839 $htmlcharset = extract_html_charset($leader) if $type eq 'text/html';
4840 if ($htmlcharset && $charset ne 'US-ASCII') {
4841 $htmlcharset = undef if $htmlcharset =~ /^(?:utf-8|us-ascii)$/i
4844 return ("$type; charset=$htmlcharset", $leader) if $htmlcharset;
4845 my $defcharset = $default_text_plain_charset || '';
4846 $defcharset =~ s/^\s+//;
4847 $defcharset =~ s/\s+$//;
4848 $defcharset = '' if $charset && $charset ne 'US-ASCII' && $defcharset =~ /^(?:utf-8|us-ascii)$/i;
4849 return ("$type; charset=" . ($defcharset || 'ISO-8859-1'), $leader);
4852 # peek the first upto 128 bytes off a file handle
4853 sub peek128bytes {
4854 my $fd = shift;
4856 use IO::Handle;
4857 use bytes;
4859 my $prefix128;
4860 return '' unless $fd && read($fd, $prefix128, 128);
4862 # In the general case, we're guaranteed only to be able to ungetc one
4863 # character (provided, of course, we actually got a character first).
4865 # However, we know:
4867 # 1) we are dealing with a :perlio layer since blob_mimetype will have
4868 # already been called at least once on the file handle before us
4870 # 2) we have an $fd positioned at the start of the input stream and
4871 # therefore know we were positioned at a buffer boundary before
4872 # reading the initial upto 128 bytes
4874 # 3) the buffer size is at least 512 bytes
4876 # 4) we are careful to only unget raw bytes
4878 # 5) we are attempting to unget exactly the same number of bytes we got
4880 # Given the above conditions we will ALWAYS be able to safely unget
4881 # the $prefix128 value we just got.
4883 # In fact, we could read up to 511 bytes and still be sure.
4884 # (Reading 512 might pop us into the next internal buffer, but probably
4885 # not since that could break the always able to unget at least the one
4886 # you just got guarantee.)
4888 map {$fd->ungetc(ord($_))} reverse(split //, $prefix128);
4890 return $prefix128;
4893 # guess file syntax for syntax highlighting; return undef if no highlighting
4894 # the name of syntax can (in the future) depend on syntax highlighter used
4895 sub guess_file_syntax {
4896 my ($fd, $mimetype, $file_name) = @_;
4897 return undef unless $fd && defined $file_name &&
4898 defined $mimetype && $mimetype =~ m!^text/.+!i;
4899 my $basename = basename($file_name, '.in');
4900 return $highlight_basename{$basename}
4901 if exists $highlight_basename{$basename};
4903 # Peek to see if there's a shebang or xml line.
4904 # We always operate on bytes when testing this.
4906 use bytes;
4907 my $shebang = peek128bytes($fd);
4908 if (length($shebang) >= 4 && $shebang =~ /^#!/) { # 4 would be '#!/x'
4909 foreach my $key (keys %highlight_shebang) {
4910 my $ar = ref($highlight_shebang{$key}) ?
4911 $highlight_shebang{$key} :
4912 [$highlight_shebang{key}];
4913 map {return $key if $shebang =~ /$_/} @$ar;
4916 return 'xml' if $shebang =~ m!^\s*<\?xml\s!; # "xml" must be lowercase
4919 $basename =~ /\.([^.]*)$/;
4920 my $ext = $1 or return undef;
4921 return $highlight_ext{$ext}
4922 if exists $highlight_ext{$ext};
4924 return undef;
4927 # run highlighter and return FD of its output,
4928 # or return original FD if no highlighting
4929 sub run_highlighter {
4930 my ($fd, $syntax) = @_;
4931 return $fd unless $fd && !eof($fd) && defined $highlight_bin && defined $syntax;
4933 defined(my $hifd = cmd_pipe $posix_shell_bin, '-c',
4934 quote_command(git_cmd(), "cat-file", "blob", $hash)." | ".
4935 $to_utf8_pipe_command.
4936 quote_command($highlight_bin).
4937 " --replace-tabs=8 --fragment --syntax $syntax")
4938 or die_error(500, "Couldn't open file or run syntax highlighter");
4939 if (eof $hifd) {
4940 # just in case, should not happen as we tested !eof($fd) above
4941 return $fd if close($hifd);
4943 # should not happen
4944 !$! or die_error(500, "Couldn't close syntax highighter pipe");
4946 # leaving us with the only possibility a non-zero exit status (possibly a signal);
4947 # instead of dying horribly on this, just skip the highlighting
4948 # but do output a message about it to STDERR that will end up in the log
4949 print STDERR "warning: skipping failed highlight for --syntax $syntax: ".
4950 sprintf("child exit status 0x%x\n", $?);
4951 return $fd
4953 close $fd;
4954 return ($hifd, 1);
4957 ## ======================================================================
4958 ## functions printing HTML: header, footer, error page
4960 sub get_page_title {
4961 my $title = to_utf8($site_name);
4963 unless (defined $project) {
4964 if (defined $project_filter) {
4965 $title .= " - projects in '" . esc_path($project_filter) . "'";
4967 return $title;
4969 $title .= " - " . to_utf8($project);
4971 return $title unless (defined $action);
4972 my $action_print = $action eq 'blame_incremental' ? 'blame' : $action;
4973 $title .= "/$action_print"; # $action is US-ASCII (7bit ASCII)
4975 return $title unless (defined $file_name);
4976 $title .= " - " . esc_path($file_name);
4977 if ($action eq "tree" && $file_name !~ m|/$|) {
4978 $title .= "/";
4981 return $title;
4984 sub get_content_type_html {
4985 # We do not ever emit application/xhtml+xml since that gives us
4986 # no benefits and it makes many browsers (e.g. Firefox) exceedingly
4987 # strict, which is troublesome for example when showing user-supplied
4988 # README.html files.
4989 return 'text/html';
4992 sub print_feed_meta {
4993 if (defined $project) {
4994 my %href_params = get_feed_info();
4995 if (!exists $href_params{'-title'}) {
4996 $href_params{'-title'} = 'log';
4999 foreach my $format (qw(RSS Atom)) {
5000 my $type = lc($format);
5001 my %link_attr = (
5002 '-rel' => 'alternate',
5003 '-title' => esc_attr("$project - $href_params{'-title'} - $format feed"),
5004 '-type' => "application/$type+xml"
5007 $href_params{'extra_options'} = undef;
5008 $href_params{'action'} = $type;
5009 $link_attr{'-href'} = href(%href_params);
5010 print "<link ".
5011 "rel=\"$link_attr{'-rel'}\" ".
5012 "title=\"$link_attr{'-title'}\" ".
5013 "href=\"$link_attr{'-href'}\" ".
5014 "type=\"$link_attr{'-type'}\" ".
5015 "/>\n";
5017 $href_params{'extra_options'} = '--no-merges';
5018 $link_attr{'-href'} = href(%href_params);
5019 $link_attr{'-title'} .= ' (no merges)';
5020 print "<link ".
5021 "rel=\"$link_attr{'-rel'}\" ".
5022 "title=\"$link_attr{'-title'}\" ".
5023 "href=\"$link_attr{'-href'}\" ".
5024 "type=\"$link_attr{'-type'}\" ".
5025 "/>\n";
5028 } else {
5029 printf('<link rel="alternate" title="%s projects list" '.
5030 'href="%s" type="text/plain; charset=utf-8" />'."\n",
5031 esc_attr($site_name), href(project=>undef, action=>"project_index"));
5032 printf('<link rel="alternate" title="%s projects feeds" '.
5033 'href="%s" type="text/x-opml" />'."\n",
5034 esc_attr($site_name), href(project=>undef, action=>"opml"));
5038 sub print_header_links {
5039 my $status = shift;
5041 # print out each stylesheet that exist, providing backwards capability
5042 # for those people who defined $stylesheet in a config file
5043 if (defined $stylesheet) {
5044 print '<link rel="stylesheet" type="text/css" href="'.esc_url($stylesheet).'"/>'."\n";
5045 } else {
5046 foreach my $stylesheet (@stylesheets) {
5047 next unless $stylesheet;
5048 print '<link rel="stylesheet" type="text/css" href="'.esc_url($stylesheet).'"/>'."\n";
5051 print_feed_meta()
5052 if ($status eq '200 OK');
5053 if (defined $favicon) {
5054 print qq(<link rel="shortcut icon" href=").esc_url($favicon).qq(" type="image/png" />\n);
5058 sub print_nav_breadcrumbs_path {
5059 my $dirprefix = undef;
5060 while (my $part = shift) {
5061 $dirprefix .= "/" if defined $dirprefix;
5062 $dirprefix .= $part;
5063 print $cgi->a({-href => href(project => undef,
5064 project_filter => $dirprefix,
5065 action => "project_list")},
5066 esc_html($part)) . " / ";
5070 sub print_nav_breadcrumbs {
5071 my %opts = @_;
5073 for my $crumb (@extra_breadcrumbs, [ $home_link_str => $home_link ]) {
5074 print $cgi->a({-href => esc_url($crumb->[1])}, $crumb->[0]) . " / ";
5076 if (defined $project) {
5077 my @dirname = split '/', $project;
5078 my $projectbasename = pop @dirname;
5079 print_nav_breadcrumbs_path(@dirname);
5080 print $cgi->a({-href => href(action=>"summary")}, esc_html($projectbasename));
5081 if (defined $action) {
5082 my $action_print = $action ;
5083 $action_print = 'blame' if $action_print eq 'blame_incremental';
5084 if (defined $opts{-action_extra}) {
5085 $action_print = $cgi->a({-href => href(action=>$action)},
5086 $action);
5088 print " / $action_print";
5090 if (defined $opts{-action_extra}) {
5091 print " / $opts{-action_extra}";
5093 print "\n";
5094 } elsif (defined $project_filter) {
5095 print_nav_breadcrumbs_path(split '/', $project_filter);
5099 sub print_search_form {
5100 if (!defined $searchtext) {
5101 $searchtext = "";
5103 my $search_hash;
5104 if (defined $hash_base) {
5105 $search_hash = $hash_base;
5106 } elsif (defined $hash) {
5107 $search_hash = $hash;
5108 } else {
5109 $search_hash = "HEAD";
5111 # We can't use href() here because we need to encode the
5112 # URL parameters into the form, not into the action link.
5113 my $action = $my_uri;
5114 my $use_pathinfo = gitweb_check_feature('pathinfo');
5115 if ($use_pathinfo) {
5116 # See notes about doubled / in href()
5117 $action =~ s,/$,,;
5118 $action .= "/".esc_path_info($project);
5120 print $cgi->start_form(-method => "get", -action => $action) .
5121 "<div class=\"search\">\n" .
5122 (!$use_pathinfo &&
5123 $cgi->input({-name=>"p", -value=>$project, -type=>"hidden"}) . "\n") .
5124 $cgi->input({-name=>"a", -value=>"search", -type=>"hidden"}) . "\n" .
5125 $cgi->input({-name=>"h", -value=>$search_hash, -type=>"hidden"}) . "\n" .
5126 $cgi->popup_menu(-name => 'st', -default => 'commit',
5127 -values => ['commit', 'grep', 'author', 'committer', 'pickaxe']) .
5128 " " . $cgi->a({-href => href(action=>"search_help"),
5129 -title => "search help" }, "?") . " search:\n",
5130 $cgi->textfield(-name => "s", -value => $searchtext, -override => 1) . "\n" .
5131 "<span title=\"Extended regular expression\">" .
5132 $cgi->checkbox(-name => 'sr', -value => 1, -label => 're',
5133 -checked => $search_use_regexp) .
5134 "</span>" .
5135 "</div>" .
5136 $cgi->end_form() . "\n";
5139 sub git_header_html {
5140 my $status = shift || "200 OK";
5141 my $expires = shift;
5142 my %opts = @_;
5144 my $title = get_page_title();
5145 my $content_type = get_content_type_html();
5146 print $cgi->header(-type=>$content_type, -charset => 'utf-8',
5147 -status=> $status, -expires => $expires)
5148 unless ($opts{'-no_http_header'});
5149 my $mod_perl_version = $ENV{'MOD_PERL'} ? " $ENV{'MOD_PERL'}" : '';
5150 print <<EOF;
5151 <?xml version="1.0" encoding="utf-8"?>
5152 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
5153 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US" lang="en-US">
5154 <!-- git web interface version $version, (C) 2005-2006, Kay Sievers <kay.sievers\@vrfy.org>, Christian Gierke -->
5155 <!-- git core binaries version $git_version -->
5156 <head>
5157 <meta http-equiv="content-type" content="$content_type; charset=utf-8"/>
5158 <meta name="generator" content="gitweb/$version git/$git_version$mod_perl_version"/>
5159 <meta name="robots" content="index, nofollow"/>
5160 <title>$title</title>
5161 <script type="text/javascript">/* <![CDATA[ */
5162 function fixBlameLinks() {
5163 var allLinks = document.getElementsByTagName("a");
5164 for (var i = 0; i < allLinks.length; i++) {
5165 var link = allLinks.item(i);
5166 if (link.className == 'blamelink')
5167 link.href = link.href.replace("/blame/", "/blame_incremental/");
5170 /* ]]> */</script>
5172 # the stylesheet, favicon etc urls won't work correctly with path_info
5173 # unless we set the appropriate base URL
5174 if ($ENV{'PATH_INFO'}) {
5175 print "<base href=\"".esc_url($base_url)."\" />\n";
5177 print_header_links($status);
5179 if (defined $site_html_head_string) {
5180 print to_utf8($site_html_head_string);
5183 print "</head>\n" .
5184 "<body>\n";
5186 if (defined $site_header && -f $site_header) {
5187 insert_file($site_header);
5190 print "<div class=\"page_header\">\n";
5191 if (defined $logo) {
5192 print $cgi->a({-href => esc_url($logo_url),
5193 -title => $logo_label},
5194 $cgi->img({-src => esc_url($logo),
5195 -width => 72, -height => 27,
5196 -alt => "git",
5197 -class => "logo"}));
5199 print_nav_breadcrumbs(%opts);
5200 print "</div>\n";
5202 my $have_search = gitweb_check_feature('search');
5203 if (defined $project && $have_search) {
5204 print_search_form();
5208 sub compute_timed_interval {
5209 return "<?gitweb compute_timed_interval?>" if $cache_mode_active;
5210 return tv_interval($t0, [ gettimeofday() ]);
5213 sub compute_commands_count {
5214 return "<?gitweb compute_commands_count?>" if $cache_mode_active;
5215 my $s = $number_of_git_cmds == 1 ? '' : 's';
5216 return '<span id="generating_cmd">'.
5217 $number_of_git_cmds.
5218 "</span> git command$s";
5221 sub git_footer_html {
5222 my $feed_class = 'rss_logo';
5224 print "<div class=\"page_footer\">\n";
5225 if (defined $project) {
5226 my $descr = git_get_project_description($project);
5227 if (defined $descr) {
5228 print "<div class=\"page_footer_text\">" . esc_html($descr) . "</div>\n";
5231 my %href_params = get_feed_info();
5232 if (!%href_params) {
5233 $feed_class .= ' generic';
5235 $href_params{'-title'} ||= 'log';
5237 foreach my $format (qw(RSS Atom)) {
5238 $href_params{'action'} = lc($format);
5239 print $cgi->a({-href => href(%href_params),
5240 -title => "$href_params{'-title'} $format feed",
5241 -class => $feed_class}, $format)."\n";
5244 } else {
5245 print $cgi->a({-href => href(project=>undef, action=>"opml",
5246 project_filter => $project_filter),
5247 -class => $feed_class}, "OPML") . " ";
5248 print $cgi->a({-href => href(project=>undef, action=>"project_index",
5249 project_filter => $project_filter),
5250 -class => $feed_class}, "TXT") . "\n";
5252 print "</div>\n"; # class="page_footer"
5254 if (defined $t0 && gitweb_check_feature('timed')) {
5255 print "<div id=\"generating_info\">\n";
5256 print 'This page took '.
5257 '<span id="generating_time" class="time_span">'.
5258 compute_timed_interval().
5259 ' seconds </span>'.
5260 ' and '.
5261 compute_commands_count().
5262 " to generate.\n";
5263 print "</div>\n"; # class="page_footer"
5266 if (defined $site_footer && -f $site_footer) {
5267 insert_file($site_footer);
5270 print qq!<script type="text/javascript" src="!.esc_url($javascript).qq!"></script>\n!;
5271 if (defined $action &&
5272 $action eq 'blame_incremental') {
5273 print qq!<script type="text/javascript">\n!.
5274 qq!startBlame("!. href(action=>"blame_data", -replay=>1) .qq!",\n!.
5275 qq! "!. href() .qq!");\n!.
5276 qq!</script>\n!;
5277 } else {
5278 my ($jstimezone, $tz_cookie, $datetime_class) =
5279 gitweb_get_feature('javascript-timezone');
5281 print qq!<script type="text/javascript">\n!.
5282 qq!window.onload = function () {\n!;
5283 if (gitweb_check_feature('blame_incremental')) {
5284 print qq! fixBlameLinks();\n!;
5286 if (gitweb_check_feature('javascript-actions')) {
5287 print qq! fixLinks();\n!;
5289 if ($jstimezone && $tz_cookie && $datetime_class) {
5290 print qq! var tz_cookie = { name: '$tz_cookie', expires: 14, path: '/' };\n!. # in days
5291 qq! onloadTZSetup('$jstimezone', tz_cookie, '$datetime_class');\n!;
5293 print qq!};\n!.
5294 qq!</script>\n!;
5297 print "</body>\n" .
5298 "</html>";
5301 # die_error(<http_status_code>, <error_message>[, <detailed_html_description>])
5302 # Example: die_error(404, 'Hash not found')
5303 # By convention, use the following status codes (as defined in RFC 2616):
5304 # 400: Invalid or missing CGI parameters, or
5305 # requested object exists but has wrong type.
5306 # 403: Requested feature (like "pickaxe" or "snapshot") not enabled on
5307 # this server or project.
5308 # 404: Requested object/revision/project doesn't exist.
5309 # 500: The server isn't configured properly, or
5310 # an internal error occurred (e.g. failed assertions caused by bugs), or
5311 # an unknown error occurred (e.g. the git binary died unexpectedly).
5312 # 503: The server is currently unavailable (because it is overloaded,
5313 # or down for maintenance). Generally, this is a temporary state.
5314 sub die_error {
5315 my $status = shift || 500;
5316 my $error = esc_html(shift) || "Internal Server Error";
5317 my $extra = shift;
5318 my %opts = @_;
5320 my %http_responses = (
5321 400 => '400 Bad Request',
5322 403 => '403 Forbidden',
5323 404 => '404 Not Found',
5324 500 => '500 Internal Server Error',
5325 503 => '503 Service Unavailable',
5327 git_header_html($http_responses{$status}, undef, %opts);
5328 print <<EOF;
5329 <div class="page_body">
5330 <br /><br />
5331 $status - $error
5332 <br />
5334 if (defined $extra) {
5335 print "<hr />\n" .
5336 "$extra\n";
5338 print "</div>\n";
5340 git_footer_html();
5341 CORE::die
5342 unless ($opts{'-error_handler'});
5345 ## ----------------------------------------------------------------------
5346 ## functions printing or outputting HTML: navigation
5348 sub git_print_page_nav {
5349 my ($current, $suppress, $head, $treehead, $treebase, $extra) = @_;
5350 $extra = '' if !defined $extra; # pager or formats
5352 my @navs = qw(summary log commit commitdiff tree refs);
5353 if ($suppress) {
5354 my %omit;
5355 if (ref($suppress) eq 'ARRAY') {
5356 %omit = map { ($_ => 1) } @$suppress;
5357 } else {
5358 %omit = ($suppress => 1);
5360 @navs = grep { !$omit{$_} } @navs;
5363 my %arg = map { $_ => {action=>$_} } @navs;
5364 if (defined $head) {
5365 for (qw(commit commitdiff)) {
5366 $arg{$_}{'hash'} = $head;
5368 if ($current =~ m/^(tree | log | shortlog | commit | commitdiff | search)$/x) {
5369 $arg{'log'}{'hash'} = $head;
5373 $arg{'log'}{'action'} = 'shortlog';
5374 if ($current eq 'log') {
5375 $current = 'shortlog';
5376 } elsif ($current eq 'shortlog') {
5377 $current = 'log';
5379 $arg{'tree'}{'hash'} = $treehead if defined $treehead;
5380 $arg{'tree'}{'hash_base'} = $treebase if defined $treebase;
5382 my @actions = gitweb_get_feature('actions');
5383 my $escname = $project;
5384 $escname =~ s/[+]/%2B/g;
5385 my %repl = (
5386 '%' => '%',
5387 'n' => $project, # project name
5388 'f' => $git_dir, # project path within filesystem
5389 'h' => $treehead || '', # current hash ('h' parameter)
5390 'b' => $treebase || '', # hash base ('hb' parameter)
5391 'e' => $escname, # project name with '+' escaped
5393 while (@actions) {
5394 my ($label, $link, $pos) = splice(@actions,0,3);
5395 # insert
5396 @navs = map { $_ eq $pos ? ($_, $label) : $_ } @navs;
5397 # munch munch
5398 $link =~ s/%([%nfhbe])/$repl{$1}/g;
5399 $arg{$label}{'_href'} = $link;
5402 print "<div class=\"page_nav\">\n" .
5403 (join " | ",
5404 map { $_ eq $current ?
5405 $_ : $cgi->a({-href => ($arg{$_}{_href} ? $arg{$_}{_href} : href(%{$arg{$_}}))}, "$_")
5406 } @navs);
5407 print "<br/>\n$extra<br/>\n" .
5408 "</div>\n";
5411 # returns a submenu for the nagivation of the refs views (tags, heads,
5412 # remotes) with the current view disabled and the remotes view only
5413 # available if the feature is enabled
5414 sub format_ref_views {
5415 my ($current) = @_;
5416 my @ref_views = qw{tags heads};
5417 push @ref_views, 'remotes' if gitweb_check_feature('remote_heads');
5418 return join " | ", map {
5419 $_ eq $current ? $_ :
5420 $cgi->a({-href => href(action=>$_)}, $_)
5421 } @ref_views
5424 sub format_paging_nav {
5425 my ($action, $page, $has_next_link) = @_;
5426 my $paging_nav;
5429 if ($page > 0) {
5430 $paging_nav .=
5431 $cgi->a({-href => href(-replay=>1, page=>undef)}, "first") .
5432 " &#183; " .
5433 $cgi->a({-href => href(-replay=>1, page=>$page-1),
5434 -accesskey => "p", -title => "Alt-p"}, "prev");
5435 } else {
5436 $paging_nav .= "first &#183; prev";
5439 if ($has_next_link) {
5440 $paging_nav .= " &#183; " .
5441 $cgi->a({-href => href(-replay=>1, page=>$page+1),
5442 -accesskey => "n", -title => "Alt-n"}, "next");
5443 } else {
5444 $paging_nav .= " &#183; next";
5447 return $paging_nav;
5450 sub format_log_nav {
5451 my ($action, $page, $has_next_link) = @_;
5452 my $paging_nav;
5454 if ($action eq 'shortlog') {
5455 $paging_nav .= 'shortlog';
5456 } else {
5457 $paging_nav .= $cgi->a({-href => href(action=>'shortlog', -replay=>1)}, 'shortlog');
5459 $paging_nav .= ' | ';
5460 if ($action eq 'log') {
5461 $paging_nav .= 'fulllog';
5462 } else {
5463 $paging_nav .= $cgi->a({-href => href(action=>'log', -replay=>1)}, 'fulllog');
5466 $paging_nav .= " | " . format_paging_nav($action, $page, $has_next_link);
5467 return $paging_nav;
5470 ## ......................................................................
5471 ## functions printing or outputting HTML: div
5473 sub git_print_header_div {
5474 my ($action, $title, $hash, $hash_base, $extra) = @_;
5475 my %args = ();
5476 defined $extra or $extra = '';
5478 $args{'action'} = $action;
5479 $args{'hash'} = $hash if $hash;
5480 $args{'hash_base'} = $hash_base if $hash_base;
5482 my $link1 = $cgi->a({-href => href(%args), -class => "title"},
5483 $title ? $title : $action);
5484 my $link2 = $cgi->a({-href => href(%args), -class => "cover"}, "");
5485 print "<div class=\"header\">\n" . '<span class="title">' .
5486 $link1 . $extra . $link2 . '</span>' . "\n</div>\n";
5489 sub format_repo_url {
5490 my ($name, $url) = @_;
5491 return "<tr class=\"metadata_url\"><td>$name</td><td>$url</td></tr>\n";
5494 # Group output by placing it in a DIV element and adding a header.
5495 # Options for start_div() can be provided by passing a hash reference as the
5496 # first parameter to the function.
5497 # Options to git_print_header_div() can be provided by passing an array
5498 # reference. This must follow the options to start_div if they are present.
5499 # The content can be a scalar, which is output as-is, a scalar reference, which
5500 # is output after html escaping, an IO handle passed either as *handle or
5501 # *handle{IO}, or a function reference. In the latter case all following
5502 # parameters will be taken as argument to the content function call.
5503 sub git_print_section {
5504 my ($div_args, $header_args, $content);
5505 my $arg = shift;
5506 if (ref($arg) eq 'HASH') {
5507 $div_args = $arg;
5508 $arg = shift;
5510 if (ref($arg) eq 'ARRAY') {
5511 $header_args = $arg;
5512 $arg = shift;
5514 $content = $arg;
5516 print $cgi->start_div($div_args);
5517 git_print_header_div(@$header_args);
5519 if (ref($content) eq 'CODE') {
5520 $content->(@_);
5521 } elsif (ref($content) eq 'SCALAR') {
5522 print esc_html($$content);
5523 } elsif (ref($content) eq 'GLOB' or ref($content) eq 'IO::Handle') {
5524 while (<$content>) {
5525 print to_utf8($_);
5527 } elsif (!ref($content) && defined($content)) {
5528 print $content;
5531 print $cgi->end_div;
5534 sub format_timestamp_html {
5535 my $date = shift;
5536 my $useatnight = shift;
5537 defined($useatnight) or $useatnight = 1;
5538 my $strtime = $date->{'rfc2822'};
5540 my (undef, undef, $datetime_class) =
5541 gitweb_get_feature('javascript-timezone');
5542 if ($datetime_class) {
5543 $strtime = qq!<span class="$datetime_class">$strtime</span>!;
5546 my $localtime_format = '(%d %02d:%02d %s)';
5547 if ($useatnight && $date->{'hour_local'} < 6) {
5548 $localtime_format = '(%d <span class="atnight">%02d:%02d</span> %s)';
5550 $strtime .= ' ' .
5551 sprintf($localtime_format, $date->{'mday_local'},
5552 $date->{'hour_local'}, $date->{'minute_local'}, $date->{'tz_local'});
5554 return $strtime;
5557 sub format_lastrefresh_row {
5558 return "<?gitweb format_lastrefresh_row?>" if $cache_mode_active;
5559 my %rd = parse_file_date('.last_refresh');
5560 if (defined $rd{'rfc2822'}) {
5561 return "<tr id=\"metadata_lrefresh\"><td>last&#160;refresh</td>" .
5562 "<td>".format_timestamp_html(\%rd,0)."</td></tr>";
5564 return "";
5567 # Outputs the author name and date in long form
5568 sub git_print_authorship {
5569 my $co = shift;
5570 my %opts = @_;
5571 my $tag = $opts{-tag} || 'div';
5572 my $author = $co->{'author_name'};
5574 my %ad = parse_date($co->{'author_epoch'}, $co->{'author_tz'});
5575 print "<$tag class=\"author_date\">" .
5576 format_search_author($author, "author", esc_html($author)) .
5577 " [".format_timestamp_html(\%ad)."]".
5578 git_get_avatar($co->{'author_email'}, -pad_before => 1) .
5579 "</$tag>\n";
5582 # Outputs table rows containing the full author or committer information,
5583 # in the format expected for 'commit' view (& similar).
5584 # Parameters are a commit hash reference, followed by the list of people
5585 # to output information for. If the list is empty it defaults to both
5586 # author and committer.
5587 sub git_print_authorship_rows {
5588 my $co = shift;
5589 # too bad we can't use @people = @_ || ('author', 'committer')
5590 my @people = @_;
5591 @people = ('author', 'committer') unless @people;
5592 foreach my $who (@people) {
5593 my %wd = parse_date($co->{"${who}_epoch"}, $co->{"${who}_tz"});
5594 print "<tr><td>$who</td><td>" .
5595 format_search_author($co->{"${who}_name"}, $who,
5596 esc_html($co->{"${who}_name"})) . " " .
5597 format_search_author($co->{"${who}_email"}, $who,
5598 esc_html("<" . $co->{"${who}_email"} . ">")) .
5599 "</td><td rowspan=\"2\">" .
5600 git_get_avatar($co->{"${who}_email"}, -size => 'double') .
5601 "</td></tr>\n" .
5602 "<tr>" .
5603 "<td></td><td>" .
5604 format_timestamp_html(\%wd) .
5605 "</td>" .
5606 "</tr>\n";
5610 sub git_print_page_path {
5611 my $name = shift;
5612 my $type = shift;
5613 my $hb = shift;
5616 print "<div class=\"page_path\">";
5617 print $cgi->a({-href => href(action=>"tree", hash_base=>$hb),
5618 -title => 'tree root'}, to_utf8("[$project]"));
5619 print " / ";
5620 if (defined $name) {
5621 my @dirname = split '/', $name;
5622 my $basename = pop @dirname;
5623 my $fullname = '';
5625 foreach my $dir (@dirname) {
5626 $fullname .= ($fullname ? '/' : '') . $dir;
5627 print $cgi->a({-href => href(action=>"tree", file_name=>$fullname,
5628 hash_base=>$hb),
5629 -title => $fullname}, esc_path($dir));
5630 print " / ";
5632 if (defined $type && $type eq 'blob') {
5633 print $cgi->a({-href => href(action=>"blob_plain", file_name=>$file_name,
5634 hash_base=>$hb),
5635 -title => $name}, esc_path($basename));
5636 } elsif (defined $type && $type eq 'tree') {
5637 print $cgi->a({-href => href(action=>"tree", file_name=>$file_name,
5638 hash_base=>$hb),
5639 -title => $name}, esc_path($basename));
5640 print " / ";
5641 } else {
5642 print esc_path($basename);
5645 print "<br/></div>\n";
5648 sub git_print_log {
5649 my $log = shift;
5650 my %opts = @_;
5652 if ($opts{'-remove_title'}) {
5653 # remove title, i.e. first line of log
5654 shift @$log;
5656 # remove leading empty lines
5657 while (defined $log->[0] && $log->[0] eq "") {
5658 shift @$log;
5661 # print log
5662 my $skip_blank_line = 0;
5663 foreach my $line (@$log) {
5664 if ($line =~ m/^\s*([A-Z][-A-Za-z]*-[Bb]y|C[Cc]): /) {
5665 if (! $opts{'-remove_signoff'}) {
5666 print "<span class=\"signoff\">" . esc_html($line) . "</span><br/>\n";
5667 $skip_blank_line = 1;
5669 next;
5672 if ($line =~ m,\s*([a-z]*link): (https?://\S+),i) {
5673 if (! $opts{'-remove_signoff'}) {
5674 print "<span class=\"signoff\">" . esc_html($1) . ": " .
5675 "<a href=\"" . esc_html($2) . "\">" . esc_html($2) . "</a>" .
5676 "</span><br/>\n";
5677 $skip_blank_line = 1;
5679 next;
5682 # print only one empty line
5683 # do not print empty line after signoff
5684 if ($line eq "") {
5685 next if ($skip_blank_line);
5686 $skip_blank_line = 1;
5687 } else {
5688 $skip_blank_line = 0;
5691 print format_log_line_html($line) . "<br/>\n";
5694 if ($opts{'-final_empty_line'}) {
5695 # end with single empty line
5696 print "<br/>\n" unless $skip_blank_line;
5700 # return link target (what link points to)
5701 sub git_get_link_target {
5702 my $hash = shift;
5703 my $link_target;
5705 # read link
5706 defined(my $fd = git_cmd_pipe "cat-file", "blob", $hash)
5707 or return;
5709 local $/ = undef;
5710 $link_target = to_utf8(scalar <$fd>);
5712 close $fd
5713 or return;
5715 return $link_target;
5718 # given link target, and the directory (basedir) the link is in,
5719 # return target of link relative to top directory (top tree);
5720 # return undef if it is not possible (including absolute links).
5721 sub normalize_link_target {
5722 my ($link_target, $basedir) = @_;
5724 # absolute symlinks (beginning with '/') cannot be normalized
5725 return if (substr($link_target, 0, 1) eq '/');
5727 # normalize link target to path from top (root) tree (dir)
5728 my $path;
5729 if ($basedir) {
5730 $path = $basedir . '/' . $link_target;
5731 } else {
5732 # we are in top (root) tree (dir)
5733 $path = $link_target;
5736 # remove //, /./, and /../
5737 my @path_parts;
5738 foreach my $part (split('/', $path)) {
5739 # discard '.' and ''
5740 next if (!$part || $part eq '.');
5741 # handle '..'
5742 if ($part eq '..') {
5743 if (@path_parts) {
5744 pop @path_parts;
5745 } else {
5746 # link leads outside repository (outside top dir)
5747 return;
5749 } else {
5750 push @path_parts, $part;
5753 $path = join('/', @path_parts);
5755 return $path;
5758 # print tree entry (row of git_tree), but without encompassing <tr> element
5759 sub git_print_tree_entry {
5760 my ($t, $basedir, $hash_base, $have_blame) = @_;
5762 my %base_key = ();
5763 $base_key{'hash_base'} = $hash_base if defined $hash_base;
5765 # The format of a table row is: mode list link. Where mode is
5766 # the mode of the entry, list is the name of the entry, an href,
5767 # and link is the action links of the entry.
5769 print "<td class=\"mode\">" . mode_str($t->{'mode'}) . "</td>\n";
5770 if (exists $t->{'size'}) {
5771 print "<td class=\"size\">$t->{'size'}</td>\n";
5773 if ($t->{'type'} eq "blob") {
5774 print "<td class=\"list\">" .
5775 $cgi->a({-href => href(action=>"blob", hash=>$t->{'hash'},
5776 file_name=>"$basedir$t->{'name'}", %base_key),
5777 -class => "list"}, esc_path($t->{'name'}));
5778 if (S_ISLNK(oct $t->{'mode'})) {
5779 my $link_target = git_get_link_target($t->{'hash'});
5780 if ($link_target) {
5781 my $norm_target = normalize_link_target($link_target, $basedir);
5782 if (defined $norm_target) {
5783 print " -> " .
5784 $cgi->a({-href => href(action=>"object", hash_base=>$hash_base,
5785 file_name=>$norm_target),
5786 -title => $norm_target}, esc_path($link_target));
5787 } else {
5788 print " -> " . esc_path($link_target);
5792 print "</td>\n";
5793 print "<td class=\"link\">";
5794 print $cgi->a({-href => href(action=>"blob", hash=>$t->{'hash'},
5795 file_name=>"$basedir$t->{'name'}", %base_key)},
5796 "blob");
5797 if ($have_blame) {
5798 print " | " .
5799 $cgi->a({-href => href(action=>"blame", hash=>$t->{'hash'},
5800 file_name=>"$basedir$t->{'name'}", %base_key),
5801 -class => "blamelink"},
5802 "blame");
5804 if (defined $hash_base) {
5805 print " | " .
5806 $cgi->a({-href => href(action=>"history", hash_base=>$hash_base,
5807 hash=>$t->{'hash'}, file_name=>"$basedir$t->{'name'}")},
5808 "history");
5810 print " | " .
5811 $cgi->a({-href => href(action=>"blob_plain", hash_base=>$hash_base,
5812 file_name=>"$basedir$t->{'name'}")},
5813 "raw");
5814 print "</td>\n";
5816 } elsif ($t->{'type'} eq "tree") {
5817 print "<td class=\"list\">";
5818 print $cgi->a({-href => href(action=>"tree", hash=>$t->{'hash'},
5819 file_name=>"$basedir$t->{'name'}",
5820 %base_key)},
5821 esc_path($t->{'name'}));
5822 print "</td>\n";
5823 print "<td class=\"link\">";
5824 print $cgi->a({-href => href(action=>"tree", hash=>$t->{'hash'},
5825 file_name=>"$basedir$t->{'name'}",
5826 %base_key)},
5827 "tree");
5828 if (defined $hash_base) {
5829 print " | " .
5830 $cgi->a({-href => href(action=>"history", hash_base=>$hash_base,
5831 file_name=>"$basedir$t->{'name'}")},
5832 "history");
5834 print "</td>\n";
5835 } else {
5836 # unknown object: we can only present history for it
5837 # (this includes 'commit' object, i.e. submodule support)
5838 print "<td class=\"list\">" .
5839 esc_path($t->{'name'}) .
5840 "</td>\n";
5841 print "<td class=\"link\">";
5842 if (defined $hash_base) {
5843 print $cgi->a({-href => href(action=>"history",
5844 hash_base=>$hash_base,
5845 file_name=>"$basedir$t->{'name'}")},
5846 "history");
5848 print "</td>\n";
5852 ## ......................................................................
5853 ## functions printing large fragments of HTML
5855 # get pre-image filenames for merge (combined) diff
5856 sub fill_from_file_info {
5857 my ($diff, @parents) = @_;
5859 $diff->{'from_file'} = [ ];
5860 $diff->{'from_file'}[$diff->{'nparents'} - 1] = undef;
5861 for (my $i = 0; $i < $diff->{'nparents'}; $i++) {
5862 if ($diff->{'status'}[$i] eq 'R' ||
5863 $diff->{'status'}[$i] eq 'C') {
5864 $diff->{'from_file'}[$i] =
5865 git_get_path_by_hash($parents[$i], $diff->{'from_id'}[$i]);
5869 return $diff;
5872 # is current raw difftree line of file deletion
5873 sub is_deleted {
5874 my $diffinfo = shift;
5876 return $diffinfo->{'to_id'} eq ('0' x 40);
5879 # does patch correspond to [previous] difftree raw line
5880 # $diffinfo - hashref of parsed raw diff format
5881 # $patchinfo - hashref of parsed patch diff format
5882 # (the same keys as in $diffinfo)
5883 sub is_patch_split {
5884 my ($diffinfo, $patchinfo) = @_;
5886 return defined $diffinfo && defined $patchinfo
5887 && $diffinfo->{'to_file'} eq $patchinfo->{'to_file'};
5891 sub git_difftree_body {
5892 my ($difftree, $hash, @parents) = @_;
5893 my ($parent) = $parents[0];
5894 my $have_blame = gitweb_check_feature('blame');
5895 print "<div class=\"list_head\">\n";
5896 if ($#{$difftree} > 10) {
5897 print(($#{$difftree} + 1) . " files changed:\n");
5899 print "</div>\n";
5901 print "<table class=\"" .
5902 (@parents > 1 ? "combined " : "") .
5903 "diff_tree\">\n";
5905 # header only for combined diff in 'commitdiff' view
5906 my $has_header = @$difftree && @parents > 1 && $action eq 'commitdiff';
5907 if ($has_header) {
5908 # table header
5909 print "<thead><tr>\n" .
5910 "<th></th><th></th>\n"; # filename, patchN link
5911 for (my $i = 0; $i < @parents; $i++) {
5912 my $par = $parents[$i];
5913 print "<th>" .
5914 $cgi->a({-href => href(action=>"commitdiff",
5915 hash=>$hash, hash_parent=>$par),
5916 -title => 'commitdiff to parent number ' .
5917 ($i+1) . ': ' . substr($par,0,7)},
5918 $i+1) .
5919 "&#160;</th>\n";
5921 print "</tr></thead>\n<tbody>\n";
5924 my $alternate = 1;
5925 my $patchno = 0;
5926 foreach my $line (@{$difftree}) {
5927 my $diff = parsed_difftree_line($line);
5929 if ($alternate) {
5930 print "<tr class=\"dark\">\n";
5931 } else {
5932 print "<tr class=\"light\">\n";
5934 $alternate ^= 1;
5936 if (exists $diff->{'nparents'}) { # combined diff
5938 fill_from_file_info($diff, @parents)
5939 unless exists $diff->{'from_file'};
5941 if (!is_deleted($diff)) {
5942 # file exists in the result (child) commit
5943 print "<td>" .
5944 $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
5945 file_name=>$diff->{'to_file'},
5946 hash_base=>$hash),
5947 -class => "list"}, esc_path($diff->{'to_file'})) .
5948 "</td>\n";
5949 } else {
5950 print "<td>" .
5951 esc_path($diff->{'to_file'}) .
5952 "</td>\n";
5955 if ($action eq 'commitdiff') {
5956 # link to patch
5957 $patchno++;
5958 print "<td class=\"link\">" .
5959 $cgi->a({-href => href(-anchor=>"patch$patchno")},
5960 "patch") .
5961 " | " .
5962 "</td>\n";
5965 my $has_history = 0;
5966 my $not_deleted = 0;
5967 for (my $i = 0; $i < $diff->{'nparents'}; $i++) {
5968 my $hash_parent = $parents[$i];
5969 my $from_hash = $diff->{'from_id'}[$i];
5970 my $from_path = $diff->{'from_file'}[$i];
5971 my $status = $diff->{'status'}[$i];
5973 $has_history ||= ($status ne 'A');
5974 $not_deleted ||= ($status ne 'D');
5976 if ($status eq 'A') {
5977 print "<td class=\"link\" align=\"right\"> | </td>\n";
5978 } elsif ($status eq 'D') {
5979 print "<td class=\"link\">" .
5980 $cgi->a({-href => href(action=>"blob",
5981 hash_base=>$hash,
5982 hash=>$from_hash,
5983 file_name=>$from_path)},
5984 "blob" . ($i+1)) .
5985 " | </td>\n";
5986 } else {
5987 if ($diff->{'to_id'} eq $from_hash) {
5988 print "<td class=\"link nochange\">";
5989 } else {
5990 print "<td class=\"link\">";
5992 print $cgi->a({-href => href(action=>"blobdiff",
5993 hash=>$diff->{'to_id'},
5994 hash_parent=>$from_hash,
5995 hash_base=>$hash,
5996 hash_parent_base=>$hash_parent,
5997 file_name=>$diff->{'to_file'},
5998 file_parent=>$from_path)},
5999 "diff" . ($i+1)) .
6000 " | </td>\n";
6004 print "<td class=\"link\">";
6005 if ($not_deleted) {
6006 print $cgi->a({-href => href(action=>"blob",
6007 hash=>$diff->{'to_id'},
6008 file_name=>$diff->{'to_file'},
6009 hash_base=>$hash)},
6010 "blob");
6011 print " | " if ($has_history);
6013 if ($has_history) {
6014 print $cgi->a({-href => href(action=>"history",
6015 file_name=>$diff->{'to_file'},
6016 hash_base=>$hash)},
6017 "history");
6019 print "</td>\n";
6021 print "</tr>\n";
6022 next; # instead of 'else' clause, to avoid extra indent
6024 # else ordinary diff
6026 my ($to_mode_oct, $to_mode_str, $to_file_type);
6027 my ($from_mode_oct, $from_mode_str, $from_file_type);
6028 if ($diff->{'to_mode'} ne ('0' x 6)) {
6029 $to_mode_oct = oct $diff->{'to_mode'};
6030 if (S_ISREG($to_mode_oct)) { # only for regular file
6031 $to_mode_str = sprintf("%04o", $to_mode_oct & 0777); # permission bits
6033 $to_file_type = file_type($diff->{'to_mode'});
6035 if ($diff->{'from_mode'} ne ('0' x 6)) {
6036 $from_mode_oct = oct $diff->{'from_mode'};
6037 if (S_ISREG($from_mode_oct)) { # only for regular file
6038 $from_mode_str = sprintf("%04o", $from_mode_oct & 0777); # permission bits
6040 $from_file_type = file_type($diff->{'from_mode'});
6043 if ($diff->{'status'} eq "A") { # created
6044 my $mode_chng = "<span class=\"file_status new\">[new $to_file_type";
6045 $mode_chng .= " with mode: $to_mode_str" if $to_mode_str;
6046 $mode_chng .= "]</span>";
6047 print "<td>";
6048 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
6049 hash_base=>$hash, file_name=>$diff->{'file'}),
6050 -class => "list"}, esc_path($diff->{'file'}));
6051 print "</td>\n";
6052 print "<td>$mode_chng</td>\n";
6053 print "<td class=\"link\">";
6054 if ($action eq 'commitdiff') {
6055 # link to patch
6056 $patchno++;
6057 print $cgi->a({-href => href(-anchor=>"patch$patchno")},
6058 "patch") .
6059 " | ";
6061 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
6062 hash_base=>$hash, file_name=>$diff->{'file'})},
6063 "blob");
6064 print "</td>\n";
6066 } elsif ($diff->{'status'} eq "D") { # deleted
6067 my $mode_chng = "<span class=\"file_status deleted\">[deleted $from_file_type]</span>";
6068 print "<td>";
6069 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'from_id'},
6070 hash_base=>$parent, file_name=>$diff->{'file'}),
6071 -class => "list"}, esc_path($diff->{'file'}));
6072 print "</td>\n";
6073 print "<td>$mode_chng</td>\n";
6074 print "<td class=\"link\">";
6075 if ($action eq 'commitdiff') {
6076 # link to patch
6077 $patchno++;
6078 print $cgi->a({-href => href(-anchor=>"patch$patchno")},
6079 "patch") .
6080 " | ";
6082 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'from_id'},
6083 hash_base=>$parent, file_name=>$diff->{'file'})},
6084 "blob") . " | ";
6085 if ($have_blame) {
6086 print $cgi->a({-href => href(action=>"blame", hash_base=>$parent,
6087 file_name=>$diff->{'file'}),
6088 -class => "blamelink"},
6089 "blame") . " | ";
6091 print $cgi->a({-href => href(action=>"history", hash_base=>$parent,
6092 file_name=>$diff->{'file'})},
6093 "history");
6094 print "</td>\n";
6096 } elsif ($diff->{'status'} eq "M" || $diff->{'status'} eq "T") { # modified, or type changed
6097 my $mode_chnge = "";
6098 if ($diff->{'from_mode'} != $diff->{'to_mode'}) {
6099 $mode_chnge = "<span class=\"file_status mode_chnge\">[changed";
6100 if ($from_file_type ne $to_file_type) {
6101 $mode_chnge .= " from $from_file_type to $to_file_type";
6103 if (($from_mode_oct & 0777) != ($to_mode_oct & 0777)) {
6104 if ($from_mode_str && $to_mode_str) {
6105 $mode_chnge .= " mode: $from_mode_str->$to_mode_str";
6106 } elsif ($to_mode_str) {
6107 $mode_chnge .= " mode: $to_mode_str";
6110 $mode_chnge .= "]</span>\n";
6112 print "<td>";
6113 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
6114 hash_base=>$hash, file_name=>$diff->{'file'}),
6115 -class => "list"}, esc_path($diff->{'file'}));
6116 print "</td>\n";
6117 print "<td>$mode_chnge</td>\n";
6118 print "<td class=\"link\">";
6119 if ($action eq 'commitdiff') {
6120 # link to patch
6121 $patchno++;
6122 print $cgi->a({-href => href(-anchor=>"patch$patchno")},
6123 "patch") .
6124 " | ";
6125 } elsif ($diff->{'to_id'} ne $diff->{'from_id'}) {
6126 # "commit" view and modified file (not onlu mode changed)
6127 print $cgi->a({-href => href(action=>"blobdiff",
6128 hash=>$diff->{'to_id'}, hash_parent=>$diff->{'from_id'},
6129 hash_base=>$hash, hash_parent_base=>$parent,
6130 file_name=>$diff->{'file'})},
6131 "diff") .
6132 " | ";
6134 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
6135 hash_base=>$hash, file_name=>$diff->{'file'})},
6136 "blob") . " | ";
6137 if ($have_blame) {
6138 print $cgi->a({-href => href(action=>"blame", hash_base=>$hash,
6139 file_name=>$diff->{'file'}),
6140 -class => "blamelink"},
6141 "blame") . " | ";
6143 print $cgi->a({-href => href(action=>"history", hash_base=>$hash,
6144 file_name=>$diff->{'file'})},
6145 "history");
6146 print "</td>\n";
6148 } elsif ($diff->{'status'} eq "R" || $diff->{'status'} eq "C") { # renamed or copied
6149 my %status_name = ('R' => 'moved', 'C' => 'copied');
6150 my $nstatus = $status_name{$diff->{'status'}};
6151 my $mode_chng = "";
6152 if ($diff->{'from_mode'} != $diff->{'to_mode'}) {
6153 # mode also for directories, so we cannot use $to_mode_str
6154 $mode_chng = sprintf(", mode: %04o", $to_mode_oct & 0777);
6156 print "<td>" .
6157 $cgi->a({-href => href(action=>"blob", hash_base=>$hash,
6158 hash=>$diff->{'to_id'}, file_name=>$diff->{'to_file'}),
6159 -class => "list"}, esc_path($diff->{'to_file'})) . "</td>\n" .
6160 "<td><span class=\"file_status $nstatus\">[$nstatus from " .
6161 $cgi->a({-href => href(action=>"blob", hash_base=>$parent,
6162 hash=>$diff->{'from_id'}, file_name=>$diff->{'from_file'}),
6163 -class => "list"}, esc_path($diff->{'from_file'})) .
6164 " with " . (int $diff->{'similarity'}) . "% similarity$mode_chng]</span></td>\n" .
6165 "<td class=\"link\">";
6166 if ($action eq 'commitdiff') {
6167 # link to patch
6168 $patchno++;
6169 print $cgi->a({-href => href(-anchor=>"patch$patchno")},
6170 "patch") .
6171 " | ";
6172 } elsif ($diff->{'to_id'} ne $diff->{'from_id'}) {
6173 # "commit" view and modified file (not only pure rename or copy)
6174 print $cgi->a({-href => href(action=>"blobdiff",
6175 hash=>$diff->{'to_id'}, hash_parent=>$diff->{'from_id'},
6176 hash_base=>$hash, hash_parent_base=>$parent,
6177 file_name=>$diff->{'to_file'}, file_parent=>$diff->{'from_file'})},
6178 "diff") .
6179 " | ";
6181 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
6182 hash_base=>$parent, file_name=>$diff->{'to_file'})},
6183 "blob") . " | ";
6184 if ($have_blame) {
6185 print $cgi->a({-href => href(action=>"blame", hash_base=>$hash,
6186 file_name=>$diff->{'to_file'}),
6187 -class => "blamelink"},
6188 "blame") . " | ";
6190 print $cgi->a({-href => href(action=>"history", hash_base=>$hash,
6191 file_name=>$diff->{'to_file'})},
6192 "history");
6193 print "</td>\n";
6195 } # we should not encounter Unmerged (U) or Unknown (X) status
6196 print "</tr>\n";
6198 print "</tbody>" if $has_header;
6199 print "</table>\n";
6202 # Print context lines and then rem/add lines in a side-by-side manner.
6203 sub print_sidebyside_diff_lines {
6204 my ($ctx, $rem, $add) = @_;
6206 # print context block before add/rem block
6207 if (@$ctx) {
6208 print join '',
6209 '<div class="chunk_block ctx">',
6210 '<div class="old">',
6211 @$ctx,
6212 '</div>',
6213 '<div class="new">',
6214 @$ctx,
6215 '</div>',
6216 '</div>';
6219 if (!@$add) {
6220 # pure removal
6221 print join '',
6222 '<div class="chunk_block rem">',
6223 '<div class="old">',
6224 @$rem,
6225 '</div>',
6226 '</div>';
6227 } elsif (!@$rem) {
6228 # pure addition
6229 print join '',
6230 '<div class="chunk_block add">',
6231 '<div class="new">',
6232 @$add,
6233 '</div>',
6234 '</div>';
6235 } else {
6236 print join '',
6237 '<div class="chunk_block chg">',
6238 '<div class="old">',
6239 @$rem,
6240 '</div>',
6241 '<div class="new">',
6242 @$add,
6243 '</div>',
6244 '</div>';
6248 # Print context lines and then rem/add lines in inline manner.
6249 sub print_inline_diff_lines {
6250 my ($ctx, $rem, $add) = @_;
6252 print @$ctx, @$rem, @$add;
6255 # Format removed and added line, mark changed part and HTML-format them.
6256 # Implementation is based on contrib/diff-highlight
6257 sub format_rem_add_lines_pair {
6258 my ($rem, $add, $num_parents) = @_;
6260 # We need to untabify lines before split()'ing them;
6261 # otherwise offsets would be invalid.
6262 chomp $rem;
6263 chomp $add;
6264 $rem = untabify($rem);
6265 $add = untabify($add);
6267 my @rem = split(//, $rem);
6268 my @add = split(//, $add);
6269 my ($esc_rem, $esc_add);
6270 # Ignore leading +/- characters for each parent.
6271 my ($prefix_len, $suffix_len) = ($num_parents, 0);
6272 my ($prefix_has_nonspace, $suffix_has_nonspace);
6274 my $shorter = (@rem < @add) ? @rem : @add;
6275 while ($prefix_len < $shorter) {
6276 last if ($rem[$prefix_len] ne $add[$prefix_len]);
6278 $prefix_has_nonspace = 1 if ($rem[$prefix_len] !~ /\s/);
6279 $prefix_len++;
6282 while ($prefix_len + $suffix_len < $shorter) {
6283 last if ($rem[-1 - $suffix_len] ne $add[-1 - $suffix_len]);
6285 $suffix_has_nonspace = 1 if ($rem[-1 - $suffix_len] !~ /\s/);
6286 $suffix_len++;
6289 # Mark lines that are different from each other, but have some common
6290 # part that isn't whitespace. If lines are completely different, don't
6291 # mark them because that would make output unreadable, especially if
6292 # diff consists of multiple lines.
6293 if ($prefix_has_nonspace || $suffix_has_nonspace) {
6294 $esc_rem = esc_html_hl_regions($rem, 'marked',
6295 [$prefix_len, @rem - $suffix_len], -nbsp=>1);
6296 $esc_add = esc_html_hl_regions($add, 'marked',
6297 [$prefix_len, @add - $suffix_len], -nbsp=>1);
6298 } else {
6299 $esc_rem = esc_html($rem, -nbsp=>1);
6300 $esc_add = esc_html($add, -nbsp=>1);
6303 return format_diff_line(\$esc_rem, 'rem'),
6304 format_diff_line(\$esc_add, 'add');
6307 # HTML-format diff context, removed and added lines.
6308 sub format_ctx_rem_add_lines {
6309 my ($ctx, $rem, $add, $num_parents) = @_;
6310 my (@new_ctx, @new_rem, @new_add);
6311 my $can_highlight = 0;
6312 my $is_combined = ($num_parents > 1);
6314 # Highlight if every removed line has a corresponding added line.
6315 if (@$add > 0 && @$add == @$rem) {
6316 $can_highlight = 1;
6318 # Highlight lines in combined diff only if the chunk contains
6319 # diff between the same version, e.g.
6321 # - a
6322 # - b
6323 # + c
6324 # + d
6326 # Otherwise the highlightling would be confusing.
6327 if ($is_combined) {
6328 for (my $i = 0; $i < @$add; $i++) {
6329 my $prefix_rem = substr($rem->[$i], 0, $num_parents);
6330 my $prefix_add = substr($add->[$i], 0, $num_parents);
6332 $prefix_rem =~ s/-/+/g;
6334 if ($prefix_rem ne $prefix_add) {
6335 $can_highlight = 0;
6336 last;
6342 if ($can_highlight) {
6343 for (my $i = 0; $i < @$add; $i++) {
6344 my ($line_rem, $line_add) = format_rem_add_lines_pair(
6345 $rem->[$i], $add->[$i], $num_parents);
6346 push @new_rem, $line_rem;
6347 push @new_add, $line_add;
6349 } else {
6350 @new_rem = map { format_diff_line($_, 'rem') } @$rem;
6351 @new_add = map { format_diff_line($_, 'add') } @$add;
6354 @new_ctx = map { format_diff_line($_, 'ctx') } @$ctx;
6356 return (\@new_ctx, \@new_rem, \@new_add);
6359 # Print context lines and then rem/add lines.
6360 sub print_diff_lines {
6361 my ($ctx, $rem, $add, $diff_style, $num_parents) = @_;
6362 my $is_combined = $num_parents > 1;
6364 ($ctx, $rem, $add) = format_ctx_rem_add_lines($ctx, $rem, $add,
6365 $num_parents);
6367 if ($diff_style eq 'sidebyside' && !$is_combined) {
6368 print_sidebyside_diff_lines($ctx, $rem, $add);
6369 } else {
6370 # default 'inline' style and unknown styles
6371 print_inline_diff_lines($ctx, $rem, $add);
6375 sub print_diff_chunk {
6376 my ($diff_style, $num_parents, $from, $to, @chunk) = @_;
6377 my (@ctx, @rem, @add);
6379 # The class of the previous line.
6380 my $prev_class = '';
6382 return unless @chunk;
6384 # incomplete last line might be among removed or added lines,
6385 # or both, or among context lines: find which
6386 for (my $i = 1; $i < @chunk; $i++) {
6387 if ($chunk[$i][0] eq 'incomplete') {
6388 $chunk[$i][0] = $chunk[$i-1][0];
6392 # guardian
6393 push @chunk, ["", ""];
6395 foreach my $line_info (@chunk) {
6396 my ($class, $line) = @$line_info;
6398 # print chunk headers
6399 if ($class && $class eq 'chunk_header') {
6400 print format_diff_line($line, $class, $from, $to);
6401 next;
6404 ## print from accumulator when have some add/rem lines or end
6405 # of chunk (flush context lines), or when have add and rem
6406 # lines and new block is reached (otherwise add/rem lines could
6407 # be reordered)
6408 if (!$class || ((@rem || @add) && $class eq 'ctx') ||
6409 (@rem && @add && $class ne $prev_class)) {
6410 print_diff_lines(\@ctx, \@rem, \@add,
6411 $diff_style, $num_parents);
6412 @ctx = @rem = @add = ();
6415 ## adding lines to accumulator
6416 # guardian value
6417 last unless $line;
6418 # rem, add or change
6419 if ($class eq 'rem') {
6420 push @rem, $line;
6421 } elsif ($class eq 'add') {
6422 push @add, $line;
6424 # context line
6425 if ($class eq 'ctx') {
6426 push @ctx, $line;
6429 $prev_class = $class;
6433 sub git_patchset_body {
6434 my ($fd, $diff_style, $difftree, $hash, @hash_parents) = @_;
6435 my ($hash_parent) = $hash_parents[0];
6437 my $is_combined = (@hash_parents > 1);
6438 my $patch_idx = 0;
6439 my $patch_number = 0;
6440 my $patch_line;
6441 my $diffinfo;
6442 my $to_name;
6443 my (%from, %to);
6444 my @chunk; # for side-by-side diff
6446 print "<div class=\"patchset\">\n";
6448 # skip to first patch
6449 while ($patch_line = to_utf8(scalar <$fd>)) {
6450 chomp $patch_line;
6452 last if ($patch_line =~ m/^diff /);
6455 PATCH:
6456 while ($patch_line) {
6458 # parse "git diff" header line
6459 if ($patch_line =~ m/^diff --git (\"(?:[^\\\"]*(?:\\.[^\\\"]*)*)\"|[^ "]*) (.*)$/) {
6460 # $1 is from_name, which we do not use
6461 $to_name = unquote($2);
6462 $to_name =~ s!^b/!!;
6463 } elsif ($patch_line =~ m/^diff --(cc|combined) ("?.*"?)$/) {
6464 # $1 is 'cc' or 'combined', which we do not use
6465 $to_name = unquote($2);
6466 } else {
6467 $to_name = undef;
6470 # check if current patch belong to current raw line
6471 # and parse raw git-diff line if needed
6472 if (is_patch_split($diffinfo, { 'to_file' => $to_name })) {
6473 # this is continuation of a split patch
6474 print "<div class=\"patch cont\">\n";
6475 } else {
6476 # advance raw git-diff output if needed
6477 $patch_idx++ if defined $diffinfo;
6479 # read and prepare patch information
6480 $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
6482 # compact combined diff output can have some patches skipped
6483 # find which patch (using pathname of result) we are at now;
6484 if ($is_combined) {
6485 while ($to_name ne $diffinfo->{'to_file'}) {
6486 print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n" .
6487 format_diff_cc_simplified($diffinfo, @hash_parents) .
6488 "</div>\n"; # class="patch"
6490 $patch_idx++;
6491 $patch_number++;
6493 last if $patch_idx > $#$difftree;
6494 $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
6498 # modifies %from, %to hashes
6499 parse_from_to_diffinfo($diffinfo, \%from, \%to, @hash_parents);
6501 # this is first patch for raw difftree line with $patch_idx index
6502 # we index @$difftree array from 0, but number patches from 1
6503 print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n";
6506 # git diff header
6507 #assert($patch_line =~ m/^diff /) if DEBUG;
6508 #assert($patch_line !~ m!$/$!) if DEBUG; # is chomp-ed
6509 $patch_number++;
6510 # print "git diff" header
6511 print format_git_diff_header_line($patch_line, $diffinfo,
6512 \%from, \%to);
6514 # print extended diff header
6515 print "<div class=\"diff extended_header\">\n";
6516 EXTENDED_HEADER:
6517 while ($patch_line = to_utf8(scalar<$fd>)) {
6518 chomp $patch_line;
6520 last EXTENDED_HEADER if ($patch_line =~ m/^--- |^diff /);
6522 print format_extended_diff_header_line($patch_line, $diffinfo,
6523 \%from, \%to);
6525 print "</div>\n"; # class="diff extended_header"
6527 # from-file/to-file diff header
6528 if (! $patch_line) {
6529 print "</div>\n"; # class="patch"
6530 last PATCH;
6532 next PATCH if ($patch_line =~ m/^diff /);
6533 #assert($patch_line =~ m/^---/) if DEBUG;
6535 my $last_patch_line = $patch_line;
6536 $patch_line = to_utf8(scalar <$fd>);
6537 chomp $patch_line;
6538 #assert($patch_line =~ m/^\+\+\+/) if DEBUG;
6540 print format_diff_from_to_header($last_patch_line, $patch_line,
6541 $diffinfo, \%from, \%to,
6542 @hash_parents);
6544 # the patch itself
6545 LINE:
6546 while ($patch_line = to_utf8(scalar <$fd>)) {
6547 chomp $patch_line;
6549 next PATCH if ($patch_line =~ m/^diff /);
6551 my $class = diff_line_class($patch_line, \%from, \%to);
6553 if ($class eq 'chunk_header') {
6554 print_diff_chunk($diff_style, scalar @hash_parents, \%from, \%to, @chunk);
6555 @chunk = ();
6558 push @chunk, [ $class, $patch_line ];
6561 } continue {
6562 if (@chunk) {
6563 print_diff_chunk($diff_style, scalar @hash_parents, \%from, \%to, @chunk);
6564 @chunk = ();
6566 print "</div>\n"; # class="patch"
6569 # for compact combined (--cc) format, with chunk and patch simplification
6570 # the patchset might be empty, but there might be unprocessed raw lines
6571 for (++$patch_idx if $patch_number > 0;
6572 $patch_idx < @$difftree;
6573 ++$patch_idx) {
6574 # read and prepare patch information
6575 $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
6577 # generate anchor for "patch" links in difftree / whatchanged part
6578 print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n" .
6579 format_diff_cc_simplified($diffinfo, @hash_parents) .
6580 "</div>\n"; # class="patch"
6582 $patch_number++;
6585 if ($patch_number == 0) {
6586 if (@hash_parents > 1) {
6587 print "<div class=\"diff nodifferences\">Trivial merge</div>\n";
6588 } else {
6589 print "<div class=\"diff nodifferences\">No differences found</div>\n";
6593 print "</div>\n"; # class="patchset"
6596 # . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
6598 sub git_project_search_form {
6599 my ($searchtext, $search_use_regexp) = @_;
6601 my $limit = '';
6602 if ($project_filter) {
6603 $limit = " in '$project_filter'";
6606 print "<div class=\"projsearch\">\n";
6607 print $cgi->start_form(-method => 'get', -action => $my_uri) .
6608 $cgi->hidden(-name => 'a', -value => 'project_list') . "\n";
6609 print $cgi->hidden(-name => 'pf', -value => $project_filter). "\n"
6610 if (defined $project_filter);
6611 print $cgi->textfield(-name => 's', -value => $searchtext,
6612 -title => "Search project by name and description$limit",
6613 -size => 60) . "\n" .
6614 "<span title=\"Extended regular expression\">" .
6615 $cgi->checkbox(-name => 'sr', -value => 1, -label => 're',
6616 -checked => $search_use_regexp) .
6617 "</span>\n" .
6618 $cgi->submit(-name => 'btnS', -value => 'Search') .
6619 $cgi->end_form() . "\n" .
6620 "<span class=\"projectlist_link\">" .
6621 $cgi->a({-href => href(project => undef, searchtext => undef,
6622 action => 'project_list',
6623 project_filter => $project_filter)},
6624 esc_html("List all projects$limit")) . "</span><br />\n";
6625 print "<span class=\"projectlist_link\">" .
6626 $cgi->a({-href => href(project => undef, searchtext => undef,
6627 action => 'project_list',
6628 project_filter => undef)},
6629 esc_html("List all projects")) . "</span>\n" if $project_filter;
6630 print "</div>\n";
6633 # entry for given @keys needs filling if at least one of keys in list
6634 # is not present in %$project_info
6635 sub project_info_needs_filling {
6636 my ($project_info, @keys) = @_;
6638 # return List::MoreUtils::any { !exists $project_info->{$_} } @keys;
6639 foreach my $key (@keys) {
6640 if (!exists $project_info->{$key}) {
6641 return 1;
6644 return;
6647 sub git_cache_file_format {
6648 return GITWEB_CACHE_FORMAT .
6649 (gitweb_check_feature('forks') ? " (forks)" : "");
6652 sub git_retrieve_cache_file {
6653 my $cache_file = shift;
6655 use Storable qw(retrieve);
6657 if ((my $dump = eval { retrieve($cache_file) })) {
6658 return $$dump[1] if
6659 ref($dump) eq 'ARRAY' &&
6660 @$dump == 2 &&
6661 ref($$dump[1]) eq 'ARRAY' &&
6662 @{$$dump[1]} == 2 &&
6663 ref(${$$dump[1]}[0]) eq 'ARRAY' &&
6664 ref(${$$dump[1]}[1]) eq 'HASH' &&
6665 $$dump[0] eq git_cache_file_format();
6668 return undef;
6671 sub git_store_cache_file {
6672 my ($cache_file, $cachedata) = @_;
6674 use File::Basename qw(dirname);
6675 use File::stat;
6676 use POSIX qw(:fcntl_h);
6677 use Storable qw(store_fd);
6679 my $result = undef;
6680 my $cache_d = dirname($cache_file);
6681 my $mask = umask();
6682 umask($mask & ~0070) if $cache_grpshared;
6683 if ((-d $cache_d || mkdir($cache_d, $cache_grpshared ? 0770 : 0700)) &&
6684 sysopen(my $fd, "$cache_file.lock", O_WRONLY|O_CREAT|O_EXCL, $cache_grpshared ? 0660 : 0600)) {
6685 store_fd([git_cache_file_format(), $cachedata], $fd);
6686 close $fd;
6687 rename "$cache_file.lock", $cache_file;
6688 $result = stat($cache_file)->mtime;
6690 umask($mask) if $cache_grpshared;
6691 return $result;
6694 sub verify_cached_project {
6695 my ($hashref, $path) = @_;
6696 return undef unless $path;
6697 delete $$hashref{$path}, return undef unless is_valid_project($path);
6698 return $$hashref{$path} if exists $$hashref{$path};
6700 # A valid project was requested but it's not yet in the cache
6701 # Manufacture a minimal project entry (path, name, description)
6702 # Also provide age, but only if it's available via $lastactivity_file
6704 my %proj = ('path' => $path);
6705 my $val = git_get_project_description($path);
6706 defined $val or $val = '';
6707 $proj{'descr_long'} = $val;
6708 $proj{'descr'} = chop_str($val, $projects_list_description_width, 5);
6709 unless ($omit_owner) {
6710 $val = git_get_project_owner($path);
6711 defined $val or $val = '';
6712 $proj{'owner'} = $val;
6714 unless ($omit_age_column) {
6715 ($val) = git_get_last_activity($path, 1);
6716 $proj{'age_epoch'} = $val if defined $val;
6718 $$hashref{$path} = \%proj;
6719 return \%proj;
6722 sub git_filter_cached_projects {
6723 my ($cache, $projlist, $verify) = @_;
6724 my $hashref = $$cache[1];
6725 my $sub = $verify ?
6726 sub {verify_cached_project($hashref, $_[0])} :
6727 sub {$$hashref{$_[0]}};
6728 return map {
6729 my $c = &$sub($_->{'path'});
6730 defined $c ? ($_ = $c) : ()
6731 } @$projlist;
6734 # fills project list info (age, description, owner, category, forks, etc.)
6735 # for each project in the list, removing invalid projects from
6736 # returned list, or fill only specified info.
6738 # Invalid projects are removed from the returned list if and only if you
6739 # ask 'age_epoch' to be filled, because they are the only fields
6740 # that run unconditionally git command that requires repository, and
6741 # therefore do always check if project repository is invalid.
6743 # USAGE:
6744 # * fill_project_list_info(\@project_list, 'descr_long', 'ctags')
6745 # ensures that 'descr_long' and 'ctags' fields are filled
6746 # * @project_list = fill_project_list_info(\@project_list)
6747 # ensures that all fields are filled (and invalid projects removed)
6749 # NOTE: modifies $projlist, but does not remove entries from it
6750 sub fill_project_list_info {
6751 my ($projlist, @wanted_keys) = @_;
6753 my $rebuild = @wanted_keys && $wanted_keys[0] eq 'rebuild-cache' && shift @wanted_keys;
6754 return fill_project_list_info_uncached($projlist, @wanted_keys)
6755 unless $projlist_cache_lifetime && $projlist_cache_lifetime > 0;
6757 use File::stat;
6759 my $cache_lifetime = $rebuild ? 0 : $projlist_cache_lifetime;
6760 my $cache_file = "$cache_dir/$projlist_cache_name";
6762 my @projects;
6763 my $stale = 0;
6764 my $now = time();
6765 my $cache_mtime;
6766 if ($cache_lifetime && -f $cache_file) {
6767 $cache_mtime = stat($cache_file)->mtime;
6768 $cache_dump = undef if $cache_mtime &&
6769 (!$cache_dump_mtime || $cache_dump_mtime != $cache_mtime);
6771 if (defined $cache_mtime && # caching is on and $cache_file exists
6772 $cache_mtime + $cache_lifetime*60 > $now &&
6773 ($cache_dump || ($cache_dump = git_retrieve_cache_file($cache_file)))) {
6774 # Cache hit.
6775 $cache_dump_mtime = $cache_mtime;
6776 $stale = $now - $cache_mtime;
6777 my $verify = ($action eq 'summary' || $action eq 'forks') &&
6778 gitweb_check_feature('forks');
6779 @projects = git_filter_cached_projects($cache_dump, $projlist, $verify);
6781 } else { # Cache miss.
6782 if (defined $cache_mtime) {
6783 # Postpone timeout by two minutes so that we get
6784 # enough time to do our job, or to be more exact
6785 # make cache expire after two minutes from now.
6786 my $time = $now - $cache_lifetime*60 + 120;
6787 utime $time, $time, $cache_file;
6789 my @all_projects = git_get_projects_list();
6790 my %all_projects_filled = map { ( $_->{'path'} => $_ ) }
6791 fill_project_list_info_uncached(\@all_projects);
6792 map { $all_projects_filled{$_->{'path'}} = $_ }
6793 filter_forks_from_projects_list([values(%all_projects_filled)])
6794 if gitweb_check_feature('forks');
6795 $cache_dump = [[sort {$a->{'path'} cmp $b->{'path'}} values(%all_projects_filled)],
6796 \%all_projects_filled];
6797 $cache_dump_mtime = git_store_cache_file($cache_file, $cache_dump);
6798 @projects = git_filter_cached_projects($cache_dump, $projlist);
6801 if ($cache_lifetime && $stale > 0) {
6802 print "<div class=\"stale_info\">Cached version (${stale}s old)</div>\n"
6803 unless $shown_stale_message;
6804 $shown_stale_message = 1;
6807 return @projects;
6810 sub fill_project_list_info_uncached {
6811 my ($projlist, @wanted_keys) = @_;
6812 my @projects;
6813 my $filter_set = sub { return @_; };
6814 if (@wanted_keys) {
6815 my %wanted_keys = map { $_ => 1 } @wanted_keys;
6816 $filter_set = sub { return grep { $wanted_keys{$_} } @_; };
6819 my $show_ctags = gitweb_check_feature('ctags');
6820 PROJECT:
6821 foreach my $pr (@$projlist) {
6822 if (project_info_needs_filling($pr, $filter_set->('age_epoch'))) {
6823 my (@activity) = git_get_last_activity($pr->{'path'});
6824 unless (@activity) {
6825 next PROJECT;
6827 ($pr->{'age_epoch'}) = @activity;
6829 if (project_info_needs_filling($pr, $filter_set->('descr', 'descr_long'))) {
6830 my $descr = git_get_project_description($pr->{'path'}) || "";
6831 $descr = to_utf8($descr);
6832 $pr->{'descr_long'} = $descr;
6833 $pr->{'descr'} = chop_str($descr, $projects_list_description_width, 5);
6835 if (project_info_needs_filling($pr, $filter_set->('owner'))) {
6836 $pr->{'owner'} = git_get_project_owner("$pr->{'path'}") || "";
6838 if ($show_ctags &&
6839 project_info_needs_filling($pr, $filter_set->('ctags'))) {
6840 $pr->{'ctags'} = git_get_project_ctags($pr->{'path'});
6842 if ($projects_list_group_categories &&
6843 project_info_needs_filling($pr, $filter_set->('category'))) {
6844 my $cat = git_get_project_category($pr->{'path'}) ||
6845 $project_list_default_category;
6846 $pr->{'category'} = to_utf8($cat);
6849 push @projects, $pr;
6852 return @projects;
6855 sub sort_projects_list {
6856 my ($projlist, $order) = @_;
6858 sub order_str {
6859 my $key = shift;
6860 return sub { lc($a->{$key}) cmp lc($b->{$key}) };
6863 sub order_reverse_num_then_undef {
6864 my $key = shift;
6865 return sub {
6866 defined $a->{$key} ?
6867 (defined $b->{$key} ? $b->{$key} <=> $a->{$key} : -1) :
6868 (defined $b->{$key} ? 1 : 0)
6872 my %orderings = (
6873 project => order_str('path'),
6874 descr => order_str('descr_long'),
6875 owner => order_str('owner'),
6876 age => order_reverse_num_then_undef('age_epoch'),
6879 my $ordering = $orderings{$order};
6880 return defined $ordering ? sort $ordering @$projlist : @$projlist;
6883 # returns a hash of categories, containing the list of project
6884 # belonging to each category
6885 sub build_projlist_by_category {
6886 my ($projlist, $from, $to) = @_;
6887 my %categories;
6889 $from = 0 unless defined $from;
6890 $to = $#$projlist if (!defined $to || $#$projlist < $to);
6892 for (my $i = $from; $i <= $to; $i++) {
6893 my $pr = $projlist->[$i];
6894 push @{$categories{ $pr->{'category'} }}, $pr;
6897 return wantarray ? %categories : \%categories;
6900 # print 'sort by' <th> element, generating 'sort by $name' replay link
6901 # if that order is not selected
6902 sub print_sort_th {
6903 print format_sort_th(@_);
6906 sub format_sort_th {
6907 my ($name, $order, $header) = @_;
6908 my $sort_th = "";
6909 $header ||= ucfirst($name);
6911 if ($order eq $name) {
6912 $sort_th .= "<th>$header</th>\n";
6913 } else {
6914 $sort_th .= "<th>" .
6915 $cgi->a({-href => href(-replay=>1, order=>$name),
6916 -class => "header"}, $header) .
6917 "</th>\n";
6920 return $sort_th;
6923 sub git_project_list_rows {
6924 my ($projlist, $from, $to, $check_forks) = @_;
6926 $from = 0 unless defined $from;
6927 $to = $#$projlist if (!defined $to || $#$projlist < $to);
6929 my $now = time;
6930 my $alternate = 1;
6931 for (my $i = $from; $i <= $to; $i++) {
6932 my $pr = $projlist->[$i];
6934 if ($alternate) {
6935 print "<tr class=\"dark\">\n";
6936 } else {
6937 print "<tr class=\"light\">\n";
6939 $alternate ^= 1;
6941 if ($check_forks) {
6942 print "<td>";
6943 if ($pr->{'forks'}) {
6944 my $nforks = scalar @{$pr->{'forks'}};
6945 my $s = $nforks == 1 ? '' : 's';
6946 if ($nforks > 0) {
6947 print $cgi->a({-href => href(project=>$pr->{'path'}, action=>"forks"),
6948 -title => "$nforks fork$s"}, "+");
6949 } else {
6950 print $cgi->span({-title => "$nforks fork$s"}, "+");
6953 print "</td>\n";
6955 my $path = $pr->{'path'};
6956 my $dotgit = $path =~ s/\.git$// ? '.git' : '';
6957 print "<td>" . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary"),
6958 -class => "list"},
6959 esc_html_match_hl($path, $search_regexp).$dotgit) .
6960 "</td>\n" .
6961 "<td>" . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary"),
6962 -class => "list",
6963 -title => $pr->{'descr_long'}},
6964 $search_regexp
6965 ? esc_html_match_hl_chopped($pr->{'descr_long'},
6966 $pr->{'descr'}, $search_regexp)
6967 : esc_html($pr->{'descr'})) .
6968 "</td>\n";
6969 unless ($omit_owner) {
6970 print "<td><i>" . ($owner_link_hook
6971 ? $cgi->a({-href => $owner_link_hook->($pr->{'owner'}), -class => "list"},
6972 chop_and_escape_str($pr->{'owner'}, 15))
6973 : chop_and_escape_str($pr->{'owner'}, 15)) . "</i></td>\n";
6975 unless ($omit_age_column) {
6976 my ($age_epoch, $age_string) = ($pr->{'age_epoch'});
6977 $age_string = defined $age_epoch ? age_string($age_epoch, $now) : "No commits";
6978 print "<td class=\"". age_class($age_epoch, $now) . "\">" . $age_string . "</td>\n";
6980 print"<td class=\"link\">" .
6981 $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary")}, "summary") . " | " .
6982 $cgi->a({-href => href(project=>$pr->{'path'}, action=>"shortlog")}, "log") . " | " .
6983 $cgi->a({-href => href(project=>$pr->{'path'}, action=>"tree")}, "tree") .
6984 ($pr->{'forks'} ? " | " . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"forks")}, "forks") : '') .
6985 "</td>\n" .
6986 "</tr>\n";
6990 sub git_project_list_body {
6991 # actually uses global variable $project
6992 my ($projlist, $order, $from, $to, $extra, $no_header, $ctags_action, $keep_top) = @_;
6993 my @projects = @$projlist;
6995 my $check_forks = gitweb_check_feature('forks');
6996 my $show_ctags = gitweb_check_feature('ctags');
6997 my $tagfilter = $show_ctags ? $input_params{'ctag_filter'} : undef;
6998 $check_forks = undef
6999 if ($tagfilter || $search_regexp);
7001 # filtering out forks before filling info allows us to do less work
7002 if ($check_forks) {
7003 @projects = filter_forks_from_projects_list(\@projects);
7004 push @projects, { 'path' => "$project_filter.git" }
7005 if $project_filter && $keep_top && is_valid_project("$project_filter.git");
7007 # search_projects_list pre-fills required info
7008 @projects = search_projects_list(\@projects,
7009 'search_regexp' => $search_regexp,
7010 'tagfilter' => $tagfilter)
7011 if ($tagfilter || $search_regexp);
7012 # fill the rest
7013 my @all_fields = ('descr', 'descr_long', 'ctags', 'category');
7014 push @all_fields, 'age_epoch' unless($omit_age_column);
7015 push @all_fields, 'owner' unless($omit_owner);
7016 @projects = fill_project_list_info(\@projects, @all_fields);
7018 $order ||= $default_projects_order;
7019 $from = 0 unless defined $from;
7020 $to = $#projects if (!defined $to || $#projects < $to);
7022 # short circuit
7023 if ($from > $to) {
7024 print "<center>\n".
7025 "<b>No such projects found</b><br />\n".
7026 "Click ".$cgi->a({-href=>href(project=>undef,action=>'project_list')},"here")." to view all projects<br />\n".
7027 "</center>\n<br />\n";
7028 return;
7031 @projects = sort_projects_list(\@projects, $order);
7033 if ($show_ctags) {
7034 my $ctags = git_gather_all_ctags(\@projects);
7035 my $cloud = git_populate_project_tagcloud($ctags, $ctags_action||'project_list');
7036 print git_show_project_tagcloud($cloud, 64);
7039 print "<table class=\"project_list\">\n";
7040 unless ($no_header) {
7041 print "<tr>\n";
7042 if ($check_forks) {
7043 print "<th></th>\n";
7045 print_sort_th('project', $order, 'Project');
7046 print_sort_th('descr', $order, 'Description');
7047 print_sort_th('owner', $order, 'Owner') unless $omit_owner;
7048 print_sort_th('age', $order, 'Last Change') unless $omit_age_column;
7049 print "<th></th>\n" . # for links
7050 "</tr>\n";
7053 if ($projects_list_group_categories) {
7054 # only display categories with projects in the $from-$to window
7055 @projects = sort {$a->{'category'} cmp $b->{'category'}} @projects[$from..$to];
7056 my %categories = build_projlist_by_category(\@projects, $from, $to);
7057 foreach my $cat (sort keys %categories) {
7058 unless ($cat eq "") {
7059 print "<tr>\n";
7060 if ($check_forks) {
7061 print "<td></td>\n";
7063 print "<td class=\"category\" colspan=\"5\">".esc_html($cat)."</td>\n";
7064 print "</tr>\n";
7067 git_project_list_rows($categories{$cat}, undef, undef, $check_forks);
7069 } else {
7070 git_project_list_rows(\@projects, $from, $to, $check_forks);
7073 if (defined $extra) {
7074 print "<tr>\n";
7075 if ($check_forks) {
7076 print "<td></td>\n";
7078 print "<td colspan=\"5\">$extra</td>\n" .
7079 "</tr>\n";
7081 print "</table>\n";
7084 sub git_log_body {
7085 # uses global variable $project
7086 my ($commitlist, $from, $to, $refs, $extra) = @_;
7088 $from = 0 unless defined $from;
7089 $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
7091 for (my $i = 0; $i <= $to; $i++) {
7092 my %co = %{$commitlist->[$i]};
7093 next if !%co;
7094 my $commit = $co{'id'};
7095 my $ref = format_ref_marker($refs, $commit);
7096 git_print_header_div('commit',
7097 "<span class=\"age\">$co{'age_string'}</span>" .
7098 esc_html($co{'title'}),
7099 $commit, undef, $ref);
7100 print "<div class=\"title_text\">\n" .
7101 "<div class=\"log_link\">\n" .
7102 $cgi->a({-href => href(action=>"commit", hash=>$commit)}, "commit") .
7103 " | " .
7104 $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff") .
7105 " | " .
7106 $cgi->a({-href => href(action=>"tree", hash=>$commit, hash_base=>$commit)}, "tree") .
7107 "<br/>\n" .
7108 "</div>\n";
7109 git_print_authorship(\%co, -tag => 'span');
7110 print "<br/>\n</div>\n";
7112 print "<div class=\"log_body\">\n";
7113 git_print_log($co{'comment'}, -final_empty_line=> 1);
7114 print "</div>\n";
7116 if ($extra) {
7117 print "<div class=\"page_nav\">\n";
7118 print "$extra\n";
7119 print "</div>\n";
7123 sub git_shortlog_body {
7124 # uses global variable $project
7125 my ($commitlist, $from, $to, $refs, $extra) = @_;
7127 $from = 0 unless defined $from;
7128 $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
7130 print "<table class=\"shortlog\">\n";
7131 my $alternate = 1;
7132 for (my $i = $from; $i <= $to; $i++) {
7133 my %co = %{$commitlist->[$i]};
7134 my $commit = $co{'id'};
7135 my $ref = format_ref_marker($refs, $commit);
7136 if ($alternate) {
7137 print "<tr class=\"dark\">\n";
7138 } else {
7139 print "<tr class=\"light\">\n";
7141 $alternate ^= 1;
7142 # git_summary() used print "<td><i>$co{'age_string'}</i></td>\n" .
7143 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
7144 format_author_html('td', \%co, 10) . "<td>";
7145 print format_subject_html($co{'title'}, $co{'title_short'},
7146 href(action=>"commit", hash=>$commit), $ref);
7147 print "</td>\n" .
7148 "<td class=\"link\">" .
7149 $cgi->a({-href => href(action=>"commit", hash=>$commit)}, "commit") . " | " .
7150 $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff") . " | " .
7151 $cgi->a({-href => href(action=>"tree", hash=>$commit, hash_base=>$commit)}, "tree");
7152 my $snapshot_links = format_snapshot_links($commit);
7153 if (defined $snapshot_links) {
7154 print " | " . $snapshot_links;
7156 print "</td>\n" .
7157 "</tr>\n";
7159 if (defined $extra) {
7160 print "<tr>\n" .
7161 "<td colspan=\"4\">$extra</td>\n" .
7162 "</tr>\n";
7164 print "</table>\n";
7167 sub git_history_body {
7168 # Warning: assumes constant type (blob or tree) during history
7169 my ($commitlist, $from, $to, $refs, $extra,
7170 $file_name, $file_hash, $ftype) = @_;
7172 $from = 0 unless defined $from;
7173 $to = $#{$commitlist} unless (defined $to && $to <= $#{$commitlist});
7175 print "<table class=\"history\">\n";
7176 my $alternate = 1;
7177 for (my $i = $from; $i <= $to; $i++) {
7178 my %co = %{$commitlist->[$i]};
7179 if (!%co) {
7180 next;
7182 my $commit = $co{'id'};
7184 my $ref = format_ref_marker($refs, $commit);
7186 if ($alternate) {
7187 print "<tr class=\"dark\">\n";
7188 } else {
7189 print "<tr class=\"light\">\n";
7191 $alternate ^= 1;
7192 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
7193 # shortlog: format_author_html('td', \%co, 10)
7194 format_author_html('td', \%co, 15, 3) . "<td>";
7195 # originally git_history used chop_str($co{'title'}, 50)
7196 print format_subject_html($co{'title'}, $co{'title_short'},
7197 href(action=>"commit", hash=>$commit), $ref);
7198 print "</td>\n" .
7199 "<td class=\"link\">" .
7200 $cgi->a({-href => href(action=>$ftype, hash_base=>$commit, file_name=>$file_name)}, $ftype) . " | " .
7201 $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff");
7203 if ($ftype eq 'blob') {
7204 my $blob_current = $file_hash;
7205 my $blob_parent = git_get_hash_by_path($commit, $file_name);
7206 if (defined $blob_current && defined $blob_parent &&
7207 $blob_current ne $blob_parent) {
7208 print " | " .
7209 $cgi->a({-href => href(action=>"blobdiff",
7210 hash=>$blob_current, hash_parent=>$blob_parent,
7211 hash_base=>$hash_base, hash_parent_base=>$commit,
7212 file_name=>$file_name)},
7213 "diff to current");
7216 print "</td>\n" .
7217 "</tr>\n";
7219 if (defined $extra) {
7220 print "<tr>\n" .
7221 "<td colspan=\"4\">$extra</td>\n" .
7222 "</tr>\n";
7224 print "</table>\n";
7227 sub git_tags_body {
7228 # uses global variable $project
7229 my ($taglist, $from, $to, $extra, $head_at, $full, $order) = @_;
7230 $from = 0 unless defined $from;
7231 $to = $#{$taglist} if (!defined $to || $#{$taglist} < $to);
7232 $order ||= $default_refs_order;
7234 print "<table class=\"tags\">\n";
7235 if ($full) {
7236 print "<tr class=\"tags_header\">\n";
7237 print_sort_th('age', $order, 'Last Change');
7238 print_sort_th('name', $order, 'Name');
7239 print "<th></th>\n" . # for comment
7240 "<th></th>\n" . # for tag
7241 "<th></th>\n" . # for links
7242 "</tr>\n";
7244 my $alternate = 1;
7245 for (my $i = $from; $i <= $to; $i++) {
7246 my $entry = $taglist->[$i];
7247 my %tag = %$entry;
7248 my $comment = $tag{'subject'};
7249 my $comment_short;
7250 if (defined $comment) {
7251 $comment_short = chop_str($comment, 30, 5);
7253 my $curr = defined $head_at && $tag{'id'} eq $head_at;
7254 if ($alternate) {
7255 print "<tr class=\"dark\">\n";
7256 } else {
7257 print "<tr class=\"light\">\n";
7259 $alternate ^= 1;
7260 if (defined $tag{'age'}) {
7261 print "<td><i>$tag{'age'}</i></td>\n";
7262 } else {
7263 print "<td></td>\n";
7265 print(($curr ? "<td class=\"current_head\">" : "<td>") .
7266 $cgi->a({-href => href(action=>$tag{'reftype'}, hash=>$tag{'refid'}),
7267 -class => "list name"}, esc_html($tag{'name'})) .
7268 "</td>\n" .
7269 "<td>");
7270 if (defined $comment) {
7271 print format_subject_html($comment, $comment_short,
7272 href(action=>"tag", hash=>$tag{'id'}));
7274 print "</td>\n" .
7275 "<td class=\"selflink\">";
7276 if ($tag{'type'} eq "tag") {
7277 print $cgi->a({-href => href(action=>"tag", hash=>$tag{'id'})}, "tag");
7278 } else {
7279 print "&#160;";
7281 print "</td>\n" .
7282 "<td class=\"link\">" . " | " .
7283 $cgi->a({-href => href(action=>$tag{'reftype'}, hash=>$tag{'refid'})}, $tag{'reftype'});
7284 if ($tag{'reftype'} eq "commit") {
7285 print " | " . $cgi->a({-href => href(action=>"shortlog", hash=>$tag{'fullname'})}, "log");
7286 print " | " . $cgi->a({-href => href(action=>"tree", hash=>$tag{'fullname'})}, "tree") if $full;
7287 } elsif ($tag{'reftype'} eq "blob") {
7288 print " | " . $cgi->a({-href => href(action=>"blob_plain", hash=>$tag{'refid'})}, "raw");
7290 print "</td>\n" .
7291 "</tr>";
7293 if (defined $extra) {
7294 print "<tr>\n" .
7295 "<td colspan=\"5\">$extra</td>\n" .
7296 "</tr>\n";
7298 print "</table>\n";
7301 sub git_heads_body {
7302 # uses global variable $project
7303 my ($headlist, $head_at, $from, $to, $extra) = @_;
7304 $from = 0 unless defined $from;
7305 $to = $#{$headlist} if (!defined $to || $#{$headlist} < $to);
7307 print "<table class=\"heads\">\n";
7308 my $alternate = 1;
7309 for (my $i = $from; $i <= $to; $i++) {
7310 my $entry = $headlist->[$i];
7311 my %ref = %$entry;
7312 my $curr = defined $head_at && $ref{'id'} eq $head_at;
7313 if ($alternate) {
7314 print "<tr class=\"dark\">\n";
7315 } else {
7316 print "<tr class=\"light\">\n";
7318 $alternate ^= 1;
7319 print "<td><i>$ref{'age'}</i></td>\n" .
7320 ($curr ? "<td class=\"current_head\">" : "<td>") .
7321 $cgi->a({-href => href(action=>"shortlog", hash=>$ref{'fullname'}),
7322 -class => "list name"},esc_html($ref{'name'})) .
7323 "</td>\n" .
7324 "<td class=\"link\">" .
7325 $cgi->a({-href => href(action=>"shortlog", hash=>$ref{'fullname'})}, "log") . " | " .
7326 $cgi->a({-href => href(action=>"tree", hash=>$ref{'fullname'}, hash_base=>$ref{'fullname'})}, "tree") .
7327 "</td>\n" .
7328 "</tr>";
7330 if (defined $extra) {
7331 print "<tr>\n" .
7332 "<td colspan=\"3\">$extra</td>\n" .
7333 "</tr>\n";
7335 print "</table>\n";
7338 # Display a single remote block
7339 sub git_remote_block {
7340 my ($remote, $rdata, $limit, $head) = @_;
7342 my $heads = $rdata->{'heads'};
7343 my $fetch = $rdata->{'fetch'};
7344 my $push = $rdata->{'push'};
7346 my $urls_table = "<table class=\"projects_list\">\n" ;
7348 if (defined $fetch) {
7349 if ($fetch eq $push) {
7350 $urls_table .= format_repo_url("URL", $fetch);
7351 } else {
7352 $urls_table .= format_repo_url("Fetch&#160;URL", $fetch);
7353 $urls_table .= format_repo_url("Push&#160;URL", $push) if defined $push;
7355 } elsif (defined $push) {
7356 $urls_table .= format_repo_url("Push&#160;URL", $push);
7357 } else {
7358 $urls_table .= format_repo_url("", "No remote URL");
7361 $urls_table .= "</table>\n";
7363 my $dots;
7364 if (defined $limit && $limit < @$heads) {
7365 $dots = $cgi->a({-href => href(action=>"remotes", hash=>$remote)}, "...");
7368 print $urls_table;
7369 git_heads_body($heads, $head, 0, $limit, $dots);
7372 # Display a list of remote names with the respective fetch and push URLs
7373 sub git_remotes_list {
7374 my ($remotedata, $limit) = @_;
7375 print "<table class=\"heads\">\n";
7376 my $alternate = 1;
7377 my @remotes = sort keys %$remotedata;
7379 my $limited = $limit && $limit < @remotes;
7381 $#remotes = $limit - 1 if $limited;
7383 while (my $remote = shift @remotes) {
7384 my $rdata = $remotedata->{$remote};
7385 my $fetch = $rdata->{'fetch'};
7386 my $push = $rdata->{'push'};
7387 if ($alternate) {
7388 print "<tr class=\"dark\">\n";
7389 } else {
7390 print "<tr class=\"light\">\n";
7392 $alternate ^= 1;
7393 print "<td>" .
7394 $cgi->a({-href=> href(action=>'remotes', hash=>$remote),
7395 -class=> "list name"},esc_html($remote)) .
7396 "</td>";
7397 print "<td class=\"link\">" .
7398 (defined $fetch ? $cgi->a({-href=> $fetch}, "fetch") : "fetch") .
7399 " | " .
7400 (defined $push ? $cgi->a({-href=> $push}, "push") : "push") .
7401 "</td>";
7403 print "</tr>\n";
7406 if ($limited) {
7407 print "<tr>\n" .
7408 "<td colspan=\"3\">" .
7409 $cgi->a({-href => href(action=>"remotes")}, "...") .
7410 "</td>\n" . "</tr>\n";
7413 print "</table>";
7416 # Display remote heads grouped by remote, unless there are too many
7417 # remotes, in which case we only display the remote names
7418 sub git_remotes_body {
7419 my ($remotedata, $limit, $head) = @_;
7420 if ($limit and $limit < keys %$remotedata) {
7421 git_remotes_list($remotedata, $limit);
7422 } else {
7423 fill_remote_heads($remotedata);
7424 while (my ($remote, $rdata) = each %$remotedata) {
7425 git_print_section({-class=>"remote", -id=>$remote},
7426 ["remotes", $remote, $remote], sub {
7427 git_remote_block($remote, $rdata, $limit, $head);
7433 sub git_search_message {
7434 my %co = @_;
7436 my $greptype;
7437 if ($searchtype eq 'commit') {
7438 $greptype = "--grep=";
7439 } elsif ($searchtype eq 'author') {
7440 $greptype = "--author=";
7441 } elsif ($searchtype eq 'committer') {
7442 $greptype = "--committer=";
7444 $greptype .= $searchtext;
7445 my @commitlist = parse_commits($hash, 101, (100 * $page), undef,
7446 $greptype, '--regexp-ignore-case',
7447 $search_use_regexp ? '--extended-regexp' : '--fixed-strings');
7449 my $paging_nav = '';
7450 if ($page > 0) {
7451 $paging_nav .=
7452 $cgi->a({-href => href(-replay=>1, page=>undef)},
7453 "first") .
7454 " &#183; " .
7455 $cgi->a({-href => href(-replay=>1, page=>$page-1),
7456 -accesskey => "p", -title => "Alt-p"}, "prev");
7457 } else {
7458 $paging_nav .= "first &#183; prev";
7460 my $next_link = '';
7461 if ($#commitlist >= 100) {
7462 $next_link =
7463 $cgi->a({-href => href(-replay=>1, page=>$page+1),
7464 -accesskey => "n", -title => "Alt-n"}, "next");
7465 $paging_nav .= " &#183; $next_link";
7466 } else {
7467 $paging_nav .= " &#183; next";
7470 git_header_html();
7472 git_print_page_nav('','', $hash,$co{'tree'},$hash, $paging_nav);
7473 git_print_header_div('commit', esc_html($co{'title'}), $hash);
7474 if ($page == 0 && !@commitlist) {
7475 print "<p>No match.</p>\n";
7476 } else {
7477 git_search_grep_body(\@commitlist, 0, 99, $next_link);
7480 git_footer_html();
7483 sub git_search_changes {
7484 my %co = @_;
7486 local $/ = "\n";
7487 defined(my $fd = git_cmd_pipe '--no-pager', 'log', @diff_opts,
7488 '--pretty=format:%H', '--no-abbrev', '--raw', "-S$searchtext",
7489 ($search_use_regexp ? '--pickaxe-regex' : ()))
7490 or die_error(500, "Open git-log failed");
7492 git_header_html();
7494 git_print_page_nav('','', $hash,$co{'tree'},$hash);
7495 git_print_header_div('commit', esc_html($co{'title'}), $hash);
7497 print "<table class=\"pickaxe search\">\n";
7498 my $alternate = 1;
7499 undef %co;
7500 my @files;
7501 while (my $line = to_utf8(scalar <$fd>)) {
7502 chomp $line;
7503 next unless $line;
7505 my %set = parse_difftree_raw_line($line);
7506 if (defined $set{'commit'}) {
7507 # finish previous commit
7508 if (%co) {
7509 print "</td>\n" .
7510 "<td class=\"link\">" .
7511 $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})},
7512 "commit") .
7513 " | " .
7514 $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'},
7515 hash_base=>$co{'id'})},
7516 "tree") .
7517 "</td>\n" .
7518 "</tr>\n";
7521 if ($alternate) {
7522 print "<tr class=\"dark\">\n";
7523 } else {
7524 print "<tr class=\"light\">\n";
7526 $alternate ^= 1;
7527 %co = parse_commit($set{'commit'});
7528 my $author = chop_and_escape_str($co{'author_name'}, 15, 5);
7529 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
7530 "<td><i>$author</i></td>\n" .
7531 "<td>" .
7532 $cgi->a({-href => href(action=>"commit", hash=>$co{'id'}),
7533 -class => "list subject"},
7534 chop_and_escape_str($co{'title'}, 50) . "<br/>");
7535 } elsif (defined $set{'to_id'}) {
7536 next if ($set{'to_id'} =~ m/^0{40}$/);
7538 print $cgi->a({-href => href(action=>"blob", hash_base=>$co{'id'},
7539 hash=>$set{'to_id'}, file_name=>$set{'to_file'}),
7540 -class => "list"},
7541 "<span class=\"match\">" . esc_path($set{'file'}) . "</span>") .
7542 "<br/>\n";
7545 close $fd;
7547 # finish last commit (warning: repetition!)
7548 if (%co) {
7549 print "</td>\n" .
7550 "<td class=\"link\">" .
7551 $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})},
7552 "commit") .
7553 " | " .
7554 $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'},
7555 hash_base=>$co{'id'})},
7556 "tree") .
7557 "</td>\n" .
7558 "</tr>\n";
7561 print "</table>\n";
7563 git_footer_html();
7566 sub git_search_files {
7567 my %co = @_;
7569 local $/ = "\n";
7570 defined(my $fd = git_cmd_pipe 'grep', '-n', '-z',
7571 $search_use_regexp ? ('-E', '-i') : '-F',
7572 $searchtext, $co{'tree'})
7573 or die_error(500, "Open git-grep failed");
7575 git_header_html();
7577 git_print_page_nav('','', $hash,$co{'tree'},$hash);
7578 git_print_header_div('commit', esc_html($co{'title'}), $hash);
7580 print "<table class=\"grep_search\">\n";
7581 my $alternate = 1;
7582 my $matches = 0;
7583 my $lastfile = '';
7584 my $file_href;
7585 while (my $line = to_utf8(scalar <$fd>)) {
7586 chomp $line;
7587 my ($file, $lno, $ltext, $binary);
7588 last if ($matches++ > 1000);
7589 if ($line =~ /^Binary file (.+) matches$/) {
7590 $file = $1;
7591 $binary = 1;
7592 } else {
7593 ($file, $lno, $ltext) = split(/\0/, $line, 3);
7594 $file =~ s/^$co{'tree'}://;
7596 if ($file ne $lastfile) {
7597 $lastfile and print "</td></tr>\n";
7598 if ($alternate++) {
7599 print "<tr class=\"dark\">\n";
7600 } else {
7601 print "<tr class=\"light\">\n";
7603 $file_href = href(action=>"blob", hash_base=>$co{'id'},
7604 file_name=>$file);
7605 print "<td class=\"list\">".
7606 $cgi->a({-href => $file_href, -class => "list"}, esc_path($file));
7607 print "</td><td>\n";
7608 $lastfile = $file;
7610 if ($binary) {
7611 print "<div class=\"binary\">Binary file</div>\n";
7612 } else {
7613 $ltext = untabify($ltext);
7614 if ($ltext =~ m/^(.*)($search_regexp)(.*)$/i) {
7615 $ltext = esc_html($1, -nbsp=>1);
7616 $ltext .= '<span class="match">';
7617 $ltext .= esc_html($2, -nbsp=>1);
7618 $ltext .= '</span>';
7619 $ltext .= esc_html($3, -nbsp=>1);
7620 } else {
7621 $ltext = esc_html($ltext, -nbsp=>1);
7623 print "<div class=\"pre\">" .
7624 $cgi->a({-href => $file_href.'#l'.$lno,
7625 -class => "linenr"}, sprintf('%4i ', $lno)) .
7626 $ltext . "</div>\n";
7629 if ($lastfile) {
7630 print "</td></tr>\n";
7631 if ($matches > 1000) {
7632 print "<div class=\"diff nodifferences\">Too many matches, listing trimmed</div>\n";
7634 } else {
7635 print "<div class=\"diff nodifferences\">No matches found</div>\n";
7637 close $fd;
7639 print "</table>\n";
7641 git_footer_html();
7644 sub git_search_grep_body {
7645 my ($commitlist, $from, $to, $extra) = @_;
7646 $from = 0 unless defined $from;
7647 $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
7649 print "<table class=\"commit_search\">\n";
7650 my $alternate = 1;
7651 for (my $i = $from; $i <= $to; $i++) {
7652 my %co = %{$commitlist->[$i]};
7653 if (!%co) {
7654 next;
7656 my $commit = $co{'id'};
7657 if ($alternate) {
7658 print "<tr class=\"dark\">\n";
7659 } else {
7660 print "<tr class=\"light\">\n";
7662 $alternate ^= 1;
7663 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
7664 format_author_html('td', \%co, 15, 5) .
7665 "<td>" .
7666 $cgi->a({-href => href(action=>"commit", hash=>$co{'id'}),
7667 -class => "list subject"},
7668 chop_and_escape_str($co{'title'}, 50) . "<br/>");
7669 my $comment = $co{'comment'};
7670 foreach my $line (@$comment) {
7671 if ($line =~ m/^(.*?)($search_regexp)(.*)$/i) {
7672 my ($lead, $match, $trail) = ($1, $2, $3);
7673 $match = chop_str($match, 70, 5, 'center');
7674 my $contextlen = int((80 - length($match))/2);
7675 $contextlen = 30 if ($contextlen > 30);
7676 $lead = chop_str($lead, $contextlen, 10, 'left');
7677 $trail = chop_str($trail, $contextlen, 10, 'right');
7679 $lead = esc_html($lead);
7680 $match = esc_html($match);
7681 $trail = esc_html($trail);
7683 print "$lead<span class=\"match\">$match</span>$trail<br />";
7686 print "</td>\n" .
7687 "<td class=\"link\">" .
7688 $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") .
7689 " | " .
7690 $cgi->a({-href => href(action=>"commitdiff", hash=>$co{'id'})}, "commitdiff") .
7691 " | " .
7692 $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree");
7693 print "</td>\n" .
7694 "</tr>\n";
7696 if (defined $extra) {
7697 print "<tr>\n" .
7698 "<td colspan=\"3\">$extra</td>\n" .
7699 "</tr>\n";
7701 print "</table>\n";
7704 ## ======================================================================
7705 ## ======================================================================
7706 ## actions
7708 sub git_project_list_load {
7709 my $empty_list_ok = shift;
7710 my $order = $input_params{'order'};
7711 if (defined $order && $order !~ m/none|project|descr|owner|age/) {
7712 die_error(400, "Unknown order parameter");
7715 my @list = git_get_projects_list($project_filter, $strict_export);
7716 if ($project_filter && (!@list || !gitweb_check_feature('forks'))) {
7717 push @list, { 'path' => "$project_filter.git" }
7718 if is_valid_project("$project_filter.git");
7720 if (!@list) {
7721 die_error(404, "No projects found") unless $empty_list_ok;
7724 return (\@list, $order);
7727 sub git_frontpage {
7728 my ($projlist, $order);
7730 if ($frontpage_no_project_list) {
7731 $project = undef;
7732 $project_filter = undef;
7733 } else {
7734 ($projlist, $order) = git_project_list_load(1);
7736 git_header_html();
7737 if (defined $home_text && -f $home_text) {
7738 print "<div class=\"index_include\">\n";
7739 insert_file($home_text);
7740 print "</div>\n";
7742 git_project_search_form($searchtext, $search_use_regexp);
7743 if ($frontpage_no_project_list) {
7744 my $show_ctags = gitweb_check_feature('ctags');
7745 if ($frontpage_no_project_list == 1 and $show_ctags) {
7746 my @projects = git_get_projects_list($project_filter, $strict_export);
7747 @projects = filter_forks_from_projects_list(\@projects) if gitweb_check_feature('forks');
7748 @projects = fill_project_list_info(\@projects, 'ctags');
7749 my $ctags = git_gather_all_ctags(\@projects);
7750 my $cloud = git_populate_project_tagcloud($ctags, 'project_list');
7751 print git_show_project_tagcloud($cloud, 64);
7753 } else {
7754 git_project_list_body($projlist, $order, undef, undef, undef, undef, undef, 1);
7756 git_footer_html();
7759 sub git_project_list {
7760 my ($projlist, $order) = git_project_list_load();
7761 git_header_html();
7762 if (!$frontpage_no_project_list && defined $home_text && -f $home_text) {
7763 print "<div class=\"index_include\">\n";
7764 insert_file($home_text);
7765 print "</div>\n";
7767 git_project_search_form();
7768 git_project_list_body($projlist, $order, undef, undef, undef, undef, undef, 1);
7769 git_footer_html();
7772 sub git_forks {
7773 my $order = $input_params{'order'};
7774 if (defined $order && $order !~ m/none|project|descr|owner|age/) {
7775 die_error(400, "Unknown order parameter");
7778 my $filter = $project;
7779 $filter =~ s/\.git$//;
7780 my @list = git_get_projects_list($filter);
7781 if (!@list) {
7782 die_error(404, "No forks found");
7785 git_header_html();
7786 git_print_page_nav('','');
7787 git_print_header_div('summary', "$project forks");
7788 git_project_list_body(\@list, $order, undef, undef, undef, undef, 'forks');
7789 git_footer_html();
7792 sub git_project_index {
7793 my @projects = git_get_projects_list($project_filter, $strict_export);
7794 if (!@projects) {
7795 die_error(404, "No projects found");
7798 print $cgi->header(
7799 -type => 'text/plain',
7800 -charset => 'utf-8',
7801 -content_disposition => 'inline; filename="index.aux"');
7803 foreach my $pr (@projects) {
7804 if (!exists $pr->{'owner'}) {
7805 $pr->{'owner'} = git_get_project_owner("$pr->{'path'}");
7808 my ($path, $owner) = ($pr->{'path'}, $pr->{'owner'});
7809 # quote as in CGI::Util::encode, but keep the slash, and use '+' for ' '
7810 $path =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf("%%%02X", ord($1))/eg;
7811 $owner =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf("%%%02X", ord($1))/eg;
7812 $path =~ s/ /\+/g;
7813 $owner =~ s/ /\+/g;
7815 print "$path $owner\n";
7819 sub git_summary {
7820 my $descr = git_get_project_description($project) || "none";
7821 my %co = parse_commit("HEAD");
7822 my %cd = %co ? parse_date($co{'committer_epoch'}, $co{'committer_tz'}) : ();
7823 my $head = $co{'id'};
7824 my $remote_heads = gitweb_check_feature('remote_heads');
7826 my $owner = git_get_project_owner($project);
7827 my $homepage = git_get_project_config('homepage');
7828 my $base_url = git_get_project_config('baseurl');
7830 my $refs = git_get_references();
7831 # These get_*_list functions return one more to allow us to see if
7832 # there are more ...
7833 my @taglist = git_get_tags_list(16);
7834 my @headlist = git_get_heads_list(16);
7835 my %remotedata = $remote_heads ? git_get_remotes_list() : ();
7836 my @forklist;
7837 my $check_forks = gitweb_check_feature('forks');
7839 if ($check_forks) {
7840 # find forks of a project
7841 my $filter = $project;
7842 $filter =~ s/\.git$//;
7843 @forklist = git_get_projects_list($filter);
7844 # filter out forks of forks
7845 @forklist = filter_forks_from_projects_list(\@forklist)
7846 if (@forklist);
7849 git_header_html();
7850 git_print_page_nav('summary','', $head);
7852 if ($check_forks and $project =~ m#/#) {
7853 my $xproject = $project; $xproject =~ s#/[^/]+$#.git#; #
7854 if (is_valid_project($xproject)) {
7855 my $r = $cgi->a({-href=> href(project => $xproject, action => 'summary')}, $xproject);
7856 print <<EOT;
7857 <div class="forkinfo">
7858 This project is a fork of the $r project. If you have that one
7859 already cloned locally, you can use
7860 <pre>git clone --reference /path/to/your/$xproject/incarnation mirror_URL</pre>
7861 to save bandwidth during cloning.
7862 </div>
7867 print "<div class=\"title\">&#160;</div>\n";
7868 print "<table class=\"projects_list\">\n" .
7869 "<tr id=\"metadata_desc\"><td>description</td><td>" . esc_html($descr) . "</td></tr>\n";
7870 if ($homepage) {
7871 print "<tr id=\"metadata_homepage\"><td>homepage&#160;URL</td><td>" . $cgi->a({-href => $homepage}, $homepage) . "</td></tr>\n";
7873 if ($base_url) {
7874 print "<tr id=\"metadata_baseurl\"><td>repository&#160;URL</td><td>" . esc_html($base_url) . "</td></tr>\n";
7876 if ($owner and not $omit_owner) {
7877 print "<tr id=\"metadata_owner\"><td>owner</td><td>" . ($owner_link_hook
7878 ? $cgi->a({-href => $owner_link_hook->($owner)}, email_obfuscate($owner))
7879 : email_obfuscate($owner)) . "</td></tr>\n";
7881 if (defined $cd{'rfc2822'}) {
7882 print "<tr id=\"metadata_lchange\"><td>last&#160;change</td>" .
7883 "<td>".format_timestamp_html(\%cd)."</td></tr>\n";
7885 print format_lastrefresh_row(), "\n";
7887 # use per project git URL list in $projectroot/$project/cloneurl
7888 # or make project git URL from git base URL and project name
7889 my $url_tag = $base_url ? "mirror&#160;URL" : "URL";
7890 my $url_class = "metadata_url";
7891 my @url_list = git_get_project_url_list($project);
7892 unless (@url_list) {
7893 @url_list = @git_base_url_list;
7894 if ((git_get_project_config("showpush", '--bool')||'false') eq "true" ||
7895 -f "$projectroot/$project/.nofetch") {
7896 my $pushidx = @url_list;
7897 foreach (@git_base_push_urls) {
7898 if ($https_hint_html && !ref($_) && $_ =~ /^https:/i) {
7899 push(@url_list, [$_, $https_hint_html]);
7900 } else {
7901 push(@url_list, $_);
7904 if ($#url_list >= $pushidx) {
7905 my $pushtag = "push&#160;URL";
7906 my $classtag = "metadata_pushurl";
7907 if (ref($url_list[$pushidx])) {
7908 $url_list[$pushidx] = [
7909 ${$url_list[$pushidx]}[0],
7910 ${$url_list[$pushidx]}[1],
7911 $pushtag,
7912 $classtag];
7913 } else {
7914 $url_list[$pushidx] = [
7915 $url_list[$pushidx],
7916 undef,
7917 $pushtag,
7918 $classtag];
7921 } else {
7922 push(@url_list, @git_base_mirror_urls);
7924 for (my $i=0; $i<=$#url_list; ++$i) {
7925 if (ref($url_list[$i])) {
7926 $url_list[$i] = [
7927 ${$url_list[$i]}[0] . "/$project",
7928 ${$url_list[$i]}[1],
7929 ${$url_list[$i]}[2],
7930 ${$url_list[$i]}[3]];
7931 } else {
7932 $url_list[$i] .= "/$project";
7936 foreach (@url_list) {
7937 next unless $_;
7938 my $git_url;
7939 my $html_hint = "";
7940 my $next_tag = undef;
7941 my $next_class = undef;
7942 if (ref($_)) {
7943 $git_url = $$_[0];
7944 $html_hint = "&#160;" . $$_[1] if defined($$_[1]);
7945 $next_tag = $$_[2];
7946 $next_class = $$_[3];
7947 } else {
7948 $git_url = $_;
7950 next unless $git_url;
7951 $url_class = $next_class if $next_class;
7952 $url_tag = $next_tag if $next_tag;
7953 print "<tr class=\"$url_class\"><td>$url_tag</td><td>$git_url$html_hint</td></tr>\n";
7954 $url_tag = "";
7957 if (defined($git_base_bundles_url) && -d "$projectroot/$project/bundles") {
7958 my $projname = $project;
7959 $projname =~ s|^.*/||;
7960 my $url = "$git_base_bundles_url/$project/bundles";
7961 print format_repo_url(
7962 "bundle&#160;info",
7963 "<a rel='nofollow' href='$url'>$projname downloadable bundles</a>");
7966 # Tag cloud
7967 my $show_ctags = gitweb_check_feature('ctags');
7968 if ($show_ctags) {
7969 my $ctags = git_get_project_ctags($project);
7970 if (%$ctags || $show_ctags !~ /^\d+$/) {
7971 # without ability to add tags, don't show if there are none
7972 my $cloud = git_populate_project_tagcloud($ctags, 'project_list');
7973 print "<tr id=\"metadata_ctags\">" .
7974 "<td style=\"vertical-align:middle\">content&#160;tags<br />";
7975 print "</td>\n<td>" unless %$ctags;
7976 print "<form action=\"$show_ctags\" method=\"post\" style=\"white-space:nowrap\">" .
7977 "<input type=\"hidden\" name=\"p\" value=\"$project\"/>" .
7978 "add: <input type=\"text\" name=\"t\" size=\"8\" /></form>"
7979 unless $show_ctags =~ /^\d+$/;
7980 print "</td>\n<td>" if %$ctags;
7981 print git_show_project_tagcloud($cloud, 48)."</td>" .
7982 "</tr>\n";
7986 print "</table>\n";
7988 # If XSS prevention is on, we don't include README.html.
7989 # TODO: Allow a readme in some safe format.
7990 if (!$prevent_xss) {
7991 my $readme_name = "readme";
7992 my $readme;
7993 if (-s "$projectroot/$project/README.html") {
7994 $readme = collect_html_file("$projectroot/$project/README.html");
7995 } else {
7996 $readme = collect_output($git_automatic_readme_html, "$projectroot/$project");
7997 if ($readme && $readme =~ /^<!-- README NAME: ((?:[^-]|(?:-(?!-)))+) -->/) {
7998 $readme_name = $1;
7999 $readme =~ s/^<!--(?:[^-]|(?:-(?!-)))*-->\n?//;
8002 if (defined($readme)) {
8003 $readme =~ s/^\s+//s;
8004 $readme =~ s/\s+$//s;
8005 print "<div class=\"title\">$readme_name</div>\n",
8006 "<div class=\"readme\">\n",
8007 $readme,
8008 "\n</div>\n"
8009 if $readme ne '';
8013 # we need to request one more than 16 (0..15) to check if
8014 # those 16 are all
8015 my @commitlist = $head ? parse_commits($head, 17) : ();
8016 if (@commitlist) {
8017 git_print_header_div('shortlog');
8018 git_shortlog_body(\@commitlist, 0, 15, $refs,
8019 $#commitlist <= 15 ? undef :
8020 $cgi->a({-href => href(action=>"shortlog")}, "..."));
8023 if (@taglist) {
8024 git_print_header_div('tags');
8025 git_tags_body(\@taglist, 0, 15,
8026 $#taglist <= 15 ? undef :
8027 $cgi->a({-href => href(action=>"tags")}, "..."));
8030 if (@headlist) {
8031 git_print_header_div('heads');
8032 git_heads_body(\@headlist, $head, 0, 15,
8033 $#headlist <= 15 ? undef :
8034 $cgi->a({-href => href(action=>"heads")}, "..."));
8037 if (%remotedata) {
8038 git_print_header_div('remotes');
8039 git_remotes_body(\%remotedata, 15, $head);
8042 if (@forklist) {
8043 git_print_header_div('forks');
8044 git_project_list_body(\@forklist, 'age', 0, 15,
8045 $#forklist <= 15 ? undef :
8046 $cgi->a({-href => href(action=>"forks")}, "..."),
8047 'no_header', 'forks');
8050 git_footer_html();
8053 sub git_tag {
8054 my %tag = parse_tag($hash);
8056 if (! %tag) {
8057 die_error(404, "Unknown tag object");
8060 my $fullhash;
8061 $fullhash = $hash if $hash =~ m/^[0-9a-fA-F]{40}$/;
8062 $fullhash = git_get_full_hash($project, $hash) unless $fullhash;
8064 my $obj = $tag{'object'};
8065 git_header_html();
8066 if ($tag{'type'} eq 'commit') {
8067 git_print_page_nav('','', $obj,undef,$obj);
8068 git_print_header_div('commit', esc_html($tag{'name'}), $hash);
8069 } else {
8070 if ($tag{'type'} eq 'tree') {
8071 git_print_page_nav('',['commit','commitdiff'], undef,undef,$obj);
8072 } else {
8073 git_print_page_nav('',['commit','commitdiff','tree'], undef,undef,undef);
8075 print "<div class=\"title\">".esc_html($hash)."</div>\n";
8077 print "<div class=\"title_text\">\n" .
8078 "<table class=\"object_header\">\n" .
8079 "<tr><td>tag</td><td class=\"sha1\">$fullhash</td></tr>\n" .
8080 "<tr>\n" .
8081 "<td>object</td>\n" .
8082 "<td>" . $cgi->a({-class => "list", -href => href(action=>$tag{'type'}, hash=>$tag{'object'})},
8083 $tag{'object'}) . "</td>\n" .
8084 "<td class=\"link\">" . $cgi->a({-href => href(action=>$tag{'type'}, hash=>$tag{'object'})},
8085 $tag{'type'}) . "</td>\n" .
8086 "</tr>\n";
8087 if (defined($tag{'author'})) {
8088 git_print_authorship_rows(\%tag, 'author');
8090 print "</table>\n\n" .
8091 "</div>\n";
8092 print "<div class=\"page_body\">";
8093 my $comment = $tag{'comment'};
8094 foreach my $line (@$comment) {
8095 chomp $line;
8096 print esc_html($line, -nbsp=>1) . "<br/>\n";
8098 print "</div>\n";
8099 git_footer_html();
8102 sub git_blame_common {
8103 my $format = shift || 'porcelain';
8104 if ($format eq 'porcelain' && $input_params{'javascript'}) {
8105 $format = 'incremental';
8106 $action = 'blame_incremental'; # for page title etc
8109 # permissions
8110 gitweb_check_feature('blame')
8111 or die_error(403, "Blame view not allowed");
8113 # error checking
8114 die_error(400, "No file name given") unless $file_name;
8115 $hash_base ||= git_get_head_hash($project);
8116 die_error(404, "Couldn't find base commit") unless $hash_base;
8117 my %co = parse_commit($hash_base)
8118 or die_error(404, "Commit not found");
8119 my $ftype = "blob";
8120 if (!defined $hash) {
8121 $hash = git_get_hash_by_path($hash_base, $file_name, "blob")
8122 or die_error(404, "Error looking up file");
8123 } else {
8124 $ftype = git_get_type($hash);
8125 if ($ftype !~ "blob") {
8126 die_error(400, "Object is not a blob");
8130 my $fd;
8131 if ($format eq 'incremental') {
8132 # get file contents (as base)
8133 defined($fd = git_cmd_pipe 'cat-file', 'blob', $hash)
8134 or die_error(500, "Open git-cat-file failed");
8135 } elsif ($format eq 'data') {
8136 # run git-blame --incremental
8137 defined($fd = git_cmd_pipe "blame", "--incremental",
8138 $hash_base, "--", $file_name)
8139 or die_error(500, "Open git-blame --incremental failed");
8140 } else {
8141 # run git-blame --porcelain
8142 defined($fd = git_cmd_pipe "blame", '-p',
8143 $hash_base, '--', $file_name)
8144 or die_error(500, "Open git-blame --porcelain failed");
8147 # incremental blame data returns early
8148 if ($format eq 'data') {
8149 print $cgi->header(
8150 -type=>"text/plain", -charset => "utf-8",
8151 -status=> "200 OK");
8152 local $| = 1; # output autoflush
8153 while (<$fd>) {
8154 print to_utf8($_);
8156 close $fd
8157 or print "ERROR $!\n";
8159 print 'END';
8160 if (defined $t0 && gitweb_check_feature('timed')) {
8161 print ' '.
8162 tv_interval($t0, [ gettimeofday() ]).
8163 ' '.$number_of_git_cmds;
8165 print "\n";
8167 return;
8170 # page header
8171 git_header_html();
8172 my $formats_nav =
8173 $cgi->a({-href => href(action=>"blob", -replay=>1)},
8174 "blob");
8175 $formats_nav .=
8176 " | " .
8177 $cgi->a({-href => href(action=>"history", -replay=>1)},
8178 "history") .
8179 " | " .
8180 $cgi->a({-href => href(action=>$action, file_name=>$file_name)},
8181 "HEAD");
8182 git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
8183 git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
8184 git_print_page_path($file_name, $ftype, $hash_base);
8186 # page body
8187 if ($format eq 'incremental') {
8188 print "<noscript>\n<div class=\"error\"><center><b>\n".
8189 "This page requires JavaScript to run.\n Use ".
8190 $cgi->a({-href => href(action=>'blame',javascript=>0,-replay=>1)},
8191 'this page').
8192 " instead.\n".
8193 "</b></center></div>\n</noscript>\n";
8195 print qq!<div id="progress_bar" style="width: 100%; background-color: yellow"></div>\n!;
8198 print qq!<div class="page_body">\n!;
8199 print qq!<div id="progress_info">... / ...</div>\n!
8200 if ($format eq 'incremental');
8201 print qq!<table id="blame_table" class="blame" width="100%">\n!.
8202 #qq!<col width="5.5em" /><col width="2.5em" /><col width="*" />\n!.
8203 qq!<thead>\n!.
8204 qq!<tr><th nowrap="nowrap" style="white-space:nowrap">!.
8205 qq!Commit&#160;<a href="javascript:extra_blame_columns()" id="columns_expander" !.
8206 qq!title="toggles blame author information display">[+]</a></th>!.
8207 qq!<th class="extra_column">Author</th><th class="extra_column">Date</th>!.
8208 qq!<th>Line</th><th width="100%">Data</th></tr>\n!.
8209 qq!</thead>\n!.
8210 qq!<tbody>\n!;
8212 my @rev_color = qw(light dark);
8213 my $num_colors = scalar(@rev_color);
8214 my $current_color = 0;
8216 if ($format eq 'incremental') {
8217 my $color_class = $rev_color[$current_color];
8219 #contents of a file
8220 my $linenr = 0;
8221 LINE:
8222 while (my $line = to_utf8(scalar <$fd>)) {
8223 chomp $line;
8224 $linenr++;
8226 print qq!<tr id="l$linenr" class="$color_class">!.
8227 qq!<td class="sha1"><a href=""> </a></td>!.
8228 qq!<td class="extra_column" nowrap="nowrap"></td>!.
8229 qq!<td class="extra_column" nowrap="nowrap"></td>!.
8230 qq!<td class="linenr">!.
8231 qq!<a class="linenr" href="">$linenr</a></td>!;
8232 print qq!<td class="pre">! . esc_html($line) . "</td>\n";
8233 print qq!</tr>\n!;
8236 } else { # porcelain, i.e. ordinary blame
8237 my %metainfo = (); # saves information about commits
8239 # blame data
8240 LINE:
8241 while (my $line = to_utf8(scalar <$fd>)) {
8242 chomp $line;
8243 # the header: <SHA-1> <src lineno> <dst lineno> [<lines in group>]
8244 # no <lines in group> for subsequent lines in group of lines
8245 my ($full_rev, $orig_lineno, $lineno, $group_size) =
8246 ($line =~ /^([0-9a-f]{40}) (\d+) (\d+)(?: (\d+))?$/);
8247 if (!exists $metainfo{$full_rev}) {
8248 $metainfo{$full_rev} = { 'nprevious' => 0 };
8250 my $meta = $metainfo{$full_rev};
8251 my $data;
8252 while ($data = to_utf8(scalar <$fd>)) {
8253 chomp $data;
8254 last if ($data =~ s/^\t//); # contents of line
8255 if ($data =~ /^(\S+)(?: (.*))?$/) {
8256 $meta->{$1} = $2 unless exists $meta->{$1};
8258 if ($data =~ /^previous /) {
8259 $meta->{'nprevious'}++;
8262 my $short_rev = substr($full_rev, 0, 8);
8263 my $author = $meta->{'author'};
8264 my %date =
8265 parse_date($meta->{'author-time'}, $meta->{'author-tz'});
8266 my $date = $date{'iso-tz'};
8267 if ($group_size) {
8268 $current_color = ($current_color + 1) % $num_colors;
8270 my $tr_class = $rev_color[$current_color];
8271 $tr_class .= ' boundary' if (exists $meta->{'boundary'});
8272 $tr_class .= ' no-previous' if ($meta->{'nprevious'} == 0);
8273 $tr_class .= ' multiple-previous' if ($meta->{'nprevious'} > 1);
8274 print "<tr id=\"l$lineno\" class=\"$tr_class\">\n";
8275 if ($group_size) {
8276 my $rowspan = $group_size > 1 ? " rowspan=\"$group_size\"" : "";
8277 print "<td class=\"sha1\"";
8278 print " title=\"". esc_html($author) . ", $date\"";
8279 print "$rowspan>";
8280 print $cgi->a({-href => href(action=>"commit",
8281 hash=>$full_rev,
8282 file_name=>$file_name)},
8283 esc_html($short_rev));
8284 if ($group_size >= 2) {
8285 my @author_initials = ($author =~ /\b([[:upper:]])\B/g);
8286 if (@author_initials) {
8287 print "<br />" .
8288 esc_html(join('', @author_initials));
8289 # or join('.', ...)
8292 print "</td>\n";
8293 print "<td class=\"extra_column\" nowrap=\"nowrap\"$rowspan>". esc_html($author) . "</td>";
8294 print "<td class=\"extra_column\" nowrap=\"nowrap\"$rowspan>". $date . "</td>";
8296 # 'previous' <sha1 of parent commit> <filename at commit>
8297 if (exists $meta->{'previous'} &&
8298 $meta->{'previous'} =~ /^([a-fA-F0-9]{40}) (.*)$/) {
8299 $meta->{'parent'} = $1;
8300 $meta->{'file_parent'} = unquote($2);
8302 my $linenr_commit =
8303 exists($meta->{'parent'}) ?
8304 $meta->{'parent'} : $full_rev;
8305 my $linenr_filename =
8306 exists($meta->{'file_parent'}) ?
8307 $meta->{'file_parent'} : unquote($meta->{'filename'});
8308 my $blamed = href(action => 'blame',
8309 file_name => $linenr_filename,
8310 hash_base => $linenr_commit);
8311 print "<td class=\"linenr\">";
8312 print $cgi->a({ -href => "$blamed#l$orig_lineno",
8313 -class => "linenr" },
8314 esc_html($lineno));
8315 print "</td>";
8316 print "<td class=\"pre\">" . esc_html($data) . "</td>\n";
8317 print "</tr>\n";
8318 } # end while
8322 # footer
8323 print "</tbody>\n".
8324 "</table>\n"; # class="blame"
8325 print "</div>\n"; # class="blame_body"
8326 close $fd
8327 or print "Reading blob failed\n";
8329 git_footer_html();
8332 sub git_blame {
8333 git_blame_common();
8336 sub git_blame_incremental {
8337 git_blame_common('incremental');
8340 sub git_blame_data {
8341 git_blame_common('data');
8344 sub git_tags {
8345 my $head = git_get_head_hash($project);
8346 git_header_html();
8347 git_print_page_nav('','', $head,undef,$head,format_ref_views('tags'));
8348 git_print_header_div('summary', $project);
8350 my @tagslist = git_get_tags_list();
8351 if (@tagslist) {
8352 git_tags_body(\@tagslist);
8354 git_footer_html();
8357 sub git_refs {
8358 my $order = $input_params{'order'};
8359 if (defined $order && $order !~ m/age|name/) {
8360 die_error(400, "Unknown order parameter");
8363 my $head = git_get_head_hash($project);
8364 git_header_html();
8365 git_print_page_nav('','', $head,undef,$head,format_ref_views('refs'));
8366 git_print_header_div('summary', $project);
8368 my @refslist = git_get_tags_list(undef, 1, $order);
8369 if (@refslist) {
8370 git_tags_body(\@refslist, undef, undef, undef, $head, 1, $order);
8372 git_footer_html();
8375 sub git_heads {
8376 my $head = git_get_head_hash($project);
8377 git_header_html();
8378 git_print_page_nav('','', $head,undef,$head,format_ref_views('heads'));
8379 git_print_header_div('summary', $project);
8381 my @headslist = git_get_heads_list();
8382 if (@headslist) {
8383 git_heads_body(\@headslist, $head);
8385 git_footer_html();
8388 # used both for single remote view and for list of all the remotes
8389 sub git_remotes {
8390 gitweb_check_feature('remote_heads')
8391 or die_error(403, "Remote heads view is disabled");
8393 my $head = git_get_head_hash($project);
8394 my $remote = $input_params{'hash'};
8396 my $remotedata = git_get_remotes_list($remote);
8397 die_error(500, "Unable to get remote information") unless defined $remotedata;
8399 unless (%$remotedata) {
8400 die_error(404, defined $remote ?
8401 "Remote $remote not found" :
8402 "No remotes found");
8405 git_header_html(undef, undef, -action_extra => $remote);
8406 git_print_page_nav('', '', $head, undef, $head,
8407 format_ref_views($remote ? '' : 'remotes'));
8409 fill_remote_heads($remotedata);
8410 if (defined $remote) {
8411 git_print_header_div('remotes', "$remote remote for $project");
8412 git_remote_block($remote, $remotedata->{$remote}, undef, $head);
8413 } else {
8414 git_print_header_div('summary', "$project remotes");
8415 git_remotes_body($remotedata, undef, $head);
8418 git_footer_html();
8421 sub git_blob_plain {
8422 my $type = shift;
8423 my $expires;
8425 if (!defined $hash) {
8426 if (defined $file_name) {
8427 my $base = $hash_base || git_get_head_hash($project);
8428 $hash = git_get_hash_by_path($base, $file_name, "blob")
8429 or die_error(404, "Cannot find file");
8430 } else {
8431 die_error(400, "No file name defined");
8433 } elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) {
8434 # blobs defined by non-textual hash id's can be cached
8435 $expires = "+1d";
8438 defined(my $fd = git_cmd_pipe "cat-file", "blob", $hash)
8439 or die_error(500, "Open git-cat-file blob '$hash' failed");
8440 binmode($fd);
8442 # content-type (can include charset)
8443 my $leader;
8444 ($type, $leader) = blob_contenttype($fd, $file_name, $type);
8446 # "save as" filename, even when no $file_name is given
8447 my $save_as = "$hash";
8448 if (defined $file_name) {
8449 $save_as = $file_name;
8450 } elsif ($type =~ m/^text\//) {
8451 $save_as .= '.txt';
8454 # With XSS prevention on, blobs of all types except a few known safe
8455 # ones are served with "Content-Disposition: attachment" to make sure
8456 # they don't run in our security domain. For certain image types,
8457 # blob view writes an <img> tag referring to blob_plain view, and we
8458 # want to be sure not to break that by serving the image as an
8459 # attachment (though Firefox 3 doesn't seem to care).
8460 my $sandbox = $prevent_xss &&
8461 $type !~ m!^(?:text/[a-z]+|image/(?:gif|png|jpeg))(?:[ ;]|$)!;
8463 # serve text/* as text/plain
8464 if ($prevent_xss &&
8465 ($type =~ m!^text/[a-z]+\b(.*)$! ||
8466 ($type =~ m!^[a-z]+/[a-z]\+xml\b(.*)$! && -T $fd))) {
8467 my $rest = $1;
8468 $rest = defined $rest ? $rest : '';
8469 $type = "text/plain$rest";
8472 print $cgi->header(
8473 -type => $type,
8474 -expires => $expires,
8475 -content_disposition =>
8476 ($sandbox ? 'attachment' : 'inline')
8477 . '; filename="' . $save_as . '"');
8478 binmode STDOUT, ':raw';
8479 $fcgi_raw_mode = 1;
8480 print $leader if defined $leader;
8481 my $buf;
8482 while (read($fd, $buf, 32768)) {
8483 print $buf;
8485 binmode STDOUT, ':utf8'; # as set at the beginning of gitweb.cgi
8486 $fcgi_raw_mode = 0;
8487 close $fd;
8490 sub git_blob {
8491 my $expires;
8493 if (!defined $hash) {
8494 if (defined $file_name) {
8495 my $base = $hash_base || git_get_head_hash($project);
8496 $hash = git_get_hash_by_path($base, $file_name, "blob")
8497 or die_error(404, "Cannot find file");
8498 } else {
8499 die_error(400, "No file name defined");
8501 } elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) {
8502 # blobs defined by non-textual hash id's can be cached
8503 $expires = "+1d";
8505 my $fullhash = git_get_full_hash($project, "$hash^{blob}");
8506 die_error(404, "No such blob") unless defined($fullhash);
8508 my $have_blame = gitweb_check_feature('blame');
8509 defined(my $fd = git_cmd_pipe "cat-file", "blob", $fullhash)
8510 or die_error(500, "Couldn't cat $file_name, $hash");
8511 binmode($fd);
8512 my $mimetype = blob_mimetype($fd, $file_name);
8513 # use 'blob_plain' (aka 'raw') view for files that cannot be displayed
8514 if ($mimetype !~ m!^(?:text/|image/(?:gif|png|jpeg)$)! && -B $fd) {
8515 close $fd;
8516 return git_blob_plain($mimetype);
8518 # we can have blame only for text/* mimetype
8519 $have_blame &&= ($mimetype =~ m!^text/!);
8521 my $highlight = gitweb_check_feature('highlight') && defined $highlight_bin;
8522 my $syntax = guess_file_syntax($fd, $mimetype, $file_name) if $highlight;
8523 my $highlight_mode_active;
8524 ($fd, $highlight_mode_active) = run_highlighter($fd, $syntax) if $syntax;
8526 git_header_html(undef, $expires);
8527 my $formats_nav = '';
8528 if (defined $hash_base && (my %co = parse_commit($hash_base))) {
8529 if (defined $file_name) {
8530 if ($have_blame) {
8531 $formats_nav .=
8532 $cgi->a({-href => href(action=>"blame", -replay=>1),
8533 -class => "blamelink"},
8534 "blame") .
8535 " | ";
8537 $formats_nav .=
8538 $cgi->a({-href => href(action=>"history", -replay=>1)},
8539 "history") .
8540 " | " .
8541 $cgi->a({-href => href(action=>"blob_plain", -replay=>1)},
8542 "raw") .
8543 " | " .
8544 $cgi->a({-href => href(action=>"blob",
8545 hash_base=>"HEAD", file_name=>$file_name)},
8546 "HEAD");
8547 } else {
8548 $formats_nav .=
8549 $cgi->a({-href => href(action=>"blob_plain", -replay=>1)},
8550 "raw");
8552 git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
8553 git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
8554 } else {
8555 git_print_page_nav('',['commit','commitdiff','tree'], undef,undef,undef);
8556 print "<div class=\"title\">".esc_html($hash)."</div>\n";
8558 git_print_page_path($file_name, "blob", $hash_base);
8559 print "<div class=\"title_text\">\n" .
8560 "<table class=\"object_header\">\n";
8561 print "<tr><td>blob</td><td class=\"sha1\">$fullhash</td></tr>\n";
8562 print "</table>".
8563 "</div>\n";
8564 print "<div class=\"page_body\">\n";
8565 if ($mimetype =~ m!^image/!) {
8566 print qq!<img class="blob" type="!.esc_attr($mimetype).qq!"!;
8567 if ($file_name) {
8568 print qq! alt="!.esc_attr($file_name).qq!" title="!.esc_attr($file_name).qq!"!;
8570 print qq! src="! .
8571 href(action=>"blob_plain", hash=>$hash,
8572 hash_base=>$hash_base, file_name=>$file_name) .
8573 qq!" />\n!;
8574 close $fd; # ignore likely EPIPE error from child
8575 } else {
8576 my $nr;
8577 while (my $line = to_utf8(scalar <$fd>)) {
8578 chomp $line;
8579 $nr++;
8580 $line = untabify($line);
8581 printf qq!<div class="pre"><a id="l%i" href="%s#l%i" class="linenr">%4i </a>%s</div>\n!,
8582 $nr, esc_attr(href(-replay => 1)), $nr, $nr,
8583 $highlight_mode_active ? sanitize($line) : esc_html($line, -nbsp=>1);
8585 close $fd
8586 or print "Reading blob failed.\n";
8588 print "</div>";
8589 git_footer_html();
8592 sub git_tree {
8593 if (!defined $hash_base) {
8594 $hash_base = "HEAD";
8596 if (!defined $hash) {
8597 if (defined $file_name) {
8598 $hash = git_get_hash_by_path($hash_base, $file_name, "tree");
8599 } else {
8600 $hash = $hash_base;
8603 die_error(404, "No such tree") unless defined($hash);
8604 my $fullhash = git_get_full_hash($project, "$hash^{tree}");
8605 die_error(404, "No such tree") unless defined($fullhash);
8607 my $show_sizes = gitweb_check_feature('show-sizes');
8608 my $have_blame = gitweb_check_feature('blame');
8610 my @entries = ();
8612 local $/ = "\0";
8613 defined(my $fd = git_cmd_pipe "ls-tree", '-z',
8614 ($show_sizes ? '-l' : ()), @extra_options, $fullhash)
8615 or die_error(500, "Open git-ls-tree failed");
8616 @entries = map { chomp; to_utf8($_) } <$fd>;
8617 close $fd
8618 or die_error(404, "Reading tree failed");
8621 git_header_html();
8622 my $basedir = '';
8623 if (defined $hash_base && (my %co = parse_commit($hash_base))) {
8624 my $refs = git_get_references();
8625 my $ref = format_ref_marker($refs, $co{'id'});
8626 my @views_nav = ();
8627 if (defined $file_name) {
8628 push @views_nav,
8629 $cgi->a({-href => href(action=>"history", -replay=>1)},
8630 "history"),
8631 $cgi->a({-href => href(action=>"tree",
8632 hash_base=>"HEAD", file_name=>$file_name)},
8633 "HEAD"),
8635 my $snapshot_links = format_snapshot_links($hash);
8636 if (defined $snapshot_links) {
8637 # FIXME: Should be available when we have no hash base as well.
8638 push @views_nav, $snapshot_links;
8640 git_print_page_nav('tree','', $hash_base, undef, undef,
8641 join(' | ', @views_nav));
8642 git_print_header_div('commit', esc_html($co{'title'}), $hash_base, undef, $ref);
8643 } else {
8644 git_print_page_nav('tree',['commit','commitdiff'], undef,undef,$hash_base);
8645 undef $hash_base;
8646 print "<div class=\"title\">".esc_html($hash)."</div>\n";
8648 if (defined $file_name) {
8649 $basedir = $file_name;
8650 if ($basedir ne '' && substr($basedir, -1) ne '/') {
8651 $basedir .= '/';
8653 git_print_page_path($file_name, 'tree', $hash_base);
8655 print "<div class=\"title_text\">\n" .
8656 "<table class=\"object_header\">\n";
8657 print "<tr><td>tree</td><td class=\"sha1\">$fullhash</td></tr>\n";
8658 print "</table>".
8659 "</div>\n";
8660 print "<div class=\"page_body\">\n";
8661 print "<table class=\"tree\">\n";
8662 my $alternate = 1;
8663 # '..' (top directory) link if possible
8664 if (defined $hash_base &&
8665 defined $file_name && $file_name =~ m![^/]+$!) {
8666 if ($alternate) {
8667 print "<tr class=\"dark\">\n";
8668 } else {
8669 print "<tr class=\"light\">\n";
8671 $alternate ^= 1;
8673 my $up = $file_name;
8674 $up =~ s!/?[^/]+$!!;
8675 undef $up unless $up;
8676 # based on git_print_tree_entry
8677 print '<td class="mode">' . mode_str('040000') . "</td>\n";
8678 print '<td class="size">&#160;</td>'."\n" if $show_sizes;
8679 print '<td class="list">';
8680 print $cgi->a({-href => href(action=>"tree",
8681 hash_base=>$hash_base,
8682 file_name=>$up)},
8683 "..");
8684 print "</td>\n";
8685 print "<td class=\"link\"></td>\n";
8687 print "</tr>\n";
8689 foreach my $line (@entries) {
8690 my %t = parse_ls_tree_line($line, -z => 1, -l => $show_sizes);
8692 if ($alternate) {
8693 print "<tr class=\"dark\">\n";
8694 } else {
8695 print "<tr class=\"light\">\n";
8697 $alternate ^= 1;
8699 git_print_tree_entry(\%t, $basedir, $hash_base, $have_blame);
8701 print "</tr>\n";
8703 print "</table>\n" .
8704 "</div>";
8705 git_footer_html();
8708 sub sanitize_for_filename {
8709 my $name = shift;
8711 $name =~ s!/!-!g;
8712 $name =~ s/[^[:alnum:]_.-]//g;
8714 return $name;
8717 sub snapshot_name {
8718 my ($project, $hash) = @_;
8720 # path/to/project.git -> project
8721 # path/to/project/.git -> project
8722 my $name = to_utf8($project);
8723 $name =~ s,([^/])/*\.git$,$1,;
8724 $name = sanitize_for_filename(basename($name));
8726 my $ver = $hash;
8727 if ($hash =~ /^[0-9a-fA-F]+$/) {
8728 # shorten SHA-1 hash
8729 my $full_hash = git_get_full_hash($project, $hash);
8730 if ($full_hash =~ /^$hash/ && length($hash) > 7) {
8731 $ver = git_get_short_hash($project, $hash);
8733 } elsif ($hash =~ m!^refs/tags/(.*)$!) {
8734 # tags don't need shortened SHA-1 hash
8735 $ver = $1;
8736 } else {
8737 # branches and other need shortened SHA-1 hash
8738 my $strip_refs = join '|', map { quotemeta } get_branch_refs();
8739 if ($hash =~ m!^refs/($strip_refs|remotes)/(.*)$!) {
8740 my $ref_dir = (defined $1) ? $1 : '';
8741 $ver = $2;
8743 $ref_dir = sanitize_for_filename($ref_dir);
8744 # for refs neither in heads nor remotes we want to
8745 # add a ref dir to archive name
8746 if ($ref_dir ne '' and $ref_dir ne 'heads' and $ref_dir ne 'remotes') {
8747 $ver = $ref_dir . '-' . $ver;
8750 $ver .= '-' . git_get_short_hash($project, $hash);
8752 # special case of sanitization for filename - we change
8753 # slashes to dots instead of dashes
8754 # in case of hierarchical branch names
8755 $ver =~ s!/!.!g;
8756 $ver =~ s/[^[:alnum:]_.-]//g;
8758 # name = project-version_string
8759 $name = "$name-$ver";
8761 return wantarray ? ($name, $name) : $name;
8764 sub exit_if_unmodified_since {
8765 my ($latest_epoch) = @_;
8766 our $cgi;
8768 my $if_modified = $cgi->http('IF_MODIFIED_SINCE');
8769 if (defined $if_modified) {
8770 my $since;
8771 if (eval { require HTTP::Date; 1; }) {
8772 $since = HTTP::Date::str2time($if_modified);
8773 } elsif (eval { require Time::ParseDate; 1; }) {
8774 $since = Time::ParseDate::parsedate($if_modified, GMT => 1);
8776 if (defined $since && $latest_epoch <= $since) {
8777 my %latest_date = parse_date($latest_epoch);
8778 print $cgi->header(
8779 -last_modified => $latest_date{'rfc2822'},
8780 -status => '304 Not Modified');
8781 CORE::die;
8786 sub git_snapshot {
8787 my $format = $input_params{'snapshot_format'};
8788 if (!@snapshot_fmts) {
8789 die_error(403, "Snapshots not allowed");
8791 # default to first supported snapshot format
8792 $format ||= $snapshot_fmts[0];
8793 if ($format !~ m/^[a-z0-9]+$/) {
8794 die_error(400, "Invalid snapshot format parameter");
8795 } elsif (!exists($known_snapshot_formats{$format})) {
8796 die_error(400, "Unknown snapshot format");
8797 } elsif ($known_snapshot_formats{$format}{'disabled'}) {
8798 die_error(403, "Snapshot format not allowed");
8799 } elsif (!grep($_ eq $format, @snapshot_fmts)) {
8800 die_error(403, "Unsupported snapshot format");
8803 my $type = git_get_type("$hash^{}");
8804 if (!$type) {
8805 die_error(404, 'Object does not exist');
8806 } elsif ($type eq 'blob') {
8807 die_error(400, 'Object is not a tree-ish');
8810 my ($name, $prefix) = snapshot_name($project, $hash);
8811 my $filename = "$name$known_snapshot_formats{$format}{'suffix'}";
8813 my %co = parse_commit($hash);
8814 exit_if_unmodified_since($co{'committer_epoch'}) if %co;
8816 my @cmd = (
8817 git_cmd(), 'archive',
8818 "--format=$known_snapshot_formats{$format}{'format'}",
8819 "--prefix=$prefix/", $hash);
8820 if (exists $known_snapshot_formats{$format}{'compressor'}) {
8821 @cmd = ($posix_shell_bin, '-c', quote_command(@cmd) .
8822 ' | ' . quote_command(@{$known_snapshot_formats{$format}{'compressor'}}));
8825 $filename =~ s/(["\\])/\\$1/g;
8826 my %latest_date;
8827 if (%co) {
8828 %latest_date = parse_date($co{'committer_epoch'}, $co{'committer_tz'});
8831 print $cgi->header(
8832 -type => $known_snapshot_formats{$format}{'type'},
8833 -content_disposition => 'inline; filename="' . $filename . '"',
8834 %co ? (-last_modified => $latest_date{'rfc2822'}) : (),
8835 -status => '200 OK');
8837 defined(my $fd = cmd_pipe @cmd)
8838 or die_error(500, "Execute git-archive failed");
8839 binmode($fd);
8840 binmode STDOUT, ':raw';
8841 $fcgi_raw_mode = 1;
8842 my $buf;
8843 while (read($fd, $buf, 32768)) {
8844 print $buf;
8846 binmode STDOUT, ':utf8'; # as set at the beginning of gitweb.cgi
8847 $fcgi_raw_mode = 0;
8848 close $fd;
8851 sub git_log_generic {
8852 my ($fmt_name, $body_subr, $base, $parent, $file_name, $file_hash) = @_;
8854 my $head = git_get_head_hash($project);
8855 if (!defined $base) {
8856 $base = $head;
8858 if (!defined $page) {
8859 $page = 0;
8861 my $refs = git_get_references();
8863 my $commit_hash = $base;
8864 if (defined $parent) {
8865 $commit_hash = "$parent..$base";
8867 my @commitlist =
8868 parse_commits($commit_hash, 101, (100 * $page),
8869 defined $file_name ? ($file_name, "--full-history") : ());
8871 my $ftype;
8872 if (!defined $file_hash && defined $file_name) {
8873 # some commits could have deleted file in question,
8874 # and not have it in tree, but one of them has to have it
8875 for (my $i = 0; $i < @commitlist; $i++) {
8876 $file_hash = git_get_hash_by_path($commitlist[$i]{'id'}, $file_name);
8877 last if defined $file_hash;
8880 if (defined $file_hash) {
8881 $ftype = git_get_type($file_hash);
8883 if (defined $file_name && !defined $ftype) {
8884 die_error(500, "Unknown type of object");
8886 my %co;
8887 if (defined $file_name) {
8888 %co = parse_commit($base)
8889 or die_error(404, "Unknown commit object");
8893 my $paging_nav = format_log_nav($fmt_name, $page, $#commitlist >= 100);
8894 my $next_link = '';
8895 if ($#commitlist >= 100) {
8896 $next_link =
8897 $cgi->a({-href => href(-replay=>1, page=>$page+1),
8898 -accesskey => "n", -title => "Alt-n"}, "next");
8900 my ($patch_max) = gitweb_get_feature('patches');
8901 if ($patch_max && !defined $file_name) {
8902 if ($patch_max < 0 || @commitlist <= $patch_max) {
8903 $paging_nav .= " &#183; " .
8904 $cgi->a({-href => href(action=>"patches", -replay=>1)},
8905 "patches");
8910 local $action = 'log';
8911 git_header_html();
8913 git_print_page_nav($fmt_name,'', $hash,$hash,$hash, $paging_nav);
8914 if (defined $file_name) {
8915 git_print_header_div('commit', esc_html($co{'title'}), $base);
8916 } else {
8917 git_print_header_div('summary', $project)
8919 git_print_page_path($file_name, $ftype, $hash_base)
8920 if (defined $file_name);
8922 $body_subr->(\@commitlist, 0, 99, $refs, $next_link,
8923 $file_name, $file_hash, $ftype);
8925 git_footer_html();
8928 sub git_log {
8929 git_log_generic('log', \&git_log_body,
8930 $hash, $hash_parent);
8933 sub git_commit {
8934 $hash ||= $hash_base || "HEAD";
8935 my %co = parse_commit($hash)
8936 or die_error(404, "Unknown commit object");
8938 my $parent = $co{'parent'};
8939 my $parents = $co{'parents'}; # listref
8941 # we need to prepare $formats_nav before any parameter munging
8942 my $formats_nav;
8943 if (!defined $parent) {
8944 # --root commitdiff
8945 $formats_nav .= '(initial)';
8946 } elsif (@$parents == 1) {
8947 # single parent commit
8948 $formats_nav .=
8949 '(parent: ' .
8950 $cgi->a({-href => href(action=>"commit",
8951 hash=>$parent)},
8952 esc_html(substr($parent, 0, 7))) .
8953 ')';
8954 } else {
8955 # merge commit
8956 $formats_nav .=
8957 '(merge: ' .
8958 join(' ', map {
8959 $cgi->a({-href => href(action=>"commit",
8960 hash=>$_)},
8961 esc_html(substr($_, 0, 7)));
8962 } @$parents ) .
8963 ')';
8965 if (gitweb_check_feature('patches') && @$parents <= 1) {
8966 $formats_nav .= " | " .
8967 $cgi->a({-href => href(action=>"patch", -replay=>1)},
8968 "patch");
8971 if (!defined $parent) {
8972 $parent = "--root";
8974 my @difftree;
8975 defined(my $fd = git_cmd_pipe "diff-tree", '-r', "--no-commit-id",
8976 @diff_opts,
8977 (@$parents <= 1 ? $parent : '-c'),
8978 $hash, "--")
8979 or die_error(500, "Open git-diff-tree failed");
8980 @difftree = map { chomp; to_utf8($_) } <$fd>;
8981 close $fd or die_error(404, "Reading git-diff-tree failed");
8983 # non-textual hash id's can be cached
8984 my $expires;
8985 if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
8986 $expires = "+1d";
8988 my $refs = git_get_references();
8989 my $ref = format_ref_marker($refs, $co{'id'});
8991 git_header_html(undef, $expires);
8992 git_print_page_nav('commit', '',
8993 $hash, $co{'tree'}, $hash,
8994 $formats_nav);
8996 if (defined $co{'parent'}) {
8997 git_print_header_div('commitdiff', esc_html($co{'title'}), $hash, undef, $ref);
8998 } else {
8999 git_print_header_div('tree', esc_html($co{'title'}), $co{'tree'}, $hash, $ref);
9001 print "<div class=\"title_text\">\n" .
9002 "<table class=\"object_header\">\n";
9003 print "<tr><td>commit</td><td class=\"sha1\">$co{'id'}</td></tr>\n";
9004 git_print_authorship_rows(\%co);
9005 print "<tr>" .
9006 "<td>tree</td>" .
9007 "<td class=\"sha1\">" .
9008 $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$hash),
9009 class => "list"}, $co{'tree'}) .
9010 "</td>" .
9011 "<td class=\"link\">" .
9012 $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$hash)},
9013 "tree");
9014 my $snapshot_links = format_snapshot_links($hash);
9015 if (defined $snapshot_links) {
9016 print " | " . $snapshot_links;
9018 print "</td>" .
9019 "</tr>\n";
9021 foreach my $par (@$parents) {
9022 print "<tr>" .
9023 "<td>parent</td>" .
9024 "<td class=\"sha1\">" .
9025 $cgi->a({-href => href(action=>"commit", hash=>$par),
9026 class => "list"}, $par) .
9027 "</td>" .
9028 "<td class=\"link\">" .
9029 $cgi->a({-href => href(action=>"commit", hash=>$par)}, "commit") .
9030 " | " .
9031 $cgi->a({-href => href(action=>"commitdiff", hash=>$hash, hash_parent=>$par)}, "diff") .
9032 "</td>" .
9033 "</tr>\n";
9035 print "</table>".
9036 "</div>\n";
9038 print "<div class=\"page_body\">\n";
9039 git_print_log($co{'comment'});
9040 print "</div>\n";
9042 git_difftree_body(\@difftree, $hash, @$parents);
9044 git_footer_html();
9047 sub git_object {
9048 # object is defined by:
9049 # - hash or hash_base alone
9050 # - hash_base and file_name
9051 my $type;
9053 # - hash or hash_base alone
9054 if ($hash || ($hash_base && !defined $file_name)) {
9055 my $object_id = $hash || $hash_base;
9057 defined(my $fd = git_cmd_pipe 'cat-file', '-t', $object_id)
9058 or die_error(404, "Object does not exist");
9059 $type = <$fd>;
9060 defined $type && chomp $type;
9061 close $fd
9062 or die_error(404, "Object does not exist");
9064 # - hash_base and file_name
9065 } elsif ($hash_base && defined $file_name) {
9066 $file_name =~ s,/+$,,;
9068 system(git_cmd(), "cat-file", '-e', $hash_base) == 0
9069 or die_error(404, "Base object does not exist");
9071 # here errors should not happen
9072 defined(my $fd = git_cmd_pipe "ls-tree", $hash_base, "--", $file_name)
9073 or die_error(500, "Open git-ls-tree failed");
9074 my $line = to_utf8(scalar <$fd>);
9075 close $fd;
9077 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'
9078 unless ($line && $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t/) {
9079 die_error(404, "File or directory for given base does not exist");
9081 $type = $2;
9082 $hash = $3;
9083 } else {
9084 die_error(400, "Not enough information to find object");
9087 print $cgi->redirect(-uri => href(action=>$type, -full=>1,
9088 hash=>$hash, hash_base=>$hash_base,
9089 file_name=>$file_name),
9090 -status => '302 Found');
9093 sub git_blobdiff {
9094 my $format = shift || 'html';
9095 my $diff_style = $input_params{'diff_style'} || 'inline';
9097 my $fd;
9098 my @difftree;
9099 my %diffinfo;
9100 my $expires;
9102 # preparing $fd and %diffinfo for git_patchset_body
9103 # new style URI
9104 if (defined $hash_base && defined $hash_parent_base) {
9105 if (defined $file_name) {
9106 # read raw output
9107 defined($fd = git_cmd_pipe "diff-tree", '-r', @diff_opts,
9108 $hash_parent_base, $hash_base,
9109 "--", (defined $file_parent ? $file_parent : ()), $file_name)
9110 or die_error(500, "Open git-diff-tree failed");
9111 @difftree = map { chomp; to_utf8($_) } <$fd>;
9112 close $fd
9113 or die_error(404, "Reading git-diff-tree failed");
9114 @difftree
9115 or die_error(404, "Blob diff not found");
9117 } elsif (defined $hash &&
9118 $hash =~ /[0-9a-fA-F]{40}/) {
9119 # try to find filename from $hash
9121 # read filtered raw output
9122 defined($fd = git_cmd_pipe "diff-tree", '-r', @diff_opts,
9123 $hash_parent_base, $hash_base, "--")
9124 or die_error(500, "Open git-diff-tree failed");
9125 @difftree =
9126 # ':100644 100644 03b21826... 3b93d5e7... M ls-files.c'
9127 # $hash == to_id
9128 grep { /^:[0-7]{6} [0-7]{6} [0-9a-fA-F]{40} $hash/ }
9129 map { chomp; to_utf8($_) } <$fd>;
9130 close $fd
9131 or die_error(404, "Reading git-diff-tree failed");
9132 @difftree
9133 or die_error(404, "Blob diff not found");
9135 } else {
9136 die_error(400, "Missing one of the blob diff parameters");
9139 if (@difftree > 1) {
9140 die_error(400, "Ambiguous blob diff specification");
9143 %diffinfo = parse_difftree_raw_line($difftree[0]);
9144 $file_parent ||= $diffinfo{'from_file'} || $file_name;
9145 $file_name ||= $diffinfo{'to_file'};
9147 $hash_parent ||= $diffinfo{'from_id'};
9148 $hash ||= $diffinfo{'to_id'};
9150 # non-textual hash id's can be cached
9151 if ($hash_base =~ m/^[0-9a-fA-F]{40}$/ &&
9152 $hash_parent_base =~ m/^[0-9a-fA-F]{40}$/) {
9153 $expires = '+1d';
9156 # open patch output
9157 defined($fd = git_cmd_pipe "diff-tree", '-r', @diff_opts,
9158 '-p', ($format eq 'html' ? "--full-index" : ()),
9159 $hash_parent_base, $hash_base,
9160 "--", (defined $file_parent ? $file_parent : ()), $file_name)
9161 or die_error(500, "Open git-diff-tree failed");
9164 # old/legacy style URI -- not generated anymore since 1.4.3.
9165 if (!%diffinfo) {
9166 die_error('404 Not Found', "Missing one of the blob diff parameters")
9169 # header
9170 if ($format eq 'html') {
9171 my $formats_nav =
9172 $cgi->a({-href => href(action=>"blobdiff_plain", -replay=>1)},
9173 "raw");
9174 $formats_nav .= diff_style_nav($diff_style);
9175 git_header_html(undef, $expires);
9176 if (defined $hash_base && (my %co = parse_commit($hash_base))) {
9177 git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
9178 git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
9179 } else {
9180 print "<div class=\"page_nav\"><br/>$formats_nav<br/></div>\n";
9181 print "<div class=\"title\">".esc_html("$hash vs $hash_parent")."</div>\n";
9183 if (defined $file_name) {
9184 git_print_page_path($file_name, "blob", $hash_base);
9185 } else {
9186 print "<div class=\"page_path\"></div>\n";
9189 } elsif ($format eq 'plain') {
9190 print $cgi->header(
9191 -type => 'text/plain',
9192 -charset => 'utf-8',
9193 -expires => $expires,
9194 -content_disposition => 'inline; filename="' . "$file_name" . '.patch"');
9196 print "X-Git-Url: " . $cgi->self_url() . "\n\n";
9198 } else {
9199 die_error(400, "Unknown blobdiff format");
9202 # patch
9203 if ($format eq 'html') {
9204 print "<div class=\"page_body\">\n";
9206 git_patchset_body($fd, $diff_style,
9207 [ \%diffinfo ], $hash_base, $hash_parent_base);
9208 close $fd;
9210 print "</div>\n"; # class="page_body"
9211 git_footer_html();
9213 } else {
9214 while (my $line = to_utf8(scalar <$fd>)) {
9215 $line =~ s!a/($hash|$hash_parent)!'a/'.esc_path($diffinfo{'from_file'})!eg;
9216 $line =~ s!b/($hash|$hash_parent)!'b/'.esc_path($diffinfo{'to_file'})!eg;
9218 print $line;
9220 last if $line =~ m!^\+\+\+!;
9222 while (<$fd>) {
9223 print to_utf8($_);
9225 close $fd;
9229 sub git_blobdiff_plain {
9230 git_blobdiff('plain');
9233 # assumes that it is added as later part of already existing navigation,
9234 # so it returns "| foo | bar" rather than just "foo | bar"
9235 sub diff_style_nav {
9236 my ($diff_style, $is_combined) = @_;
9237 $diff_style ||= 'inline';
9239 return "" if ($is_combined);
9241 my @styles = (inline => 'inline', 'sidebyside' => 'side by side');
9242 my %styles = @styles;
9243 @styles =
9244 @styles[ map { $_ * 2 } 0..$#styles/2 ];
9246 return join '',
9247 map { " | ".$_ }
9248 map {
9249 $_ eq $diff_style ? $styles{$_} :
9250 $cgi->a({-href => href(-replay=>1, diff_style => $_)}, $styles{$_})
9251 } @styles;
9254 sub git_commitdiff {
9255 my %params = @_;
9256 my $format = $params{-format} || 'html';
9257 my $diff_style = $input_params{'diff_style'} || 'inline';
9259 my ($patch_max) = gitweb_get_feature('patches');
9260 if ($format eq 'patch') {
9261 die_error(403, "Patch view not allowed") unless $patch_max;
9264 $hash ||= $hash_base || "HEAD";
9265 my %co = parse_commit($hash)
9266 or die_error(404, "Unknown commit object");
9268 # choose format for commitdiff for merge
9269 if (! defined $hash_parent && @{$co{'parents'}} > 1) {
9270 $hash_parent = '--cc';
9272 # we need to prepare $formats_nav before almost any parameter munging
9273 my $formats_nav;
9274 if ($format eq 'html') {
9275 $formats_nav =
9276 $cgi->a({-href => href(action=>"commitdiff_plain", -replay=>1)},
9277 "raw");
9278 if ($patch_max && @{$co{'parents'}} <= 1) {
9279 $formats_nav .= " | " .
9280 $cgi->a({-href => href(action=>"patch", -replay=>1)},
9281 "patch");
9283 $formats_nav .= diff_style_nav($diff_style, @{$co{'parents'}} > 1);
9285 if (defined $hash_parent &&
9286 $hash_parent ne '-c' && $hash_parent ne '--cc') {
9287 # commitdiff with two commits given
9288 my $hash_parent_short = $hash_parent;
9289 if ($hash_parent =~ m/^[0-9a-fA-F]{40}$/) {
9290 $hash_parent_short = substr($hash_parent, 0, 7);
9292 $formats_nav .=
9293 ' (from';
9294 for (my $i = 0; $i < @{$co{'parents'}}; $i++) {
9295 if ($co{'parents'}[$i] eq $hash_parent) {
9296 $formats_nav .= ' parent ' . ($i+1);
9297 last;
9300 $formats_nav .= ': ' .
9301 $cgi->a({-href => href(-replay=>1,
9302 hash=>$hash_parent, hash_base=>undef)},
9303 esc_html($hash_parent_short)) .
9304 ')';
9305 } elsif (!$co{'parent'}) {
9306 # --root commitdiff
9307 $formats_nav .= ' (initial)';
9308 } elsif (scalar @{$co{'parents'}} == 1) {
9309 # single parent commit
9310 $formats_nav .=
9311 ' (parent: ' .
9312 $cgi->a({-href => href(-replay=>1,
9313 hash=>$co{'parent'}, hash_base=>undef)},
9314 esc_html(substr($co{'parent'}, 0, 7))) .
9315 ')';
9316 } else {
9317 # merge commit
9318 if ($hash_parent eq '--cc') {
9319 $formats_nav .= ' | ' .
9320 $cgi->a({-href => href(-replay=>1,
9321 hash=>$hash, hash_parent=>'-c')},
9322 'combined');
9323 } else { # $hash_parent eq '-c'
9324 $formats_nav .= ' | ' .
9325 $cgi->a({-href => href(-replay=>1,
9326 hash=>$hash, hash_parent=>'--cc')},
9327 'compact');
9329 $formats_nav .=
9330 ' (merge: ' .
9331 join(' ', map {
9332 $cgi->a({-href => href(-replay=>1,
9333 hash=>$_, hash_base=>undef)},
9334 esc_html(substr($_, 0, 7)));
9335 } @{$co{'parents'}} ) .
9336 ')';
9340 my $hash_parent_param = $hash_parent;
9341 if (!defined $hash_parent_param) {
9342 # --cc for multiple parents, --root for parentless
9343 $hash_parent_param =
9344 @{$co{'parents'}} > 1 ? '--cc' : $co{'parent'} || '--root';
9347 # read commitdiff
9348 my $fd;
9349 my @difftree;
9350 if ($format eq 'html') {
9351 defined($fd = git_cmd_pipe "diff-tree", '-r', @diff_opts,
9352 "--no-commit-id", "--patch-with-raw", "--full-index",
9353 $hash_parent_param, $hash, "--")
9354 or die_error(500, "Open git-diff-tree failed");
9356 while (my $line = to_utf8(scalar <$fd>)) {
9357 chomp $line;
9358 # empty line ends raw part of diff-tree output
9359 last unless $line;
9360 push @difftree, scalar parse_difftree_raw_line($line);
9363 } elsif ($format eq 'plain') {
9364 defined($fd = git_cmd_pipe "diff-tree", '-r', @diff_opts,
9365 '-p', $hash_parent_param, $hash, "--")
9366 or die_error(500, "Open git-diff-tree failed");
9367 } elsif ($format eq 'patch') {
9368 # For commit ranges, we limit the output to the number of
9369 # patches specified in the 'patches' feature.
9370 # For single commits, we limit the output to a single patch,
9371 # diverging from the git-format-patch default.
9372 my @commit_spec = ();
9373 if ($hash_parent) {
9374 if ($patch_max > 0) {
9375 push @commit_spec, "-$patch_max";
9377 push @commit_spec, '-n', "$hash_parent..$hash";
9378 } else {
9379 if ($params{-single}) {
9380 push @commit_spec, '-1';
9381 } else {
9382 if ($patch_max > 0) {
9383 push @commit_spec, "-$patch_max";
9385 push @commit_spec, "-n";
9387 push @commit_spec, '--root', $hash;
9389 defined($fd = git_cmd_pipe "format-patch", @diff_opts,
9390 '--encoding=utf8', '--stdout', @commit_spec)
9391 or die_error(500, "Open git-format-patch failed");
9392 } else {
9393 die_error(400, "Unknown commitdiff format");
9396 # non-textual hash id's can be cached
9397 my $expires;
9398 if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
9399 $expires = "+1d";
9402 # write commit message
9403 if ($format eq 'html') {
9404 my $refs = git_get_references();
9405 my $ref = format_ref_marker($refs, $co{'id'});
9407 git_header_html(undef, $expires);
9408 git_print_page_nav('commitdiff','', $hash,$co{'tree'},$hash, $formats_nav);
9409 git_print_header_div('commit', esc_html($co{'title'}), $hash, undef, $ref);
9410 print "<div class=\"title_text\">\n" .
9411 "<table class=\"object_header\">\n";
9412 git_print_authorship_rows(\%co);
9413 print "</table>".
9414 "</div>\n";
9415 print "<div class=\"page_body\">\n";
9416 if (@{$co{'comment'}} > 1) {
9417 print "<div class=\"log\">\n";
9418 git_print_log($co{'comment'}, -final_empty_line=> 1, -remove_title => 1);
9419 print "</div>\n"; # class="log"
9422 } elsif ($format eq 'plain') {
9423 my $refs = git_get_references("tags");
9424 my $tagname = git_get_rev_name_tags($hash);
9425 my $filename = basename($project) . "-$hash.patch";
9427 print $cgi->header(
9428 -type => 'text/plain',
9429 -charset => 'utf-8',
9430 -expires => $expires,
9431 -content_disposition => 'inline; filename="' . "$filename" . '"');
9432 my %ad = parse_date($co{'author_epoch'}, $co{'author_tz'});
9433 print "From: " . to_utf8($co{'author'}) . "\n";
9434 print "Date: $ad{'rfc2822'} ($ad{'tz_local'})\n";
9435 print "Subject: " . to_utf8($co{'title'}) . "\n";
9437 print "X-Git-Tag: $tagname\n" if $tagname;
9438 print "X-Git-Url: " . $cgi->self_url() . "\n\n";
9440 foreach my $line (@{$co{'comment'}}) {
9441 print to_utf8($line) . "\n";
9443 print "---\n\n";
9444 } elsif ($format eq 'patch') {
9445 my $filename = basename($project) . "-$hash.patch";
9447 print $cgi->header(
9448 -type => 'text/plain',
9449 -charset => 'utf-8',
9450 -expires => $expires,
9451 -content_disposition => 'inline; filename="' . "$filename" . '"');
9454 # write patch
9455 if ($format eq 'html') {
9456 my $use_parents = !defined $hash_parent ||
9457 $hash_parent eq '-c' || $hash_parent eq '--cc';
9458 git_difftree_body(\@difftree, $hash,
9459 $use_parents ? @{$co{'parents'}} : $hash_parent);
9460 print "<br/>\n";
9462 git_patchset_body($fd, $diff_style,
9463 \@difftree, $hash,
9464 $use_parents ? @{$co{'parents'}} : $hash_parent);
9465 close $fd;
9466 print "</div>\n"; # class="page_body"
9467 git_footer_html();
9469 } elsif ($format eq 'plain') {
9470 while (<$fd>) {
9471 print to_utf8($_);
9473 close $fd
9474 or print "Reading git-diff-tree failed\n";
9475 } elsif ($format eq 'patch') {
9476 while (<$fd>) {
9477 print to_utf8($_);
9479 close $fd
9480 or print "Reading git-format-patch failed\n";
9484 sub git_commitdiff_plain {
9485 git_commitdiff(-format => 'plain');
9488 # format-patch-style patches
9489 sub git_patch {
9490 git_commitdiff(-format => 'patch', -single => 1);
9493 sub git_patches {
9494 git_commitdiff(-format => 'patch');
9497 sub git_history {
9498 git_log_generic('history', \&git_history_body,
9499 $hash_base, $hash_parent_base,
9500 $file_name, $hash);
9503 sub git_search {
9504 $searchtype ||= 'commit';
9506 # check if appropriate features are enabled
9507 gitweb_check_feature('search')
9508 or die_error(403, "Search is disabled");
9509 if ($searchtype eq 'pickaxe') {
9510 # pickaxe may take all resources of your box and run for several minutes
9511 # with every query - so decide by yourself how public you make this feature
9512 gitweb_check_feature('pickaxe')
9513 or die_error(403, "Pickaxe search is disabled");
9515 if ($searchtype eq 'grep') {
9516 # grep search might be potentially CPU-intensive, too
9517 gitweb_check_feature('grep')
9518 or die_error(403, "Grep search is disabled");
9520 if ($search_use_regexp) {
9521 # regular expression search can be disabled to avoid potentially
9522 # malicious regular expressions
9523 gitweb_check_feature('regexp')
9524 or die_error(403, "Regular expression search is disabled");
9527 if (!defined $searchtext) {
9528 die_error(400, "Text field is empty");
9530 if (!defined $hash) {
9531 $hash = git_get_head_hash($project);
9533 my %co = parse_commit($hash);
9534 if (!%co) {
9535 die_error(404, "Unknown commit object");
9537 if (!defined $page) {
9538 $page = 0;
9541 if ($searchtype eq 'commit' ||
9542 $searchtype eq 'author' ||
9543 $searchtype eq 'committer') {
9544 git_search_message(%co);
9545 } elsif ($searchtype eq 'pickaxe') {
9546 git_search_changes(%co);
9547 } elsif ($searchtype eq 'grep') {
9548 git_search_files(%co);
9549 } else {
9550 die_error(400, "Unknown search type");
9554 sub git_search_help {
9555 git_header_html();
9556 git_print_page_nav('','', $hash,$hash,$hash);
9557 print <<EOT;
9558 <p><strong>Pattern</strong> is by default a normal string that is matched precisely (but without
9559 regard to case, except in the case of pickaxe). However, when you check the <em>re</em> checkbox,
9560 the pattern entered is recognized as the POSIX extended
9561 <a href="http://en.wikipedia.org/wiki/Regular_expression">regular expression</a> (also case
9562 insensitive).</p>
9563 <dl>
9564 <dt><b>commit</b></dt>
9565 <dd>The commit messages and authorship information will be scanned for the given pattern.</dd>
9567 my $have_grep = gitweb_check_feature('grep');
9568 if ($have_grep) {
9569 print <<EOT;
9570 <dt><b>grep</b></dt>
9571 <dd>All files in the currently selected tree (HEAD unless you are explicitly browsing
9572 a different one) are searched for the given pattern. On large trees, this search can take
9573 a while and put some strain on the server, so please use it with some consideration. Note that
9574 due to git-grep peculiarity, currently if regexp mode is turned off, the matches are
9575 case-sensitive.</dd>
9578 print <<EOT;
9579 <dt><b>author</b></dt>
9580 <dd>Name and e-mail of the change author and date of birth of the patch will be scanned for the given pattern.</dd>
9581 <dt><b>committer</b></dt>
9582 <dd>Name and e-mail of the committer and date of commit will be scanned for the given pattern.</dd>
9584 my $have_pickaxe = gitweb_check_feature('pickaxe');
9585 if ($have_pickaxe) {
9586 print <<EOT;
9587 <dt><b>pickaxe</b></dt>
9588 <dd>All commits that caused the string to appear or disappear from any file (changes that
9589 added, removed or "modified" the string) will be listed. This search can take a while and
9590 takes a lot of strain on the server, so please use it wisely. Note that since you may be
9591 interested even in changes just changing the case as well, this search is case sensitive.</dd>
9594 print "</dl>\n";
9595 git_footer_html();
9598 sub git_shortlog {
9599 git_log_generic('shortlog', \&git_shortlog_body,
9600 $hash, $hash_parent);
9603 ## ......................................................................
9604 ## feeds (RSS, Atom; OPML)
9606 sub git_feed {
9607 my $format = shift || 'atom';
9608 my $have_blame = gitweb_check_feature('blame');
9610 # Atom: http://www.atomenabled.org/developers/syndication/
9611 # RSS: http://www.notestips.com/80256B3A007F2692/1/NAMO5P9UPQ
9612 if ($format ne 'rss' && $format ne 'atom') {
9613 die_error(400, "Unknown web feed format");
9616 # log/feed of current (HEAD) branch, log of given branch, history of file/directory
9617 my $head = $hash || 'HEAD';
9618 my @commitlist = parse_commits($head, 150, 0, $file_name);
9620 my %latest_commit;
9621 my %latest_date;
9622 my $content_type = "application/$format+xml";
9623 if (defined $cgi->http('HTTP_ACCEPT') &&
9624 $cgi->Accept('text/xml') > $cgi->Accept($content_type)) {
9625 # browser (feed reader) prefers text/xml
9626 $content_type = 'text/xml';
9628 if (defined($commitlist[0])) {
9629 %latest_commit = %{$commitlist[0]};
9630 my $latest_epoch = $latest_commit{'committer_epoch'};
9631 exit_if_unmodified_since($latest_epoch);
9632 %latest_date = parse_date($latest_epoch, $latest_commit{'committer_tz'});
9634 print $cgi->header(
9635 -type => $content_type,
9636 -charset => 'utf-8',
9637 %latest_date ? (-last_modified => $latest_date{'rfc2822'}) : (),
9638 -status => '200 OK');
9640 # Optimization: skip generating the body if client asks only
9641 # for Last-Modified date.
9642 return if ($cgi->request_method() eq 'HEAD');
9644 # header variables
9645 my $title = "$site_name - $project/$action";
9646 my $feed_type = 'log';
9647 if (defined $hash) {
9648 $title .= " - '$hash'";
9649 $feed_type = 'branch log';
9650 if (defined $file_name) {
9651 $title .= " :: $file_name";
9652 $feed_type = 'history';
9654 } elsif (defined $file_name) {
9655 $title .= " - $file_name";
9656 $feed_type = 'history';
9658 $title .= " $feed_type";
9659 $title = esc_html($title);
9660 my $descr = git_get_project_description($project);
9661 if (defined $descr) {
9662 $descr = esc_html($descr);
9663 } else {
9664 $descr = "$project " .
9665 ($format eq 'rss' ? 'RSS' : 'Atom') .
9666 " feed";
9668 my $owner = git_get_project_owner($project);
9669 $owner = esc_html($owner);
9671 #header
9672 my $alt_url;
9673 if (defined $file_name) {
9674 $alt_url = href(-full=>1, action=>"history", hash=>$hash, file_name=>$file_name);
9675 } elsif (defined $hash) {
9676 $alt_url = href(-full=>1, action=>"log", hash=>$hash);
9677 } else {
9678 $alt_url = href(-full=>1, action=>"summary");
9680 print qq!<?xml version="1.0" encoding="utf-8"?>\n!;
9681 if ($format eq 'rss') {
9682 print <<XML;
9683 <rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
9684 <channel>
9686 print "<title>$title</title>\n" .
9687 "<link>$alt_url</link>\n" .
9688 "<description>$descr</description>\n" .
9689 "<language>en</language>\n" .
9690 # project owner is responsible for 'editorial' content
9691 "<managingEditor>$owner</managingEditor>\n";
9692 if (defined $logo || defined $favicon) {
9693 # prefer the logo to the favicon, since RSS
9694 # doesn't allow both
9695 my $img = esc_url($logo || $favicon);
9696 print "<image>\n" .
9697 "<url>$img</url>\n" .
9698 "<title>$title</title>\n" .
9699 "<link>$alt_url</link>\n" .
9700 "</image>\n";
9702 if (%latest_date) {
9703 print "<pubDate>$latest_date{'rfc2822'}</pubDate>\n";
9704 print "<lastBuildDate>$latest_date{'rfc2822'}</lastBuildDate>\n";
9706 print "<generator>gitweb v.$version/$git_version</generator>\n";
9707 } elsif ($format eq 'atom') {
9708 print <<XML;
9709 <feed xmlns="http://www.w3.org/2005/Atom">
9711 print "<title>$title</title>\n" .
9712 "<subtitle>$descr</subtitle>\n" .
9713 '<link rel="alternate" type="text/html" href="' .
9714 $alt_url . '" />' . "\n" .
9715 '<link rel="self" type="' . $content_type . '" href="' .
9716 $cgi->self_url() . '" />' . "\n" .
9717 "<id>" . href(-full=>1) . "</id>\n" .
9718 # use project owner for feed author
9719 '<author><name>'. email_obfuscate($owner) . '</name></author>\n';
9720 if (defined $favicon) {
9721 print "<icon>" . esc_url($favicon) . "</icon>\n";
9723 if (defined $logo) {
9724 # not twice as wide as tall: 72 x 27 pixels
9725 print "<logo>" . esc_url($logo) . "</logo>\n";
9727 if (! %latest_date) {
9728 # dummy date to keep the feed valid until commits trickle in:
9729 print "<updated>1970-01-01T00:00:00Z</updated>\n";
9730 } else {
9731 print "<updated>$latest_date{'iso-8601'}</updated>\n";
9733 print "<generator version='$version/$git_version'>gitweb</generator>\n";
9736 # contents
9737 for (my $i = 0; $i <= $#commitlist; $i++) {
9738 my %co = %{$commitlist[$i]};
9739 my $commit = $co{'id'};
9740 # we read 150, we always show 30 and the ones more recent than 48 hours
9741 if (($i >= 20) && ((time - $co{'author_epoch'}) > 48*60*60)) {
9742 last;
9744 my %cd = parse_date($co{'author_epoch'}, $co{'author_tz'});
9746 # get list of changed files
9747 defined(my $fd = git_cmd_pipe "diff-tree", '-r', @diff_opts,
9748 $co{'parent'} || "--root",
9749 $co{'id'}, "--", (defined $file_name ? $file_name : ()))
9750 or next;
9751 my @difftree = map { chomp; to_utf8($_) } <$fd>;
9752 close $fd
9753 or next;
9755 # print element (entry, item)
9756 my $co_url = href(-full=>1, action=>"commitdiff", hash=>$commit);
9757 if ($format eq 'rss') {
9758 print "<item>\n" .
9759 "<title>" . esc_html($co{'title'}) . "</title>\n" .
9760 "<author>" . esc_html($co{'author'}) . "</author>\n" .
9761 "<pubDate>$cd{'rfc2822'}</pubDate>\n" .
9762 "<guid isPermaLink=\"true\">$co_url</guid>\n" .
9763 "<link>$co_url</link>\n" .
9764 "<description>" . esc_html($co{'title'}) . "</description>\n" .
9765 "<content:encoded>" .
9766 "<![CDATA[\n";
9767 } elsif ($format eq 'atom') {
9768 print "<entry>\n" .
9769 "<title type=\"html\">" . esc_html($co{'title'}) . "</title>\n" .
9770 "<updated>$cd{'iso-8601'}</updated>\n" .
9771 "<author>\n" .
9772 " <name>" . esc_html($co{'author_name'}) . "</name>\n";
9773 if ($co{'author_email'}) {
9774 print " <email>" . esc_html($co{'author_email'}) . "</email>\n";
9776 print "</author>\n" .
9777 # use committer for contributor
9778 "<contributor>\n" .
9779 " <name>" . esc_html($co{'committer_name'}) . "</name>\n";
9780 if ($co{'committer_email'}) {
9781 print " <email>" . esc_html($co{'committer_email'}) . "</email>\n";
9783 print "</contributor>\n" .
9784 "<published>$cd{'iso-8601'}</published>\n" .
9785 "<link rel=\"alternate\" type=\"text/html\" href=\"$co_url\" />\n" .
9786 "<id>$co_url</id>\n" .
9787 "<content type=\"xhtml\" xml:base=\"" . esc_url($my_url) . "\">\n" .
9788 "<div xmlns=\"http://www.w3.org/1999/xhtml\">\n";
9790 my $comment = $co{'comment'};
9791 print "<pre>\n";
9792 foreach my $line (@$comment) {
9793 $line = esc_html($line);
9794 print "$line\n";
9796 print "</pre><ul>\n";
9797 foreach my $difftree_line (@difftree) {
9798 my %difftree = parse_difftree_raw_line($difftree_line);
9799 next if !$difftree{'from_id'};
9801 my $file = $difftree{'file'} || $difftree{'to_file'};
9803 print "<li>" .
9804 "[" .
9805 $cgi->a({-href => href(-full=>1, action=>"blobdiff",
9806 hash=>$difftree{'to_id'}, hash_parent=>$difftree{'from_id'},
9807 hash_base=>$co{'id'}, hash_parent_base=>$co{'parent'},
9808 file_name=>$file, file_parent=>$difftree{'from_file'}),
9809 -title => "diff"}, 'D');
9810 if ($have_blame) {
9811 print $cgi->a({-href => href(-full=>1, action=>"blame",
9812 file_name=>$file, hash_base=>$commit),
9813 -class => "blamelink",
9814 -title => "blame"}, 'B');
9816 # if this is not a feed of a file history
9817 if (!defined $file_name || $file_name ne $file) {
9818 print $cgi->a({-href => href(-full=>1, action=>"history",
9819 file_name=>$file, hash=>$commit),
9820 -title => "history"}, 'H');
9822 $file = esc_path($file);
9823 print "] ".
9824 "$file</li>\n";
9826 if ($format eq 'rss') {
9827 print "</ul>]]>\n" .
9828 "</content:encoded>\n" .
9829 "</item>\n";
9830 } elsif ($format eq 'atom') {
9831 print "</ul>\n</div>\n" .
9832 "</content>\n" .
9833 "</entry>\n";
9837 # end of feed
9838 if ($format eq 'rss') {
9839 print "</channel>\n</rss>\n";
9840 } elsif ($format eq 'atom') {
9841 print "</feed>\n";
9845 sub git_rss {
9846 git_feed('rss');
9849 sub git_atom {
9850 git_feed('atom');
9853 sub git_opml {
9854 my @list = git_get_projects_list($project_filter, $strict_export);
9855 if (!@list) {
9856 die_error(404, "No projects found");
9859 print $cgi->header(
9860 -type => 'text/xml',
9861 -charset => 'utf-8',
9862 -content_disposition => 'inline; filename="opml.xml"');
9864 my $title = esc_html($site_name);
9865 my $filter = " within subdirectory ";
9866 if (defined $project_filter) {
9867 $filter .= esc_html($project_filter);
9868 } else {
9869 $filter = "";
9871 print <<XML;
9872 <?xml version="1.0" encoding="utf-8"?>
9873 <opml version="1.0">
9874 <head>
9875 <title>$title OPML Export$filter</title>
9876 </head>
9877 <body>
9878 <outline text="git RSS feeds">
9881 foreach my $pr (@list) {
9882 my %proj = %$pr;
9883 my $head = git_get_head_hash($proj{'path'});
9884 if (!defined $head) {
9885 next;
9887 $git_dir = "$projectroot/$proj{'path'}";
9888 my %co = parse_commit($head);
9889 if (!%co) {
9890 next;
9893 my $path = esc_html(chop_str($proj{'path'}, 25, 5));
9894 my $rss = href('project' => $proj{'path'}, 'action' => 'rss', -full => 1);
9895 my $html = href('project' => $proj{'path'}, 'action' => 'summary', -full => 1);
9896 print "<outline type=\"rss\" text=\"$path\" title=\"$path\" xmlUrl=\"$rss\" htmlUrl=\"$html\"/>\n";
9898 print <<XML;
9899 </outline>
9900 </body>
9901 </opml>