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
13 use CGI
qw(:standard :escapeHTML -nosticky);
14 use CGI
::Util
qw(unescape);
15 use CGI
::Carp
qw(fatalsToBrowser set_message);
19 use File
::Basename
qw(basename);
21 use Time
::HiRes
qw(gettimeofday tv_interval);
23 use constant GITWEB_CACHE_FORMAT
=> "Gitweb Cache Format 3";
24 binmode STDOUT
, ':utf8';
26 if (!defined($CGI::VERSION
) || $CGI::VERSION
< 4.08) {
27 eval 'sub CGI::multi_param { CGI::param(@_) }'
30 our $t0 = [ gettimeofday
() ];
31 our $number_of_git_cmds = 0;
32 our ($mdotsep, $barsep, $spcsep);
35 *mdotsep
= \'<span
class="mdotsep"> · </span>';
36 *barsep
= \'<span
class="barsep"> | </span>';
37 *spcsep
= \'<span
class="spcsep"> </span>';
38 CGI
->compile() if $ENV{'MOD_PERL'};
41 our $version = "++GIT_VERSION++";
43 our ($my_url, $my_uri, $base_url, $path_info, $home_link);
47 our $my_url = $cgi->url();
48 our $my_uri = $cgi->url(-absolute
=> 1);
50 # Base URL for relative URLs in gitweb ($logo, $favicon, ...),
51 # needed and used only for URLs with nonempty PATH_INFO
52 # This must be an absolute URL (i.e. no scheme, host or port), NOT a full one
53 our $base_url = $my_uri || '/';
55 # When the script is used as DirectoryIndex, the URL does not contain the name
56 # of the script file itself, and $cgi->url() fails to strip PATH_INFO, so we
57 # have to do it ourselves. We make $path_info global because it's also used
60 # Another issue with the script being the DirectoryIndex is that the resulting
61 # $my_url data is not the full script URL: this is good, because we want
62 # generated links to keep implying the script name if it wasn't explicitly
63 # indicated in the URL we're handling, but it means that $my_url cannot be used
65 # Therefore, if we needed to strip PATH_INFO, then we know that we have
66 # to build the base URL ourselves:
67 our $path_info = decode_utf8
($ENV{"PATH_INFO"});
69 # $path_info has already been URL-decoded by the web server, but
70 # $my_url and $my_uri have not. URL-decode them so we can properly
72 $my_url = unescape
($my_url);
73 $my_uri = unescape
($my_uri);
74 if ($my_url =~ s
,\Q
$path_info\E
$,, &&
75 $my_uri =~ s
,\Q
$path_info\E
$,, &&
76 defined $ENV{'SCRIPT_NAME'}) {
77 $base_url = $ENV{'SCRIPT_NAME'} || '/';
81 # target of the home link on top of all pages
82 our $home_link = $my_uri || "/";
85 # core git executable to use
86 # this can just be "git" if your webserver has a sensible PATH
87 our $GIT = "++GIT_BINDIR++/git";
89 # absolute fs-path which will be prepended to the project path
90 #our $projectroot = "/pub/scm";
91 our $projectroot = "++GITWEB_PROJECTROOT++";
93 # fs traversing limit for getting project list
94 # the number is relative to the projectroot
95 our $project_maxdepth = "++GITWEB_PROJECT_MAXDEPTH++";
97 # string of the home link on top of all pages
98 our $home_link_str = "++GITWEB_HOME_LINK_STR++";
100 # extra breadcrumbs preceding the home link
101 our @extra_breadcrumbs = ();
103 # name of your site or organization to appear in page titles
104 # replace this with something more descriptive for clearer bookmarks
105 our $site_name = "++GITWEB_SITENAME++"
106 || ($ENV{'SERVER_NAME'} || "Untitled") . " Git";
108 # html snippet to include in the <head> section of each page
109 our $site_html_head_string = "++GITWEB_SITE_HTML_HEAD_STRING++";
110 # filename of html text to include at top of each page
111 our $site_header = "++GITWEB_SITE_HEADER++";
112 # html text to include at home page
113 our $home_text = "++GITWEB_HOMETEXT++";
114 # filename of html text to include at bottom of each page
115 our $site_footer = "++GITWEB_SITE_FOOTER++";
118 our @stylesheets = ("++GITWEB_CSS++");
119 # URI of a single stylesheet, which can be overridden in GITWEB_CONFIG.
120 our $stylesheet = undef;
121 # URI of GIT logo (72x27 size)
122 our $logo = "++GITWEB_LOGO++";
123 # URI of GIT favicon, assumed to be image/png type
124 our $favicon = "++GITWEB_FAVICON++";
125 # URI of gitweb.js (JavaScript code for gitweb)
126 our $javascript = "++GITWEB_JS++";
128 # URI and label (title) of GIT logo link
129 #our $logo_url = "http://www.kernel.org/pub/software/scm/git/docs/";
130 #our $logo_label = "git documentation";
131 our $logo_url = "http://git-scm.com/";
132 our $logo_label = "git homepage";
134 # source of projects list
135 our $projects_list = "++GITWEB_LIST++";
137 # the width (in characters) of the projects list "Description" column
138 our $projects_list_description_width = 25;
140 # group projects by category on the projects list
141 # (enabled if this variable evaluates to true)
142 our $projects_list_group_categories = 0;
144 # default category if none specified
145 # (leave the empty string for no category)
146 our $project_list_default_category = "";
148 # default order of projects list
149 # valid values are none, project, descr, owner, and age
150 our $default_projects_order = "project";
152 # default order of refs list
153 # valid values are age and name
154 our $default_refs_order = "age";
156 # show repository only if this file exists
157 # (only effective if this variable evaluates to true)
158 our $export_ok = "++GITWEB_EXPORT_OK++";
160 # don't generate age column on the projects list page
161 our $omit_age_column = 0;
163 # use contents of this file (in iso, iso-strict or raw format) as
164 # the last activity data if it exists and is a valid date
165 our $lastactivity_file = undef;
167 # don't generate information about owners of repositories
170 # owner link hook given owner name (full and NOT obfuscated)
171 # should return full URL-escaped link to attach to owner, for example:
172 # sub { return "/showowner.cgi?owner=".CGI::Util::escape($_[0]); }
173 our $owner_link_hook = undef;
175 # show repository only if this subroutine returns true
176 # when given the path to the project, for example:
177 # sub { return -e "$_[0]/git-daemon-export-ok"; }
178 our $export_auth_hook = undef;
180 # only allow viewing of repositories also shown on the overview page
181 our $strict_export = "++GITWEB_STRICT_EXPORT++";
183 # base URL for bundle info link shown on summary page, but only if
184 # this config item is defined AND a 'bundles' subdirectory exists
185 # in the project's repository.
186 # i.e. full URL is "git_base_bundles_url/$project/bundles"
187 our $git_base_bundles_url = undef;
189 # list of git base URLs used for URL to where fetch project from,
190 # i.e. full URL is "$git_base_url/$project"
191 our @git_base_url_list = grep { $_ ne '' } ("++GITWEB_BASE_URL++");
193 # URLs designated for pushing new changes, extended by the
194 # project name (i.e. "$git_base_push_url[0]/$project")
195 our @git_base_push_urls = ();
197 # https hint html inserted right after any https push URL (undef for none)
198 our $https_hint_html = undef;
200 # default blob_plain mimetype and default charset for text/plain blob
201 our $default_blob_plain_mimetype = 'application/octet-stream';
202 our $default_text_plain_charset = undef;
204 # file to use for guessing MIME types before trying /etc/mime.types
205 # (relative to the current git repository)
206 our $mimetypes_file = undef;
208 # assume this charset if line contains non-UTF-8 characters;
209 # it should be valid encoding (see Encoding::Supported(3pm) for list),
210 # for which encoding all byte sequences are valid, for example
211 # 'iso-8859-1' aka 'latin1' (it is decoded without checking, so it
212 # could be even 'utf-8' for the old behavior)
213 our $fallback_encoding = 'latin1';
215 # rename detection options for git-diff and git-diff-tree
216 # - default is '-M', with the cost proportional to
217 # (number of removed files) * (number of new files).
218 # - more costly is '-C' (which implies '-M'), with the cost proportional to
219 # (number of changed files + number of removed files) * (number of new files)
220 # - even more costly is '-C', '--find-copies-harder' with cost
221 # (number of files in the original tree) * (number of new files)
222 # - one might want to include '-B' option, e.g. '-B', '-M'
223 our @diff_opts = ('-M'); # taken from git_commit
225 # cache directory (relative to $GIT_DIR) for project-specific html page caches.
226 # the directory must exist and be writable by the process running gitweb.
227 # additionally some actions must be selected for caching in %html_cache_actions
228 # - default is 'htmlcache'
229 our $html_cache_dir = 'htmlcache';
231 # which actions to cache in $html_cache_dir
232 # if $html_cache_dir exists (relative to $GIT_DIR) and is writable by the
233 # process running gitweb, then any actions selected here will have their output
234 # cached and the cache file will be returned instead of regenerating the page
235 # if it exists. For this to be useful, an external process must create the
236 # 'changed' file (if it does not already exist) in the $html_cache_dir whenever
237 # the project information has been changed. Alternatively it may create a
238 # "$action.changed" file (if it does not exist) instead to limit the changes
239 # to just "$action" instead of any action. If 'changed' or "$action.changed"
240 # exist, then the cached version will never be used for "$action" and a new
241 # cache page will be regenerated (and the "changed" files removed as appropriate).
243 # Additionally if $projlist_cache_lifetime is > 0 AND the forks feature has been
244 # enabled ('$feature{'forks'}{'default'} = [1];') then additionally an external
245 # process must create the 'forkchange' file or update its timestamp if it already
246 # exists whenever a fork is added to or removed from the project (as well as
247 # create the 'changed' or "$action.changed" file). Otherwise the "forks"
248 # section on the summary page may remain out-of-date indefinately.
251 # currently only caching of the summary page is supported
252 # - to enable caching of the summary page use:
253 # $html_cache_actions{'summary'} = 1;
254 our %html_cache_actions = ();
256 # utility to automatically produce a default README.html if README.html is
257 # enabled and it does not exist or is 0 bytes in length. If this is set to an
258 # executable utility that takes an absolute path to a .git directory as its
259 # first argument and outputs an HTML fragment to use for README.html, then
260 # it will be called when README.html is enabled but empty or missing.
261 our $git_automatic_readme_html = undef;
263 # Disables features that would allow repository owners to inject script into
265 our $prevent_xss = 0;
267 # Path to a POSIX shell. Needed to run $highlight_bin and a snapshot compressor.
268 # Only used when highlight is enabled or snapshots with compressors are enabled.
269 our $posix_shell_bin = "++POSIX_SHELL_BIN++";
271 # Path to the highlight executable to use (must be the one from
272 # http://www.andre-simon.de due to assumptions about parameters and output).
273 # Useful if highlight is not installed on your webserver's PATH.
274 # [Default: highlight]
275 our $highlight_bin = "++HIGHLIGHT_BIN++";
277 # Whether to include project list on the gitweb front page; 0 means yes,
278 # 1 means no list but show tag cloud if enabled (all projects still need
279 # to be scanned, unless the info is cached), 2 means no list and no tag cloud
281 our $frontpage_no_project_list = 0;
283 # projects list cache for busy sites with many projects;
284 # if you set this to non-zero, it will be used as the cached
285 # index lifetime in minutes
287 # the cached list version is stored in $cache_dir/$cache_name and can
288 # be tweaked by other scripts running with the same uid as gitweb -
289 # use this ONLY at secure installations; only single gitweb project
290 # root per system is supported, unless you tweak configuration!
291 our $projlist_cache_lifetime = 0; # in minutes
292 # FHS compliant $cache_dir would be "/var/cache/gitweb"
294 (defined $ENV{'TMPDIR'} ?
$ENV{'TMPDIR'} : '/tmp').'/gitweb';
295 our $projlist_cache_name = 'gitweb.index.cache';
296 our $cache_grpshared = 0;
298 # information about snapshot formats that gitweb is capable of serving
299 our %known_snapshot_formats = (
301 # 'display' => display name,
302 # 'type' => mime type,
303 # 'suffix' => filename suffix,
304 # 'format' => --format for git-archive,
305 # 'compressor' => [compressor command and arguments]
306 # (array reference, optional)
307 # 'disabled' => boolean (optional)}
310 'display' => 'tar.gz',
311 'type' => 'application/x-gzip',
312 'suffix' => '.tar.gz',
314 'compressor' => ['gzip', '-n']},
317 'display' => 'tar.bz2',
318 'type' => 'application/x-bzip2',
319 'suffix' => '.tar.bz2',
321 'compressor' => ['bzip2']},
324 'display' => 'tar.xz',
325 'type' => 'application/x-xz',
326 'suffix' => '.tar.xz',
328 'compressor' => ['xz'],
333 'type' => 'application/x-zip',
338 # Aliases so we understand old gitweb.snapshot values in repository
340 our %known_snapshot_format_aliases = (
345 # backward compatibility: legacy gitweb config support
346 'x-gzip' => undef, 'gz' => undef,
347 'x-bzip2' => undef, 'bz2' => undef,
348 'x-zip' => undef, '' => undef,
351 # Pixel sizes for icons and avatars. If the default font sizes or lineheights
352 # are changed, it may be appropriate to change these values too via
359 # Used to set the maximum load that we will still respond to gitweb queries.
360 # If server load exceed this value then return "503 server busy" error.
361 # If gitweb cannot determined server load, it is taken to be 0.
362 # Leave it undefined (or set to 'undef') to turn off load checking.
365 # configuration for 'highlight' (http://www.andre-simon.de/)
367 our %highlight_basename = (
370 'SConstruct' => 'py', # SCons equivalent of Makefile
371 'Makefile' => 'make',
372 'makefile' => 'make',
373 'GNUmakefile' => 'make',
374 'BSDmakefile' => 'make',
376 # match by shebang regex
377 our %highlight_shebang = (
378 # Each entry has a key which is the syntax to use and
379 # a value which is either a qr regex or an array of qr regexs to match
380 # against the first 128 (less if the blob is shorter) BYTES of the blob.
381 # We match /usr/bin/env items separately to require "/usr/bin/env" and
382 # allow a limited subset of NAME=value items to appear.
383 'awk' => [ qr
,^#!\s*/(?:\w+/)*(?:[gnm]?awk)(?:\s|$),mo,
384 qr
,^#!\s*/usr/bin/env\s+(?:\w+=\w*\s+)*(?:[gnm]?awk)(?:\s|$),mo ],
385 'make' => [ qr
,^#!\s*/(?:\w+/)*(?:g?make)(?:\s|$),mo,
386 qr
,^#!\s*/usr/bin/env\s+(?:\w+=\w*\s+)*(?:g?make)(?:\s|$),mo ],
387 'php' => [ qr
,^#!\s*/(?:\w+/)*(?:php)(?:\s|$),mo,
388 qr
,^#!\s*/usr/bin/env\s+(?:\w+=\w*\s+)*(?:php)(?:\s|$),mo ],
389 'pl' => [ qr
,^#!\s*/(?:\w+/)*(?:perl)(?:\s|$),mo,
390 qr
,^#!\s*/usr/bin/env\s+(?:\w+=\w*\s+)*(?:perl)(?:\s|$),mo ],
391 'py' => [ qr
,^#!\s*/(?:\w+/)*(?:python)(?:\s|$),mo,
392 qr
,^#!\s*/usr/bin/env\s+(?:\w+=\w*\s+)*(?:python)(?:\s|$),mo ],
393 'sh' => [ qr
,^#!\s*/(?:\w+/)*(?:[bd]ash|t?csh|[akz]?sh)(?:\s|$),mo,
394 qr
,^#!\s*/usr/bin/env\s+(?:\w+=\w*\s+)*(?:[bd]ash|t?csh|[akz]?sh)(?:\s|$),mo ],
395 'rb' => [ qr
,^#!\s*/(?:\w+/)*(?:perl)(?:\s|$),mo,
396 qr
,^#!\s*/usr/bin/env\s+(?:\w+=\w*\s+)*(?:perl)(?:\s|$),mo ],
399 our %highlight_ext = (
400 # main extensions, defining name of syntax;
401 # see files in /usr/share/highlight/langDefs/ directory
402 (map { $_ => $_ } qw(
403 4gl a4c abnf abp ada agda ahk ampl amtrix applescript arc
404 arm as asm asp aspect ats au3 avenue awk bat bb bbcode bib
405 bms bnf boo c cb cfc chl clipper clojure clp cob cs css d
406 diff dot dylan e ebnf erl euphoria exp f90 flx for frink fs
407 go haskell hcl html httpd hx icl icn idl idlang ili
408 inc_luatex ini inp io iss j java js jsp lbn ldif lgt lhs
409 lisp lotos ls lsl lua ly make mel mercury mib miranda ml mo
410 mod2 mod3 mpl ms mssql n nas nbc nice nrx nsi nut nxc oberon
411 objc octave oorexx os oz pas php pike pl pl1 pov pro
412 progress ps ps1 psl pure py pyx q qmake qu r rb rebol rexx
413 rnc s sas sc scala scilab sh sma smalltalk sml sno spec spn
414 sql sybase tcl tcsh tex ttcn3 vala vb verilog vhd xml xpp y
416 # alternate extensions, see /etc/highlight/filetypes.conf
417 (map { $_ => '4gl' } qw(informix)),
418 (map { $_ => 'a4c' } qw(ascend)),
419 (map { $_ => 'abp' } qw(abp4)),
420 (map { $_ => 'ada' } qw(a adb ads gnad)),
421 (map { $_ => 'ahk' } qw(autohotkey)),
422 (map { $_ => 'ampl' } qw(dat run)),
423 (map { $_ => 'amtrix' } qw(hnd s4 s4h s4t t4)),
424 (map { $_ => 'as' } qw(actionscript)),
425 (map { $_ => 'asm' } qw(29k 68s 68x a51 assembler x68 x86)),
426 (map { $_ => 'asp' } qw(asa)),
427 (map { $_ => 'aspect' } qw(was wud)),
428 (map { $_ => 'ats' } qw(dats)),
429 (map { $_ => 'au3' } qw(autoit)),
430 (map { $_ => 'bat' } qw(cmd)),
431 (map { $_ => 'bb' } qw(blitzbasic)),
432 (map { $_ => 'bib' } qw(bibtex)),
433 (map { $_ => 'c' } qw(c++ cc cpp cu cxx h hh hpp hxx)),
434 (map { $_ => 'cb' } qw(clearbasic)),
435 (map { $_ => 'cfc' } qw(cfm coldfusion)),
436 (map { $_ => 'chl' } qw(chill)),
437 (map { $_ => 'cob' } qw(cbl cobol)),
438 (map { $_ => 'cs' } qw(csharp)),
439 (map { $_ => 'diff' } qw(patch)),
440 (map { $_ => 'dot' } qw(graphviz)),
441 (map { $_ => 'e' } qw(eiffel se)),
442 (map { $_ => 'erl' } qw(erlang hrl)),
443 (map { $_ => 'euphoria' } qw(eu ew ex exu exw wxu)),
444 (map { $_ => 'exp' } qw(express)),
445 (map { $_ => 'f90' } qw(f95)),
446 (map { $_ => 'flx' } qw(felix)),
447 (map { $_ => 'for' } qw(f f77 ftn)),
448 (map { $_ => 'fs' } qw(fsharp fsx)),
449 (map { $_ => 'haskell' } qw(hs)),
450 (map { $_ => 'html' } qw(htm xhtml)),
451 (map { $_ => 'hx' } qw(haxe)),
452 (map { $_ => 'icl' } qw(clean)),
453 (map { $_ => 'icn' } qw(icon)),
454 (map { $_ => 'ili' } qw(interlis)),
455 (map { $_ => 'inp' } qw(fame)),
456 (map { $_ => 'iss' } qw(innosetup)),
457 (map { $_ => 'j' } qw(jasmin)),
458 (map { $_ => 'java' } qw(groovy grv)),
459 (map { $_ => 'lbn' } qw(luban)),
460 (map { $_ => 'lgt' } qw(logtalk)),
461 (map { $_ => 'lisp' } qw(cl clisp el lsp sbcl scom)),
462 (map { $_ => 'ls' } qw(lotus)),
463 (map { $_ => 'lsl' } qw(lindenscript)),
464 (map { $_ => 'ly' } qw(lilypond)),
465 (map { $_ => 'make' } qw(mak mk kmk)),
466 (map { $_ => 'mel' } qw(maya)),
467 (map { $_ => 'mib' } qw(smi snmp)),
468 (map { $_ => 'ml' } qw(mli ocaml)),
469 (map { $_ => 'mo' } qw(modelica)),
470 (map { $_ => 'mod2' } qw(def mod)),
471 (map { $_ => 'mod3' } qw(i3 m3)),
472 (map { $_ => 'mpl' } qw(maple)),
473 (map { $_ => 'n' } qw(nemerle)),
474 (map { $_ => 'nas' } qw(nasal)),
475 (map { $_ => 'nrx' } qw(netrexx)),
476 (map { $_ => 'nsi' } qw(nsis)),
477 (map { $_ => 'nut' } qw(squirrel)),
478 (map { $_ => 'oberon' } qw(ooc)),
479 (map { $_ => 'objc' } qw(M m mm)),
480 (map { $_ => 'php' } qw(php3 php4 php5 php6)),
481 (map { $_ => 'pike' } qw(pmod)),
482 (map { $_ => 'pl' } qw(perl plex plx pm)),
483 (map { $_ => 'pl1' } qw(bdy ff fp fpp rpp sf sp spb spe spp sps wf wp wpb wpp wps)),
484 (map { $_ => 'progress' } qw(i p w)),
485 (map { $_ => 'py' } qw(python)),
486 (map { $_ => 'pyx' } qw(pyrex)),
487 (map { $_ => 'rb' } qw(pp rjs ruby)),
488 (map { $_ => 'rexx' } qw(rex rx the)),
489 (map { $_ => 'sc' } qw(paradox)),
490 (map { $_ => 'scilab' } qw(sce sci)),
491 (map { $_ => 'sh' } qw(bash ebuild eclass ksh zsh)),
492 (map { $_ => 'sma' } qw(small)),
493 (map { $_ => 'smalltalk' } qw(gst sq st)),
494 (map { $_ => 'sno' } qw(snobal)),
495 (map { $_ => 'sybase' } qw(sp)),
496 (map { $_ => 'tcl' } qw(itcl wish)),
497 (map { $_ => 'tex' } qw(cls sty)),
498 (map { $_ => 'vb' } qw(bas basic bi vbs)),
499 (map { $_ => 'verilog' } qw(v)),
500 (map { $_ => 'xml' } qw(dtd ecf ent hdr hub jnlp nrm plist resx sgm sgml svg tld vxml wml xsd xsl)),
501 (map { $_ => 'y' } qw(bison)),
504 # You define site-wide feature defaults here; override them with
505 # $GITWEB_CONFIG as necessary.
508 # 'sub' => feature-sub (subroutine),
509 # 'override' => allow-override (boolean),
510 # 'default' => [ default options...] (array reference)}
512 # if feature is overridable (it means that allow-override has true value),
513 # then feature-sub will be called with default options as parameters;
514 # return value of feature-sub indicates if to enable specified feature
516 # if there is no 'sub' key (no feature-sub), then feature cannot be
519 # use gitweb_get_feature(<feature>) to retrieve the <feature> value
520 # (an array) or gitweb_check_feature(<feature>) to check if <feature>
523 # Enable the 'blame' blob view, showing the last commit that modified
524 # each line in the file. This can be very CPU-intensive.
526 # To enable system wide have in $GITWEB_CONFIG
527 # $feature{'blame'}{'default'} = [1];
528 # To have project specific config enable override in $GITWEB_CONFIG
529 # $feature{'blame'}{'override'} = 1;
530 # and in project config gitweb.blame = 0|1;
532 'sub' => sub { feature_bool
('blame', @_) },
536 # Enable the 'incremental blame' blob view, which uses javascript to
537 # incrementally show the revisions of lines as they are discovered
538 # in the history. It is better for large histories, files and slow
539 # servers, but requires javascript in the client and can slow down the
540 # browser on large files.
542 # To enable system wide have in $GITWEB_CONFIG
543 # $feature{'blame_incremental'}{'default'} = [1];
544 # To have project specific config enable override in $GITWEB_CONFIG
545 # $feature{'blame_incremental'}{'override'} = 1;
546 # and in project config gitweb.blame_incremental = 0|1;
547 'blame_incremental' => {
548 'sub' => sub { feature_bool
('blame_incremental', @_) },
552 # Enable the 'snapshot' link, providing a compressed archive of any
553 # tree. This can potentially generate high traffic if you have large
556 # Value is a list of formats defined in %known_snapshot_formats that
558 # To disable system wide have in $GITWEB_CONFIG
559 # $feature{'snapshot'}{'default'} = [];
560 # To have project specific config enable override in $GITWEB_CONFIG
561 # $feature{'snapshot'}{'override'} = 1;
562 # and in project config, a comma-separated list of formats or "none"
563 # to disable. Example: gitweb.snapshot = tbz2,zip;
565 'sub' => \
&feature_snapshot
,
567 'default' => ['tgz']},
569 # Enable text search, which will list the commits which match author,
570 # committer or commit text to a given string. Enabled by default.
571 # Project specific override is not supported.
573 # Note that this controls all search features, which means that if
574 # it is disabled, then 'grep' and 'pickaxe' search would also be
580 # Enable grep search, which will list the files in currently selected
581 # tree containing the given string. Enabled by default. This can be
582 # potentially CPU-intensive, of course.
583 # Note that you need to have 'search' feature enabled too.
585 # To enable system wide have in $GITWEB_CONFIG
586 # $feature{'grep'}{'default'} = [1];
587 # To have project specific config enable override in $GITWEB_CONFIG
588 # $feature{'grep'}{'override'} = 1;
589 # and in project config gitweb.grep = 0|1;
591 'sub' => sub { feature_bool
('grep', @_) },
595 # Enable the pickaxe search, which will list the commits that modified
596 # a given string in a file. This can be practical and quite faster
597 # alternative to 'blame', but still potentially CPU-intensive.
598 # Note that you need to have 'search' feature enabled too.
600 # To enable system wide have in $GITWEB_CONFIG
601 # $feature{'pickaxe'}{'default'} = [1];
602 # To have project specific config enable override in $GITWEB_CONFIG
603 # $feature{'pickaxe'}{'override'} = 1;
604 # and in project config gitweb.pickaxe = 0|1;
606 'sub' => sub { feature_bool
('pickaxe', @_) },
610 # Enable showing size of blobs in a 'tree' view, in a separate
611 # column, similar to what 'ls -l' does. This cost a bit of IO.
613 # To disable system wide have in $GITWEB_CONFIG
614 # $feature{'show-sizes'}{'default'} = [0];
615 # To have project specific config enable override in $GITWEB_CONFIG
616 # $feature{'show-sizes'}{'override'} = 1;
617 # and in project config gitweb.showsizes = 0|1;
619 'sub' => sub { feature_bool
('showsizes', @_) },
623 # Make gitweb use an alternative format of the URLs which can be
624 # more readable and natural-looking: project name is embedded
625 # directly in the path and the query string contains other
626 # auxiliary information. All gitweb installations recognize
627 # URL in either format; this configures in which formats gitweb
630 # To enable system wide have in $GITWEB_CONFIG
631 # $feature{'pathinfo'}{'default'} = [1];
632 # Project specific override is not supported.
634 # Note that you will need to change the default location of CSS,
635 # favicon, logo and possibly other files to an absolute URL. Also,
636 # if gitweb.cgi serves as your indexfile, you will need to force
637 # $my_uri to contain the script name in your $GITWEB_CONFIG (and you
638 # will also likely want to set $home_link if you're setting $my_uri).
643 # Make gitweb consider projects in project root subdirectories
644 # to be forks of existing projects. Given project $projname.git,
645 # projects matching $projname/*.git will not be shown in the main
646 # projects list, instead a '+' mark will be added to $projname
647 # there and a 'forks' view will be enabled for the project, listing
648 # all the forks. If project list is taken from a file, forks have
649 # to be listed after the main project.
651 # To enable system wide have in $GITWEB_CONFIG
652 # $feature{'forks'}{'default'} = [1];
653 # Project specific override is not supported.
658 # Insert custom links to the action bar of all project pages.
659 # This enables you mainly to link to third-party scripts integrating
660 # into gitweb; e.g. git-browser for graphical history representation
661 # or custom web-based repository administration interface.
663 # The 'default' value consists of a list of triplets in the form
664 # (label, link, position) where position is the label after which
665 # to insert the link and link is a format string where %n expands
666 # to the project name, %f to the project path within the filesystem,
667 # %h to the current hash (h gitweb parameter) and %b to the current
668 # hash base (hb gitweb parameter); %% expands to %. %e expands to the
669 # project name where all '+' characters have been replaced with '%2B'.
671 # To enable system wide have in $GITWEB_CONFIG e.g.
672 # $feature{'actions'}{'default'} = [('graphiclog',
673 # '/git-browser/by-commit.html?r=%n', 'summary')];
674 # Project specific override is not supported.
679 # Allow gitweb scan project content tags of project repository,
680 # and display the popular Web 2.0-ish "tag cloud" near the projects
681 # list. Note that this is something COMPLETELY different from the
684 # gitweb by itself can show existing tags, but it does not handle
685 # tagging itself; you need to do it externally, outside gitweb.
686 # The format is described in git_get_project_ctags() subroutine.
687 # You may want to install the HTML::TagCloud Perl module to get
688 # a pretty tag cloud instead of just a list of tags.
690 # To enable system wide have in $GITWEB_CONFIG
691 # $feature{'ctags'}{'default'} = [1];
692 # Project specific override is not supported.
694 # A value of 0 means no ctags display or editing. A value of
695 # 1 enables ctags display but never editing. A non-empty value
696 # that is not a string of digits enables ctags display AND the
697 # ability to add tags using a form that uses method POST and
698 # an action value set to the configured 'ctags' value.
703 # The maximum number of patches in a patchset generated in patch
704 # view. Set this to 0 or undef to disable patch view, or to a
705 # negative number to remove any limit.
707 # To disable system wide have in $GITWEB_CONFIG
708 # $feature{'patches'}{'default'} = [0];
709 # To have project specific config enable override in $GITWEB_CONFIG
710 # $feature{'patches'}{'override'} = 1;
711 # and in project config gitweb.patches = 0|n;
712 # where n is the maximum number of patches allowed in a patchset.
714 'sub' => \
&feature_patches
,
718 # Avatar support. When this feature is enabled, views such as
719 # shortlog or commit will display an avatar associated with
720 # the email of the committer(s) and/or author(s).
722 # Currently available providers are gravatar and picon.
723 # If an unknown provider is specified, the feature is disabled.
725 # Gravatar depends on Digest::MD5.
726 # Picon currently relies on the indiana.edu database.
728 # To enable system wide have in $GITWEB_CONFIG
729 # $feature{'avatar'}{'default'} = ['<provider>'];
730 # where <provider> is either gravatar or picon.
731 # To have project specific config enable override in $GITWEB_CONFIG
732 # $feature{'avatar'}{'override'} = 1;
733 # and in project config gitweb.avatar = <provider>;
735 'sub' => \
&feature_avatar
,
739 # Enable displaying how much time and how many git commands
740 # it took to generate and display page. Disabled by default.
741 # Project specific override is not supported.
746 # Enable turning some links into links to actions which require
747 # JavaScript to run (like 'blame_incremental'). Not enabled by
748 # default. Project specific override is currently not supported.
749 'javascript-actions' => {
753 # Enable and configure ability to change common timezone for dates
754 # in gitweb output via JavaScript. Enabled by default.
755 # Project specific override is not supported.
756 'javascript-timezone' => {
759 'local', # default timezone: 'utc', 'local', or '(-|+)HHMM' format,
760 # or undef to turn off this feature
761 'gitweb_tz', # name of cookie where to store selected timezone
762 'datetime', # CSS class used to mark up dates for manipulation
765 # Syntax highlighting support. This is based on Daniel Svensson's
766 # and Sham Chukoury's work in gitweb-xmms2.git.
767 # It requires the 'highlight' program present in $PATH,
768 # and therefore is disabled by default.
770 # To enable system wide have in $GITWEB_CONFIG
771 # $feature{'highlight'}{'default'} = [1];
774 'sub' => sub { feature_bool
('highlight', @_) },
778 # Enable displaying of remote heads in the heads list
780 # To enable system wide have in $GITWEB_CONFIG
781 # $feature{'remote_heads'}{'default'} = [1];
782 # To have project specific config enable override in $GITWEB_CONFIG
783 # $feature{'remote_heads'}{'override'} = 1;
784 # and in project config gitweb.remoteheads = 0|1;
786 'sub' => sub { feature_bool
('remote_heads', @_) },
790 # Enable showing branches under other refs in addition to heads
792 # To set system wide extra branch refs have in $GITWEB_CONFIG
793 # $feature{'extra-branch-refs'}{'default'} = ['dirs', 'of', 'choice'];
794 # To have project specific config enable override in $GITWEB_CONFIG
795 # $feature{'extra-branch-refs'}{'override'} = 1;
796 # and in project config gitweb.extrabranchrefs = dirs of choice
797 # Every directory is separated with whitespace.
799 'extra-branch-refs' => {
800 'sub' => \
&feature_extra_branch_refs
,
805 sub gitweb_get_feature
{
807 return unless exists $feature{$name};
808 my ($sub, $override, @defaults) = (
809 $feature{$name}{'sub'},
810 $feature{$name}{'override'},
811 @
{$feature{$name}{'default'}});
812 # project specific override is possible only if we have project
813 our $git_dir; # global variable, declared later
814 if (!$override || !defined $git_dir) {
818 warn "feature $name is not overridable";
821 return $sub->(@defaults);
824 # A wrapper to check if a given feature is enabled.
825 # With this, you can say
827 # my $bool_feat = gitweb_check_feature('bool_feat');
828 # gitweb_check_feature('bool_feat') or somecode;
832 # my ($bool_feat) = gitweb_get_feature('bool_feat');
833 # (gitweb_get_feature('bool_feat'))[0] or somecode;
835 sub gitweb_check_feature
{
836 return (gitweb_get_feature
(@_))[0];
842 my ($val) = git_get_project_config
($key, '--bool');
846 } elsif ($val eq 'true') {
848 } elsif ($val eq 'false') {
853 sub feature_snapshot
{
856 my ($val) = git_get_project_config
('snapshot');
859 @fmts = ($val eq 'none' ?
() : split /\s*[,\s]\s*/, $val);
865 sub feature_patches
{
866 my @val = (git_get_project_config
('patches', '--int'));
876 my @val = (git_get_project_config
('avatar'));
878 return @val ?
@val : @_;
881 sub feature_extra_branch_refs
{
882 my (@branch_refs) = @_;
883 my $values = git_get_project_config
('extrabranchrefs');
886 $values = config_to_multi
($values);
888 foreach my $value (@
{$values}) {
889 push @branch_refs, split /\s+/, $value;
896 # checking HEAD file with -e is fragile if the repository was
897 # initialized long time ago (i.e. symlink HEAD) and was pack-ref'ed
899 sub check_head_link
{
901 return 0 unless -d
"$dir/objects" && -x _
;
902 return 0 unless -d
"$dir/refs" && -x _
;
903 my $headfile = "$dir/HEAD";
904 return -l
$headfile ?
905 readlink($headfile) =~ /^refs\/heads\
// : -f
$headfile;
908 sub check_export_ok
{
910 return (check_head_link
($dir) &&
911 (!$export_ok || -e
"$dir/$export_ok") &&
912 (!$export_auth_hook || $export_auth_hook->($dir)));
915 # process alternate names for backward compatibility
916 # filter out unsupported (unknown) snapshot formats
917 sub filter_snapshot_fmts
{
921 exists $known_snapshot_format_aliases{$_} ?
922 $known_snapshot_format_aliases{$_} : $_} @fmts;
924 exists $known_snapshot_formats{$_} &&
925 !$known_snapshot_formats{$_}{'disabled'}} @fmts;
928 sub filter_and_validate_refs
{
930 my %unique_refs = ();
932 foreach my $ref (@refs) {
933 die_error
(500, "Invalid ref '$ref' in 'extra-branch-refs' feature") unless (is_valid_ref_format
($ref));
934 # 'heads' are added implicitly in get_branch_refs().
935 $unique_refs{$ref} = 1 if ($ref ne 'heads');
937 return sort keys %unique_refs;
940 # If it is set to code reference, it is code that it is to be run once per
941 # request, allowing updating configurations that change with each request,
942 # while running other code in config file only once.
944 # Otherwise, if it is false then gitweb would process config file only once;
945 # if it is true then gitweb config would be run for each request.
946 our $per_request_config = 1;
948 # If true and fileno STDIN is 0 and getsockname succeeds and getpeername fails
949 # with ENOTCONN, then FCGI mode will be activated automatically in just the
950 # same way as though the --fcgi option had been given instead.
953 # read and parse gitweb config file given by its parameter.
954 # returns true on success, false on recoverable error, allowing
955 # to chain this subroutine, using first file that exists.
956 # dies on errors during parsing config file, as it is unrecoverable.
957 sub read_config_file
{
958 my $filename = shift;
959 return unless defined $filename;
960 # die if there are errors parsing config file
969 our ($GITWEB_CONFIG, $GITWEB_CONFIG_SYSTEM, $GITWEB_CONFIG_COMMON);
970 sub evaluate_gitweb_config
{
971 our $GITWEB_CONFIG = $ENV{'GITWEB_CONFIG'} || "++GITWEB_CONFIG++";
972 our $GITWEB_CONFIG_SYSTEM = $ENV{'GITWEB_CONFIG_SYSTEM'} || "++GITWEB_CONFIG_SYSTEM++";
973 our $GITWEB_CONFIG_COMMON = $ENV{'GITWEB_CONFIG_COMMON'} || "++GITWEB_CONFIG_COMMON++";
975 # Protect against duplications of file names, to not read config twice.
976 # Only one of $GITWEB_CONFIG and $GITWEB_CONFIG_SYSTEM is used, so
977 # there possibility of duplication of filename there doesn't matter.
978 $GITWEB_CONFIG = "" if ($GITWEB_CONFIG eq $GITWEB_CONFIG_COMMON);
979 $GITWEB_CONFIG_SYSTEM = "" if ($GITWEB_CONFIG_SYSTEM eq $GITWEB_CONFIG_COMMON);
981 # Common system-wide settings for convenience.
982 # Those settings can be ovverriden by GITWEB_CONFIG or GITWEB_CONFIG_SYSTEM.
983 read_config_file
($GITWEB_CONFIG_COMMON);
985 # Use first config file that exists. This means use the per-instance
986 # GITWEB_CONFIG if exists, otherwise use GITWEB_SYSTEM_CONFIG.
987 read_config_file
($GITWEB_CONFIG) and return;
988 read_config_file
($GITWEB_CONFIG_SYSTEM);
992 our $to_utf8_pipe_command = '';
994 sub evaluate_encoding
{
995 my $requested = $fallback_encoding || 'ISO-8859-1';
996 my $obj = Encode
::find_encoding
($requested) or
997 die_error
(400, "Requested fallback encoding not found");
998 if ($obj->name eq 'iso-8859-1') {
999 # Use Windows-1252 instead as required by the HTML 5 standard
1000 my $altobj = Encode
::find_encoding
('Windows-1252');
1001 $obj = $altobj if $altobj;
1003 $encode_object = $obj;
1004 my $nm = lc($encode_object->name);
1005 unless ($nm eq 'cp1252' || $nm eq 'ascii' || $nm eq 'utf8' ||
1006 $nm =~ /^utf-8/ || $nm =~ /^iso-8859-/) {
1007 $to_utf8_pipe_command =
1008 quote_command
($^X
, '-CO', '-MEncode=decode,FB_DEFAULT', '-pse',
1009 '$_ = decode($fe, $_, FB_DEFAULT) if !utf8::decode($_);',
1010 '--', "-fe=$fallback_encoding")." | ";
1014 sub evaluate_email_obfuscate
{
1017 if (!$email && eval { require HTML
::Email
::Obfuscate
; 1 }) {
1018 $email = HTML
::Email
::Obfuscate
->new(lite
=> 1);
1022 # Get loadavg of system, to compare against $maxload.
1023 # Currently it requires '/proc/loadavg' present to get loadavg;
1024 # if it is not present it returns 0, which means no load checking.
1026 if( -e
'/proc/loadavg' ){
1027 open my $fd, '<', '/proc/loadavg'
1029 my @load = split(/\s+/, scalar <$fd>);
1032 # The first three columns measure CPU and IO utilization of the last one,
1033 # five, and 10 minute periods. The fourth column shows the number of
1034 # currently running processes and the total number of processes in the m/n
1035 # format. The last column displays the last process ID used.
1036 return $load[0] || 0;
1038 # additional checks for load average should go here for things that don't export
1044 # version of the core git binary
1046 sub evaluate_git_version
{
1047 our $git_version = $version;
1051 if (defined $maxload && get_loadavg
() > $maxload) {
1052 die_error
(503, "The load average on the server is too high");
1056 # ======================================================================
1057 # input validation and dispatch
1059 # input parameters can be collected from a variety of sources (presently, CGI
1060 # and PATH_INFO), so we define an %input_params hash that collects them all
1061 # together during validation: this allows subsequent uses (e.g. href()) to be
1062 # agnostic of the parameter origin
1064 our %input_params = ();
1066 # input parameters are stored with the long parameter name as key. This will
1067 # also be used in the href subroutine to convert parameters to their CGI
1068 # equivalent, and since the href() usage is the most frequent one, we store
1069 # the name -> CGI key mapping here, instead of the reverse.
1071 # XXX: Warning: If you touch this, check the search form for updating,
1074 our @cgi_param_mapping = (
1078 file_parent
=> "fp",
1080 hash_parent
=> "hp",
1082 hash_parent_base
=> "hpb",
1087 snapshot_format
=> "sf",
1089 extra_options
=> "opt",
1090 search_use_regexp
=> "sr",
1093 project_filter
=> "pf",
1094 # this must be last entry (for manipulation from JavaScript)
1097 our %cgi_param_mapping = @cgi_param_mapping;
1099 # we will also need to know the possible actions, for validation
1101 "blame" => \
&git_blame
,
1102 "blame_incremental" => \
&git_blame_incremental
,
1103 "blame_data" => \
&git_blame_data
,
1104 "blobdiff" => \
&git_blobdiff
,
1105 "blobdiff_plain" => \
&git_blobdiff_plain
,
1106 "blob" => \
&git_blob
,
1107 "blob_plain" => \
&git_blob_plain
,
1108 "commitdiff" => \
&git_commitdiff
,
1109 "commitdiff_plain" => \
&git_commitdiff_plain
,
1110 "commit" => \
&git_commit
,
1111 "forks" => \
&git_forks
,
1112 "heads" => \
&git_heads
,
1113 "history" => \
&git_history
,
1115 "patch" => \
&git_patch
,
1116 "patches" => \
&git_patches
,
1117 "refs" => \
&git_refs
,
1118 "remotes" => \
&git_remotes
,
1120 "atom" => \
&git_atom
,
1121 "search" => \
&git_search
,
1122 "search_help" => \
&git_search_help
,
1123 "shortlog" => \
&git_shortlog
,
1124 "summary" => \
&git_summary
,
1126 "tags" => \
&git_tags
,
1127 "tree" => \
&git_tree
,
1128 "snapshot" => \
&git_snapshot
,
1129 "object" => \
&git_object
,
1130 # those below don't need $project
1131 "opml" => \
&git_opml
,
1132 "frontpage" => \
&git_frontpage
,
1133 "project_list" => \
&git_project_list
,
1134 "project_index" => \
&git_project_index
,
1137 # the only actions we will allow to be cached
1138 my %supported_cache_actions;
1139 BEGIN {%supported_cache_actions = map {( $_ => 1 )} qw(summary)}
1141 # finally, we have the hash of allowed extra_options for the commands that
1143 our %allowed_options = (
1144 "--no-merges" => [ qw(rss atom log shortlog history) ],
1147 # fill %input_params with the CGI parameters. All values except for 'opt'
1148 # should be single values, but opt can be an array. We should probably
1149 # build an array of parameters that can be multi-valued, but since for the time
1150 # being it's only this one, we just single it out
1151 sub evaluate_query_params
{
1154 while (my ($name, $symbol) = each %cgi_param_mapping) {
1155 if ($symbol eq 'opt') {
1156 $input_params{$name} = [ map { decode_utf8
($_) } $cgi->multi_param($symbol) ];
1158 $input_params{$name} = decode_utf8
($cgi->param($symbol));
1162 # Backwards compatibility - by_tag= <=> t=
1163 if ($input_params{'ctag'}) {
1164 $input_params{'ctag_filter'} = $input_params{'ctag'};
1168 # now read PATH_INFO and update the parameter list for missing parameters
1169 sub evaluate_path_info
{
1170 return if defined $input_params{'project'};
1171 return if !$path_info;
1172 $path_info =~ s
,^/+,,;
1173 return if !$path_info;
1175 # find which part of PATH_INFO is project
1176 my $project = $path_info;
1177 $project =~ s
,/+$,,;
1178 while ($project && !check_head_link
("$projectroot/$project")) {
1179 $project =~ s
,/*[^/]*$,,;
1181 return unless $project;
1182 $input_params{'project'} = $project;
1184 # do not change any parameters if an action is given using the query string
1185 return if $input_params{'action'};
1186 $path_info =~ s
,^\Q
$project\E
/*,,;
1188 # next, check if we have an action
1189 my $action = $path_info;
1190 $action =~ s
,/.*$,,;
1191 if (exists $actions{$action}) {
1192 $path_info =~ s
,^$action/*,,;
1193 $input_params{'action'} = $action;
1196 # list of actions that want hash_base instead of hash, but can have no
1197 # pathname (f) parameter
1203 # we want to catch, among others
1204 # [$hash_parent_base[:$file_parent]..]$hash_parent[:$file_name]
1205 my ($parentrefname, $parentpathname, $refname, $pathname) =
1206 ($path_info =~ /^(?:(.+?)(?::(.+))?\.\.)?([^:]+?)?(?::(.+))?$/);
1208 # first, analyze the 'current' part
1209 if (defined $pathname) {
1210 # we got "branch:filename" or "branch:dir/"
1211 # we could use git_get_type(branch:pathname), but:
1212 # - it needs $git_dir
1213 # - it does a git() call
1214 # - the convention of terminating directories with a slash
1215 # makes it superfluous
1216 # - embedding the action in the PATH_INFO would make it even
1218 $pathname =~ s
,^/+,,;
1219 if (!$pathname || substr($pathname, -1) eq "/") {
1220 $input_params{'action'} ||= "tree";
1221 $pathname =~ s
,/$,,;
1223 # the default action depends on whether we had parent info
1225 if ($parentrefname) {
1226 $input_params{'action'} ||= "blobdiff_plain";
1228 $input_params{'action'} ||= "blob_plain";
1231 $input_params{'hash_base'} ||= $refname;
1232 $input_params{'file_name'} ||= $pathname;
1233 } elsif (defined $refname) {
1234 # we got "branch". In this case we have to choose if we have to
1235 # set hash or hash_base.
1237 # Most of the actions without a pathname only want hash to be
1238 # set, except for the ones specified in @wants_base that want
1239 # hash_base instead. It should also be noted that hand-crafted
1240 # links having 'history' as an action and no pathname or hash
1241 # set will fail, but that happens regardless of PATH_INFO.
1242 if (defined $parentrefname) {
1243 # if there is parent let the default be 'shortlog' action
1244 # (for http://git.example.com/repo.git/A..B links); if there
1245 # is no parent, dispatch will detect type of object and set
1246 # action appropriately if required (if action is not set)
1247 $input_params{'action'} ||= "shortlog";
1249 if ($input_params{'action'} &&
1250 grep { $_ eq $input_params{'action'} } @wants_base) {
1251 $input_params{'hash_base'} ||= $refname;
1253 $input_params{'hash'} ||= $refname;
1257 # next, handle the 'parent' part, if present
1258 if (defined $parentrefname) {
1259 # a missing pathspec defaults to the 'current' filename, allowing e.g.
1260 # someproject/blobdiff/oldrev..newrev:/filename
1261 if ($parentpathname) {
1262 $parentpathname =~ s
,^/+,,;
1263 $parentpathname =~ s
,/$,,;
1264 $input_params{'file_parent'} ||= $parentpathname;
1266 $input_params{'file_parent'} ||= $input_params{'file_name'};
1268 # we assume that hash_parent_base is wanted if a path was specified,
1269 # or if the action wants hash_base instead of hash
1270 if (defined $input_params{'file_parent'} ||
1271 grep { $_ eq $input_params{'action'} } @wants_base) {
1272 $input_params{'hash_parent_base'} ||= $parentrefname;
1274 $input_params{'hash_parent'} ||= $parentrefname;
1278 # for the snapshot action, we allow URLs in the form
1279 # $project/snapshot/$hash.ext
1280 # where .ext determines the snapshot and gets removed from the
1281 # passed $refname to provide the $hash.
1283 # To be able to tell that $refname includes the format extension, we
1284 # require the following two conditions to be satisfied:
1285 # - the hash input parameter MUST have been set from the $refname part
1286 # of the URL (i.e. they must be equal)
1287 # - the snapshot format MUST NOT have been defined already (e.g. from
1289 # It's also useless to try any matching unless $refname has a dot,
1290 # so we check for that too
1291 if (defined $input_params{'action'} &&
1292 $input_params{'action'} eq 'snapshot' &&
1293 defined $refname && index($refname, '.') != -1 &&
1294 $refname eq $input_params{'hash'} &&
1295 !defined $input_params{'snapshot_format'}) {
1296 # We loop over the known snapshot formats, checking for
1297 # extensions. Allowed extensions are both the defined suffix
1298 # (which includes the initial dot already) and the snapshot
1299 # format key itself, with a prepended dot
1300 while (my ($fmt, $opt) = each %known_snapshot_formats) {
1301 my $hash = $refname;
1302 unless ($hash =~ s/(\Q$opt->{'suffix'}\E|\Q.$fmt\E)$//) {
1306 # a valid suffix was found, so set the snapshot format
1307 # and reset the hash parameter
1308 $input_params{'snapshot_format'} = $fmt;
1309 $input_params{'hash'} = $hash;
1310 # we also set the format suffix to the one requested
1311 # in the URL: this way a request for e.g. .tgz returns
1312 # a .tgz instead of a .tar.gz
1313 $known_snapshot_formats{$fmt}{'suffix'} = $sfx;
1319 our ($action, $project, $file_name, $file_parent, $hash, $hash_parent, $hash_base,
1320 $hash_parent_base, @extra_options, $page, $searchtype, $search_use_regexp,
1321 $searchtext, $search_regexp, $project_filter);
1322 sub evaluate_and_validate_params
{
1323 our $action = $input_params{'action'};
1324 if (defined $action) {
1325 if (!is_valid_action
($action)) {
1326 die_error
(400, "Invalid action parameter");
1330 # parameters which are pathnames
1331 our $project = $input_params{'project'};
1332 if (defined $project) {
1333 if (!is_valid_project
($project)) {
1335 die_error
(404, "No such project");
1339 our $project_filter = $input_params{'project_filter'};
1340 if (defined $project_filter) {
1341 if (!is_valid_pathname
($project_filter)) {
1342 die_error
(404, "Invalid project_filter parameter");
1346 our $file_name = $input_params{'file_name'};
1347 if (defined $file_name) {
1348 if (!is_valid_pathname
($file_name)) {
1349 die_error
(400, "Invalid file parameter");
1353 our $file_parent = $input_params{'file_parent'};
1354 if (defined $file_parent) {
1355 if (!is_valid_pathname
($file_parent)) {
1356 die_error
(400, "Invalid file parent parameter");
1360 # parameters which are refnames
1361 our $hash = $input_params{'hash'};
1362 if (defined $hash) {
1363 if (!is_valid_refname
($hash)) {
1364 die_error
(400, "Invalid hash parameter");
1368 our $hash_parent = $input_params{'hash_parent'};
1369 if (defined $hash_parent) {
1370 if (!is_valid_refname
($hash_parent)) {
1371 die_error
(400, "Invalid hash parent parameter");
1375 our $hash_base = $input_params{'hash_base'};
1376 if (defined $hash_base) {
1377 if (!is_valid_refname
($hash_base)) {
1378 die_error
(400, "Invalid hash base parameter");
1382 our @extra_options = @
{$input_params{'extra_options'}};
1383 # @extra_options is always defined, since it can only be (currently) set from
1384 # CGI, and $cgi->param() returns the empty array in array context if the param
1386 foreach my $opt (@extra_options) {
1387 if (not exists $allowed_options{$opt}) {
1388 die_error
(400, "Invalid option parameter");
1390 if (not grep(/^$action$/, @
{$allowed_options{$opt}})) {
1391 die_error
(400, "Invalid option parameter for this action");
1395 our $hash_parent_base = $input_params{'hash_parent_base'};
1396 if (defined $hash_parent_base) {
1397 if (!is_valid_refname
($hash_parent_base)) {
1398 die_error
(400, "Invalid hash parent base parameter");
1403 our $page = $input_params{'page'};
1404 if (defined $page) {
1405 if ($page =~ m/[^0-9]/) {
1406 die_error
(400, "Invalid page parameter");
1410 our $searchtype = $input_params{'searchtype'};
1411 if (defined $searchtype) {
1412 if ($searchtype =~ m/[^a-z]/) {
1413 die_error
(400, "Invalid searchtype parameter");
1417 our $search_use_regexp = $input_params{'search_use_regexp'};
1419 our $searchtext = $input_params{'searchtext'};
1420 our $search_regexp = undef;
1421 if (defined $searchtext) {
1422 if (length($searchtext) < 2) {
1423 die_error
(403, "At least two characters are required for search parameter");
1425 if ($search_use_regexp) {
1426 $search_regexp = $searchtext;
1427 if (!eval { qr/$search_regexp/; 1; }) {
1428 (my $error = $@
) =~ s/ at \S+ line \d+.*\n?//;
1429 die_error
(400, "Invalid search regexp '$search_regexp'",
1433 $search_regexp = quotemeta $searchtext;
1438 # path to the current git repository
1440 sub evaluate_git_dir
{
1441 our $git_dir = $project ?
"$projectroot/$project" : undef;
1444 our (@snapshot_fmts, $git_avatar, @extra_branch_refs);
1445 sub configure_gitweb_features
{
1446 # list of supported snapshot formats
1447 our @snapshot_fmts = gitweb_get_feature
('snapshot');
1448 @snapshot_fmts = filter_snapshot_fmts
(@snapshot_fmts);
1450 # check that the avatar feature is set to a known provider name,
1451 # and for each provider check if the dependencies are satisfied.
1452 # if the provider name is invalid or the dependencies are not met,
1453 # reset $git_avatar to the empty string.
1454 our ($git_avatar) = gitweb_get_feature
('avatar');
1455 if ($git_avatar eq 'gravatar') {
1456 $git_avatar = '' unless (eval { require Digest
::MD5
; 1; });
1457 } elsif ($git_avatar eq 'picon') {
1463 our @extra_branch_refs = gitweb_get_feature
('extra-branch-refs');
1464 @extra_branch_refs = filter_and_validate_refs
(@extra_branch_refs);
1467 sub get_branch_refs
{
1468 return ('heads', @extra_branch_refs);
1471 # custom error handler: 'die <message>' is Internal Server Error
1472 sub handle_errors_html
{
1473 my $msg = shift; # it is already HTML escaped
1475 # to avoid infinite loop where error occurs in die_error,
1476 # change handler to default handler, disabling handle_errors_html
1477 set_message
("Error occurred when inside die_error:\n$msg");
1479 # you cannot jump out of die_error when called as error handler;
1480 # the subroutine set via CGI::Carp::set_message is called _after_
1481 # HTTP headers are already written, so it cannot write them itself
1482 die_error
(undef, undef, $msg, -error_handler
=> 1, -no_http_header
=> 1);
1484 set_message
(\
&handle_errors_html
);
1486 our $shown_stale_message = 0;
1487 our $cache_dump = undef;
1488 our $cache_dump_mtime = undef;
1491 my $cache_mode_active;
1493 if (!defined $action) {
1494 if (defined $hash) {
1495 $action = git_get_type
($hash);
1496 $action or die_error
(404, "Object does not exist");
1497 } elsif (defined $hash_base && defined $file_name) {
1498 $action = git_get_type
("$hash_base:$file_name");
1499 $action or die_error
(404, "File or directory does not exist");
1500 } elsif (defined $project) {
1501 $action = 'summary';
1503 $action = 'frontpage';
1506 if (!defined($actions{$action})) {
1507 die_error
(400, "Unknown action");
1509 if ($action !~ m/^(?:opml|frontpage|project_list|project_index)$/ &&
1511 die_error
(400, "Project needed");
1514 my $defstyle = $stylesheet;
1515 local $stylesheet = $defstyle;
1516 if ($ENV{'HTTP_COOKIE'} && ";$ENV{'HTTP_COOKIE'};" =~ /;\s*style\s*=\s*([a-z][a-z0-9_-]*)\s*;/) {{
1518 last unless $ENV{'DOCUMENT_ROOT'} && -r
"$ENV{'DOCUMENT_ROOT'}/style/$stylename.css";
1519 $stylesheet = "/style/$stylename.css";
1522 my $cached_page = $supported_cache_actions{$action}
1523 ? cached_action_page
($action)
1525 goto DUMPCACHE
if $cached_page;
1526 local *SAVEOUT
= *STDOUT
;
1527 $cache_mode_active = $supported_cache_actions{$action}
1528 ? cached_action_start
($action)
1531 configure_gitweb_features
();
1532 $actions{$action}->();
1534 return unless $cache_mode_active;
1536 $cached_page = cached_action_finish
($action);
1541 $cache_mode_active = 0;
1542 # Avoid any extra unwanted encoding steps as $cached_page is raw bytes
1543 binmode STDOUT
, ':raw';
1544 our $fcgi_raw_mode = 1;
1545 print expand_gitweb_pi
($cached_page, time);
1546 binmode STDOUT
, ':utf8'; # as set at the beginning of gitweb.cgi
1551 our $t0 = [ gettimeofday
() ]
1553 our $number_of_git_cmds = 0;
1556 our $first_request = 1;
1557 our $evaluate_uri_force = undef;
1561 # Only allow GET and HEAD methods
1562 if (!$ENV{'REQUEST_METHOD'} || ($ENV{'REQUEST_METHOD'} ne 'GET' && $ENV{'REQUEST_METHOD'} ne 'HEAD')) {
1564 Status: 405 Method Not Allowed
1565 Content-Type: text/plain
1568 405 Method Not Allowed
1574 &$evaluate_uri_force() if $evaluate_uri_force;
1575 if ($per_request_config) {
1576 if (ref($per_request_config) eq 'CODE') {
1577 $per_request_config->();
1578 } elsif (!$first_request) {
1579 evaluate_gitweb_config
();
1580 evaluate_email_obfuscate
();
1585 # $projectroot and $projects_list might be set in gitweb config file
1586 $projects_list ||= $projectroot;
1588 evaluate_query_params
();
1589 evaluate_path_info
();
1590 evaluate_and_validate_params
();
1596 our $is_last_request = sub { 1 };
1597 our ($pre_dispatch_hook, $post_dispatch_hook, $pre_listen_hook);
1601 our $fcgi_nproc_active = 0;
1602 our $fcgi_raw_mode = 0;
1605 my $stdinfno = fileno STDIN
;
1606 return 0 unless defined $stdinfno && $stdinfno == 0;
1607 return 0 unless getsockname STDIN
;
1608 return 0 if getpeername STDIN
;
1609 return $!{ENOTCONN
}?
1:0;
1611 sub configure_as_fcgi
{
1612 return if $fcgi_mode;
1617 # We have gone to great effort to make sure that all incoming data has
1618 # been converted from whatever format it was in into UTF-8. We have
1619 # even taken care to make sure the output handle is in ':utf8' mode.
1620 # Now along comes FCGI and blows it with:
1622 # Use of wide characters in FCGI::Stream::PRINT is deprecated
1623 # and will stop wprking[sic] in a future version of FCGI
1625 # To fix this we replace FCGI::Stream::PRINT with our own routine that
1626 # first encodes everything and then calls the original routine, but
1627 # not if $fcgi_raw_mode is true (then we just call the original routine).
1629 # Note that we could do this by using utf8::is_utf8 to check instead
1630 # of having a $fcgi_raw_mode global, but that would be slower to run
1631 # the test on each element and much slower than skipping the conversion
1632 # entirely when we know we're outputting raw bytes.
1633 my $orig = \
&FCGI
::Stream
::PRINT
;
1634 undef *FCGI
::Stream
::PRINT
;
1635 *FCGI
::Stream
::PRINT
= sub {
1636 @_ = (shift, map {my $x=$_; utf8
::encode
($x); $x} @_)
1637 unless $fcgi_raw_mode;
1641 our $CGI = 'CGI::Fast';
1645 my $request_number = 0;
1646 # let each child service 100 requests
1647 our $is_last_request = sub { ++$request_number >= 100 };
1650 my $script_name = $ENV{'SCRIPT_NAME'} || $ENV{'SCRIPT_FILENAME'} || __FILE__
;
1652 if $script_name =~ /\.fcgi$/ || ($auto_fcgi && is_fcgi
());
1654 my $nproc_sub = sub {
1655 my ($arg, $val) = @_;
1656 return unless eval { require FCGI
::ProcManager
; 1; };
1657 $fcgi_nproc_active = 1;
1658 my $proc_manager = FCGI
::ProcManager
->new({
1659 n_processes
=> $val,
1661 our $pre_listen_hook = sub { $proc_manager->pm_manage() };
1662 our $pre_dispatch_hook = sub { $proc_manager->pm_pre_dispatch() };
1663 our $post_dispatch_hook = sub { $proc_manager->pm_post_dispatch() };
1666 require Getopt
::Long
;
1667 Getopt
::Long
::GetOptions
(
1668 'fastcgi|fcgi|f' => \
&configure_as_fcgi
,
1669 'nproc|n=i' => $nproc_sub,
1672 if (!$fcgi_nproc_active && defined $ENV{'GITWEB_FCGI_NPROC'} && $ENV{'GITWEB_FCGI_NPROC'} =~ /^\d+$/) {
1673 &$nproc_sub('nproc', $ENV{'GITWEB_FCGI_NPROC'});
1677 # Any "our" variable that could possibly influence correct handling of
1678 # a CGI request MUST be reset in this subroutine
1679 sub _reset_globals
{
1680 # Note that $t0 and $number_of_git_commands are handled by reset_timer
1681 our %input_params = ();
1682 our ($action, $project, $file_name, $file_parent, $hash, $hash_parent, $hash_base,
1683 $hash_parent_base, @extra_options, $page, $searchtype, $search_use_regexp,
1684 $searchtext, $search_regexp, $project_filter) = ();
1685 our $git_dir = undef;
1686 our (@snapshot_fmts, $git_avatar, @extra_branch_refs) = ();
1687 our %avatar_cache = ();
1688 our $config_file = '';
1690 our $gitweb_project_owner = undef;
1691 our $shown_stale_message = 0;
1692 our $fcgi_raw_mode = 0;
1693 keys %known_snapshot_formats; # reset 'each' iterator
1697 evaluate_gitweb_config
();
1698 evaluate_encoding
();
1699 evaluate_email_obfuscate
();
1700 evaluate_git_version
();
1701 my ($ml, $mi, $bu, $hl, $subroutine) = ($my_url, $my_uri, $base_url, $home_link, '');
1702 $subroutine .= '$my_url = $ml;' if defined $my_url && $my_url ne '';
1703 $subroutine .= '$my_uri = $mi;' if defined $my_uri; # this one can be ""
1704 $subroutine .= '$base_url = $bu;' if defined $base_url && $base_url ne '';
1705 $subroutine .= '$home_link = $hl;' if defined $home_link && $home_link ne '';
1706 $evaluate_uri_force = eval "sub {$subroutine}" if $subroutine;
1710 $pre_listen_hook->()
1711 if $pre_listen_hook;
1714 while ($cgi = $CGI->new()) {
1715 $pre_dispatch_hook->()
1716 if $pre_dispatch_hook;
1718 # most globals can simply be reset
1721 # evaluate_path_info corrupts %known_snapshot_formats
1722 # so we need a deepish copy of it -- note that
1723 # _reset_globals already took care of resetting its
1724 # hash iterator that evaluate_path_info also leaves
1725 # in an indeterminate state
1727 while (my ($k,$v) = each(%known_snapshot_formats)) {
1728 $formats{$k} = {%{$known_snapshot_formats{$k}}};
1730 local *known_snapshot_formats
= \
%formats;
1732 eval {run_request
()};
1734 $post_dispatch_hook->()
1735 if $post_dispatch_hook;
1738 last REQUEST
if ($is_last_request->());
1746 if (defined caller) {
1747 # wrapped in a subroutine processing requests,
1748 # e.g. mod_perl with ModPerl::Registry, or PSGI with Plack::App::WrapCGI
1751 # pure CGI script, serving single request
1755 ## ======================================================================
1758 # possible values of extra options
1759 # -full => 0|1 - use absolute/full URL ($my_uri/$my_url as base)
1760 # -replay => 1 - start from a current view (replay with modifications)
1761 # -path_info => 0|1 - don't use/use path_info URL (if possible)
1762 # -anchor => ANCHOR - add #ANCHOR to end of URL, implies -replay if used alone
1765 # default is to use -absolute url() i.e. $my_uri
1766 my $href = $params{-full
} ?
$my_url : $my_uri;
1768 # implicit -replay, must be first of implicit params
1769 $params{-replay
} = 1 if (keys %params == 1 && $params{-anchor
});
1771 $params{'project'} = $project unless exists $params{'project'};
1773 if ($params{-replay
}) {
1774 while (my ($name, $symbol) = each %cgi_param_mapping) {
1775 if (!exists $params{$name}) {
1776 $params{$name} = $input_params{$name};
1781 my $use_pathinfo = gitweb_check_feature
('pathinfo');
1782 if (defined $params{'project'} &&
1783 (exists $params{-path_info
} ?
$params{-path_info
} : $use_pathinfo)) {
1784 # try to put as many parameters as possible in PATH_INFO:
1787 # - hash_parent or hash_parent_base:/file_parent
1788 # - hash or hash_base:/filename
1789 # - the snapshot_format as an appropriate suffix
1791 # When the script is the root DirectoryIndex for the domain,
1792 # $href here would be something like http://gitweb.example.com/
1793 # Thus, we strip any trailing / from $href, to spare us double
1794 # slashes in the final URL
1797 # Then add the project name, if present
1798 $href .= "/".esc_path_info
($params{'project'});
1799 delete $params{'project'};
1801 # since we destructively absorb parameters, we keep this
1802 # boolean that remembers if we're handling a snapshot
1803 my $is_snapshot = $params{'action'} eq 'snapshot';
1805 # Summary just uses the project path URL, any other action is
1807 if (defined $params{'action'}) {
1808 $href .= "/".esc_path_info
($params{'action'})
1809 unless $params{'action'} eq 'summary';
1810 delete $params{'action'};
1813 # Next, we put hash_parent_base:/file_parent..hash_base:/file_name,
1814 # stripping nonexistent or useless pieces
1815 $href .= "/" if ($params{'hash_base'} || $params{'hash_parent_base'}
1816 || $params{'hash_parent'} || $params{'hash'});
1817 if (defined $params{'hash_base'}) {
1818 if (defined $params{'hash_parent_base'}) {
1819 $href .= esc_path_info
($params{'hash_parent_base'});
1820 # skip the file_parent if it's the same as the file_name
1821 if (defined $params{'file_parent'}) {
1822 if (defined $params{'file_name'} && $params{'file_parent'} eq $params{'file_name'}) {
1823 delete $params{'file_parent'};
1824 } elsif ($params{'file_parent'} !~ /\.\./) {
1825 $href .= ":/".esc_path_info
($params{'file_parent'});
1826 delete $params{'file_parent'};
1830 delete $params{'hash_parent'};
1831 delete $params{'hash_parent_base'};
1832 } elsif (defined $params{'hash_parent'}) {
1833 $href .= esc_path_info
($params{'hash_parent'}). "..";
1834 delete $params{'hash_parent'};
1837 $href .= esc_path_info
($params{'hash_base'});
1838 if (defined $params{'file_name'} && $params{'file_name'} !~ /\.\./) {
1839 $href .= ":/".esc_path_info
($params{'file_name'});
1840 delete $params{'file_name'};
1842 delete $params{'hash'};
1843 delete $params{'hash_base'};
1844 } elsif (defined $params{'hash'}) {
1845 $href .= esc_path_info
($params{'hash'});
1846 delete $params{'hash'};
1849 # If the action was a snapshot, we can absorb the
1850 # snapshot_format parameter too
1852 my $fmt = $params{'snapshot_format'};
1853 # snapshot_format should always be defined when href()
1854 # is called, but just in case some code forgets, we
1855 # fall back to the default
1856 $fmt ||= $snapshot_fmts[0];
1857 $href .= $known_snapshot_formats{$fmt}{'suffix'};
1858 delete $params{'snapshot_format'};
1862 # now encode the parameters explicitly
1864 for (my $i = 0; $i < @cgi_param_mapping; $i += 2) {
1865 my ($name, $symbol) = ($cgi_param_mapping[$i], $cgi_param_mapping[$i+1]);
1866 if (defined $params{$name}) {
1867 if (ref($params{$name}) eq "ARRAY") {
1868 foreach my $par (@
{$params{$name}}) {
1869 push @result, $symbol . "=" . esc_param
($par);
1872 push @result, $symbol . "=" . esc_param
($params{$name});
1876 $href .= "?" . join(';', @result) if scalar @result;
1878 # final transformation: trailing spaces must be escaped (URI-encoded)
1879 $href =~ s/(\s+)$/CGI::escape($1)/e;
1881 if ($params{-anchor
}) {
1882 $href .= "#".esc_param
($params{-anchor
});
1889 ## ======================================================================
1890 ## validation, quoting/unquoting and escaping
1892 sub is_valid_action
{
1894 return undef unless exists $actions{$input};
1898 sub is_valid_project
{
1901 return unless defined $input;
1902 if (!is_valid_pathname
($input) ||
1903 $input =~ m!^/*_! ||
1904 $input =~ m!\.\.! ||
1905 !($input =~ m!\.git/*$!) ||
1906 $input =~ m!\.git/.*\.git/*$!i ||
1907 !(-d
"$projectroot/$input") ||
1908 !check_export_ok
("$projectroot/$input") ||
1909 ($strict_export && !project_in_list
($input))) {
1916 sub is_valid_pathname
{
1919 return undef unless defined $input;
1920 # no '.' or '..' as elements of path, i.e. no '.' or '..'
1921 # at the beginning, at the end, and between slashes.
1922 # also this catches doubled slashes
1923 if ($input =~ m!(^|/)(|\.|\.\.)(/|$)!) {
1926 # no null characters
1927 if ($input =~ m!\0!) {
1933 sub is_valid_ref_format
{
1936 return undef unless defined $input;
1937 # restrictions on ref name according to git-check-ref-format
1938 if ($input =~ m!(/\.|\.\.|[\000-\040\177 ~^:?*\[]|/$)!) {
1944 sub is_valid_refname
{
1947 return undef unless defined $input;
1948 # textual hashes are O.K.
1949 if ($input =~ m/^[0-9a-fA-F]{40}$/) {
1952 # it must be correct pathname
1953 is_valid_pathname
($input) or return undef;
1954 # check git-check-ref-format restrictions
1955 is_valid_ref_format
($input) or return undef;
1959 # decode sequences of octets in utf8 into Perl's internal form,
1960 # which is utf-8 with utf8 flag set if needed. gitweb writes out
1961 # in utf-8 thanks to "binmode STDOUT, ':utf8'" at beginning
1964 return undef unless defined $str;
1966 if (utf8
::is_utf8
($str) || utf8
::decode
($str)) {
1969 return $encode_object->decode($str, Encode
::FB_DEFAULT
);
1973 # quote unsafe chars, but keep the slash, even when it's not
1974 # correct, but quoted slashes look too horrible in bookmarks
1977 return undef unless defined $str;
1978 $str =~ s/([^A-Za-z0-9\-_.~()\/:@ ]+)/CGI
::escape
($1)/eg
;
1983 # the quoting rules for path_info fragment are slightly different
1986 return undef unless defined $str;
1988 # path_info doesn't treat '+' as space (specially), but '?' must be escaped
1989 $str =~ s/([^A-Za-z0-9\-_.~();\/;:@&= +]+)/CGI
::escape
($1)/eg
;
1994 # quote unsafe chars in whole URL, so some characters cannot be quoted
1997 return undef unless defined $str;
1998 $str =~ s/([^A-Za-z0-9\-_.~();\/;?:@&= ]+)/CGI
::escape
($1)/eg
;
2003 # quote unsafe characters in HTML attributes
2006 # for XHTML conformance escaping '"' to '"' is not enough
2007 return esc_html
(@_);
2010 # replace invalid utf8 character with SUBSTITUTION sequence
2015 return undef unless defined $str;
2017 $str = to_utf8
($str);
2018 $str = $cgi->escapeHTML($str);
2019 if ($opts{'-nbsp'}) {
2020 $str =~ s/ / /g;
2023 $str =~ s
|([[:cntrl
:]])|(($1 ne "\t") ? quot_cec
($1) : $1)|eg
;
2027 # quote control characters and escape filename to HTML
2032 return undef unless defined $str;
2034 $str = to_utf8
($str);
2035 $str = $cgi->escapeHTML($str);
2036 if ($opts{'-nbsp'}) {
2037 $str =~ s/ / /g;
2040 $str =~ s
|([[:cntrl
:]])|quot_cec
($1)|eg
;
2044 # Sanitize for use in XHTML + application/xml+xhtm (valid XML 1.0)
2048 return undef unless defined $str;
2050 $str = to_utf8
($str);
2052 $str =~ s
|([[:cntrl
:]])|(index("\t\n\r", $1) != -1 ?
$1 : quot_cec
($1))|eg
;
2056 # Make control characters "printable", using character escape codes (CEC)
2060 my %es = ( # character escape codes, aka escape sequences
2061 "\t" => '\t', # tab (HT)
2062 "\n" => '\n', # line feed (LF)
2063 "\r" => '\r', # carrige return (CR)
2064 "\f" => '\f', # form feed (FF)
2065 "\b" => '\b', # backspace (BS)
2066 "\a" => '\a', # alarm (bell) (BEL)
2067 "\e" => '\e', # escape (ESC)
2068 "\013" => '\v', # vertical tab (VT)
2069 "\000" => '\0', # nul character (NUL)
2071 my $chr = ( (exists $es{$cntrl})
2073 : sprintf('\x%02x', ord($cntrl)) );
2074 if ($opts{-nohtml
}) {
2077 return "<span class=\"cntrl\">$chr</span>";
2081 # Alternatively use unicode control pictures codepoints,
2082 # Unicode "printable representation" (PR)
2087 my $chr = sprintf('&#%04d;', 0x2400+ord($cntrl));
2088 if ($opts{-nohtml
}) {
2091 return "<span class=\"cntrl\">$chr</span>";
2095 # git may return quoted and escaped filenames
2101 my %es = ( # character escape codes, aka escape sequences
2102 't' => "\t", # tab (HT, TAB)
2103 'n' => "\n", # newline (NL)
2104 'r' => "\r", # return (CR)
2105 'f' => "\f", # form feed (FF)
2106 'b' => "\b", # backspace (BS)
2107 'a' => "\a", # alarm (bell) (BEL)
2108 'e' => "\e", # escape (ESC)
2109 'v' => "\013", # vertical tab (VT)
2112 if ($seq =~ m/^[0-7]{1,3}$/) {
2113 # octal char sequence
2114 return chr(oct($seq));
2115 } elsif (exists $es{$seq}) {
2116 # C escape sequence, aka character escape code
2119 # quoted ordinary character
2123 if ($str =~ m/^"(.*)"$/) {
2126 $str =~ s/\\([^0-7]|[0-7]{1,3})/unq($1)/eg;
2131 # escape tabs (convert tabs to spaces)
2135 while ((my $pos = index($line, "\t")) != -1) {
2136 if (my $count = (8 - ($pos % 8))) {
2137 my $spaces = ' ' x
$count;
2138 $line =~ s/\t/$spaces/;
2145 sub project_in_list
{
2146 my $project = shift;
2147 my @list = git_get_projects_list
();
2148 return @list && scalar(grep { $_->{'path'} eq $project } @list);
2151 sub cached_page_precondition_check
{
2154 $action eq 'summary' &&
2155 $projlist_cache_lifetime > 0 &&
2156 gitweb_check_feature
('forks');
2158 # Note that ALL the 'forkchange' logic is in this function.
2159 # It does NOT belong in cached_action_page NOR in cached_action_start
2160 # NOR in cached_action_finish. None of those functions should know anything
2161 # about nor do anything to any of the 'forkchange'/"$action.forkchange" files.
2163 # besides the basic 'changed' "$action.changed" check, we may only use
2164 # a summary cache if:
2166 # 1) we are not using a project list cache file
2168 # 2) we are not using the 'forks' feature
2170 # 3) there is no 'forkchange' nor "$action.forkchange" file in $html_cache_dir
2172 # 4) there is no cache file ("$cache_dir/$projlist_cache_name")
2174 # 5) the OLDER of 'forkchange'/"$action.forkchange" is NEWER than the cache file
2176 # Otherwise we must re-generate the cache because we've had a fork change
2177 # (either a fork was added or a fork was removed) AND the change has been
2178 # picked up in the cache file AND we've not got that in our cached copy
2180 # For (5) regenerating the cached page wouldn't get us anything if the project
2181 # cache file is older than the 'forkchange'/"$action.forkchange" because the
2182 # forks information comes from the project cache file and it's clearly not
2183 # picked up the changes yet so we may continue to use a cached page until it does.
2185 my $htmlcd = "$projectroot/$project/$html_cache_dir";
2186 my $fc_mt = (stat("$htmlcd/forkchange"))[9];
2187 my $afc_mt = (stat("$htmlcd/$action.forkchange"))[9];
2188 return 1 unless defined($fc_mt) || defined($afc_mt);
2189 my $prj_mt = (stat("$cache_dir/$projlist_cache_name"))[9];
2190 return 1 unless $prj_mt;
2191 my $old_mt = $fc_mt;
2192 $old_mt = $afc_mt if !defined($old_mt) || (defined($afc_mt) && $afc_mt < $old_mt);
2193 return 1 if $old_mt > $prj_mt;
2195 # We're going to regenerate the cached page because we know the project cache
2196 # has new fork information that we cannot possibly have in our cached copy.
2198 # However, if both 'forkchange' and "$action.forkchange" exist and one of
2199 # them is older than the project cache and one of them is newer, we still
2200 # need to regenerate the page cache, but we will also need to do it again
2201 # in the future because there's yet another fork update not yet in the cache.
2203 # So we make sure to touch "$action.changed" to force a cache regeneration
2204 # and then we remove either or both of 'forkchange'/"$action.forkchange" if
2205 # they're older than the project cache (they've served their purpose, we're
2206 # forcing a page regeneration by touching "$action.changed" but the project
2207 # cache was rebuilt since then so there are no more pending fork updates to
2208 # pick up in the future and they need to go).
2210 # For best results, the external code that touches 'forkchange' should always
2211 # touch 'forkchange' and additionally touch 'summary.forkchange' but only
2212 # if it does not already exist. That way the cached page will be regenerated
2213 # each time it's requested and ANY fork updates are available in the proj
2214 # cache rather than waiting until they all are before updating.
2216 # Note that we take a shortcut here and will zap 'forkchange' since we know
2217 # that it only affects the 'summary' cache. If, in the future, it affects
2218 # other cache types, it will first need to be propogated down to
2219 # "$action.forkchange" for those types before we zap it.
2222 open $fd, '>', "$htmlcd/$action.changed" and close $fd;
2223 $fc_mt=undef, unlink "$htmlcd/forkchange" if defined $fc_mt && $fc_mt < $prj_mt;
2224 $afc_mt=undef, unlink "$htmlcd/$action.forkchange" if defined $afc_mt && $afc_mt < $prj_mt;
2226 # Now we propagate 'forkchange' to "$action.forkchange" if we have the
2227 # one and not the other.
2229 if (defined $fc_mt && ! defined $afc_mt) {
2230 open $fd, '>', "$htmlcd/$action.forkchange" and close $fd;
2231 -e
"$htmlcd/$action.forkchange" and
2232 utime($fc_mt, $fc_mt, "$htmlcd/$action.forkchange") and
2233 unlink "$htmlcd/forkchange";
2239 sub cached_action_page
{
2242 return undef unless $action && $html_cache_actions{$action} && $html_cache_dir;
2243 my $htmlcd = "$projectroot/$project/$html_cache_dir";
2244 return undef if -e
"$htmlcd/changed" || -e
"$htmlcd/$action.changed";
2245 return undef unless cached_page_precondition_check
($action);
2246 open my $fd, '<', "$htmlcd/$action" or return undef;
2249 my $cached_page = <$fd>;
2250 close $fd or return undef;
2251 return $cached_page;
2254 package Git
::Gitweb
::CacheFile
;
2257 use POSIX
qw(:fcntl_h);
2259 my $cachefile = shift;
2261 sysopen(my $self, $cachefile, O_WRONLY
|O_CREAT
|O_EXCL
, 0664)
2263 $$self->{'cachefile'} = $cachefile;
2264 $$self->{'opened'} = 1;
2265 $$self->{'contents'} = '';
2266 return bless $self, $class;
2271 if ($$self->{'opened'}) {
2272 $$self->{'opened'} = 0;
2273 my $result = close $self;
2274 unlink $$self->{'cachefile'} unless $result;
2282 if ($$self->{'opened'}) {
2283 $self->CLOSE() and unlink $$self->{'cachefile'};
2289 @_ = (map {my $x=$_; utf8
::encode
($x); $x} @_) unless $fcgi_raw_mode;
2290 print $self @_ if $$self->{'opened'};
2291 $$self->{'contents'} .= join('', @_);
2297 my $template = shift;
2298 return $self->PRINT(sprintf $template, @_);
2303 return $$self->{'contents'};
2308 # Caller is responsible for preserving STDOUT beforehand if needed
2309 sub cached_action_start
{
2312 return undef unless $html_cache_actions{$action} && $html_cache_dir;
2313 my $htmlcd = "$projectroot/$project/$html_cache_dir";
2314 return undef unless -d
$htmlcd;
2315 if (-e
"$htmlcd/changed") {
2316 foreach my $cacheable (keys(%html_cache_actions)) {
2317 next unless $supported_cache_actions{$cacheable} &&
2318 $html_cache_actions{$cacheable};
2320 open $fd, '>', "$htmlcd/$cacheable.changed"
2323 unlink "$htmlcd/changed";
2326 tie
*CACHEFILE
, 'Git::Gitweb::CacheFile', "$htmlcd/$action.lock" or return undef;
2327 *STDOUT
= *CACHEFILE
;
2328 unlink "$htmlcd/$action", "$htmlcd/$action.changed";
2332 # Caller is responsible for restoring STDOUT afterward if needed
2333 sub cached_action_finish
{
2338 my $obj = tied *STDOUT
;
2339 return undef unless ref($obj) eq 'Git::Gitweb::CacheFile';
2340 my $cached_page = $obj->contents;
2341 (my $result = close(STDOUT
)) or warn "couldn't close cache file on STDOUT: $!";
2342 # Do not leave STDOUT file descriptor invalid!
2344 open(NULL
, '>', File
::Spec
->devnull) or die "couldn't open NULL to devnull: $!";
2346 return $cached_page unless $result;
2347 my $htmlcd = "$projectroot/$project/$html_cache_dir";
2348 return $cached_page unless -d
$htmlcd;
2349 unlink "$htmlcd/$action.lock" unless rename "$htmlcd/$action.lock", "$htmlcd/$action";
2350 return $cached_page;
2354 BEGIN {%expand_pi_subs = (
2355 'age_string' => \
&age_string
,
2356 'age_string_date' => \
&age_string_date
,
2357 'age_string_age' => \
&age_string_age
,
2358 'compute_timed_interval' => \
&compute_timed_interval
,
2359 'compute_commands_count' => \
&compute_commands_count
,
2360 'format_lastrefresh_row' => \
&format_lastrefresh_row
,
2361 'compute_stylesheet_links' => \
&compute_stylesheet_links
,
2364 # Expands any <?gitweb...> processing instructions and returns the result
2365 sub expand_gitweb_pi
{
2368 my @time_now = gettimeofday
();
2369 $page =~ s
{<\?gitweb
(?
:\s
+([^\s
>]+)([^>]*))?\s
*\?>}
2371 (ref($expand_pi_subs{$1}) eq 'CODE' ?
2372 $expand_pi_subs{$1}->(split(' ',$2), @time_now) :
2378 ## ----------------------------------------------------------------------
2379 ## HTML aware string manipulation
2381 # Try to chop given string on a word boundary between position
2382 # $len and $len+$add_len. If there is no word boundary there,
2383 # chop at $len+$add_len. Do not chop if chopped part plus ellipsis
2384 # (marking chopped part) would be longer than given string.
2388 my $add_len = shift || 10;
2389 my $where = shift || 'right'; # 'left' | 'center' | 'right'
2391 # Make sure perl knows it is utf8 encoded so we don't
2392 # cut in the middle of a utf8 multibyte char.
2393 $str = to_utf8
($str);
2395 # allow only $len chars, but don't cut a word if it would fit in $add_len
2396 # if it doesn't fit, cut it if it's still longer than the dots we would add
2397 # remove chopped character entities entirely
2399 # when chopping in the middle, distribute $len into left and right part
2400 # return early if chopping wouldn't make string shorter
2401 if ($where eq 'center') {
2402 return $str if ($len + 5 >= length($str)); # filler is length 5
2405 return $str if ($len + 4 >= length($str)); # filler is length 4
2408 # regexps: ending and beginning with word part up to $add_len
2409 my $endre = qr/.{$len}\w{0,$add_len}/;
2410 my $begre = qr/\w{0,$add_len}.{$len}/;
2412 if ($where eq 'left') {
2413 $str =~ m/^(.*?)($begre)$/;
2414 my ($lead, $body) = ($1, $2);
2415 if (length($lead) > 4) {
2418 return "$lead$body";
2420 } elsif ($where eq 'center') {
2421 $str =~ m/^($endre)(.*)$/;
2422 my ($left, $str) = ($1, $2);
2423 $str =~ m/^(.*?)($begre)$/;
2424 my ($mid, $right) = ($1, $2);
2425 if (length($mid) > 5) {
2428 return "$left$mid$right";
2431 $str =~ m/^($endre)(.*)$/;
2434 if (length($tail) > 4) {
2437 return "$body$tail";
2441 # pass-through email filter, obfuscating it when possible
2442 sub email_obfuscate
{
2446 $str = $email->escape_html($str);
2447 # Stock HTML::Email::Obfuscate version likes to produce
2449 $str =~ s
#<(/?)B>#<$1b>#g;
2452 $str = esc_html
($str);
2453 $str =~ s/@/@/;
2458 # takes the same arguments as chop_str, but also wraps a <span> around the
2459 # result with a title attribute if it does get chopped. Additionally, the
2460 # string is HTML-escaped.
2461 sub chop_and_escape_str
{
2464 my $chopped = chop_str
(@_);
2465 $str = to_utf8
($str);
2466 if ($chopped eq $str) {
2467 return email_obfuscate
($chopped);
2470 $str =~ s/[[:cntrl:]]/?/g;
2471 return $cgi->span({-title
=>$str}, email_obfuscate
($chopped));
2475 # Highlight selected fragments of string, using given CSS class,
2476 # and escape HTML. It is assumed that fragments do not overlap.
2477 # Regions are passed as list of pairs (array references).
2479 # Example: esc_html_hl_regions("foobar", "mark", [ 0, 3 ]) returns
2480 # '<span class="mark">foo</span>bar'
2481 sub esc_html_hl_regions
{
2482 my ($str, $css_class, @sel) = @_;
2483 my %opts = grep { ref($_) ne 'ARRAY' } @sel;
2484 @sel = grep { ref($_) eq 'ARRAY' } @sel;
2485 return esc_html
($str, %opts) unless @sel;
2491 my ($begin, $end) = @
$s;
2493 # Don't create empty <span> elements.
2494 next if $end <= $begin;
2496 my $escaped = esc_html
(substr($str, $begin, $end - $begin),
2499 $out .= esc_html
(substr($str, $pos, $begin - $pos), %opts)
2500 if ($begin - $pos > 0);
2501 $out .= $cgi->span({-class => $css_class}, $escaped);
2505 $out .= esc_html
(substr($str, $pos), %opts)
2506 if ($pos < length($str));
2511 # return positions of beginning and end of each match
2513 my ($str, $regexp) = @_;
2514 return unless (defined $str && defined $regexp);
2517 while ($str =~ /$regexp/g) {
2518 push @matches, [$-[0], $+[0]];
2523 # highlight match (if any), and escape HTML
2524 sub esc_html_match_hl
{
2525 my ($str, $regexp) = @_;
2526 return esc_html
($str) unless defined $regexp;
2528 my @matches = matchpos_list
($str, $regexp);
2529 return esc_html
($str) unless @matches;
2531 return esc_html_hl_regions
($str, 'match', @matches);
2535 # highlight match (if any) of shortened string, and escape HTML
2536 sub esc_html_match_hl_chopped
{
2537 my ($str, $chopped, $regexp) = @_;
2538 return esc_html_match_hl
($str, $regexp) unless defined $chopped;
2540 my @matches = matchpos_list
($str, $regexp);
2541 return esc_html
($chopped) unless @matches;
2543 # filter matches so that we mark chopped string
2544 my $tail = "... "; # see chop_str
2545 unless ($chopped =~ s/\Q$tail\E$//) {
2548 my $chop_len = length($chopped);
2549 my $tail_len = length($tail);
2552 for my $m (@matches) {
2553 if ($m->[0] > $chop_len) {
2554 push @filtered, [ $chop_len, $chop_len + $tail_len ] if ($tail_len > 0);
2556 } elsif ($m->[1] > $chop_len) {
2557 push @filtered, [ $m->[0], $chop_len + $tail_len ];
2563 return esc_html_hl_regions
($chopped . $tail, 'match', @filtered);
2566 ## ----------------------------------------------------------------------
2567 ## functions returning short strings
2569 # CSS class for given age epoch value (in seconds)
2570 # and reference time (optional, defaults to now) as second value
2572 my ($age_epoch, $time_now) = @_;
2573 return "noage" unless defined $age_epoch;
2574 defined $time_now or $time_now = time;
2575 my $age = $time_now - $age_epoch;
2577 if ($age < 60*60*2) {
2579 } elsif ($age < 60*60*24*2) {
2586 # convert age epoch in seconds to "nn units ago" string
2587 # reference time used is now unless second argument passed in
2588 # to get the old behavior, pass 0 as the first argument and
2589 # the time in seconds as the second
2591 my ($age_epoch, $time_now) = @_;
2592 return "unknown" unless defined $age_epoch;
2593 return "<?gitweb age_string $age_epoch?>" if $cache_mode_active;
2594 defined $time_now or $time_now = time;
2595 my $age = $time_now - $age_epoch;
2598 if ($age > 60*60*24*365*2) {
2599 $age_str = (int $age/60/60/24/365);
2600 $age_str .= " years ago";
2601 } elsif ($age > 60*60*24*(365/12)*2) {
2602 $age_str = int $age/60/60/24/(365/12);
2603 $age_str .= " months ago";
2604 } elsif ($age > 60*60*24*7*2) {
2605 $age_str = int $age/60/60/24/7;
2606 $age_str .= " weeks ago";
2607 } elsif ($age > 60*60*24*2) {
2608 $age_str = int $age/60/60/24;
2609 $age_str .= " days ago";
2610 } elsif ($age > 60*60*2) {
2611 $age_str = int $age/60/60;
2612 $age_str .= " hours ago";
2613 } elsif ($age > 60*2) {
2614 $age_str = int $age/60;
2615 $age_str .= " min ago";
2616 } elsif ($age > 2) {
2617 $age_str = int $age;
2618 $age_str .= " sec ago";
2620 $age_str .= " right now";
2625 # returns age_string if the age is <= 2 weeks otherwise an absolute date
2626 # this is typically shown to the user directly with the age_string_age as a title
2627 sub age_string_date
{
2628 my ($age_epoch, $time_now) = @_;
2629 return "unknown" unless defined $age_epoch;
2630 return "<?gitweb age_string_date $age_epoch?>" if $cache_mode_active;
2631 defined $time_now or $time_now = time;
2632 my $age = $time_now - $age_epoch;
2634 if ($age > 60*60*24*7*2) {
2635 my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($age_epoch);
2636 return sprintf "%4i-%02u-%02i", 1900 + $year, $mon+1, $mday;
2638 return age_string
($age_epoch, $time_now);
2642 # returns an absolute date if the age is <= 2 weeks otherwise age_string
2643 # this is typically used for the 'title' attribute so it will show as a tooltip
2644 sub age_string_age
{
2645 my ($age_epoch, $time_now) = @_;
2646 return "unknown" unless defined $age_epoch;
2647 return "<?gitweb age_string_age $age_epoch?>" if $cache_mode_active;
2648 defined $time_now or $time_now = time;
2649 my $age = $time_now - $age_epoch;
2651 if ($age > 60*60*24*7*2) {
2652 return age_string
($age_epoch, $time_now);
2654 my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($age_epoch);
2655 return sprintf "%4i-%02u-%02i", 1900 + $year, $mon+1, $mday;
2660 S_IFINVALID
=> 0030000,
2661 S_IFGITLINK
=> 0160000,
2664 # submodule/subproject, a commit object reference
2668 return (($mode & S_IFMT
) == S_IFGITLINK
)
2671 # convert file mode in octal to symbolic file mode string
2673 my $mode = oct shift;
2675 if (S_ISGITLINK
($mode)) {
2676 return 'm---------';
2677 } elsif (S_ISDIR
($mode & S_IFMT
)) {
2678 return 'drwxr-xr-x';
2679 } elsif (S_ISLNK
($mode)) {
2680 return 'lrwxrwxrwx';
2681 } elsif (S_ISREG
($mode)) {
2682 # git cares only about the executable bit
2683 if ($mode & S_IXUSR
) {
2684 return '-rwxr-xr-x';
2686 return '-rw-r--r--';
2689 return '----------';
2693 # convert file mode in octal to file type string
2697 if ($mode !~ m/^[0-7]+$/) {
2703 if (S_ISGITLINK
($mode)) {
2705 } elsif (S_ISDIR
($mode & S_IFMT
)) {
2707 } elsif (S_ISLNK
($mode)) {
2709 } elsif (S_ISREG
($mode)) {
2716 # convert file mode in octal to file type description string
2717 sub file_type_long
{
2720 if ($mode !~ m/^[0-7]+$/) {
2726 if (S_ISGITLINK
($mode)) {
2728 } elsif (S_ISDIR
($mode & S_IFMT
)) {
2730 } elsif (S_ISLNK
($mode)) {
2732 } elsif (S_ISREG
($mode)) {
2733 if ($mode & S_IXUSR
) {
2734 return "executable";
2744 ## ----------------------------------------------------------------------
2745 ## functions returning short HTML fragments, or transforming HTML fragments
2746 ## which don't belong to other sections
2748 # format line of commit message.
2749 sub format_log_line_html
{
2752 $line = esc_html
($line, -nbsp
=>1);
2753 $line =~ s
{\b([0-9a
-fA
-F
]{8,40})\b}{
2754 $cgi->a({-href
=> href
(action
=>"object", hash
=>$1),
2755 -class => "text"}, $1);
2756 }eg
unless $line =~ /^\s*git-svn-id:/;
2761 # format marker of refs pointing to given object
2763 # the destination action is chosen based on object type and current context:
2764 # - for annotated tags, we choose the tag view unless it's the current view
2765 # already, in which case we go to shortlog view
2766 # - for other refs, we keep the current view if we're in history, shortlog or
2767 # log view, and select shortlog otherwise
2768 sub format_ref_marker
{
2769 my ($refs, $id) = @_;
2772 if (defined $refs->{$id}) {
2773 foreach my $ref (@
{$refs->{$id}}) {
2774 # this code exploits the fact that non-lightweight tags are the
2775 # only indirect objects, and that they are the only objects for which
2776 # we want to use tag instead of shortlog as action
2777 my ($type, $name) = qw();
2778 my $indirect = ($ref =~ s/\^\{\}$//);
2779 # e.g. tags/v2.6.11 or heads/next
2780 if ($ref =~ m!^(.*?)s?/(.*)$!) {
2789 $class .= " indirect" if $indirect;
2791 my $dest_action = "shortlog";
2794 $dest_action = "tag" unless $action eq "tag";
2795 } elsif ($action =~ /^(history|(short)?log)$/) {
2796 $dest_action = $action;
2800 $dest .= "refs/" unless $ref =~ m
!^refs
/!;
2803 my $link = $cgi->a({
2805 action
=>$dest_action,
2809 $markers .= "<span class=\"".esc_attr
($class)."\" title=\"".esc_attr
($ref)."\">" .
2815 return '<span class="refs">'. $markers . '</span>';
2821 # format, perhaps shortened and with markers, title line
2822 sub format_subject_html
{
2823 my ($long, $short, $href, $extra) = @_;
2824 $extra = '' unless defined($extra);
2826 if (length($short) < length($long)) {
2828 $long =~ s/[[:cntrl:]]/?/g;
2829 return $cgi->a({-href
=> $href, -class => "list subject",
2830 -title
=> to_utf8
($long)},
2831 esc_html
($short)) . $extra;
2833 return $cgi->a({-href
=> $href, -class => "list subject"},
2834 esc_html
($long)) . $extra;
2838 # Rather than recomputing the url for an email multiple times, we cache it
2839 # after the first hit. This gives a visible benefit in views where the avatar
2840 # for the same email is used repeatedly (e.g. shortlog).
2841 # The cache is shared by all avatar engines (currently gravatar only), which
2842 # are free to use it as preferred. Since only one avatar engine is used for any
2843 # given page, there's no risk for cache conflicts.
2844 our %avatar_cache = ();
2846 # Compute the picon url for a given email, by using the picon search service over at
2847 # http://www.cs.indiana.edu/picons/search.html
2849 my $email = lc shift;
2850 if (!$avatar_cache{$email}) {
2851 my ($user, $domain) = split('@', $email);
2852 $avatar_cache{$email} =
2853 "//www.cs.indiana.edu/cgi-pub/kinzler/piconsearch.cgi/" .
2855 "users+domains+unknown/up/single";
2857 return $avatar_cache{$email};
2860 # Compute the gravatar url for a given email, if it's not in the cache already.
2861 # Gravatar stores only the part of the URL before the size, since that's the
2862 # one computationally more expensive. This also allows reuse of the cache for
2863 # different sizes (for this particular engine).
2865 my $email = lc shift;
2867 $avatar_cache{$email} ||=
2868 "//www.gravatar.com/avatar/" .
2869 Digest
::MD5
::md5_hex
($email) . "?s=";
2870 return $avatar_cache{$email} . $size;
2873 # Insert an avatar for the given $email at the given $size if the feature
2875 sub git_get_avatar
{
2876 my ($email, %opts) = @_;
2877 my $pre_white = ($opts{-pad_before
} ?
" " : "");
2878 my $post_white = ($opts{-pad_after
} ?
" " : "");
2879 $opts{-size
} ||= 'default';
2880 my $size = $avatar_size{$opts{-size
}} || $avatar_size{'default'};
2882 if ($git_avatar eq 'gravatar') {
2883 $url = gravatar_url
($email, $size);
2884 } elsif ($git_avatar eq 'picon') {
2885 $url = picon_url
($email);
2887 # Other providers can be added by extending the if chain, defining $url
2888 # as needed. If no variant puts something in $url, we assume avatars
2889 # are completely disabled/unavailable.
2892 "<img width=\"$size\" " .
2893 "class=\"avatar\" " .
2894 "src=\"".esc_url
($url)."\" " .
2902 sub format_search_author
{
2903 my ($author, $searchtype, $displaytext) = @_;
2904 my $have_search = gitweb_check_feature
('search');
2908 if ($searchtype eq 'author') {
2909 $performed = "authored";
2910 } elsif ($searchtype eq 'committer') {
2911 $performed = "committed";
2914 return $cgi->a({-href
=> href
(action
=>"search", hash
=>$hash,
2915 searchtext
=>$author,
2916 searchtype
=>$searchtype), class=>"list",
2917 title
=>"Search for commits $performed by $author"},
2921 return $displaytext;
2925 # format the author name of the given commit with the given tag
2926 # the author name is chopped and escaped according to the other
2927 # optional parameters (see chop_str).
2928 sub format_author_html
{
2931 my $author = chop_and_escape_str
($co->{'author_name'}, @_);
2932 return "<$tag class=\"author\">" .
2933 format_search_author
($co->{'author_name'}, "author",
2934 git_get_avatar
($co->{'author_email'}, -pad_after
=> 1) .
2939 # format git diff header line, i.e. "diff --(git|combined|cc) ..."
2940 sub format_git_diff_header_line
{
2942 my $diffinfo = shift;
2943 my ($from, $to) = @_;
2945 if ($diffinfo->{'nparents'}) {
2947 $line =~ s!^(diff (.*?) )"?.*$!$1!;
2948 if ($to->{'href'}) {
2949 $line .= $cgi->a({-href
=> $to->{'href'}, -class => "path"},
2950 esc_path
($to->{'file'}));
2951 } else { # file was deleted (no href)
2952 $line .= esc_path
($to->{'file'});
2956 $line =~ s!^(diff (.*?) )"?a/.*$!$1!;
2957 if ($from->{'href'}) {
2958 $line .= $cgi->a({-href
=> $from->{'href'}, -class => "path"},
2959 'a/' . esc_path
($from->{'file'}));
2960 } else { # file was added (no href)
2961 $line .= 'a/' . esc_path
($from->{'file'});
2964 if ($to->{'href'}) {
2965 $line .= $cgi->a({-href
=> $to->{'href'}, -class => "path"},
2966 'b/' . esc_path
($to->{'file'}));
2967 } else { # file was deleted
2968 $line .= 'b/' . esc_path
($to->{'file'});
2972 return "<div class=\"diff header\">$line</div>\n";
2975 # format extended diff header line, before patch itself
2976 sub format_extended_diff_header_line
{
2978 my $diffinfo = shift;
2979 my ($from, $to) = @_;
2982 if ($line =~ s!^((copy|rename) from ).*$!$1! && $from->{'href'}) {
2983 $line .= $cgi->a({-href
=>$from->{'href'}, -class=>"path"},
2984 esc_path
($from->{'file'}));
2986 if ($line =~ s!^((copy|rename) to ).*$!$1! && $to->{'href'}) {
2987 $line .= $cgi->a({-href
=>$to->{'href'}, -class=>"path"},
2988 esc_path
($to->{'file'}));
2990 # match single <mode>
2991 if ($line =~ m/\s(\d{6})$/) {
2992 $line .= '<span class="info"> (' .
2993 file_type_long
($1) .
2997 if ($line =~ m/^index [0-9a-fA-F]{40},[0-9a-fA-F]{40}/) {
2998 # can match only for combined diff
3000 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
3001 if ($from->{'href'}[$i]) {
3002 $line .= $cgi->a({-href
=>$from->{'href'}[$i],
3004 substr($diffinfo->{'from_id'}[$i],0,7));
3009 $line .= ',' if ($i < $diffinfo->{'nparents'} - 1);
3012 if ($to->{'href'}) {
3013 $line .= $cgi->a({-href
=>$to->{'href'}, -class=>"hash"},
3014 substr($diffinfo->{'to_id'},0,7));
3019 } elsif ($line =~ m/^index [0-9a-fA-F]{40}..[0-9a-fA-F]{40}/) {
3020 # can match only for ordinary diff
3021 my ($from_link, $to_link);
3022 if ($from->{'href'}) {
3023 $from_link = $cgi->a({-href
=>$from->{'href'}, -class=>"hash"},
3024 substr($diffinfo->{'from_id'},0,7));
3026 $from_link = '0' x
7;
3028 if ($to->{'href'}) {
3029 $to_link = $cgi->a({-href
=>$to->{'href'}, -class=>"hash"},
3030 substr($diffinfo->{'to_id'},0,7));
3034 my ($from_id, $to_id) = ($diffinfo->{'from_id'}, $diffinfo->{'to_id'});
3035 $line =~ s!$from_id\.\.$to_id!$from_link..$to_link!;
3038 return $line . "<br/>\n";
3041 # format from-file/to-file diff header
3042 sub format_diff_from_to_header
{
3043 my ($from_line, $to_line, $diffinfo, $from, $to, @parents) = @_;
3048 #assert($line =~ m/^---/) if DEBUG;
3049 # no extra formatting for "^--- /dev/null"
3050 if (! $diffinfo->{'nparents'}) {
3051 # ordinary (single parent) diff
3052 if ($line =~ m!^--- "?a/!) {
3053 if ($from->{'href'}) {
3055 $cgi->a({-href
=>$from->{'href'}, -class=>"path"},
3056 esc_path
($from->{'file'}));
3059 esc_path
($from->{'file'});
3062 $result .= qq!<div
class="diff from_file">$line</div
>\n!;
3065 # combined diff (merge commit)
3066 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
3067 if ($from->{'href'}[$i]) {
3069 $cgi->a({-href
=>href
(action
=>"blobdiff",
3070 hash_parent
=>$diffinfo->{'from_id'}[$i],
3071 hash_parent_base
=>$parents[$i],
3072 file_parent
=>$from->{'file'}[$i],
3073 hash
=>$diffinfo->{'to_id'},
3075 file_name
=>$to->{'file'}),
3077 -title
=>"diff" . ($i+1)},
3080 $cgi->a({-href
=>$from->{'href'}[$i], -class=>"path"},
3081 esc_path
($from->{'file'}[$i]));
3083 $line = '--- /dev/null';
3085 $result .= qq!<div
class="diff from_file">$line</div
>\n!;
3090 #assert($line =~ m/^\+\+\+/) if DEBUG;
3091 # no extra formatting for "^+++ /dev/null"
3092 if ($line =~ m!^\+\+\+ "?b/!) {
3093 if ($to->{'href'}) {
3095 $cgi->a({-href
=>$to->{'href'}, -class=>"path"},
3096 esc_path
($to->{'file'}));
3099 esc_path
($to->{'file'});
3102 $result .= qq!<div
class="diff to_file">$line</div
>\n!;
3107 # create note for patch simplified by combined diff
3108 sub format_diff_cc_simplified
{
3109 my ($diffinfo, @parents) = @_;
3112 $result .= "<div class=\"diff header\">" .
3114 if (!is_deleted
($diffinfo)) {
3115 $result .= $cgi->a({-href
=> href
(action
=>"blob",
3117 hash
=>$diffinfo->{'to_id'},
3118 file_name
=>$diffinfo->{'to_file'}),
3120 esc_path
($diffinfo->{'to_file'}));
3122 $result .= esc_path
($diffinfo->{'to_file'});
3124 $result .= "</div>\n" . # class="diff header"
3125 "<div class=\"diff nodifferences\">" .
3127 "</div>\n"; # class="diff nodifferences"
3132 sub diff_line_class
{
3133 my ($line, $from, $to) = @_;
3138 if ($from && $to && ref($from->{'href'}) eq "ARRAY") {
3139 $num_sign = scalar @
{$from->{'href'}};
3142 my @diff_line_classifier = (
3143 { regexp
=> qr/^\@\@{$num_sign} /, class => "chunk_header"},
3144 { regexp
=> qr/^\\/, class => "incomplete" },
3145 { regexp
=> qr/^ {$num_sign}/, class => "ctx" },
3146 # classifier for context must come before classifier add/rem,
3147 # or we would have to use more complicated regexp, for example
3148 # qr/(?= {0,$m}\+)[+ ]{$num_sign}/, where $m = $num_sign - 1;
3149 { regexp
=> qr/^[+ ]{$num_sign}/, class => "add" },
3150 { regexp
=> qr/^[- ]{$num_sign}/, class => "rem" },
3152 for my $clsfy (@diff_line_classifier) {
3153 return $clsfy->{'class'}
3154 if ($line =~ $clsfy->{'regexp'});
3161 # assumes that $from and $to are defined and correctly filled,
3162 # and that $line holds a line of chunk header for unified diff
3163 sub format_unidiff_chunk_header
{
3164 my ($line, $from, $to) = @_;
3166 my ($from_text, $from_start, $from_lines, $to_text, $to_start, $to_lines, $section) =
3167 $line =~ m/^\@{2} (-(\d+)(?:,(\d+))?) (\+(\d+)(?:,(\d+))?) \@{2}(.*)$/;
3169 $from_lines = 0 unless defined $from_lines;
3170 $to_lines = 0 unless defined $to_lines;
3172 if ($from->{'href'}) {
3173 $from_text = $cgi->a({-href
=>"$from->{'href'}#l$from_start",
3174 -class=>"list"}, $from_text);
3176 if ($to->{'href'}) {
3177 $to_text = $cgi->a({-href
=>"$to->{'href'}#l$to_start",
3178 -class=>"list"}, $to_text);
3180 $line = "<span class=\"chunk_info\">@@ $from_text $to_text @@</span>" .
3181 "<span class=\"section\">" . esc_html
($section, -nbsp
=>1) . "</span>";
3185 # assumes that $from and $to are defined and correctly filled,
3186 # and that $line holds a line of chunk header for combined diff
3187 sub format_cc_diff_chunk_header
{
3188 my ($line, $from, $to) = @_;
3190 my ($prefix, $ranges, $section) = $line =~ m/^(\@+) (.*?) \@+(.*)$/;
3191 my (@from_text, @from_start, @from_nlines, $to_text, $to_start, $to_nlines);
3193 @from_text = split(' ', $ranges);
3194 for (my $i = 0; $i < @from_text; ++$i) {
3195 ($from_start[$i], $from_nlines[$i]) =
3196 (split(',', substr($from_text[$i], 1)), 0);
3199 $to_text = pop @from_text;
3200 $to_start = pop @from_start;
3201 $to_nlines = pop @from_nlines;
3203 $line = "<span class=\"chunk_info\">$prefix ";
3204 for (my $i = 0; $i < @from_text; ++$i) {
3205 if ($from->{'href'}[$i]) {
3206 $line .= $cgi->a({-href
=>"$from->{'href'}[$i]#l$from_start[$i]",
3207 -class=>"list"}, $from_text[$i]);
3209 $line .= $from_text[$i];
3213 if ($to->{'href'}) {
3214 $line .= $cgi->a({-href
=>"$to->{'href'}#l$to_start",
3215 -class=>"list"}, $to_text);
3219 $line .= " $prefix</span>" .
3220 "<span class=\"section\">" . esc_html
($section, -nbsp
=>1) . "</span>";
3224 # process patch (diff) line (not to be used for diff headers),
3225 # returning HTML-formatted (but not wrapped) line.
3226 # If the line is passed as a reference, it is treated as HTML and not
3228 sub format_diff_line
{
3229 my ($line, $diff_class, $from, $to) = @_;
3235 $line = untabify
($line);
3237 if ($from && $to && $line =~ m/^\@{2} /) {
3238 $line = format_unidiff_chunk_header
($line, $from, $to);
3239 } elsif ($from && $to && $line =~ m/^\@{3}/) {
3240 $line = format_cc_diff_chunk_header
($line, $from, $to);
3242 $line = esc_html
($line, -nbsp
=>1);
3246 my $diff_classes = "diff diff_body";
3247 $diff_classes .= " $diff_class" if ($diff_class);
3248 $line = "<div class=\"$diff_classes\">$line</div>\n";
3253 # Generates undef or something like "_snapshot_" or "snapshot (_tbz2_ _zip_)",
3254 # linked. Pass the hash of the tree/commit to snapshot.
3255 sub format_snapshot_links
{
3257 my $num_fmts = @snapshot_fmts;
3258 if ($num_fmts > 1) {
3259 # A parenthesized list of links bearing format names.
3260 # e.g. "snapshot (_tar.gz_ _zip_)"
3261 return "<span class=\"snapshots\">snapshot (" . join(' ', map
3268 }, $known_snapshot_formats{$_}{'display'})
3269 , @snapshot_fmts) . ")</span>";
3270 } elsif ($num_fmts == 1) {
3271 # A single "snapshot" link whose tooltip bears the format name.
3273 my ($fmt) = @snapshot_fmts;
3274 return "<span class=\"snapshots\">" .
3279 snapshot_format
=>$fmt
3281 -title
=> "in format: $known_snapshot_formats{$fmt}{'display'}"
3282 }, "snapshot") . "</span>";
3283 } else { # $num_fmts == 0
3288 ## ......................................................................
3289 ## functions returning values to be passed, perhaps after some
3290 ## transformation, to other functions; e.g. returning arguments to href()
3292 # returns hash to be passed to href to generate gitweb URL
3293 # in -title key it returns description of link
3295 my $format = shift || 'Atom';
3296 my %res = (action
=> lc($format));
3297 my $matched_ref = 0;
3299 # feed links are possible only for project views
3300 return unless (defined $project);
3301 # some views should link to OPML, or to generic project feed,
3302 # or don't have specific feed yet (so they should use generic)
3303 return if (!$action || $action =~ /^(?:tags|heads|forks|tag|search)$/x);
3306 # branches refs uses 'refs/' + $get_branch_refs()[x] + '/' prefix
3307 # (fullname) to differentiate from tag links; this also makes
3308 # possible to detect branch links
3309 for my $ref (get_branch_refs
()) {
3310 if ((defined $hash_base && $hash_base =~ m!^refs/\Q$ref\E/(.*)$!) ||
3311 (defined $hash && $hash =~ m!^refs/\Q$ref\E/(.*)$!)) {
3313 $matched_ref = $ref;
3317 # find log type for feed description (title)
3319 if (defined $file_name) {
3320 $type = "history of $file_name";
3321 $type .= "/" if ($action eq 'tree');
3322 $type .= " on '$branch'" if (defined $branch);
3324 $type = "log of $branch" if (defined $branch);
3327 $res{-title
} = $type;
3328 $res{'hash'} = (defined $branch ?
"refs/$matched_ref/$branch" : undef);
3329 $res{'file_name'} = $file_name;
3334 ## ----------------------------------------------------------------------
3335 ## git utility subroutines, invoking git commands
3337 # returns path to the core git executable and the --git-dir parameter as list
3339 $number_of_git_cmds++;
3340 return $GIT, '--git-dir='.$git_dir;
3343 # opens a "-|" cmd pipe handle with 2>/dev/null and returns it
3346 # In order to be compatible with FCGI mode we must use POSIX
3347 # and access the STDERR_FILENO file descriptor directly
3349 use POSIX
qw(STDERR_FILENO dup dup2);
3351 open(my $null, '>', File
::Spec
->devnull) or die "couldn't open devnull: $!";
3352 (my $saveerr = dup
(STDERR_FILENO
)) or die "couldn't dup STDERR: $!";
3353 my $dup2ok = dup2
(fileno($null), STDERR_FILENO
);
3354 close($null) or !$dup2ok or die "couldn't close NULL: $!";
3355 $dup2ok or POSIX
::close($saveerr), die "couldn't dup NULL to STDERR: $!";
3356 my $result = open(my $fd, "-|", @_);
3357 $dup2ok = dup2
($saveerr, STDERR_FILENO
);
3358 POSIX
::close($saveerr) or !$dup2ok or die "couldn't close SAVEERR: $!";
3359 $dup2ok or die "couldn't dup SAVERR to STDERR: $!";
3361 return $result ?
$fd : undef;
3364 # opens a "-|" git_cmd pipe handle with 2>/dev/null and returns it
3366 return cmd_pipe git_cmd
(), @_;
3369 # quote the given arguments for passing them to the shell
3370 # quote_command("command", "arg 1", "arg with ' and ! characters")
3371 # => "'command' 'arg 1' 'arg with '\'' and '\!' characters'"
3372 # Try to avoid using this function wherever possible.
3375 map { my $a = $_; $a =~ s/(['!])/'\\$1'/g; "'$a'" } @_ );
3378 # get HEAD ref of given project as hash
3379 sub git_get_head_hash
{
3380 return git_get_full_hash
(shift, 'HEAD');
3383 sub git_get_full_hash
{
3384 return git_get_hash
(@_);
3387 sub git_get_short_hash
{
3388 return git_get_hash
(@_, '--short=7');
3392 my ($project, $hash, @options) = @_;
3393 my $o_git_dir = $git_dir;
3395 $git_dir = "$projectroot/$project";
3396 if (defined(my $fd = git_cmd_pipe
'rev-parse',
3397 '--verify', '-q', @options, $hash)) {
3399 chomp $retval if defined $retval;
3402 if (defined $o_git_dir) {
3403 $git_dir = $o_git_dir;
3408 # get type of given object
3412 defined(my $fd = git_cmd_pipe
"cat-file", '-t', $hash) or return;
3414 close $fd or return;
3419 # repository configuration
3420 our $config_file = '';
3423 # store multiple values for single key as anonymous array reference
3424 # single values stored directly in the hash, not as [ <value> ]
3425 sub hash_set_multi
{
3426 my ($hash, $key, $value) = @_;
3428 if (!exists $hash->{$key}) {
3429 $hash->{$key} = $value;
3430 } elsif (!ref $hash->{$key}) {
3431 $hash->{$key} = [ $hash->{$key}, $value ];
3433 push @
{$hash->{$key}}, $value;
3437 # return hash of git project configuration
3438 # optionally limited to some section, e.g. 'gitweb'
3439 sub git_parse_project_config
{
3440 my $section_regexp = shift;
3445 defined(my $fh = git_cmd_pipe
"config", '-z', '-l')
3448 while (my $keyval = to_utf8
(scalar <$fh>)) {
3450 my ($key, $value) = split(/\n/, $keyval, 2);
3452 hash_set_multi
(\
%config, $key, $value)
3453 if (!defined $section_regexp || $key =~ /^(?:$section_regexp)\./o);
3460 # convert config value to boolean: 'true' or 'false'
3461 # no value, number > 0, 'true' and 'yes' values are true
3462 # rest of values are treated as false (never as error)
3463 sub config_to_bool
{
3466 return 1 if !defined $val; # section.key
3468 # strip leading and trailing whitespace
3472 return (($val =~ /^\d+$/ && $val) || # section.key = 1
3473 ($val =~ /^(?:true|yes)$/i)); # section.key = true
3476 # convert config value to simple decimal number
3477 # an optional value suffix of 'k', 'm', or 'g' will cause the value
3478 # to be multiplied by 1024, 1048576, or 1073741824
3482 # strip leading and trailing whitespace
3486 if (my ($num, $unit) = ($val =~ /^([0-9]*)([kmg])$/i)) {
3488 # unknown unit is treated as 1
3489 return $num * ($unit eq 'g' ?
1073741824 :
3490 $unit eq 'm' ?
1048576 :
3491 $unit eq 'k' ?
1024 : 1);
3496 # convert config value to array reference, if needed
3497 sub config_to_multi
{
3500 return ref($val) ?
$val : (defined($val) ?
[ $val ] : []);
3503 sub git_get_project_config
{
3504 my ($key, $type) = @_;
3506 return unless defined $git_dir;
3509 return unless ($key);
3510 # only subsection, if exists, is case sensitive,
3511 # and not lowercased by 'git config -z -l'
3512 if (my ($hi, $mi, $lo) = ($key =~ /^([^.]*)\.(.*)\.([^.]*)$/)) {
3514 $key = join(".", lc($hi), $mi, lc($lo));
3515 return if ($lo =~ /\W/ || $hi =~ /\W/);
3519 return if ($key =~ /\W/);
3521 $key =~ s/^gitweb\.//;
3524 if (defined $type) {
3527 unless ($type eq 'bool' || $type eq 'int');
3531 if (!defined $config_file ||
3532 $config_file ne "$git_dir/config") {
3533 %config = git_parse_project_config
('gitweb');
3534 $config_file = "$git_dir/config";
3537 # check if config variable (key) exists
3538 return unless exists $config{"gitweb.$key"};
3541 if (!defined $type) {
3542 return $config{"gitweb.$key"};
3543 } elsif ($type eq 'bool') {
3544 # backward compatibility: 'git config --bool' returns true/false
3545 return config_to_bool
($config{"gitweb.$key"}) ?
'true' : 'false';
3546 } elsif ($type eq 'int') {
3547 return config_to_int
($config{"gitweb.$key"});
3549 return $config{"gitweb.$key"};
3552 # get hash of given path at given ref
3553 sub git_get_hash_by_path
{
3555 my $path = shift || return undef;
3560 defined(my $fd = git_cmd_pipe
"ls-tree", $base, "--", $path)
3561 or die_error
(500, "Open git-ls-tree failed");
3562 my $line = to_utf8
(scalar <$fd>);
3563 close $fd or return undef;
3565 if (!defined $line) {
3566 # there is no tree or hash given by $path at $base
3570 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'
3571 $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t/;
3572 if (defined $type && $type ne $2) {
3573 # type doesn't match
3579 # get path of entry with given hash at given tree-ish (ref)
3580 # used to get 'from' filename for combined diff (merge commit) for renames
3581 sub git_get_path_by_hash
{
3582 my $base = shift || return;
3583 my $hash = shift || return;
3587 defined(my $fd = git_cmd_pipe
"ls-tree", '-r', '-t', '-z', $base)
3589 while (my $line = to_utf8
(scalar <$fd>)) {
3592 #'040000 tree 595596a6a9117ddba9fe379b6b012b558bac8423 gitweb'
3593 #'100644 blob e02e90f0429be0d2a69b76571101f20b8f75530f gitweb/README'
3594 if ($line =~ m/(?:[0-9]+) (?:.+) $hash\t(.+)$/) {
3603 ## ......................................................................
3604 ## git utility functions, directly accessing git repository
3606 # get the value of config variable either from file named as the variable
3607 # itself in the repository ($GIT_DIR/$name file), or from gitweb.$name
3608 # configuration variable in the repository config file.
3609 sub git_get_file_or_project_config
{
3610 my ($path, $name) = @_;
3612 $git_dir = "$projectroot/$path";
3613 open my $fd, '<', "$git_dir/$name"
3614 or return git_get_project_config
($name);
3615 my $conf = to_utf8
(scalar <$fd>);
3617 if (defined $conf) {
3623 sub git_get_project_description
{
3625 return git_get_file_or_project_config
($path, 'description');
3628 sub git_get_project_category
{
3630 return git_get_file_or_project_config
($path, 'category');
3634 # supported formats:
3635 # * $GIT_DIR/ctags/<tagname> file (in 'ctags' subdirectory)
3636 # - if its contents is a number, use it as tag weight,
3637 # - otherwise add a tag with weight 1
3638 # * $GIT_DIR/ctags file, each line is a tag (with weight 1)
3639 # the same value multiple times increases tag weight
3640 # * `gitweb.ctag' multi-valued repo config variable
3641 sub git_get_project_ctags
{
3642 my $project = shift;
3645 $git_dir = "$projectroot/$project";
3646 if (opendir my $dh, "$git_dir/ctags") {
3647 my @files = grep { -f
$_ } map { "$git_dir/ctags/$_" } readdir($dh);
3648 foreach my $tagfile (@files) {
3649 open my $ct, '<', $tagfile
3655 (my $ctag = $tagfile) =~ s
#.*/##;
3656 $ctag = to_utf8
($ctag);
3657 if ($val =~ /^\d+$/) {
3658 $ctags->{$ctag} = $val;
3660 $ctags->{$ctag} = 1;
3665 } elsif (open my $fh, '<', "$git_dir/ctags") {
3666 while (my $line = to_utf8
(scalar <$fh>)) {
3668 $ctags->{$line}++ if $line;
3673 my $taglist = config_to_multi
(git_get_project_config
('ctag'));
3674 foreach my $tag (@
$taglist) {
3682 # return hash, where keys are content tags ('ctags'),
3683 # and values are sum of weights of given tag in every project
3684 sub git_gather_all_ctags
{
3685 my $projects = shift;
3688 foreach my $p (@
$projects) {
3689 foreach my $ct (keys %{$p->{'ctags'}}) {
3690 $ctags->{$ct} += $p->{'ctags'}->{$ct};
3697 sub git_populate_project_tagcloud
{
3698 my ($ctags, $action) = @_;
3700 # First, merge different-cased tags; tags vote on casing
3702 foreach (keys %$ctags) {
3703 $ctags_lc{lc $_}->{count
} += $ctags->{$_};
3704 if (not $ctags_lc{lc $_}->{topcount
}
3705 or $ctags_lc{lc $_}->{topcount
} < $ctags->{$_}) {
3706 $ctags_lc{lc $_}->{topcount
} = $ctags->{$_};
3707 $ctags_lc{lc $_}->{topname
} = $_;
3712 my $matched = $input_params{'ctag_filter'};
3713 if (eval { require HTML
::TagCloud
; 1; }) {
3714 $cloud = HTML
::TagCloud
->new;
3715 foreach my $ctag (sort keys %ctags_lc) {
3716 # Pad the title with spaces so that the cloud looks
3718 my $title = esc_html
($ctags_lc{$ctag}->{topname
});
3719 $title =~ s/ / /g;
3720 $title =~ s/^/ /g;
3721 $title =~ s/$/ /g;
3722 if (defined $matched && $matched eq $ctag) {
3723 $title = qq(<span
class="match">$title</span
>);
3725 $cloud->add($title, href
(-replay
=>1, action
=>$action, ctag_filter
=>$ctag),
3726 $ctags_lc{$ctag}->{count
});
3730 foreach my $ctag (keys %ctags_lc) {
3731 my $title = esc_html
($ctags_lc{$ctag}->{topname
}, -nbsp
=>1);
3732 if (defined $matched && $matched eq $ctag) {
3733 $title = qq(<span
class="match">$title</span
>);
3735 $cloud->{$ctag}{count
} = $ctags_lc{$ctag}->{count
};
3736 $cloud->{$ctag}{ctag
} =
3737 $cgi->a({-href
=>href
(-replay
=>1, action
=>$action, ctag_filter
=>$ctag)}, $title);
3743 sub git_show_project_tagcloud
{
3744 my ($cloud, $count) = @_;
3745 if (ref $cloud eq 'HTML::TagCloud') {
3746 return $cloud->html_and_css($count);
3748 my @tags = sort { $cloud->{$a}->{'count'} <=> $cloud->{$b}->{'count'} } keys %$cloud;
3750 '<div id="htmltagcloud"'.($project ?
'' : ' align="center"').'>' .
3752 $cloud->{$_}->{'ctag'}
3753 } splice(@tags, 0, $count)) .
3758 sub git_get_project_url_list
{
3761 $git_dir = "$projectroot/$path";
3762 open my $fd, '<', "$git_dir/cloneurl"
3763 or return wantarray ?
3764 @
{ config_to_multi
(git_get_project_config
('url')) } :
3765 config_to_multi
(git_get_project_config
('url'));
3766 my @git_project_url_list = map { chomp; to_utf8
($_) } <$fd>;
3769 return wantarray ?
@git_project_url_list : \
@git_project_url_list;
3772 sub git_get_projects_list
{
3773 my $filter = shift || '';
3774 my $paranoid = shift;
3777 if (-d
$projects_list) {
3778 # search in directory
3779 my $dir = $projects_list;
3780 # remove the trailing "/"
3782 my $pfxlen = length("$dir");
3783 my $pfxdepth = ($dir =~ tr!/!!);
3784 # when filtering, search only given subdirectory
3785 if ($filter && !$paranoid) {
3791 follow_fast
=> 1, # follow symbolic links
3792 follow_skip
=> 2, # ignore duplicates
3793 dangling_symlinks
=> 0, # ignore dangling symlinks, silently
3796 our $project_maxdepth;
3798 # skip project-list toplevel, if we get it.
3799 return if (m!^[/.]$!);
3800 # only directories can be git repositories
3801 return unless (-d
$_);
3802 # don't traverse too deep (Find is super slow on os x)
3803 # $project_maxdepth excludes depth of $projectroot
3804 if (($File::Find
::name
=~ tr!/!!) - $pfxdepth > $project_maxdepth) {
3805 $File::Find
::prune
= 1;
3809 my $path = substr($File::Find
::name
, $pfxlen + 1);
3810 # paranoidly only filter here
3811 if ($paranoid && $filter && $path !~ m!^\Q$filter\E/!) {
3814 # we check related file in $projectroot
3815 if (check_export_ok
("$projectroot/$path")) {
3816 push @list, { path
=> $path };
3817 $File::Find
::prune
= 1;
3822 } elsif (-f
$projects_list) {
3823 # read from file(url-encoded):
3824 # 'git%2Fgit.git Linus+Torvalds'
3825 # 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin'
3826 # 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman'
3827 open my $fd, '<', $projects_list or return;
3829 while (my $line = <$fd>) {
3831 my ($path, $owner) = split ' ', $line;
3832 $path = unescape
($path);
3833 $owner = unescape
($owner);
3834 if (!defined $path) {
3837 # if $filter is rpovided, check if $path begins with $filter
3838 if ($filter && $path !~ m!^\Q$filter\E/!) {
3841 if (check_export_ok
("$projectroot/$path")) {
3846 $pr->{'owner'} = to_utf8
($owner);
3856 # written with help of Tree::Trie module (Perl Artistic License, GPL compatibile)
3857 # as side effects it sets 'forks' field to list of forks for forked projects
3858 sub filter_forks_from_projects_list
{
3859 my $projects = shift;
3861 my %trie; # prefix tree of directories (path components)
3862 # generate trie out of those directories that might contain forks
3863 foreach my $pr (@
$projects) {
3864 my $path = $pr->{'path'};
3865 $path =~ s/\.git$//; # forks of 'repo.git' are in 'repo/' directory
3866 next if ($path =~ m!/$!); # skip non-bare repositories, e.g. 'repo/.git'
3867 next unless ($path); # skip '.git' repository: tests, git-instaweb
3868 next unless (-d
"$projectroot/$path"); # containing directory exists
3869 $pr->{'forks'} = []; # there can be 0 or more forks of project
3872 my @dirs = split('/', $path);
3873 # walk the trie, until either runs out of components or out of trie
3875 while (scalar @dirs &&
3876 exists($ref->{$dirs[0]})) {
3877 $ref = $ref->{shift @dirs};
3879 # create rest of trie structure from rest of components
3880 foreach my $dir (@dirs) {
3881 $ref = $ref->{$dir} = {};
3883 # create end marker, store $pr as a data
3884 $ref->{''} = $pr if (!exists $ref->{''});
3887 # filter out forks, by finding shortest prefix match for paths
3890 foreach my $pr (@
$projects) {
3894 foreach my $dir (split('/', $pr->{'path'})) {
3895 if (exists $ref->{''}) {
3896 # found [shortest] prefix, is a fork - skip it
3897 push @
{$ref->{''}{'forks'}}, $pr;
3900 if (!exists $ref->{$dir}) {
3901 # not in trie, cannot have prefix, not a fork
3902 push @filtered, $pr;
3905 # If the dir is there, we just walk one step down the trie.
3906 $ref = $ref->{$dir};
3908 # we ran out of trie
3909 # (shouldn't happen: it's either no match, or end marker)
3910 push @filtered, $pr;
3916 # note: fill_project_list_info must be run first,
3917 # for 'descr_long' and 'ctags' to be filled
3918 sub search_projects_list
{
3919 my ($projlist, %opts) = @_;
3920 my $tagfilter = $opts{'tagfilter'};
3921 my $search_re = $opts{'search_regexp'};
3924 unless ($tagfilter || $search_re);
3926 # searching projects require filling to be run before it;
3927 fill_project_list_info
($projlist,
3928 $tagfilter ?
'ctags' : (),
3929 $search_re ?
('path', 'descr') : ());
3932 foreach my $pr (@
$projlist) {
3935 next unless ref($pr->{'ctags'}) eq 'HASH';
3937 grep { lc($_) eq lc($tagfilter) } keys %{$pr->{'ctags'}};
3941 my $path = $pr->{'path'};
3942 $path =~ s/\.git$//; # should not be included in search
3944 $path =~ /$search_re/ ||
3945 $pr->{'descr_long'} =~ /$search_re/;
3948 push @projects, $pr;
3954 our $gitweb_project_owner = undef;
3955 sub git_get_project_list_from_file
{
3957 return if (defined $gitweb_project_owner);
3959 $gitweb_project_owner = {};
3960 # read from file (url-encoded):
3961 # 'git%2Fgit.git Linus+Torvalds'
3962 # 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin'
3963 # 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman'
3964 if (-f
$projects_list) {
3965 open(my $fd, '<', $projects_list);
3966 while (my $line = <$fd>) {
3968 my ($pr, $ow) = split ' ', $line;
3969 $pr = unescape
($pr);
3970 $ow = unescape
($ow);
3971 $gitweb_project_owner->{$pr} = to_utf8
($ow);
3977 sub git_get_project_owner
{
3981 return undef unless $proj;
3982 $git_dir = "$projectroot/$proj";
3984 if (defined $project && $proj eq $project) {
3985 $owner = git_get_project_config
('owner');
3987 if (!defined $owner && !defined $gitweb_project_owner) {
3988 git_get_project_list_from_file
();
3990 if (!defined $owner && exists $gitweb_project_owner->{$proj}) {
3991 $owner = $gitweb_project_owner->{$proj};
3993 if (!defined $owner && (!defined $project || $proj ne $project)) {
3994 $owner = git_get_project_config
('owner');
3996 if (!defined $owner) {
3997 $owner = get_file_owner
("$git_dir");
4003 sub parse_activity_date
{
4006 if ($dstr =~ /^\s*([-+]?\d+)(?:\s+([-+]\d{4}))?\s*$/) {
4010 if ($dstr =~ /^\s*(\d{4})-(\d{2})-(\d{2})[Tt _](\d{1,2}):(\d{2}):(\d{2})(?:[ _]?([Zz]|(?:[-+]\d{1,2}:?\d{2})))?\s*$/) {
4011 my ($Y,$m,$d,$H,$M,$S,$z) = ($1,$2,$3,$4,$5,$6,$7||'');
4012 my $seconds = timegm
(0+$S, 0+$M, 0+$H, 0+$d, $m-1, $Y-1900);
4013 defined($z) && $z ne '' or $z = 'Z';
4015 substr($z,1,0) = '0' if length($z) == 4;
4017 if (uc($z) ne 'Z') {
4018 $off = 60 * (60 * (0+substr($z,1,2)) + (0+substr($z,3,2)));
4019 $off = -$off if substr($z,0,1) eq '-';
4021 return $seconds - $off;
4026 # If $quick is true only look at $lastactivity_file
4027 sub git_get_last_activity
{
4028 my ($path, $quick) = @_;
4031 $git_dir = "$projectroot/$path";
4032 if ($lastactivity_file && open($fd, "<", "$git_dir/$lastactivity_file")) {
4033 my $activity = <$fd>;
4035 return (undef) unless defined $activity;
4037 return (undef) if $activity eq '';
4038 if (my $timestamp = parse_activity_date
($activity)) {
4039 return ($timestamp);
4042 return (undef) if $quick;
4043 defined($fd = git_cmd_pipe
'for-each-ref',
4044 '--format=%(committer)',
4045 '--sort=-committerdate',
4047 map { "refs/$_" } get_branch_refs
()) or return;
4048 my $most_recent = <$fd>;
4049 close $fd or return (undef);
4050 if (defined $most_recent &&
4051 $most_recent =~ / (\d+) [-+][01]\d\d\d$/) {
4053 return ($timestamp);
4058 # Implementation note: when a single remote is wanted, we cannot use 'git
4059 # remote show -n' because that command always work (assuming it's a remote URL
4060 # if it's not defined), and we cannot use 'git remote show' because that would
4061 # try to make a network roundtrip. So the only way to find if that particular
4062 # remote is defined is to walk the list provided by 'git remote -v' and stop if
4063 # and when we find what we want.
4064 sub git_get_remotes_list
{
4068 my $fd = git_cmd_pipe
'remote', '-v';
4070 while (my $remote = to_utf8
(scalar <$fd>)) {
4072 $remote =~ s!\t(.*?)\s+\((\w+)\)$!!;
4073 next if $wanted and not $remote eq $wanted;
4074 my ($url, $key) = ($1, $2);
4076 $remotes{$remote} ||= { 'heads' => [] };
4077 $remotes{$remote}{$key} = $url;
4079 close $fd or return;
4080 return wantarray ?
%remotes : \
%remotes;
4083 # Takes a hash of remotes as first parameter and fills it by adding the
4084 # available remote heads for each of the indicated remotes.
4085 sub fill_remote_heads
{
4086 my $remotes = shift;
4087 my @heads = map { "remotes/$_" } keys %$remotes;
4088 my @remoteheads = git_get_heads_list
(undef, @heads);
4089 foreach my $remote (keys %$remotes) {
4090 $remotes->{$remote}{'heads'} = [ grep {
4091 $_->{'name'} =~ s!^$remote/!!
4096 sub git_get_references
{
4097 my $type = shift || "";
4099 # 5dc01c595e6c6ec9ccda4f6f69c131c0dd945f8c refs/tags/v2.6.11
4100 # c39ae07f393806ccf406ef966e9a15afc43cc36a refs/tags/v2.6.11^{}
4101 defined(my $fd = git_cmd_pipe
"show-ref", "--dereference",
4102 ($type ?
("--", "refs/$type") : ())) # use -- <pattern> if $type
4105 while (my $line = to_utf8
(scalar <$fd>)) {
4107 if ($line =~ m!^([0-9a-fA-F]{40})\srefs/($type.*)$!) {
4108 if (defined $refs{$1}) {
4109 push @
{$refs{$1}}, $2;
4115 close $fd or return;
4119 sub git_get_rev_name_tags
{
4120 my $hash = shift || return undef;
4122 defined(my $fd = git_cmd_pipe
"name-rev", "--tags", $hash)
4124 my $name_rev = to_utf8
(scalar <$fd>);
4127 if ($name_rev =~ m
|^$hash tags
/(.*)$|) {
4130 # catches also '$hash undefined' output
4135 ## ----------------------------------------------------------------------
4136 ## parse to hash functions
4140 my $tz = shift || "-0000";
4143 my @months = ("Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec");
4144 my @days = ("Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat");
4145 my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($epoch);
4146 $date{'hour'} = $hour;
4147 $date{'minute'} = $min;
4148 $date{'mday'} = $mday;
4149 $date{'day'} = $days[$wday];
4150 $date{'month'} = $months[$mon];
4151 $date{'rfc2822'} = sprintf "%s, %d %s %4d %02d:%02d:%02d +0000",
4152 $days[$wday], $mday, $months[$mon], 1900+$year, $hour ,$min, $sec;
4153 $date{'mday-time'} = sprintf "%d %s %02d:%02d",
4154 $mday, $months[$mon], $hour ,$min;
4155 $date{'iso-8601'} = sprintf "%04d-%02d-%02dT%02d:%02d:%02dZ",
4156 1900+$year, 1+$mon, $mday, $hour ,$min, $sec;
4158 my ($tz_sign, $tz_hour, $tz_min) =
4159 ($tz =~ m/^([-+])(\d\d)(\d\d)$/);
4160 $tz_sign = ($tz_sign eq '-' ?
-1 : +1);
4161 my $local = $epoch + $tz_sign*((($tz_hour*60) + $tz_min)*60);
4162 ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($local);
4163 $date{'hour_local'} = $hour;
4164 $date{'minute_local'} = $min;
4165 $date{'mday_local'} = $mday;
4166 $date{'tz_local'} = $tz;
4167 $date{'iso-tz'} = sprintf("%04d-%02d-%02d %02d:%02d:%02d %s",
4168 1900+$year, $mon+1, $mday,
4169 $hour, $min, $sec, $tz);
4173 sub parse_file_date
{
4175 my $mtime = (stat("$projectroot/$project/$file"))[9];
4176 return () unless defined $mtime;
4177 my $tzoffset = timegm
((localtime($mtime))[0..5]) - $mtime;
4179 if ($tzoffset <= 0) {
4183 $tzoffset = int($tzoffset/60);
4184 $tzstring .= sprintf("%02d%02d", int($tzoffset/60), $tzoffset%60);
4185 return parse_date
($mtime, $tzstring);
4193 defined(my $fd = git_cmd_pipe
"cat-file", "tag", $tag_id) or return;
4194 $tag{'id'} = $tag_id;
4195 while (my $line = to_utf8
(scalar <$fd>)) {
4197 if ($line =~ m/^object ([0-9a-fA-F]{40})$/) {
4198 $tag{'object'} = $1;
4199 } elsif ($line =~ m/^type (.+)$/) {
4201 } elsif ($line =~ m/^tag (.+)$/) {
4203 } elsif ($line =~ m/^tagger (.*) ([0-9]+) (.*)$/) {
4204 $tag{'author'} = $1;
4205 $tag{'author_epoch'} = $2;
4206 $tag{'author_tz'} = $3;
4207 if ($tag{'author'} =~ m/^([^<]+) <([^>]*)>/) {
4208 $tag{'author_name'} = $1;
4209 $tag{'author_email'} = $2;
4211 $tag{'author_name'} = $tag{'author'};
4213 } elsif ($line =~ m/--BEGIN/) {
4214 push @comment, $line;
4216 } elsif ($line eq "") {
4220 push @comment, map(to_utf8
($_), <$fd>);
4221 $tag{'comment'} = \
@comment;
4222 close $fd or return;
4223 if (!defined $tag{'name'}) {
4229 sub parse_commit_text
{
4230 my ($commit_text, $withparents) = @_;
4231 my @commit_lines = split '\n', $commit_text;
4234 pop @commit_lines; # Remove '\0'
4236 if (! @commit_lines) {
4240 my $header = shift @commit_lines;
4241 if ($header !~ m/^[0-9a-fA-F]{40}/) {
4244 ($co{'id'}, my @parents) = split ' ', $header;
4245 while (my $line = shift @commit_lines) {
4246 last if $line eq "\n";
4247 if ($line =~ m/^tree ([0-9a-fA-F]{40})$/) {
4249 } elsif ((!defined $withparents) && ($line =~ m/^parent ([0-9a-fA-F]{40})$/)) {
4251 } elsif ($line =~ m/^author (.*) ([0-9]+) (.*)$/) {
4252 $co{'author'} = to_utf8
($1);
4253 $co{'author_epoch'} = $2;
4254 $co{'author_tz'} = $3;
4255 if ($co{'author'} =~ m/^([^<]+) <([^>]*)>/) {
4256 $co{'author_name'} = $1;
4257 $co{'author_email'} = $2;
4259 $co{'author_name'} = $co{'author'};
4261 } elsif ($line =~ m/^committer (.*) ([0-9]+) (.*)$/) {
4262 $co{'committer'} = to_utf8
($1);
4263 $co{'committer_epoch'} = $2;
4264 $co{'committer_tz'} = $3;
4265 if ($co{'committer'} =~ m/^([^<]+) <([^>]*)>/) {
4266 $co{'committer_name'} = $1;
4267 $co{'committer_email'} = $2;
4269 $co{'committer_name'} = $co{'committer'};
4273 if (!defined $co{'tree'}) {
4276 $co{'parents'} = \
@parents;
4277 $co{'parent'} = $parents[0];
4279 @commit_lines = map to_utf8
($_), @commit_lines;
4280 foreach my $title (@commit_lines) {
4283 $co{'title'} = chop_str
($title, 80, 5);
4284 # remove leading stuff of merges to make the interesting part visible
4285 if (length($title) > 50) {
4286 $title =~ s/^Automatic //;
4287 $title =~ s/^merge (of|with) /Merge ... /i;
4288 if (length($title) > 50) {
4289 $title =~ s/(http|rsync):\/\///;
4291 if (length($title) > 50) {
4292 $title =~ s/(master|www|rsync)\.//;
4294 if (length($title) > 50) {
4295 $title =~ s/kernel.org:?//;
4297 if (length($title) > 50) {
4298 $title =~ s/\/pub\/scm//;
4301 $co{'title_short'} = chop_str
($title, 50, 5);
4305 if (! defined $co{'title'} || $co{'title'} eq "") {
4306 $co{'title'} = $co{'title_short'} = '(no commit message)';
4308 # remove added spaces
4309 foreach my $line (@commit_lines) {
4312 $co{'comment'} = \
@commit_lines;
4314 my $age_epoch = $co{'committer_epoch'};
4315 $co{'age_epoch'} = $age_epoch;
4316 my $time_now = time;
4317 $co{'age_string'} = age_string
($age_epoch, $time_now);
4318 $co{'age_string_date'} = age_string_date
($age_epoch, $time_now);
4319 $co{'age_string_age'} = age_string_age
($age_epoch, $time_now);
4324 my ($commit_id) = @_;
4329 defined(my $fd = git_cmd_pipe
"rev-list",
4335 or die_error
(500, "Open git-rev-list failed");
4336 %co = parse_commit_text
(<$fd>, 1);
4343 my ($commit_id, $maxcount, $skip, $filename, @args) = @_;
4351 defined(my $fd = git_cmd_pipe
"rev-list",
4354 ("--max-count=" . $maxcount),
4355 ("--skip=" . $skip),
4359 ($filename ?
($filename) : ()))
4360 or die_error
(500, "Open git-rev-list failed");
4361 while (my $line = <$fd>) {
4362 my %co = parse_commit_text
($line);
4367 return wantarray ?
@cos : \
@cos;
4370 # parse line of git-diff-tree "raw" output
4371 sub parse_difftree_raw_line
{
4375 # ':100644 100644 03b218260e99b78c6df0ed378e59ed9205ccc96d 3b93d5e7cc7f7dd4ebed13a5cc1a4ad976fc94d8 M ls-files.c'
4376 # ':100644 100644 7f9281985086971d3877aca27704f2aaf9c448ce bc190ebc71bbd923f2b728e505408f5e54bd073a M rev-tree.c'
4377 if ($line =~ m/^:([0-7]{6}) ([0-7]{6}) ([0-9a-fA-F]{40}) ([0-9a-fA-F]{40}) (.)([0-9]{0,3})\t(.*)$/) {
4378 $res{'from_mode'} = $1;
4379 $res{'to_mode'} = $2;
4380 $res{'from_id'} = $3;
4382 $res{'status'} = $5;
4383 $res{'similarity'} = $6;
4384 if ($res{'status'} eq 'R' || $res{'status'} eq 'C') { # renamed or copied
4385 ($res{'from_file'}, $res{'to_file'}) = map { unquote
($_) } split("\t", $7);
4387 $res{'from_file'} = $res{'to_file'} = $res{'file'} = unquote
($7);
4390 # '::100755 100755 100755 60e79ca1b01bc8b057abe17ddab484699a7f5fdb 94067cc5f73388f33722d52ae02f44692bc07490 94067cc5f73388f33722d52ae02f44692bc07490 MR git-gui/git-gui.sh'
4391 # combined diff (for merge commit)
4392 elsif ($line =~ s/^(::+)((?:[0-7]{6} )+)((?:[0-9a-fA-F]{40} )+)([a-zA-Z]+)\t(.*)$//) {
4393 $res{'nparents'} = length($1);
4394 $res{'from_mode'} = [ split(' ', $2) ];
4395 $res{'to_mode'} = pop @
{$res{'from_mode'}};
4396 $res{'from_id'} = [ split(' ', $3) ];
4397 $res{'to_id'} = pop @
{$res{'from_id'}};
4398 $res{'status'} = [ split('', $4) ];
4399 $res{'to_file'} = unquote
($5);
4401 # 'c512b523472485aef4fff9e57b229d9d243c967f'
4402 elsif ($line =~ m/^([0-9a-fA-F]{40})$/) {
4403 $res{'commit'} = $1;
4406 return wantarray ?
%res : \
%res;
4409 # wrapper: return parsed line of git-diff-tree "raw" output
4410 # (the argument might be raw line, or parsed info)
4411 sub parsed_difftree_line
{
4412 my $line_or_ref = shift;
4414 if (ref($line_or_ref) eq "HASH") {
4415 # pre-parsed (or generated by hand)
4416 return $line_or_ref;
4418 return parse_difftree_raw_line
($line_or_ref);
4422 # parse line of git-ls-tree output
4423 sub parse_ls_tree_line
{
4429 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa 16717 panic.c'
4430 $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40}) +(-|[0-9]+)\t(.+)$/s;
4439 $res{'name'} = unquote
($5);
4442 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'
4443 $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t(.+)$/s;
4451 $res{'name'} = unquote
($4);
4455 return wantarray ?
%res : \
%res;
4458 # generates _two_ hashes, references to which are passed as 2 and 3 argument
4459 sub parse_from_to_diffinfo
{
4460 my ($diffinfo, $from, $to, @parents) = @_;
4462 if ($diffinfo->{'nparents'}) {
4464 $from->{'file'} = [];
4465 $from->{'href'} = [];
4466 fill_from_file_info
($diffinfo, @parents)
4467 unless exists $diffinfo->{'from_file'};
4468 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
4469 $from->{'file'}[$i] =
4470 defined $diffinfo->{'from_file'}[$i] ?
4471 $diffinfo->{'from_file'}[$i] :
4472 $diffinfo->{'to_file'};
4473 if ($diffinfo->{'status'}[$i] ne "A") { # not new (added) file
4474 $from->{'href'}[$i] = href
(action
=>"blob",
4475 hash_base
=>$parents[$i],
4476 hash
=>$diffinfo->{'from_id'}[$i],
4477 file_name
=>$from->{'file'}[$i]);
4479 $from->{'href'}[$i] = undef;
4483 # ordinary (not combined) diff
4484 $from->{'file'} = $diffinfo->{'from_file'};
4485 if ($diffinfo->{'status'} ne "A") { # not new (added) file
4486 $from->{'href'} = href
(action
=>"blob", hash_base
=>$hash_parent,
4487 hash
=>$diffinfo->{'from_id'},
4488 file_name
=>$from->{'file'});
4490 delete $from->{'href'};
4494 $to->{'file'} = $diffinfo->{'to_file'};
4495 if (!is_deleted
($diffinfo)) { # file exists in result
4496 $to->{'href'} = href
(action
=>"blob", hash_base
=>$hash,
4497 hash
=>$diffinfo->{'to_id'},
4498 file_name
=>$to->{'file'});
4500 delete $to->{'href'};
4504 ## ......................................................................
4505 ## parse to array of hashes functions
4507 sub git_get_heads_list
{
4508 my ($limit, @classes) = @_;
4509 @classes = get_branch_refs
() unless @classes;
4510 my @patterns = map { "refs/$_" } @classes;
4513 defined(my $fd = git_cmd_pipe
'for-each-ref',
4514 ($limit ?
'--count='.($limit+1) : ()), '--sort=-committerdate',
4515 '--format=%(objectname) %(refname) %(subject)%00%(committer)',
4518 while (my $line = to_utf8
(scalar <$fd>)) {
4522 my ($refinfo, $committerinfo) = split(/\0/, $line);
4523 my ($hash, $name, $title) = split(' ', $refinfo, 3);
4524 my ($committer, $epoch, $tz) =
4525 ($committerinfo =~ /^(.*) ([0-9]+) (.*)$/);
4526 $ref_item{'fullname'} = $name;
4527 my $strip_refs = join '|', map { quotemeta } get_branch_refs
();
4528 $name =~ s!^refs/($strip_refs|remotes)/!!;
4529 $ref_item{'name'} = $name;
4530 # for refs neither in 'heads' nor 'remotes' we want to
4531 # show their ref dir
4532 my $ref_dir = (defined $1) ?
$1 : '';
4533 if ($ref_dir ne '' and $ref_dir ne 'heads' and $ref_dir ne 'remotes') {
4534 $ref_item{'name'} .= ' (' . $ref_dir . ')';
4537 $ref_item{'id'} = $hash;
4538 $ref_item{'title'} = $title || '(no commit message)';
4539 $ref_item{'epoch'} = $epoch;
4541 $ref_item{'age'} = age_string
($ref_item{'epoch'});
4543 $ref_item{'age'} = "unknown";
4546 push @headslist, \
%ref_item;
4550 return wantarray ?
@headslist : \
@headslist;
4553 sub git_get_tags_list
{
4556 my $all = shift || 0;
4557 my $order = shift || $default_refs_order;
4558 my $sortkey = $all && $order eq 'name' ?
'refname' : '-creatordate';
4560 defined(my $fd = git_cmd_pipe
'for-each-ref',
4561 ($limit ?
'--count='.($limit+1) : ()), "--sort=$sortkey",
4562 '--format=%(objectname) %(objecttype) %(refname) '.
4563 '%(*objectname) %(*objecttype) %(subject)%00%(creator)',
4564 ($all ?
'refs' : 'refs/tags'))
4566 while (my $line = to_utf8
(scalar <$fd>)) {
4570 my ($refinfo, $creatorinfo) = split(/\0/, $line);
4571 my ($id, $type, $name, $refid, $reftype, $title) = split(' ', $refinfo, 6);
4572 my ($creator, $epoch, $tz) =
4573 ($creatorinfo =~ /^(.*) ([0-9]+) (.*)$/);
4574 $ref_item{'fullname'} = $name;
4575 $name =~ s!^refs/!! if $all;
4576 $name =~ s!^refs/tags/!! unless $all;
4578 $ref_item{'type'} = $type;
4579 $ref_item{'id'} = $id;
4580 $ref_item{'name'} = $name;
4581 if ($type eq "tag") {
4582 $ref_item{'subject'} = $title;
4583 $ref_item{'reftype'} = $reftype;
4584 $ref_item{'refid'} = $refid;
4586 $ref_item{'reftype'} = $type;
4587 $ref_item{'refid'} = $id;
4590 if ($type eq "tag" || $type eq "commit") {
4591 $ref_item{'epoch'} = $epoch;
4593 $ref_item{'age'} = age_string
($ref_item{'epoch'});
4595 $ref_item{'age'} = "unknown";
4599 push @tagslist, \
%ref_item;
4603 return wantarray ?
@tagslist : \
@tagslist;
4606 ## ----------------------------------------------------------------------
4607 ## filesystem-related functions
4609 sub get_file_owner
{
4612 my ($dev, $ino, $mode, $nlink, $st_uid, $st_gid, $rdev, $size) = stat($path);
4613 my ($name, $passwd, $uid, $gid, $quota, $comment, $gcos, $dir, $shell) = getpwuid($st_uid);
4614 if (!defined $gcos) {
4618 $owner =~ s/[,;].*$//;
4619 return to_utf8
($owner);
4622 # assume that file exists
4624 my $filename = shift;
4626 open my $fd, '<', $filename;
4633 # return undef on failure
4634 sub collect_output
{
4635 defined(my $fd = cmd_pipe
@_) or return undef;
4640 my $result = join('', map({ to_utf8
($_) } <$fd>));
4641 close $fd or return undef;
4645 # return undef on failure
4646 # return '' if only comments
4647 sub collect_html_file
{
4648 my $filename = shift;
4650 open my $fd, '<', $filename or return undef;
4651 my $result = join('', map({ to_utf8
($_) } <$fd>));
4652 close $fd or return undef;
4653 return undef unless defined($result);
4655 $test =~ s/<!--(?:[^-]|(?:-(?!-)))*-->//gs;
4657 return $test eq '' ?
'' : $result;
4660 ## ......................................................................
4661 ## mimetype related functions
4663 sub mimetype_guess_file
{
4664 my $filename = shift;
4665 my $mimemap = shift;
4666 my $rawmode = shift;
4667 -r
$mimemap or return undef;
4670 open(my $mh, '<', $mimemap) or return undef;
4672 next if m/^#/; # skip comments
4673 my ($mimetype, @exts) = split(/\s+/);
4674 foreach my $ext (@exts) {
4675 $mimemap{$ext} = $mimetype;
4681 $ext = $1 if $filename =~ /\.([^.]*)$/;
4682 $ans = $mimemap{$ext} if $ext;
4685 $ans = 'text/html' if $l eq 'application/xhtml+xml';
4687 $ans = 'text/xml' if $l =~ m
!^application
/[^\s
:;,=]+\
+xml
$! ||
4688 $l eq 'image/svg+xml' ||
4689 $l eq 'application/xml-dtd' ||
4690 $l eq 'application/xml-external-parsed-entity';
4696 sub mimetype_guess
{
4697 my $filename = shift;
4698 my $rawmode = shift;
4700 $filename =~ /\./ or return undef;
4702 if ($mimetypes_file) {
4703 my $file = $mimetypes_file;
4704 if ($file !~ m!^/!) { # if it is relative path
4705 # it is relative to project
4706 $file = "$projectroot/$project/$file";
4708 $mime = mimetype_guess_file
($filename, $file, $rawmode);
4710 $mime ||= mimetype_guess_file
($filename, '/etc/mime.types', $rawmode);
4716 my $filename = shift;
4717 my $rawmode = shift;
4720 # The -T/-B file operators produce the wrong result unless a perlio
4721 # layer is present when the file handle is a pipe that delivers less
4722 # than 512 bytes of data before reaching EOF.
4724 # If we are running in a Perl that uses the stdio layer rather than the
4725 # unix+perlio layers we will end up adding a perlio layer on top of the
4726 # stdio layer and get a second level of buffering. This is harmless
4727 # and it makes the -T/-B file operators work properly in all cases.
4729 binmode $fd, ":perlio" or die_error
(500, "Adding perlio layer failed")
4730 unless grep /^perlio$/, PerlIO
::get_layers
($fd);
4732 $mime = mimetype_guess
($filename, $rawmode) if defined $filename;
4734 if (!$mime && $filename) {
4735 if ($filename =~ m/\.html?$/i) {
4736 $mime = 'text/html';
4737 } elsif ($filename =~ m/\.xht(?:ml)?$/i) {
4738 $mime = 'text/html';
4739 } elsif ($filename =~ m/\.te?xt?$/i) {
4740 $mime = 'text/plain';
4741 } elsif ($filename =~ m/\.(?:markdown|md)$/i) {
4742 $mime = 'text/plain';
4743 } elsif ($filename =~ m/\.png$/i) {
4744 $mime = 'image/png';
4745 } elsif ($filename =~ m/\.gif$/i) {
4746 $mime = 'image/gif';
4747 } elsif ($filename =~ m/\.jpe?g$/i) {
4748 $mime = 'image/jpeg';
4749 } elsif ($filename =~ m/\.svgz?$/i) {
4750 $mime = 'image/svg+xml';
4755 return $default_blob_plain_mimetype || 'application/octet-stream' unless $fd || $mime;
4757 $mime = -T
$fd ?
'text/plain' : 'application/octet-stream' unless $mime;
4765 return scalar($data =~ /^[\x00-\x7f]*$/);
4770 return utf8
::decode
($data);
4773 sub extract_html_charset
{
4774 return undef unless $_[0] && "$_[0]</head>" =~ m
#<head(?:\s+[^>]*)?(?<!/)>(.*?)</head\s*>#is;
4776 return $2 if $head =~ m
#<meta\s+charset\s*=\s*(['"])\s*([a-z0-9(:)_.+-]+)\s*\1\s*/?>#is;
4777 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) {
4778 my %kv = (lc($1) => $3, lc($4) => $6);
4779 my ($he, $c) = (lc($kv{'http-equiv'}), $kv{'content'});
4780 return $1 if $he && $c && $he eq 'content-type' &&
4781 $c =~ m!\s*text/html\s*;\s*charset\s*=\s*([a-z0-9(:)_.+-]+)\s*$!is;
4786 sub blob_contenttype
{
4787 my ($fd, $file_name, $type) = @_;
4789 $type ||= blob_mimetype
($fd, $file_name, 1);
4790 return $type unless $type =~ m!^text/.+!i;
4791 my ($leader, $charset, $htmlcharset);
4792 if ($fd && read($fd, $leader, 32768)) {{
4793 $charset='US-ASCII' if is_ascii
($leader);
4794 return ("$type; charset=UTF-8", $leader) if !$charset && is_valid_utf8
($leader);
4795 $charset='ISO-8859-1' unless $charset;
4796 $htmlcharset = extract_html_charset
($leader) if $type eq 'text/html';
4797 if ($htmlcharset && $charset ne 'US-ASCII') {
4798 $htmlcharset = undef if $htmlcharset =~ /^(?:utf-8|us-ascii)$/i
4801 return ("$type; charset=$htmlcharset", $leader) if $htmlcharset;
4802 my $defcharset = $default_text_plain_charset || '';
4803 $defcharset =~ s/^\s+//;
4804 $defcharset =~ s/\s+$//;
4805 $defcharset = '' if $charset && $charset ne 'US-ASCII' && $defcharset =~ /^(?:utf-8|us-ascii)$/i;
4806 return ("$type; charset=" . ($defcharset || 'ISO-8859-1'), $leader);
4809 # peek the first upto 128 bytes off a file handle
4817 return '' unless $fd && read($fd, $prefix128, 128);
4819 # In the general case, we're guaranteed only to be able to ungetc one
4820 # character (provided, of course, we actually got a character first).
4824 # 1) we are dealing with a :perlio layer since blob_mimetype will have
4825 # already been called at least once on the file handle before us
4827 # 2) we have an $fd positioned at the start of the input stream and
4828 # therefore know we were positioned at a buffer boundary before
4829 # reading the initial upto 128 bytes
4831 # 3) the buffer size is at least 512 bytes
4833 # 4) we are careful to only unget raw bytes
4835 # 5) we are attempting to unget exactly the same number of bytes we got
4837 # Given the above conditions we will ALWAYS be able to safely unget
4838 # the $prefix128 value we just got.
4840 # In fact, we could read up to 511 bytes and still be sure.
4841 # (Reading 512 might pop us into the next internal buffer, but probably
4842 # not since that could break the always able to unget at least the one
4843 # you just got guarantee.)
4845 map {$fd->ungetc(ord($_))} reverse(split //, $prefix128);
4850 # guess file syntax for syntax highlighting; return undef if no highlighting
4851 # the name of syntax can (in the future) depend on syntax highlighter used
4852 sub guess_file_syntax
{
4853 my ($fd, $mimetype, $file_name) = @_;
4854 return undef unless $fd && defined $file_name &&
4855 defined $mimetype && $mimetype =~ m!^text/.+!i;
4856 my $basename = basename
($file_name, '.in');
4857 return $highlight_basename{$basename}
4858 if exists $highlight_basename{$basename};
4860 # Peek to see if there's a shebang or xml line.
4861 # We always operate on bytes when testing this.
4864 my $shebang = peek128bytes
($fd);
4865 if (length($shebang) >= 4 && $shebang =~ /^#!/) { # 4 would be '#!/x'
4866 foreach my $key (keys %highlight_shebang) {
4867 my $ar = ref($highlight_shebang{$key}) ?
4868 $highlight_shebang{$key} :
4869 [$highlight_shebang{key
}];
4870 map {return $key if $shebang =~ /$_/} @
$ar;
4873 return 'xml' if $shebang =~ m!^\s*<\?xml\s!; # "xml" must be lowercase
4876 $basename =~ /\.([^.]*)$/;
4877 my $ext = $1 or return undef;
4878 return $highlight_ext{$ext}
4879 if exists $highlight_ext{$ext};
4884 # run highlighter and return FD of its output,
4885 # or return original FD if no highlighting
4886 sub run_highlighter
{
4887 my ($fd, $syntax) = @_;
4888 return $fd unless $fd && !eof($fd) && defined $highlight_bin && defined $syntax;
4890 defined(my $hifd = cmd_pipe
$posix_shell_bin, '-c',
4891 quote_command
(git_cmd
(), "cat-file", "blob", $hash)." | ".
4892 $to_utf8_pipe_command.
4893 quote_command
($highlight_bin).
4894 " --replace-tabs=8 --fragment --syntax $syntax")
4895 or die_error
(500, "Couldn't open file or run syntax highlighter");
4897 # just in case, should not happen as we tested !eof($fd) above
4898 return $fd if close($hifd);
4901 !$! or die_error
(500, "Couldn't close syntax highighter pipe");
4903 # leaving us with the only possibility a non-zero exit status (possibly a signal);
4904 # instead of dying horribly on this, just skip the highlighting
4905 # but do output a message about it to STDERR that will end up in the log
4906 print STDERR
"warning: skipping failed highlight for --syntax $syntax: ".
4907 sprintf("child exit status 0x%x\n", $?
);
4914 ## ======================================================================
4915 ## functions printing HTML: header, footer, error page
4917 sub get_page_title
{
4918 my $title = to_utf8
($site_name);
4920 unless (defined $project) {
4921 if (defined $project_filter) {
4922 $title .= " - projects in '" . esc_path
($project_filter) . "'";
4926 $title .= " - " . to_utf8
($project);
4928 return $title unless (defined $action);
4929 my $action_print = $action eq 'blame_incremental' ?
'blame' : $action;
4930 $title .= "/$action_print"; # $action is US-ASCII (7bit ASCII)
4932 return $title unless (defined $file_name);
4933 $title .= " - " . esc_path
($file_name);
4934 if ($action eq "tree" && $file_name !~ m
|/$|) {
4941 sub get_content_type_html
{
4942 # We do not ever emit application/xhtml+xml since that gives us
4943 # no benefits and it makes many browsers (e.g. Firefox) exceedingly
4944 # strict, which is troublesome for example when showing user-supplied
4945 # README.html files.
4949 sub print_feed_meta
{
4950 if (defined $project) {
4951 my %href_params = get_feed_info
();
4952 if (!exists $href_params{'-title'}) {
4953 $href_params{'-title'} = 'log';
4956 foreach my $format (qw(RSS Atom)) {
4957 my $type = lc($format);
4959 '-rel' => 'alternate',
4960 '-title' => esc_attr
("$project - $href_params{'-title'} - $format feed"),
4961 '-type' => "application/$type+xml"
4964 $href_params{'extra_options'} = undef;
4965 $href_params{'action'} = $type;
4966 $link_attr{'-href'} = href
(%href_params);
4968 "rel=\"$link_attr{'-rel'}\" ".
4969 "title=\"$link_attr{'-title'}\" ".
4970 "href=\"$link_attr{'-href'}\" ".
4971 "type=\"$link_attr{'-type'}\" ".
4974 $href_params{'extra_options'} = '--no-merges';
4975 $link_attr{'-href'} = href
(%href_params);
4976 $link_attr{'-title'} .= ' (no merges)';
4978 "rel=\"$link_attr{'-rel'}\" ".
4979 "title=\"$link_attr{'-title'}\" ".
4980 "href=\"$link_attr{'-href'}\" ".
4981 "type=\"$link_attr{'-type'}\" ".
4986 printf('<link rel="alternate" title="%s projects list" '.
4987 'href="%s" type="text/plain; charset=utf-8" />'."\n",
4988 esc_attr
($site_name), href
(project
=>undef, action
=>"project_index"));
4989 printf('<link rel="alternate" title="%s projects feeds" '.
4990 'href="%s" type="text/x-opml" />'."\n",
4991 esc_attr
($site_name), href
(project
=>undef, action
=>"opml"));
4995 sub compute_stylesheet_links
{
4996 return "<?gitweb compute_stylesheet_links?>" if $cache_mode_active;
4998 # include each stylesheet that exists, providing backwards capability
4999 # for those people who defined $stylesheet in a config file
5000 if (defined $stylesheet) {
5001 return '<link rel="stylesheet" type="text/css" href="'.esc_url
($stylesheet).'"/>'."\n";
5004 foreach my $stylesheet (@stylesheets) {
5005 next unless $stylesheet;
5006 $sheets .= '<link rel="stylesheet" type="text/css" href="'.esc_url
($stylesheet).'"/>'."\n";
5012 sub print_header_links
{
5015 print compute_stylesheet_links
();
5017 if ($status eq '200 OK');
5018 if (defined $favicon) {
5019 print qq(<link rel
="shortcut icon" href
=").esc_url($favicon).qq(" type
="image/png" />\n);
5023 sub print_nav_breadcrumbs_path
{
5024 my $dirprefix = undef;
5025 while (my $part = shift) {
5026 $dirprefix .= "/" if defined $dirprefix;
5027 $dirprefix .= $part;
5028 print $cgi->a({-href
=> href
(project
=> undef,
5029 project_filter
=> $dirprefix,
5030 action
=> "project_list")},
5031 esc_html
($part)) . " / ";
5035 sub print_nav_breadcrumbs
{
5038 for my $crumb (@extra_breadcrumbs, [ $home_link_str => $home_link ]) {
5039 print $cgi->a({-href
=> esc_url
($crumb->[1])}, $crumb->[0]) . " / ";
5041 if (defined $project) {
5042 my @dirname = split '/', $project;
5043 my $projectbasename = pop @dirname;
5044 print_nav_breadcrumbs_path
(@dirname);
5045 print $cgi->a({-href
=> href
(action
=>"summary")}, esc_html
($projectbasename));
5046 if (defined $action) {
5047 my $action_print = $action ;
5048 $action_print = 'blame' if $action_print eq 'blame_incremental';
5049 if (defined $opts{-action_extra
}) {
5050 $action_print = $cgi->a({-href
=> href
(action
=>$action)},
5053 print " / $action_print";
5055 if (defined $opts{-action_extra
}) {
5056 print " / $opts{-action_extra}";
5059 } elsif (defined $project_filter) {
5060 print_nav_breadcrumbs_path
(split '/', $project_filter);
5064 sub print_search_form
{
5065 if (!defined $searchtext) {
5069 if (defined $hash_base) {
5070 $search_hash = $hash_base;
5071 } elsif (defined $hash) {
5072 $search_hash = $hash;
5074 $search_hash = "HEAD";
5076 # We can't use href() here because we need to encode the
5077 # URL parameters into the form, not into the action link.
5078 my $action = $my_uri;
5079 my $use_pathinfo = gitweb_check_feature
('pathinfo');
5080 if ($use_pathinfo) {
5081 # See notes about doubled / in href()
5083 $action .= "/".esc_path_info
($project);
5085 print $cgi->start_form(-method
=> "get", -action
=> $action) .
5086 "<div class=\"search\">\n" .
5088 $cgi->input({-name
=>"p", -value
=>$project, -type
=>"hidden"}) . "\n") .
5089 $cgi->input({-name
=>"a", -value
=>"search", -type
=>"hidden"}) . "\n" .
5090 $cgi->input({-name
=>"h", -value
=>$search_hash, -type
=>"hidden"}) . "\n" .
5091 $cgi->popup_menu(-name
=> 'st', -default => 'commit',
5092 -values => ['commit', 'grep', 'author', 'committer', 'pickaxe']) .
5093 $cgi->sup($cgi->a({-href
=> href
(action
=>"search_help"),
5094 -title
=> "search help" },
5095 "<span style=\"padding-bottom:1em\">? </span>")) . " search:\n",
5096 $cgi->textfield(-name
=> "s", -value
=> $searchtext, -override
=> 1) . "\n" .
5097 "<span title=\"Extended regular expression\">" .
5098 $cgi->checkbox(-name
=> 'sr', -value
=> 1, -label
=> 're',
5099 -checked
=> $search_use_regexp) .
5102 $cgi->end_form() . "\n";
5105 sub git_header_html
{
5106 my $status = shift || "200 OK";
5107 my $expires = shift;
5110 my $title = get_page_title
();
5111 my $content_type = get_content_type_html
();
5112 print $cgi->header(-type
=>$content_type, -charset
=> 'utf-8',
5113 -status
=> $status, -expires
=> $expires)
5114 unless ($opts{'-no_http_header'});
5115 my $mod_perl_version = $ENV{'MOD_PERL'} ?
" $ENV{'MOD_PERL'}" : '';
5117 <?xml version="1.0" encoding="utf-8"?>
5118 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
5119 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US" lang="en-US">
5120 <!-- git web interface version $version, (C) 2005-2006, Kay Sievers <kay.sievers\@vrfy.org>, Christian Gierke -->
5121 <!-- git core binaries version $git_version -->
5123 <meta http-equiv="content-type" content="$content_type; charset=utf-8"/>
5124 <meta name="generator" content="gitweb/$version git/$git_version$mod_perl_version"/>
5125 <meta name="robots" content="index, nofollow"/>
5126 <title>$title</title>
5127 <script type="text/javascript">/* <![CDATA[ */
5128 function fixBlameLinks() {
5129 var allLinks = document.getElementsByTagName("a");
5130 for (var i = 0; i < allLinks.length; i++) {
5131 var link = allLinks.item(i);
5132 if (link.className == 'blamelink')
5133 link.href = link.href.replace("/blame/", "/blame_incremental/");
5138 # the stylesheet, favicon etc urls won't work correctly with path_info
5139 # unless we set the appropriate base URL
5140 if ($ENV{'PATH_INFO'}) {
5141 print "<base href=\"".esc_url
($base_url)."\" />\n";
5143 print_header_links
($status);
5145 if (defined $site_html_head_string) {
5146 print to_utf8
($site_html_head_string);
5150 "<body><span class=\"body\">\n";
5152 if (defined $site_header && -f
$site_header) {
5153 insert_file
($site_header);
5156 print "<div class=\"page_header\">\n";
5157 print "<span class=\"logo-container\"><span class=\"logo-default\">";
5158 if (defined $logo) {
5159 print $cgi->a({-href
=> esc_url
($logo_url),
5160 -title
=> $logo_label,
5161 -class => "logo-link"},
5162 $cgi->img({-src
=> esc_url
($logo),
5163 -width
=> 72, -height
=> 27,
5165 -class => "logo"}));
5167 print "</span></span><span class=\"banner-container\">";
5168 print_nav_breadcrumbs
(%opts);
5169 print "</span></div>\n";
5171 my $have_search = gitweb_check_feature
('search');
5172 if (defined $project && $have_search) {
5173 print_search_form
();
5177 sub compute_timed_interval
{
5178 return "<?gitweb compute_timed_interval?>" if $cache_mode_active;
5179 return tv_interval
($t0, [ gettimeofday
() ]);
5182 sub compute_commands_count
{
5183 return "<?gitweb compute_commands_count?>" if $cache_mode_active;
5184 my $s = $number_of_git_cmds == 1 ?
'' : 's';
5185 return '<span id="generating_cmd">'.
5186 $number_of_git_cmds.
5187 "</span> git command$s";
5190 sub git_footer_html
{
5191 my $feed_class = 'rss_logo';
5193 print "<div class=\"page_footer\">\n";
5194 if (defined $project) {
5195 my $descr = git_get_project_description
($project);
5196 if (defined $descr) {
5197 print "<div class=\"page_footer_text\">" . esc_html
($descr) . "</div>\n";
5200 my %href_params = get_feed_info
();
5201 if (!%href_params) {
5202 $feed_class .= ' generic';
5204 $href_params{'-title'} ||= 'log';
5206 foreach my $format (qw(RSS Atom)) {
5207 $href_params{'action'} = lc($format);
5208 print $cgi->a({-href
=> href
(%href_params),
5209 -title
=> "$href_params{'-title'} $format feed",
5210 -class => $feed_class}, $format)."\n";
5214 print $cgi->a({-href
=> href
(project
=>undef, action
=>"opml",
5215 project_filter
=> $project_filter),
5216 -class => $feed_class}, "OPML") . " ";
5217 print $cgi->a({-href
=> href
(project
=>undef, action
=>"project_index",
5218 project_filter
=> $project_filter),
5219 -class => $feed_class}, "TXT") . "\n";
5221 print "</div>\n"; # class="page_footer"
5223 if (defined $t0 && gitweb_check_feature
('timed')) {
5224 print "<div id=\"generating_info\">\n";
5225 print 'This page took '.
5226 '<span id="generating_time" class="time_span">'.
5227 compute_timed_interval
().
5230 compute_commands_count
().
5232 print "</div>\n"; # class="page_footer"
5235 if (defined $site_footer && -f
$site_footer) {
5236 insert_file
($site_footer);
5239 print qq!<script type
="text/javascript" src
="!.esc_url($javascript).qq!"></script
>\n!;
5240 if (defined $action &&
5241 $action eq 'blame_incremental') {
5242 print qq!<script type
="text/javascript">\n!.
5243 qq!startBlame
("!. href(action=>"blame_data
", -replay=>1) .qq!",\n!.
5244 qq! "!. href() .qq!");\n!.
5247 my ($jstimezone, $tz_cookie, $datetime_class) =
5248 gitweb_get_feature
('javascript-timezone');
5250 print qq!<script type
="text/javascript">\n!.
5251 qq!window
.onload
= function
() {\n!;
5252 if (gitweb_check_feature
('blame_incremental')) {
5253 print qq! fixBlameLinks
();\n!;
5255 if (gitweb_check_feature
('javascript-actions')) {
5256 print qq! fixLinks
();\n!;
5258 if ($jstimezone && $tz_cookie && $datetime_class) {
5259 print qq! var tz_cookie
= { name
: '$tz_cookie', expires
: 14, path
: '/' };\n!. # in days
5260 qq! onloadTZSetup
('$jstimezone', tz_cookie
, '$datetime_class');\n!;
5266 print "</span></body>\n" .
5270 # die_error(<http_status_code>, <error_message>[, <detailed_html_description>])
5271 # Example: die_error(404, 'Hash not found')
5272 # By convention, use the following status codes (as defined in RFC 2616):
5273 # 400: Invalid or missing CGI parameters, or
5274 # requested object exists but has wrong type.
5275 # 403: Requested feature (like "pickaxe" or "snapshot") not enabled on
5276 # this server or project.
5277 # 404: Requested object/revision/project doesn't exist.
5278 # 500: The server isn't configured properly, or
5279 # an internal error occurred (e.g. failed assertions caused by bugs), or
5280 # an unknown error occurred (e.g. the git binary died unexpectedly).
5281 # 503: The server is currently unavailable (because it is overloaded,
5282 # or down for maintenance). Generally, this is a temporary state.
5284 my $status = shift || 500;
5285 my $error = esc_html
(shift) || "Internal Server Error";
5289 my %http_responses = (
5290 400 => '400 Bad Request',
5291 403 => '403 Forbidden',
5292 404 => '404 Not Found',
5293 500 => '500 Internal Server Error',
5294 503 => '503 Service Unavailable',
5296 git_header_html
($http_responses{$status}, undef, %opts);
5298 <div class="page_body">
5303 if (defined $extra) {
5311 unless ($opts{'-error_handler'});
5314 ## ----------------------------------------------------------------------
5315 ## functions printing or outputting HTML: navigation
5317 # $content is wrapped in a span with class 'tab'
5318 # If $selected is true it also has class 'selected'
5319 # If $disabled is true it also has class 'disabled'
5320 # Whether or not a tab can be disabled and selected at the same time
5321 # is up to the caller
5322 # If $extra_classes is non-empty, it is a whitespace-separated list of
5323 # additional class names to include
5324 # Note that $content MUST already be html-escaped as needed because
5325 # it is included verbatim. And so are any extra class names.
5327 my ($content, $selected, $disabled, $extra_classes) = @_;
5328 my @classes = ("tab");
5329 push(@classes, "selected") if $selected;
5330 push(@classes, "disabled") if $disabled;
5331 push(@classes, split(' ', $extra_classes)) if $extra_classes;
5332 return "<span class=\"" . join(" ", @classes) . "\">". $content . "</span>";
5335 sub git_print_page_nav
{
5336 my ($current, $suppress, $head, $treehead, $treebase, $extra) = @_;
5337 $extra = '' if !defined $extra; # pager or formats
5338 $extra = "<span class=\"page_nav_sub\">".$extra."</span>" if $extra ne '';
5340 my @navs = qw(summary log commit commitdiff tree refs);
5343 if (ref($suppress) eq 'ARRAY') {
5344 %omit = map { ($_ => 1) } @
$suppress;
5346 %omit = ($suppress => 1);
5348 @navs = grep { !$omit{$_} } @navs;
5351 my %arg = map { $_ => {action
=>$_} } @navs;
5352 if (defined $head) {
5353 for (qw(commit commitdiff)) {
5354 $arg{$_}{'hash'} = $head;
5356 if ($current =~ m/^(tree | log | shortlog | commit | commitdiff | search)$/x) {
5357 $arg{'log'}{'hash'} = $head;
5361 $arg{'log'}{'action'} = 'shortlog';
5362 if ($current eq 'log') {
5363 $current = 'shortlog';
5364 } elsif ($current eq 'shortlog') {
5367 $arg{'tree'}{'hash'} = $treehead if defined $treehead;
5368 $arg{'tree'}{'hash_base'} = $treebase if defined $treebase;
5370 my @actions = gitweb_get_feature
('actions');
5371 my $escname = $project;
5372 $escname =~ s/[+]/%2B/g;
5375 'n' => $project, # project name
5376 'f' => $git_dir, # project path within filesystem
5377 'h' => $treehead || '', # current hash ('h' parameter)
5378 'b' => $treebase || '', # hash base ('hb' parameter)
5379 'e' => $escname, # project name with '+' escaped
5382 my ($label, $link, $pos) = splice(@actions,0,3);
5384 @navs = map { $_ eq $pos ?
($_, $label) : $_ } @navs;
5386 $link =~ s/%([%nfhbe])/$repl{$1}/g;
5387 $arg{$label}{'_href'} = $link;
5390 print "<div class=\"page_nav\">\n" .
5392 map { $_ eq $current ?
5394 tabspan
($cgi->a({-href
=> ($arg{$_}{_href
} ?
$arg{$_}{_href
} : href
(%{$arg{$_}}))}, "$_"))
5396 print "<br/>\n$extra<br/>\n" .
5400 # returns a submenu for the nagivation of the refs views (tags, heads,
5401 # remotes) with the current view disabled and the remotes view only
5402 # available if the feature is enabled
5403 sub format_ref_views
{
5405 my @ref_views = qw{tags heads
};
5406 push @ref_views, 'remotes' if gitweb_check_feature
('remote_heads');
5407 return join $barsep, map {
5408 $_ eq $current ? tabspan
($_, 1) :
5409 tabspan
($cgi->a({-href
=> href
(action
=>$_)}, $_))
5413 sub format_paging_nav
{
5414 my ($action, $page, $has_next_link) = @_;
5415 my $paging_nav = "<span class=\"paging_nav\">";
5418 $paging_nav .= tabspan
(
5419 $cgi->a({-href
=> href
(-replay
=>1, page
=>undef)}, "first")) .
5421 $cgi->a({-href
=> href
(-replay
=>1, page
=>$page-1),
5422 -accesskey
=> "p", -title
=> "Alt-p"}, "prev"));
5424 $paging_nav .= tabspan
("first", 1).${mdotsep
}.tabspan
("prev", 0, 1);
5427 if ($has_next_link) {
5428 $paging_nav .= $mdotsep . tabspan
(
5429 $cgi->a({-href
=> href
(-replay
=>1, page
=>$page+1),
5430 -accesskey
=> "n", -title
=> "Alt-n"}, "next"));
5432 $paging_nav .= ${mdotsep
}.tabspan
("next", 0, 1);
5435 return $paging_nav."</span>";
5438 sub format_log_nav
{
5439 my ($action, $page, $has_next_link, $extra) = @_;
5441 defined $extra or $extra = '';
5442 $extra eq '' or $extra .= $barsep;
5444 if ($action eq 'shortlog') {
5445 $paging_nav .= tabspan
('shortlog', 1);
5447 $paging_nav .= tabspan
($cgi->a({-href
=> href
(action
=>'shortlog', -replay
=>1)}, 'shortlog'));
5449 $paging_nav .= $barsep;
5450 if ($action eq 'log') {
5451 $paging_nav .= tabspan
('fulllog', 1);
5453 $paging_nav .= tabspan
($cgi->a({-href
=> href
(action
=>'log', -replay
=>1)}, 'fulllog'));
5456 $paging_nav .= $barsep . $extra . format_paging_nav
($action, $page, $has_next_link);
5460 ## ......................................................................
5461 ## functions printing or outputting HTML: div
5463 sub git_print_header_div
{
5464 my ($action, $title, $hash, $hash_base, $extra) = @_;
5466 defined $extra or $extra = '';
5468 $args{'action'} = $action;
5469 $args{'hash'} = $hash if $hash;
5470 $args{'hash_base'} = $hash_base if $hash_base;
5472 my $link1 = $cgi->a({-href
=> href
(%args), -class => "title"},
5473 $title ?
$title : $action);
5474 my $link2 = $cgi->a({-href
=> href
(%args), -class => "cover"}, "");
5475 print "<div class=\"header\">\n" . '<span class="title">' .
5476 $link1 . $extra . $link2 . '</span>' . "\n</div>\n";
5479 sub format_repo_url
{
5480 my ($name, $url) = @_;
5481 return "<tr class=\"metadata_url\"><td>$name</td><td>$url</td></tr>\n";
5484 # Group output by placing it in a DIV element and adding a header.
5485 # Options for start_div() can be provided by passing a hash reference as the
5486 # first parameter to the function.
5487 # Options to git_print_header_div() can be provided by passing an array
5488 # reference. This must follow the options to start_div if they are present.
5489 # The content can be a scalar, which is output as-is, a scalar reference, which
5490 # is output after html escaping, an IO handle passed either as *handle or
5491 # *handle{IO}, or a function reference. In the latter case all following
5492 # parameters will be taken as argument to the content function call.
5493 sub git_print_section
{
5494 my ($div_args, $header_args, $content);
5496 if (ref($arg) eq 'HASH') {
5500 if (ref($arg) eq 'ARRAY') {
5501 $header_args = $arg;
5506 print $cgi->start_div($div_args);
5507 git_print_header_div
(@
$header_args);
5509 if (ref($content) eq 'CODE') {
5511 } elsif (ref($content) eq 'SCALAR') {
5512 print esc_html
($$content);
5513 } elsif (ref($content) eq 'GLOB' or ref($content) eq 'IO::Handle') {
5514 while (<$content>) {
5517 } elsif (!ref($content) && defined($content)) {
5521 print $cgi->end_div;
5524 sub format_timestamp_html
{
5526 my $useatnight = shift;
5527 defined($useatnight) or $useatnight = 1;
5528 my $strtime = $date->{'rfc2822'};
5530 my (undef, undef, $datetime_class) =
5531 gitweb_get_feature
('javascript-timezone');
5532 if ($datetime_class) {
5533 $strtime = qq!<span
class="$datetime_class">$strtime</span
>!;
5536 my $localtime_format = '(%d %02d:%02d %s)';
5537 if ($useatnight && $date->{'hour_local'} < 6) {
5538 $localtime_format = '(%d <span class="atnight">%02d:%02d</span> %s)';
5541 sprintf($localtime_format, $date->{'mday_local'},
5542 $date->{'hour_local'}, $date->{'minute_local'}, $date->{'tz_local'});
5547 sub format_lastrefresh_row
{
5548 return "<?gitweb format_lastrefresh_row?>" if $cache_mode_active;
5549 my %rd = parse_file_date
('.last_refresh');
5550 if (defined $rd{'rfc2822'}) {
5551 return "<tr id=\"metadata_lrefresh\"><td>last refresh</td>" .
5552 "<td>".format_timestamp_html
(\
%rd,0)."</td></tr>";
5557 # Outputs the author name and date in long form
5558 sub git_print_authorship
{
5561 my $tag = $opts{-tag
} || 'div';
5562 my $author = $co->{'author_name'};
5564 my %ad = parse_date
($co->{'author_epoch'}, $co->{'author_tz'});
5565 print "<$tag class=\"author_date\">" .
5566 format_search_author
($author, "author", esc_html
($author)) .
5567 " [".format_timestamp_html
(\
%ad)."]".
5568 git_get_avatar
($co->{'author_email'}, -pad_before
=> 1) .
5572 # Outputs table rows containing the full author or committer information,
5573 # in the format expected for 'commit' view (& similar).
5574 # Parameters are a commit hash reference, followed by the list of people
5575 # to output information for. If the list is empty it defaults to both
5576 # author and committer.
5577 sub git_print_authorship_rows
{
5579 # too bad we can't use @people = @_ || ('author', 'committer')
5581 @people = ('author', 'committer') unless @people;
5582 foreach my $who (@people) {
5583 my %wd = parse_date
($co->{"${who}_epoch"}, $co->{"${who}_tz"});
5584 print "<tr><td>$who</td><td>" .
5585 format_search_author
($co->{"${who}_name"}, $who,
5586 esc_html
($co->{"${who}_name"})) . " " .
5587 format_search_author
($co->{"${who}_email"}, $who,
5588 esc_html
("<" . $co->{"${who}_email"} . ">")) .
5589 "</td><td rowspan=\"2\">" .
5590 git_get_avatar
($co->{"${who}_email"}, -size
=> 'double') .
5594 format_timestamp_html
(\
%wd) .
5600 sub git_print_page_path
{
5606 print "<div class=\"page_path\">";
5607 print $cgi->a({-href
=> href
(action
=>"tree", hash_base
=>$hb),
5608 -title
=> 'tree root'}, to_utf8
("[$project]"));
5610 if (defined $name) {
5611 my @dirname = split '/', $name;
5612 my $basename = pop @dirname;
5615 foreach my $dir (@dirname) {
5616 $fullname .= ($fullname ?
'/' : '') . $dir;
5617 print $cgi->a({-href
=> href
(action
=>"tree", file_name
=>$fullname,
5619 -title
=> $fullname}, esc_path
($dir));
5622 if (defined $type && $type eq 'blob') {
5623 print $cgi->a({-href
=> href
(action
=>"blob_plain", file_name
=>$file_name,
5625 -title
=> $name}, esc_path
($basename));
5626 } elsif (defined $type && $type eq 'tree') {
5627 print $cgi->a({-href
=> href
(action
=>"tree", file_name
=>$file_name,
5629 -title
=> $name}, esc_path
($basename));
5632 print esc_path
($basename);
5635 print "<br/></div>\n";
5642 if ($opts{'-remove_title'}) {
5643 # remove title, i.e. first line of log
5646 # remove leading empty lines
5647 while (defined $log->[0] && $log->[0] eq "") {
5652 my $skip_blank_line = 0;
5653 foreach my $line (@
$log) {
5654 if ($line =~ m/^\s*([A-Z][-A-Za-z]*-[Bb]y|C[Cc]): /) {
5655 if (! $opts{'-remove_signoff'}) {
5656 print "<span class=\"signoff\">" . esc_html
($line) . "</span><br/>\n";
5657 $skip_blank_line = 1;
5662 if ($line =~ m
,\s
*([a
-z
]*link): (https?
://\S
+),i
) {
5663 if (! $opts{'-remove_signoff'}) {
5664 print "<span class=\"signoff\">" . esc_html
($1) . ": " .
5665 "<a href=\"" . esc_html
($2) . "\">" . esc_html
($2) . "</a>" .
5667 $skip_blank_line = 1;
5672 # print only one empty line
5673 # do not print empty line after signoff
5675 next if ($skip_blank_line);
5676 $skip_blank_line = 1;
5678 $skip_blank_line = 0;
5681 print format_log_line_html
($line) . "<br/>\n";
5684 if ($opts{'-final_empty_line'}) {
5685 # end with single empty line
5686 print "<br/>\n" unless $skip_blank_line;
5690 # return link target (what link points to)
5691 sub git_get_link_target
{
5696 defined(my $fd = git_cmd_pipe
"cat-file", "blob", $hash)
5700 $link_target = to_utf8
(scalar <$fd>);
5705 return $link_target;
5708 # given link target, and the directory (basedir) the link is in,
5709 # return target of link relative to top directory (top tree);
5710 # return undef if it is not possible (including absolute links).
5711 sub normalize_link_target
{
5712 my ($link_target, $basedir) = @_;
5714 # absolute symlinks (beginning with '/') cannot be normalized
5715 return if (substr($link_target, 0, 1) eq '/');
5717 # normalize link target to path from top (root) tree (dir)
5720 $path = $basedir . '/' . $link_target;
5722 # we are in top (root) tree (dir)
5723 $path = $link_target;
5726 # remove //, /./, and /../
5728 foreach my $part (split('/', $path)) {
5729 # discard '.' and ''
5730 next if (!$part || $part eq '.');
5732 if ($part eq '..') {
5736 # link leads outside repository (outside top dir)
5740 push @path_parts, $part;
5743 $path = join('/', @path_parts);
5748 # print tree entry (row of git_tree), but without encompassing <tr> element
5749 sub git_print_tree_entry
{
5750 my ($t, $basedir, $hash_base, $have_blame) = @_;
5753 $base_key{'hash_base'} = $hash_base if defined $hash_base;
5755 # The format of a table row is: mode list link. Where mode is
5756 # the mode of the entry, list is the name of the entry, an href,
5757 # and link is the action links of the entry.
5759 print "<td class=\"mode\">" . mode_str
($t->{'mode'}) . "</td>\n";
5760 if (exists $t->{'size'}) {
5761 print "<td class=\"size\">$t->{'size'}</td>\n";
5763 if ($t->{'type'} eq "blob") {
5764 print "<td class=\"list\">" .
5765 $cgi->a({-href
=> href
(action
=>"blob", hash
=>$t->{'hash'},
5766 file_name
=>"$basedir$t->{'name'}", %base_key),
5767 -class => "list"}, esc_path
($t->{'name'}));
5768 if (S_ISLNK
(oct $t->{'mode'})) {
5769 my $link_target = git_get_link_target
($t->{'hash'});
5771 my $norm_target = normalize_link_target
($link_target, $basedir);
5772 if (defined $norm_target) {
5774 $cgi->a({-href
=> href
(action
=>"object", hash_base
=>$hash_base,
5775 file_name
=>$norm_target),
5776 -title
=> $norm_target}, esc_path
($link_target));
5778 print " -> " . esc_path
($link_target);
5783 print "<td class=\"link\">";
5784 print $cgi->a({-href
=> href
(action
=>"blob", hash
=>$t->{'hash'},
5785 file_name
=>"$basedir$t->{'name'}", %base_key)},
5789 $cgi->a({-href
=> href
(action
=>"blame", hash
=>$t->{'hash'},
5790 file_name
=>"$basedir$t->{'name'}", %base_key),
5791 -class => "blamelink"},
5794 if (defined $hash_base) {
5796 $cgi->a({-href
=> href
(action
=>"history", hash_base
=>$hash_base,
5797 hash
=>$t->{'hash'}, file_name
=>"$basedir$t->{'name'}")},
5801 $cgi->a({-href
=> href
(action
=>"blob_plain", hash_base
=>$hash_base,
5802 file_name
=>"$basedir$t->{'name'}")},
5806 } elsif ($t->{'type'} eq "tree") {
5807 print "<td class=\"list\">";
5808 print $cgi->a({-href
=> href
(action
=>"tree", hash
=>$t->{'hash'},
5809 file_name
=>"$basedir$t->{'name'}",
5811 esc_path
($t->{'name'}));
5813 print "<td class=\"link\">";
5814 print $cgi->a({-href
=> href
(action
=>"tree", hash
=>$t->{'hash'},
5815 file_name
=>"$basedir$t->{'name'}",
5818 if (defined $hash_base) {
5820 $cgi->a({-href
=> href
(action
=>"history", hash_base
=>$hash_base,
5821 file_name
=>"$basedir$t->{'name'}")},
5826 # unknown object: we can only present history for it
5827 # (this includes 'commit' object, i.e. submodule support)
5828 print "<td class=\"list\">" .
5829 esc_path
($t->{'name'}) .
5831 print "<td class=\"link\">";
5832 if (defined $hash_base) {
5833 print $cgi->a({-href
=> href
(action
=>"history",
5834 hash_base
=>$hash_base,
5835 file_name
=>"$basedir$t->{'name'}")},
5842 ## ......................................................................
5843 ## functions printing large fragments of HTML
5845 # get pre-image filenames for merge (combined) diff
5846 sub fill_from_file_info
{
5847 my ($diff, @parents) = @_;
5849 $diff->{'from_file'} = [ ];
5850 $diff->{'from_file'}[$diff->{'nparents'} - 1] = undef;
5851 for (my $i = 0; $i < $diff->{'nparents'}; $i++) {
5852 if ($diff->{'status'}[$i] eq 'R' ||
5853 $diff->{'status'}[$i] eq 'C') {
5854 $diff->{'from_file'}[$i] =
5855 git_get_path_by_hash
($parents[$i], $diff->{'from_id'}[$i]);
5862 # is current raw difftree line of file deletion
5864 my $diffinfo = shift;
5866 return $diffinfo->{'to_id'} eq ('0' x
40);
5869 # does patch correspond to [previous] difftree raw line
5870 # $diffinfo - hashref of parsed raw diff format
5871 # $patchinfo - hashref of parsed patch diff format
5872 # (the same keys as in $diffinfo)
5873 sub is_patch_split
{
5874 my ($diffinfo, $patchinfo) = @_;
5876 return defined $diffinfo && defined $patchinfo
5877 && $diffinfo->{'to_file'} eq $patchinfo->{'to_file'};
5881 sub git_difftree_body
{
5882 my ($difftree, $hash, @parents) = @_;
5883 my ($parent) = $parents[0];
5884 my $have_blame = gitweb_check_feature
('blame');
5885 print "<div class=\"list_head\">\n";
5886 if ($#{$difftree} > 10) {
5887 print(($#{$difftree} + 1) . " files changed:\n");
5891 print "<table class=\"" .
5892 (@parents > 1 ?
"combined " : "") .
5895 # header only for combined diff in 'commitdiff' view
5896 my $has_header = @
$difftree && @parents > 1 && $action eq 'commitdiff';
5899 print "<thead><tr>\n" .
5900 "<th></th><th></th>\n"; # filename, patchN link
5901 for (my $i = 0; $i < @parents; $i++) {
5902 my $par = $parents[$i];
5904 $cgi->a({-href
=> href
(action
=>"commitdiff",
5905 hash
=>$hash, hash_parent
=>$par),
5906 -title
=> 'commitdiff to parent number ' .
5907 ($i+1) . ': ' . substr($par,0,7)},
5911 print "</tr></thead>\n<tbody>\n";
5916 foreach my $line (@
{$difftree}) {
5917 my $diff = parsed_difftree_line
($line);
5920 print "<tr class=\"dark\">\n";
5922 print "<tr class=\"light\">\n";
5926 if (exists $diff->{'nparents'}) { # combined diff
5928 fill_from_file_info
($diff, @parents)
5929 unless exists $diff->{'from_file'};
5931 if (!is_deleted
($diff)) {
5932 # file exists in the result (child) commit
5934 $cgi->a({-href
=> href
(action
=>"blob", hash
=>$diff->{'to_id'},
5935 file_name
=>$diff->{'to_file'},
5937 -class => "list"}, esc_path
($diff->{'to_file'})) .
5941 esc_path
($diff->{'to_file'}) .
5945 if ($action eq 'commitdiff') {
5948 print "<td class=\"link\">" .
5949 $cgi->a({-href
=> href
(-anchor
=>"patch$patchno")},
5955 my $has_history = 0;
5956 my $not_deleted = 0;
5957 for (my $i = 0; $i < $diff->{'nparents'}; $i++) {
5958 my $hash_parent = $parents[$i];
5959 my $from_hash = $diff->{'from_id'}[$i];
5960 my $from_path = $diff->{'from_file'}[$i];
5961 my $status = $diff->{'status'}[$i];
5963 $has_history ||= ($status ne 'A');
5964 $not_deleted ||= ($status ne 'D');
5966 if ($status eq 'A') {
5967 print "<td class=\"link\" align=\"right\">$barsep</td>\n";
5968 } elsif ($status eq 'D') {
5969 print "<td class=\"link\">" .
5970 $cgi->a({-href
=> href
(action
=>"blob",
5973 file_name
=>$from_path)},
5977 if ($diff->{'to_id'} eq $from_hash) {
5978 print "<td class=\"link nochange\">";
5980 print "<td class=\"link\">";
5982 print $cgi->a({-href
=> href
(action
=>"blobdiff",
5983 hash
=>$diff->{'to_id'},
5984 hash_parent
=>$from_hash,
5986 hash_parent_base
=>$hash_parent,
5987 file_name
=>$diff->{'to_file'},
5988 file_parent
=>$from_path)},
5994 print "<td class=\"link\">";
5996 print $cgi->a({-href
=> href
(action
=>"blob",
5997 hash
=>$diff->{'to_id'},
5998 file_name
=>$diff->{'to_file'},
6001 print $barsep if ($has_history);
6004 print $cgi->a({-href
=> href
(action
=>"history",
6005 file_name
=>$diff->{'to_file'},
6012 next; # instead of 'else' clause, to avoid extra indent
6014 # else ordinary diff
6016 my ($to_mode_oct, $to_mode_str, $to_file_type);
6017 my ($from_mode_oct, $from_mode_str, $from_file_type);
6018 if ($diff->{'to_mode'} ne ('0' x
6)) {
6019 $to_mode_oct = oct $diff->{'to_mode'};
6020 if (S_ISREG
($to_mode_oct)) { # only for regular file
6021 $to_mode_str = sprintf("%04o", $to_mode_oct & 0777); # permission bits
6023 $to_file_type = file_type
($diff->{'to_mode'});
6025 if ($diff->{'from_mode'} ne ('0' x
6)) {
6026 $from_mode_oct = oct $diff->{'from_mode'};
6027 if (S_ISREG
($from_mode_oct)) { # only for regular file
6028 $from_mode_str = sprintf("%04o", $from_mode_oct & 0777); # permission bits
6030 $from_file_type = file_type
($diff->{'from_mode'});
6033 if ($diff->{'status'} eq "A") { # created
6034 my $mode_chng = "<span class=\"file_status new\">[new $to_file_type";
6035 $mode_chng .= " with mode: $to_mode_str" if $to_mode_str;
6036 $mode_chng .= "]</span>";
6038 print $cgi->a({-href
=> href
(action
=>"blob", hash
=>$diff->{'to_id'},
6039 hash_base
=>$hash, file_name
=>$diff->{'file'}),
6040 -class => "list"}, esc_path
($diff->{'file'}));
6042 print "<td>$mode_chng</td>\n";
6043 print "<td class=\"link\">";
6044 if ($action eq 'commitdiff') {
6047 print $cgi->a({-href
=> href
(-anchor
=>"patch$patchno")},
6051 print $cgi->a({-href
=> href
(action
=>"blob", hash
=>$diff->{'to_id'},
6052 hash_base
=>$hash, file_name
=>$diff->{'file'})},
6056 } elsif ($diff->{'status'} eq "D") { # deleted
6057 my $mode_chng = "<span class=\"file_status deleted\">[deleted $from_file_type]</span>";
6059 print $cgi->a({-href
=> href
(action
=>"blob", hash
=>$diff->{'from_id'},
6060 hash_base
=>$parent, file_name
=>$diff->{'file'}),
6061 -class => "list"}, esc_path
($diff->{'file'}));
6063 print "<td>$mode_chng</td>\n";
6064 print "<td class=\"link\">";
6065 if ($action eq 'commitdiff') {
6068 print $cgi->a({-href
=> href
(-anchor
=>"patch$patchno")},
6072 print $cgi->a({-href
=> href
(action
=>"blob", hash
=>$diff->{'from_id'},
6073 hash_base
=>$parent, file_name
=>$diff->{'file'})},
6076 print $cgi->a({-href
=> href
(action
=>"blame", hash_base
=>$parent,
6077 file_name
=>$diff->{'file'}),
6078 -class => "blamelink"},
6081 print $cgi->a({-href
=> href
(action
=>"history", hash_base
=>$parent,
6082 file_name
=>$diff->{'file'})},
6086 } elsif ($diff->{'status'} eq "M" || $diff->{'status'} eq "T") { # modified, or type changed
6087 my $mode_chnge = "";
6088 if ($diff->{'from_mode'} != $diff->{'to_mode'}) {
6089 $mode_chnge = "<span class=\"file_status mode_chnge\">[changed";
6090 if ($from_file_type ne $to_file_type) {
6091 $mode_chnge .= " from $from_file_type to $to_file_type";
6093 if (($from_mode_oct & 0777) != ($to_mode_oct & 0777)) {
6094 if ($from_mode_str && $to_mode_str) {
6095 $mode_chnge .= " mode: $from_mode_str->$to_mode_str";
6096 } elsif ($to_mode_str) {
6097 $mode_chnge .= " mode: $to_mode_str";
6100 $mode_chnge .= "]</span>\n";
6103 print $cgi->a({-href
=> href
(action
=>"blob", hash
=>$diff->{'to_id'},
6104 hash_base
=>$hash, file_name
=>$diff->{'file'}),
6105 -class => "list"}, esc_path
($diff->{'file'}));
6107 print "<td>$mode_chnge</td>\n";
6108 print "<td class=\"link\">";
6109 if ($action eq 'commitdiff') {
6112 print $cgi->a({-href
=> href
(-anchor
=>"patch$patchno")},
6115 } elsif ($diff->{'to_id'} ne $diff->{'from_id'}) {
6116 # "commit" view and modified file (not onlu mode changed)
6117 print $cgi->a({-href
=> href
(action
=>"blobdiff",
6118 hash
=>$diff->{'to_id'}, hash_parent
=>$diff->{'from_id'},
6119 hash_base
=>$hash, hash_parent_base
=>$parent,
6120 file_name
=>$diff->{'file'})},
6124 print $cgi->a({-href
=> href
(action
=>"blob", hash
=>$diff->{'to_id'},
6125 hash_base
=>$hash, file_name
=>$diff->{'file'})},
6128 print $cgi->a({-href
=> href
(action
=>"blame", hash_base
=>$hash,
6129 file_name
=>$diff->{'file'}),
6130 -class => "blamelink"},
6133 print $cgi->a({-href
=> href
(action
=>"history", hash_base
=>$hash,
6134 file_name
=>$diff->{'file'})},
6138 } elsif ($diff->{'status'} eq "R" || $diff->{'status'} eq "C") { # renamed or copied
6139 my %status_name = ('R' => 'moved', 'C' => 'copied');
6140 my $nstatus = $status_name{$diff->{'status'}};
6142 if ($diff->{'from_mode'} != $diff->{'to_mode'}) {
6143 # mode also for directories, so we cannot use $to_mode_str
6144 $mode_chng = sprintf(", mode: %04o", $to_mode_oct & 0777);
6147 $cgi->a({-href
=> href
(action
=>"blob", hash_base
=>$hash,
6148 hash
=>$diff->{'to_id'}, file_name
=>$diff->{'to_file'}),
6149 -class => "list"}, esc_path
($diff->{'to_file'})) . "</td>\n" .
6150 "<td><span class=\"file_status $nstatus\">[$nstatus from " .
6151 $cgi->a({-href
=> href
(action
=>"blob", hash_base
=>$parent,
6152 hash
=>$diff->{'from_id'}, file_name
=>$diff->{'from_file'}),
6153 -class => "list"}, esc_path
($diff->{'from_file'})) .
6154 " with " . (int $diff->{'similarity'}) . "% similarity$mode_chng]</span></td>\n" .
6155 "<td class=\"link\">";
6156 if ($action eq 'commitdiff') {
6159 print $cgi->a({-href
=> href
(-anchor
=>"patch$patchno")},
6162 } elsif ($diff->{'to_id'} ne $diff->{'from_id'}) {
6163 # "commit" view and modified file (not only pure rename or copy)
6164 print $cgi->a({-href
=> href
(action
=>"blobdiff",
6165 hash
=>$diff->{'to_id'}, hash_parent
=>$diff->{'from_id'},
6166 hash_base
=>$hash, hash_parent_base
=>$parent,
6167 file_name
=>$diff->{'to_file'}, file_parent
=>$diff->{'from_file'})},
6171 print $cgi->a({-href
=> href
(action
=>"blob", hash
=>$diff->{'to_id'},
6172 hash_base
=>$parent, file_name
=>$diff->{'to_file'})},
6175 print $cgi->a({-href
=> href
(action
=>"blame", hash_base
=>$hash,
6176 file_name
=>$diff->{'to_file'}),
6177 -class => "blamelink"},
6180 print $cgi->a({-href
=> href
(action
=>"history", hash_base
=>$hash,
6181 file_name
=>$diff->{'to_file'})},
6185 } # we should not encounter Unmerged (U) or Unknown (X) status
6188 print "</tbody>" if $has_header;
6192 # Print context lines and then rem/add lines in a side-by-side manner.
6193 sub print_sidebyside_diff_lines
{
6194 my ($ctx, $rem, $add) = @_;
6196 # print context block before add/rem block
6199 '<div class="chunk_block ctx">',
6200 '<div class="old">',
6203 '<div class="new">',
6212 '<div class="chunk_block rem">',
6213 '<div class="old">',
6220 '<div class="chunk_block add">',
6221 '<div class="new">',
6227 '<div class="chunk_block chg">',
6228 '<div class="old">',
6231 '<div class="new">',
6238 # Print context lines and then rem/add lines in inline manner.
6239 sub print_inline_diff_lines
{
6240 my ($ctx, $rem, $add) = @_;
6242 print @
$ctx, @
$rem, @
$add;
6245 # Format removed and added line, mark changed part and HTML-format them.
6246 # Implementation is based on contrib/diff-highlight
6247 sub format_rem_add_lines_pair
{
6248 my ($rem, $add, $num_parents) = @_;
6250 # We need to untabify lines before split()'ing them;
6251 # otherwise offsets would be invalid.
6254 $rem = untabify
($rem);
6255 $add = untabify
($add);
6257 my @rem = split(//, $rem);
6258 my @add = split(//, $add);
6259 my ($esc_rem, $esc_add);
6260 # Ignore leading +/- characters for each parent.
6261 my ($prefix_len, $suffix_len) = ($num_parents, 0);
6262 my ($prefix_has_nonspace, $suffix_has_nonspace);
6264 my $shorter = (@rem < @add) ?
@rem : @add;
6265 while ($prefix_len < $shorter) {
6266 last if ($rem[$prefix_len] ne $add[$prefix_len]);
6268 $prefix_has_nonspace = 1 if ($rem[$prefix_len] !~ /\s/);
6272 while ($prefix_len + $suffix_len < $shorter) {
6273 last if ($rem[-1 - $suffix_len] ne $add[-1 - $suffix_len]);
6275 $suffix_has_nonspace = 1 if ($rem[-1 - $suffix_len] !~ /\s/);
6279 # Mark lines that are different from each other, but have some common
6280 # part that isn't whitespace. If lines are completely different, don't
6281 # mark them because that would make output unreadable, especially if
6282 # diff consists of multiple lines.
6283 if ($prefix_has_nonspace || $suffix_has_nonspace) {
6284 $esc_rem = esc_html_hl_regions
($rem, 'marked',
6285 [$prefix_len, @rem - $suffix_len], -nbsp
=>1);
6286 $esc_add = esc_html_hl_regions
($add, 'marked',
6287 [$prefix_len, @add - $suffix_len], -nbsp
=>1);
6289 $esc_rem = esc_html
($rem, -nbsp
=>1);
6290 $esc_add = esc_html
($add, -nbsp
=>1);
6293 return format_diff_line
(\
$esc_rem, 'rem'),
6294 format_diff_line
(\
$esc_add, 'add');
6297 # HTML-format diff context, removed and added lines.
6298 sub format_ctx_rem_add_lines
{
6299 my ($ctx, $rem, $add, $num_parents) = @_;
6300 my (@new_ctx, @new_rem, @new_add);
6301 my $can_highlight = 0;
6302 my $is_combined = ($num_parents > 1);
6304 # Highlight if every removed line has a corresponding added line.
6305 if (@
$add > 0 && @
$add == @
$rem) {
6308 # Highlight lines in combined diff only if the chunk contains
6309 # diff between the same version, e.g.
6316 # Otherwise the highlightling would be confusing.
6318 for (my $i = 0; $i < @
$add; $i++) {
6319 my $prefix_rem = substr($rem->[$i], 0, $num_parents);
6320 my $prefix_add = substr($add->[$i], 0, $num_parents);
6322 $prefix_rem =~ s/-/+/g;
6324 if ($prefix_rem ne $prefix_add) {
6332 if ($can_highlight) {
6333 for (my $i = 0; $i < @
$add; $i++) {
6334 my ($line_rem, $line_add) = format_rem_add_lines_pair
(
6335 $rem->[$i], $add->[$i], $num_parents);
6336 push @new_rem, $line_rem;
6337 push @new_add, $line_add;
6340 @new_rem = map { format_diff_line
($_, 'rem') } @
$rem;
6341 @new_add = map { format_diff_line
($_, 'add') } @
$add;
6344 @new_ctx = map { format_diff_line
($_, 'ctx') } @
$ctx;
6346 return (\
@new_ctx, \
@new_rem, \
@new_add);
6349 # Print context lines and then rem/add lines.
6350 sub print_diff_lines
{
6351 my ($ctx, $rem, $add, $diff_style, $num_parents) = @_;
6352 my $is_combined = $num_parents > 1;
6354 ($ctx, $rem, $add) = format_ctx_rem_add_lines
($ctx, $rem, $add,
6357 if ($diff_style eq 'sidebyside' && !$is_combined) {
6358 print_sidebyside_diff_lines
($ctx, $rem, $add);
6360 # default 'inline' style and unknown styles
6361 print_inline_diff_lines
($ctx, $rem, $add);
6365 sub print_diff_chunk
{
6366 my ($diff_style, $num_parents, $from, $to, @chunk) = @_;
6367 my (@ctx, @rem, @add);
6369 # The class of the previous line.
6370 my $prev_class = '';
6372 return unless @chunk;
6374 # incomplete last line might be among removed or added lines,
6375 # or both, or among context lines: find which
6376 for (my $i = 1; $i < @chunk; $i++) {
6377 if ($chunk[$i][0] eq 'incomplete') {
6378 $chunk[$i][0] = $chunk[$i-1][0];
6383 push @chunk, ["", ""];
6385 foreach my $line_info (@chunk) {
6386 my ($class, $line) = @
$line_info;
6388 # print chunk headers
6389 if ($class && $class eq 'chunk_header') {
6390 print format_diff_line
($line, $class, $from, $to);
6394 ## print from accumulator when have some add/rem lines or end
6395 # of chunk (flush context lines), or when have add and rem
6396 # lines and new block is reached (otherwise add/rem lines could
6398 if (!$class || ((@rem || @add) && $class eq 'ctx') ||
6399 (@rem && @add && $class ne $prev_class)) {
6400 print_diff_lines
(\
@ctx, \
@rem, \
@add,
6401 $diff_style, $num_parents);
6402 @ctx = @rem = @add = ();
6405 ## adding lines to accumulator
6408 # rem, add or change
6409 if ($class eq 'rem') {
6411 } elsif ($class eq 'add') {
6415 if ($class eq 'ctx') {
6419 $prev_class = $class;
6423 sub git_patchset_body
{
6424 my ($fd, $diff_style, $difftree, $hash, @hash_parents) = @_;
6425 my ($hash_parent) = $hash_parents[0];
6427 my $is_combined = (@hash_parents > 1);
6429 my $patch_number = 0;
6434 my @chunk; # for side-by-side diff
6436 print "<div class=\"patchset\">\n";
6438 # skip to first patch
6439 while ($patch_line = to_utf8
(scalar <$fd>)) {
6442 last if ($patch_line =~ m/^diff /);
6446 while ($patch_line) {
6448 # parse "git diff" header line
6449 if ($patch_line =~ m/^diff --git (\"(?:[^\\\"]*(?:\\.[^\\\"]*)*)\"|[^ "]*) (.*)$/) {
6450 # $1 is from_name, which we do not use
6451 $to_name = unquote
($2);
6452 $to_name =~ s!^b/!!;
6453 } elsif ($patch_line =~ m/^diff --(cc|combined) ("?.*"?)$/) {
6454 # $1 is 'cc' or 'combined', which we do not use
6455 $to_name = unquote
($2);
6460 # check if current patch belong to current raw line
6461 # and parse raw git-diff line if needed
6462 if (is_patch_split
($diffinfo, { 'to_file' => $to_name })) {
6463 # this is continuation of a split patch
6464 print "<div class=\"patch cont\">\n";
6466 # advance raw git-diff output if needed
6467 $patch_idx++ if defined $diffinfo;
6469 # read and prepare patch information
6470 $diffinfo = parsed_difftree_line
($difftree->[$patch_idx]);
6472 # compact combined diff output can have some patches skipped
6473 # find which patch (using pathname of result) we are at now;
6475 while ($to_name ne $diffinfo->{'to_file'}) {
6476 print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n" .
6477 format_diff_cc_simplified
($diffinfo, @hash_parents) .
6478 "</div>\n"; # class="patch"
6483 last if $patch_idx > $#$difftree;
6484 $diffinfo = parsed_difftree_line
($difftree->[$patch_idx]);
6488 # modifies %from, %to hashes
6489 parse_from_to_diffinfo
($diffinfo, \
%from, \
%to, @hash_parents);
6491 # this is first patch for raw difftree line with $patch_idx index
6492 # we index @$difftree array from 0, but number patches from 1
6493 print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n";
6497 #assert($patch_line =~ m/^diff /) if DEBUG;
6498 #assert($patch_line !~ m!$/$!) if DEBUG; # is chomp-ed
6500 # print "git diff" header
6501 print format_git_diff_header_line
($patch_line, $diffinfo,
6504 # print extended diff header
6505 print "<div class=\"diff extended_header\">\n";
6507 while ($patch_line = to_utf8
(scalar<$fd>)) {
6510 last EXTENDED_HEADER
if ($patch_line =~ m/^--- |^diff /);
6512 print format_extended_diff_header_line
($patch_line, $diffinfo,
6515 print "</div>\n"; # class="diff extended_header"
6517 # from-file/to-file diff header
6518 if (! $patch_line) {
6519 print "</div>\n"; # class="patch"
6522 next PATCH
if ($patch_line =~ m/^diff /);
6523 #assert($patch_line =~ m/^---/) if DEBUG;
6525 my $last_patch_line = $patch_line;
6526 $patch_line = to_utf8
(scalar <$fd>);
6528 #assert($patch_line =~ m/^\+\+\+/) if DEBUG;
6530 print format_diff_from_to_header
($last_patch_line, $patch_line,
6531 $diffinfo, \
%from, \
%to,
6536 while ($patch_line = to_utf8
(scalar <$fd>)) {
6539 next PATCH
if ($patch_line =~ m/^diff /);
6541 my $class = diff_line_class
($patch_line, \
%from, \
%to);
6543 if ($class eq 'chunk_header') {
6544 print_diff_chunk
($diff_style, scalar @hash_parents, \
%from, \
%to, @chunk);
6548 push @chunk, [ $class, $patch_line ];
6553 print_diff_chunk
($diff_style, scalar @hash_parents, \
%from, \
%to, @chunk);
6556 print "</div>\n"; # class="patch"
6559 # for compact combined (--cc) format, with chunk and patch simplification
6560 # the patchset might be empty, but there might be unprocessed raw lines
6561 for (++$patch_idx if $patch_number > 0;
6562 $patch_idx < @
$difftree;
6564 # read and prepare patch information
6565 $diffinfo = parsed_difftree_line
($difftree->[$patch_idx]);
6567 # generate anchor for "patch" links in difftree / whatchanged part
6568 print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n" .
6569 format_diff_cc_simplified
($diffinfo, @hash_parents) .
6570 "</div>\n"; # class="patch"
6575 if ($patch_number == 0) {
6576 if (@hash_parents > 1) {
6577 print "<div class=\"diff nodifferences\">Trivial merge</div>\n";
6579 print "<div class=\"diff nodifferences\">No differences found</div>\n";
6583 print "</div>\n"; # class="patchset"
6586 # . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
6588 sub git_project_search_form
{
6589 my ($searchtext, $search_use_regexp) = @_;
6592 if ($project_filter) {
6593 $limit = " in '$project_filter'";
6596 print "<div class=\"projsearch\">\n";
6597 print $cgi->start_form(-method
=> 'get', -action
=> $my_uri) .
6598 $cgi->hidden(-name
=> 'a', -value
=> 'project_list') . "\n";
6599 print $cgi->hidden(-name
=> 'pf', -value
=> $project_filter). "\n"
6600 if (defined $project_filter);
6601 print $cgi->textfield(-name
=> 's', -value
=> $searchtext,
6602 -title
=> "Search project by name and description$limit",
6603 -size
=> 60) . "\n" .
6604 "<span title=\"Extended regular expression\">" .
6605 $cgi->checkbox(-name
=> 'sr', -value
=> 1, -label
=> 're',
6606 -checked
=> $search_use_regexp) .
6608 $cgi->submit(-name
=> 'btnS', -value
=> 'Search') .
6609 $cgi->end_form() . "\n" .
6610 "<span class=\"projectlist_link\">" .
6611 $cgi->a({-href
=> href
(project
=> undef, searchtext
=> undef,
6612 action
=> 'project_list',
6613 project_filter
=> $project_filter)},
6614 esc_html
("List all projects$limit")) . "</span><br />\n";
6615 print "<span class=\"projectlist_link\">" .
6616 $cgi->a({-href
=> href
(project
=> undef, searchtext
=> undef,
6617 action
=> 'project_list',
6618 project_filter
=> undef)},
6619 esc_html
("List all projects")) . "</span>\n" if $project_filter;
6623 # entry for given @keys needs filling if at least one of keys in list
6624 # is not present in %$project_info
6625 sub project_info_needs_filling
{
6626 my ($project_info, @keys) = @_;
6628 # return List::MoreUtils::any { !exists $project_info->{$_} } @keys;
6629 foreach my $key (@keys) {
6630 if (!exists $project_info->{$key}) {
6637 sub git_cache_file_format
{
6638 return GITWEB_CACHE_FORMAT
.
6639 (gitweb_check_feature
('forks') ?
" (forks)" : "");
6642 sub git_retrieve_cache_file
{
6643 my $cache_file = shift;
6645 use Storable
qw(retrieve);
6647 if ((my $dump = eval { retrieve
($cache_file) })) {
6649 ref($dump) eq 'ARRAY' &&
6651 ref($$dump[1]) eq 'ARRAY' &&
6652 @
{$$dump[1]} == 2 &&
6653 ref(${$$dump[1]}[0]) eq 'ARRAY' &&
6654 ref(${$$dump[1]}[1]) eq 'HASH' &&
6655 $$dump[0] eq git_cache_file_format
();
6661 sub git_store_cache_file
{
6662 my ($cache_file, $cachedata) = @_;
6664 use File
::Basename
qw(dirname);
6666 use POSIX
qw(:fcntl_h);
6667 use Storable
qw(store_fd);
6670 my $cache_d = dirname
($cache_file);
6672 umask($mask & ~0070) if $cache_grpshared;
6673 if ((-d
$cache_d || mkdir($cache_d, $cache_grpshared ?
0770 : 0700)) &&
6674 sysopen(my $fd, "$cache_file.lock", O_WRONLY
|O_CREAT
|O_EXCL
, $cache_grpshared ?
0660 : 0600)) {
6675 store_fd
([git_cache_file_format
(), $cachedata], $fd);
6677 rename "$cache_file.lock", $cache_file;
6678 $result = stat($cache_file)->mtime;
6680 umask($mask) if $cache_grpshared;
6684 sub verify_cached_project
{
6685 my ($hashref, $path) = @_;
6686 return undef unless $path;
6687 delete $$hashref{$path}, return undef unless is_valid_project
($path);
6688 return $$hashref{$path} if exists $$hashref{$path};
6690 # A valid project was requested but it's not yet in the cache
6691 # Manufacture a minimal project entry (path, name, description)
6692 # Also provide age, but only if it's available via $lastactivity_file
6694 my %proj = ('path' => $path);
6695 my $val = git_get_project_description
($path);
6696 defined $val or $val = '';
6697 $proj{'descr_long'} = $val;
6698 $proj{'descr'} = chop_str
($val, $projects_list_description_width, 5);
6699 unless ($omit_owner) {
6700 $val = git_get_project_owner
($path);
6701 defined $val or $val = '';
6702 $proj{'owner'} = $val;
6704 unless ($omit_age_column) {
6705 ($val) = git_get_last_activity
($path, 1);
6706 $proj{'age_epoch'} = $val if defined $val;
6708 $$hashref{$path} = \
%proj;
6712 sub git_filter_cached_projects
{
6713 my ($cache, $projlist, $verify) = @_;
6714 my $hashref = $$cache[1];
6716 sub {verify_cached_project
($hashref, $_[0])} :
6717 sub {$$hashref{$_[0]}};
6719 my $c = &$sub($_->{'path'});
6720 defined $c ?
($_ = $c) : ()
6724 # fills project list info (age, description, owner, category, forks, etc.)
6725 # for each project in the list, removing invalid projects from
6726 # returned list, or fill only specified info.
6728 # Invalid projects are removed from the returned list if and only if you
6729 # ask 'age_epoch' to be filled, because they are the only fields
6730 # that run unconditionally git command that requires repository, and
6731 # therefore do always check if project repository is invalid.
6734 # * fill_project_list_info(\@project_list, 'descr_long', 'ctags')
6735 # ensures that 'descr_long' and 'ctags' fields are filled
6736 # * @project_list = fill_project_list_info(\@project_list)
6737 # ensures that all fields are filled (and invalid projects removed)
6739 # NOTE: modifies $projlist, but does not remove entries from it
6740 sub fill_project_list_info
{
6741 my ($projlist, @wanted_keys) = @_;
6743 my $rebuild = @wanted_keys && $wanted_keys[0] eq 'rebuild-cache' && shift @wanted_keys;
6744 return fill_project_list_info_uncached
($projlist, @wanted_keys)
6745 unless $projlist_cache_lifetime && $projlist_cache_lifetime > 0;
6749 my $cache_lifetime = $rebuild ?
0 : $projlist_cache_lifetime;
6750 my $cache_file = "$cache_dir/$projlist_cache_name";
6756 if ($cache_lifetime && -f
$cache_file) {
6757 $cache_mtime = stat($cache_file)->mtime;
6758 $cache_dump = undef if $cache_mtime &&
6759 (!$cache_dump_mtime || $cache_dump_mtime != $cache_mtime);
6761 if (defined $cache_mtime && # caching is on and $cache_file exists
6762 $cache_mtime + $cache_lifetime*60 > $now &&
6763 ($cache_dump || ($cache_dump = git_retrieve_cache_file
($cache_file)))) {
6765 $cache_dump_mtime = $cache_mtime;
6766 $stale = $now - $cache_mtime;
6767 my $verify = ($action eq 'summary' || $action eq 'forks') &&
6768 gitweb_check_feature
('forks');
6769 @projects = git_filter_cached_projects
($cache_dump, $projlist, $verify);
6771 } else { # Cache miss.
6772 if (defined $cache_mtime) {
6773 # Postpone timeout by two minutes so that we get
6774 # enough time to do our job, or to be more exact
6775 # make cache expire after two minutes from now.
6776 my $time = $now - $cache_lifetime*60 + 120;
6777 utime $time, $time, $cache_file;
6779 my @all_projects = git_get_projects_list
();
6780 my %all_projects_filled = map { ( $_->{'path'} => $_ ) }
6781 fill_project_list_info_uncached
(\
@all_projects);
6782 map { $all_projects_filled{$_->{'path'}} = $_ }
6783 filter_forks_from_projects_list
([values(%all_projects_filled)])
6784 if gitweb_check_feature
('forks');
6785 $cache_dump = [[sort {$a->{'path'} cmp $b->{'path'}} values(%all_projects_filled)],
6786 \
%all_projects_filled];
6787 $cache_dump_mtime = git_store_cache_file
($cache_file, $cache_dump);
6788 @projects = git_filter_cached_projects
($cache_dump, $projlist);
6791 if ($cache_lifetime && $stale > 0) {
6792 print "<div class=\"stale_info\">Cached version (${stale}s old)</div>\n"
6793 unless $shown_stale_message;
6794 $shown_stale_message = 1;
6800 sub fill_project_list_info_uncached
{
6801 my ($projlist, @wanted_keys) = @_;
6803 my $filter_set = sub { return @_; };
6805 my %wanted_keys = map { $_ => 1 } @wanted_keys;
6806 $filter_set = sub { return grep { $wanted_keys{$_} } @_; };
6809 my $show_ctags = gitweb_check_feature
('ctags');
6811 foreach my $pr (@
$projlist) {
6812 if (project_info_needs_filling
($pr, $filter_set->('age_epoch'))) {
6813 my (@activity) = git_get_last_activity
($pr->{'path'});
6814 unless (@activity) {
6817 ($pr->{'age_epoch'}) = @activity;
6819 if (project_info_needs_filling
($pr, $filter_set->('descr', 'descr_long'))) {
6820 my $descr = git_get_project_description
($pr->{'path'}) || "";
6821 $descr = to_utf8
($descr);
6822 $pr->{'descr_long'} = $descr;
6823 $pr->{'descr'} = chop_str
($descr, $projects_list_description_width, 5);
6825 if (project_info_needs_filling
($pr, $filter_set->('owner'))) {
6826 $pr->{'owner'} = git_get_project_owner
("$pr->{'path'}") || "";
6829 project_info_needs_filling
($pr, $filter_set->('ctags'))) {
6830 $pr->{'ctags'} = git_get_project_ctags
($pr->{'path'});
6832 if ($projects_list_group_categories &&
6833 project_info_needs_filling
($pr, $filter_set->('category'))) {
6834 my $cat = git_get_project_category
($pr->{'path'}) ||
6835 $project_list_default_category;
6836 $pr->{'category'} = to_utf8
($cat);
6839 push @projects, $pr;
6845 sub sort_projects_list
{
6846 my ($projlist, $order) = @_;
6850 return sub { lc($a->{$key}) cmp lc($b->{$key}) };
6853 sub order_reverse_num_then_undef
{
6856 defined $a->{$key} ?
6857 (defined $b->{$key} ?
$b->{$key} <=> $a->{$key} : -1) :
6858 (defined $b->{$key} ?
1 : 0)
6863 project
=> order_str
('path'),
6864 descr
=> order_str
('descr_long'),
6865 owner
=> order_str
('owner'),
6866 age
=> order_reverse_num_then_undef
('age_epoch'),
6869 my $ordering = $orderings{$order};
6870 return defined $ordering ?
sort $ordering @
$projlist : @
$projlist;
6873 # returns a hash of categories, containing the list of project
6874 # belonging to each category
6875 sub build_projlist_by_category
{
6876 my ($projlist, $from, $to) = @_;
6879 $from = 0 unless defined $from;
6880 $to = $#$projlist if (!defined $to || $#$projlist < $to);
6882 for (my $i = $from; $i <= $to; $i++) {
6883 my $pr = $projlist->[$i];
6884 push @
{$categories{ $pr->{'category'} }}, $pr;
6887 return wantarray ?
%categories : \
%categories;
6890 # print 'sort by' <th> element, generating 'sort by $name' replay link
6891 # if that order is not selected
6893 print format_sort_th
(@_);
6896 sub format_sort_th
{
6897 my ($name, $order, $header) = @_;
6899 $header ||= ucfirst($name);
6901 if ($order eq $name) {
6902 $sort_th .= "<th>$header</th>\n";
6904 $sort_th .= "<th>" .
6905 $cgi->a({-href
=> href
(-replay
=>1, order
=>$name),
6906 -class => "header"}, $header) .
6913 sub git_project_list_rows
{
6914 my ($projlist, $from, $to, $check_forks) = @_;
6916 $from = 0 unless defined $from;
6917 $to = $#$projlist if (!defined $to || $#$projlist < $to);
6921 for (my $i = $from; $i <= $to; $i++) {
6922 my $pr = $projlist->[$i];
6925 print "<tr class=\"dark\">\n";
6927 print "<tr class=\"light\">\n";
6933 if ($pr->{'forks'}) {
6934 my $nforks = scalar @
{$pr->{'forks'}};
6935 my $s = $nforks == 1 ?
'' : 's';
6937 print $cgi->a({-href
=> href
(project
=>$pr->{'path'}, action
=>"forks"),
6938 -title
=> "$nforks fork$s"}, "+");
6940 print $cgi->span({-title
=> "$nforks fork$s"}, "+");
6945 my $path = $pr->{'path'};
6946 my $dotgit = $path =~ s/\.git$// ?
'.git' : '';
6947 print "<td>" . $cgi->a({-href
=> href
(project
=>$pr->{'path'}, action
=>"summary"),
6949 esc_html_match_hl
($path, $search_regexp).$dotgit) .
6951 "<td>" . $cgi->a({-href
=> href
(project
=>$pr->{'path'}, action
=>"summary"),
6953 -title
=> $pr->{'descr_long'}},
6955 ? esc_html_match_hl_chopped
($pr->{'descr_long'},
6956 $pr->{'descr'}, $search_regexp)
6957 : esc_html
($pr->{'descr'})) .
6959 unless ($omit_owner) {
6960 print "<td><i>" . ($owner_link_hook
6961 ?
$cgi->a({-href
=> $owner_link_hook->($pr->{'owner'}), -class => "list"},
6962 chop_and_escape_str
($pr->{'owner'}, 15))
6963 : chop_and_escape_str
($pr->{'owner'}, 15)) . "</i></td>\n";
6965 unless ($omit_age_column) {
6966 my ($age_epoch, $age_string) = ($pr->{'age_epoch'});
6967 $age_string = defined $age_epoch ? age_string
($age_epoch, $now) : "No commits";
6968 print "<td class=\"". age_class
($age_epoch, $now) . "\">" . $age_string . "</td>\n";
6970 print"<td class=\"link\">" .
6971 $cgi->a({-href
=> href
(project
=>$pr->{'path'}, action
=>"summary")}, "summary") . $barsep .
6972 $cgi->a({-href
=> href
(project
=>$pr->{'path'}, action
=>"shortlog")}, "log") . $barsep .
6973 $cgi->a({-href
=> href
(project
=>$pr->{'path'}, action
=>"tree")}, "tree") .
6974 ($pr->{'forks'} ?
$barsep . $cgi->a({-href
=> href
(project
=>$pr->{'path'}, action
=>"forks")}, "forks") : '') .
6980 sub git_project_list_body
{
6981 # actually uses global variable $project
6982 my ($projlist, $order, $from, $to, $extra, $no_header, $ctags_action, $keep_top) = @_;
6983 my @projects = @
$projlist;
6985 my $check_forks = gitweb_check_feature
('forks');
6986 my $show_ctags = gitweb_check_feature
('ctags');
6987 my $tagfilter = $show_ctags ?
$input_params{'ctag_filter'} : undef;
6988 $check_forks = undef
6989 if ($tagfilter || $search_regexp);
6991 # filtering out forks before filling info allows us to do less work
6993 @projects = filter_forks_from_projects_list
(\
@projects);
6994 push @projects, { 'path' => "$project_filter.git" }
6995 if $project_filter && $keep_top && is_valid_project
("$project_filter.git");
6997 # search_projects_list pre-fills required info
6998 @projects = search_projects_list
(\
@projects,
6999 'search_regexp' => $search_regexp,
7000 'tagfilter' => $tagfilter)
7001 if ($tagfilter || $search_regexp);
7003 my @all_fields = ('descr', 'descr_long', 'ctags', 'category');
7004 push @all_fields, 'age_epoch' unless($omit_age_column);
7005 push @all_fields, 'owner' unless($omit_owner);
7006 @projects = fill_project_list_info
(\
@projects, @all_fields);
7008 $order ||= $default_projects_order;
7009 $from = 0 unless defined $from;
7010 $to = $#projects if (!defined $to || $#projects < $to);
7015 "<b>No such projects found</b><br />\n".
7016 "Click ".$cgi->a({-href
=>href
(project
=>undef,action
=>'project_list')},"here")." to view all projects<br />\n".
7017 "</center>\n<br />\n";
7021 @projects = sort_projects_list
(\
@projects, $order);
7024 my $ctags = git_gather_all_ctags
(\
@projects);
7025 my $cloud = git_populate_project_tagcloud
($ctags, $ctags_action||'project_list');
7026 print git_show_project_tagcloud
($cloud, 64);
7029 print "<table class=\"project_list\">\n";
7030 unless ($no_header) {
7033 print "<th></th>\n";
7035 print_sort_th
('project', $order, 'Project');
7036 print_sort_th
('descr', $order, 'Description');
7037 print_sort_th
('owner', $order, 'Owner') unless $omit_owner;
7038 print_sort_th
('age', $order, 'Last Change') unless $omit_age_column;
7039 print "<th></th>\n" . # for links
7043 if ($projects_list_group_categories) {
7044 # only display categories with projects in the $from-$to window
7045 @projects = sort {$a->{'category'} cmp $b->{'category'}} @projects[$from..$to];
7046 my %categories = build_projlist_by_category
(\
@projects, $from, $to);
7047 foreach my $cat (sort keys %categories) {
7048 unless ($cat eq "") {
7051 print "<td></td>\n";
7053 print "<td class=\"category\" colspan=\"5\">".esc_html
($cat)."</td>\n";
7057 git_project_list_rows
($categories{$cat}, undef, undef, $check_forks);
7060 git_project_list_rows
(\
@projects, $from, $to, $check_forks);
7063 if (defined $extra) {
7064 print "<tr class=\"extra\">\n";
7066 print "<td></td>\n";
7068 print "<td colspan=\"5\" class=\"extra\">$extra</td>\n" .
7075 # uses global variable $project
7076 my ($commitlist, $from, $to, $refs, $extra) = @_;
7078 $from = 0 unless defined $from;
7079 $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
7081 for (my $i = 0; $i <= $to; $i++) {
7082 my %co = %{$commitlist->[$i]};
7084 my $commit = $co{'id'};
7085 my $ref = format_ref_marker
($refs, $commit);
7086 git_print_header_div
('commit',
7087 "<span class=\"age\">$co{'age_string'}</span>" .
7088 esc_html
($co{'title'}),
7089 $commit, undef, $ref);
7090 print "<div class=\"title_text\">\n" .
7091 "<div class=\"log_link\">\n" .
7092 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$commit)}, "commit") .
7094 $cgi->a({-href
=> href
(action
=>"commitdiff", hash
=>$commit)}, "commitdiff") .
7096 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$commit, hash_base
=>$commit)}, "tree") .
7099 git_print_authorship
(\
%co, -tag
=> 'span');
7100 print "<br/>\n</div>\n";
7102 print "<div class=\"log_body\">\n";
7103 git_print_log
($co{'comment'}, -final_empty_line
=> 1);
7107 print "<div class=\"page_nav_trailer\">\n";
7113 sub git_shortlog_body
{
7114 # uses global variable $project
7115 my ($commitlist, $from, $to, $refs, $extra) = @_;
7117 $from = 0 unless defined $from;
7118 $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
7120 print "<table class=\"shortlog\">\n";
7122 for (my $i = $from; $i <= $to; $i++) {
7123 my %co = %{$commitlist->[$i]};
7124 my $commit = $co{'id'};
7125 my $ref = format_ref_marker
($refs, $commit);
7127 print "<tr class=\"dark\">\n";
7129 print "<tr class=\"light\">\n";
7132 # git_summary() used print "<td><i>$co{'age_string'}</i></td>\n" .
7133 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
7134 format_author_html
('td', \
%co, 10) . "<td>";
7135 print format_subject_html
($co{'title'}, $co{'title_short'},
7136 href
(action
=>"commit", hash
=>$commit), $ref);
7138 "<td class=\"link\">" .
7139 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$commit)}, "commit") . $barsep .
7140 $cgi->a({-href
=> href
(action
=>"commitdiff", hash
=>$commit)}, "commitdiff") . $barsep .
7141 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$commit, hash_base
=>$commit)}, "tree");
7142 my $snapshot_links = format_snapshot_links
($commit);
7143 if (defined $snapshot_links) {
7144 print $barsep . $snapshot_links;
7149 if (defined $extra) {
7150 print "<tr class=\"extra\">\n" .
7151 "<td colspan=\"4\" class=\"extra\">$extra</td>\n" .
7157 sub git_history_body
{
7158 # Warning: assumes constant type (blob or tree) during history
7159 my ($commitlist, $from, $to, $refs, $extra,
7160 $file_name, $file_hash, $ftype) = @_;
7162 $from = 0 unless defined $from;
7163 $to = $#{$commitlist} unless (defined $to && $to <= $#{$commitlist});
7165 print "<table class=\"history\">\n";
7167 for (my $i = $from; $i <= $to; $i++) {
7168 my %co = %{$commitlist->[$i]};
7172 my $commit = $co{'id'};
7174 my $ref = format_ref_marker
($refs, $commit);
7177 print "<tr class=\"dark\">\n";
7179 print "<tr class=\"light\">\n";
7182 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
7183 # shortlog: format_author_html('td', \%co, 10)
7184 format_author_html
('td', \
%co, 15, 3) . "<td>";
7185 # originally git_history used chop_str($co{'title'}, 50)
7186 print format_subject_html
($co{'title'}, $co{'title_short'},
7187 href
(action
=>"commit", hash
=>$commit), $ref);
7189 "<td class=\"link\">" .
7190 $cgi->a({-href
=> href
(action
=>$ftype, hash_base
=>$commit, file_name
=>$file_name)}, $ftype) . $barsep .
7191 $cgi->a({-href
=> href
(action
=>"commitdiff", hash
=>$commit)}, "commitdiff");
7193 if ($ftype eq 'blob') {
7194 my $blob_current = $file_hash;
7195 my $blob_parent = git_get_hash_by_path
($commit, $file_name);
7196 if (defined $blob_current && defined $blob_parent &&
7197 $blob_current ne $blob_parent) {
7199 $cgi->a({-href
=> href
(action
=>"blobdiff",
7200 hash
=>$blob_current, hash_parent
=>$blob_parent,
7201 hash_base
=>$hash_base, hash_parent_base
=>$commit,
7202 file_name
=>$file_name)},
7209 if (defined $extra) {
7210 print "<tr class=\"extra\">\n" .
7211 "<td colspan=\"4\" class=\"extra\">$extra</td>\n" .
7218 # uses global variable $project
7219 my ($taglist, $from, $to, $extra, $head_at, $full, $order) = @_;
7220 $from = 0 unless defined $from;
7221 $to = $#{$taglist} if (!defined $to || $#{$taglist} < $to);
7222 $order ||= $default_refs_order;
7224 print "<table class=\"tags\">\n";
7226 print "<tr class=\"tags_header\">\n";
7227 print_sort_th
('age', $order, 'Last Change');
7228 print_sort_th
('name', $order, 'Name');
7229 print "<th></th>\n" . # for comment
7230 "<th></th>\n" . # for tag
7231 "<th></th>\n" . # for links
7235 for (my $i = $from; $i <= $to; $i++) {
7236 my $entry = $taglist->[$i];
7238 my $comment = $tag{'subject'};
7240 if (defined $comment) {
7241 $comment_short = chop_str
($comment, 30, 5);
7243 my $curr = defined $head_at && $tag{'id'} eq $head_at;
7245 print "<tr class=\"dark\">\n";
7247 print "<tr class=\"light\">\n";
7250 if (defined $tag{'age'}) {
7251 print "<td><i>$tag{'age'}</i></td>\n";
7253 print "<td></td>\n";
7255 print(($curr ?
"<td class=\"current_head\">" : "<td>") .
7256 $cgi->a({-href
=> href
(action
=>$tag{'reftype'}, hash
=>$tag{'refid'}),
7257 -class => "list name"}, esc_html
($tag{'name'})) .
7260 if (defined $comment) {
7261 print format_subject_html
($comment, $comment_short,
7262 href
(action
=>"tag", hash
=>$tag{'id'}));
7265 "<td class=\"selflink\">";
7266 if ($tag{'type'} eq "tag") {
7267 print $cgi->a({-href
=> href
(action
=>"tag", hash
=>$tag{'id'})}, "tag");
7272 "<td class=\"link\">" . $barsep .
7273 $cgi->a({-href
=> href
(action
=>$tag{'reftype'}, hash
=>$tag{'refid'})}, $tag{'reftype'});
7274 if ($tag{'reftype'} eq "commit") {
7275 print $barsep . $cgi->a({-href
=> href
(action
=>"shortlog", hash
=>$tag{'fullname'})}, "log");
7276 print $barsep . $cgi->a({-href
=> href
(action
=>"tree", hash
=>$tag{'fullname'})}, "tree") if $full;
7277 } elsif ($tag{'reftype'} eq "blob") {
7278 print $barsep . $cgi->a({-href
=> href
(action
=>"blob_plain", hash
=>$tag{'refid'})}, "raw");
7283 if (defined $extra) {
7284 print "<tr class=\"extra\">\n" .
7285 "<td colspan=\"5\" class=\"extra\">$extra</td>\n" .
7291 sub git_heads_body
{
7292 # uses global variable $project
7293 my ($headlist, $head_at, $from, $to, $extra) = @_;
7294 $from = 0 unless defined $from;
7295 $to = $#{$headlist} if (!defined $to || $#{$headlist} < $to);
7297 print "<table class=\"heads\">\n";
7299 for (my $i = $from; $i <= $to; $i++) {
7300 my $entry = $headlist->[$i];
7302 my $curr = defined $head_at && $ref{'id'} eq $head_at;
7304 print "<tr class=\"dark\">\n";
7306 print "<tr class=\"light\">\n";
7309 print "<td><i>$ref{'age'}</i></td>\n" .
7310 ($curr ?
"<td class=\"current_head\">" : "<td>") .
7311 $cgi->a({-href
=> href
(action
=>"shortlog", hash
=>$ref{'fullname'}),
7312 -class => "list name"},esc_html
($ref{'name'})) .
7314 "<td class=\"link\">" .
7315 $cgi->a({-href
=> href
(action
=>"shortlog", hash
=>$ref{'fullname'})}, "log") . $barsep .
7316 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$ref{'fullname'}, hash_base
=>$ref{'fullname'})}, "tree") .
7320 if (defined $extra) {
7321 print "<tr class=\"extra\">\n" .
7322 "<td colspan=\"3\" class=\"extra\">$extra</td>\n" .
7328 # Display a single remote block
7329 sub git_remote_block
{
7330 my ($remote, $rdata, $limit, $head) = @_;
7332 my $heads = $rdata->{'heads'};
7333 my $fetch = $rdata->{'fetch'};
7334 my $push = $rdata->{'push'};
7336 my $urls_table = "<table class=\"projects_list\">\n" ;
7338 if (defined $fetch) {
7339 if ($fetch eq $push) {
7340 $urls_table .= format_repo_url
("URL", $fetch);
7342 $urls_table .= format_repo_url
("Fetch URL", $fetch);
7343 $urls_table .= format_repo_url
("Push URL", $push) if defined $push;
7345 } elsif (defined $push) {
7346 $urls_table .= format_repo_url
("Push URL", $push);
7348 $urls_table .= format_repo_url
("", "No remote URL");
7351 $urls_table .= "</table>\n";
7354 if (defined $limit && $limit < @
$heads) {
7355 $dots = $cgi->a({-href
=> href
(action
=>"remotes", hash
=>$remote)}, "...");
7359 git_heads_body
($heads, $head, 0, $limit, $dots);
7362 # Display a list of remote names with the respective fetch and push URLs
7363 sub git_remotes_list
{
7364 my ($remotedata, $limit) = @_;
7365 print "<table class=\"heads\">\n";
7367 my @remotes = sort keys %$remotedata;
7369 my $limited = $limit && $limit < @remotes;
7371 $#remotes = $limit - 1 if $limited;
7373 while (my $remote = shift @remotes) {
7374 my $rdata = $remotedata->{$remote};
7375 my $fetch = $rdata->{'fetch'};
7376 my $push = $rdata->{'push'};
7378 print "<tr class=\"dark\">\n";
7380 print "<tr class=\"light\">\n";
7384 $cgi->a({-href
=> href
(action
=>'remotes', hash
=>$remote),
7385 -class=> "list name"},esc_html
($remote)) .
7387 print "<td class=\"link\">" .
7388 (defined $fetch ?
$cgi->a({-href
=> $fetch}, "fetch") : "fetch") .
7390 (defined $push ?
$cgi->a({-href
=> $push}, "push") : "push") .
7398 "<td colspan=\"3\">" .
7399 $cgi->a({-href
=> href
(action
=>"remotes")}, "...") .
7400 "</td>\n" . "</tr>\n";
7406 # Display remote heads grouped by remote, unless there are too many
7407 # remotes, in which case we only display the remote names
7408 sub git_remotes_body
{
7409 my ($remotedata, $limit, $head) = @_;
7410 if ($limit and $limit < keys %$remotedata) {
7411 git_remotes_list
($remotedata, $limit);
7413 fill_remote_heads
($remotedata);
7414 while (my ($remote, $rdata) = each %$remotedata) {
7415 git_print_section
({-class=>"remote", -id
=>$remote},
7416 ["remotes", $remote, $remote], sub {
7417 git_remote_block
($remote, $rdata, $limit, $head);
7423 sub git_search_message
{
7427 if ($searchtype eq 'commit') {
7428 $greptype = "--grep=";
7429 } elsif ($searchtype eq 'author') {
7430 $greptype = "--author=";
7431 } elsif ($searchtype eq 'committer') {
7432 $greptype = "--committer=";
7434 $greptype .= $searchtext;
7435 my @commitlist = parse_commits
($hash, 101, (100 * $page), undef,
7436 $greptype, '--regexp-ignore-case',
7437 $search_use_regexp ?
'--extended-regexp' : '--fixed-strings');
7439 my $paging_nav = "<span class=\"paging_nav\">";
7441 $paging_nav .= tabspan
(
7442 $cgi->a({-href
=> href
(-replay
=>1, page
=>undef)},
7445 $cgi->a({-href
=> href
(-replay
=>1, page
=>$page-1),
7446 -accesskey
=> "p", -title
=> "Alt-p"}, "prev"));
7448 $paging_nav .= tabspan
("first", 1, 0).${mdotsep
}.tabspan
("prev", 0, 1);
7451 if ($#commitlist >= 100) {
7452 $next_link = tabspan
(
7453 $cgi->a({-href
=> href
(-replay
=>1, page
=>$page+1),
7454 -accesskey
=> "n", -title
=> "Alt-n"}, "next"));
7455 $paging_nav .= "${mdotsep}$next_link";
7457 $paging_nav .= ${mdotsep
}.tabspan
("next", 0, 1);
7462 git_print_page_nav
('','', $hash,$co{'tree'},$hash, $paging_nav."</span>");
7463 git_print_header_div
('commit', esc_html
($co{'title'}), $hash);
7464 if ($page == 0 && !@commitlist) {
7465 print "<p>No match.</p>\n";
7467 git_search_grep_body
(\
@commitlist, 0, 99, $next_link);
7473 sub git_search_changes
{
7477 defined(my $fd = git_cmd_pipe
'--no-pager', 'log', @diff_opts,
7478 '--pretty=format:%H', '--no-abbrev', '--raw', "-S$searchtext",
7479 ($search_use_regexp ?
'--pickaxe-regex' : ()))
7480 or die_error
(500, "Open git-log failed");
7484 git_print_page_nav
('','', $hash,$co{'tree'},$hash);
7485 git_print_header_div
('commit', esc_html
($co{'title'}), $hash);
7487 print "<table class=\"pickaxe search\">\n";
7491 while (my $line = to_utf8
(scalar <$fd>)) {
7495 my %set = parse_difftree_raw_line
($line);
7496 if (defined $set{'commit'}) {
7497 # finish previous commit
7500 "<td class=\"link\">" .
7501 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$co{'id'})},
7504 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$co{'tree'},
7505 hash_base
=>$co{'id'})},
7512 print "<tr class=\"dark\">\n";
7514 print "<tr class=\"light\">\n";
7517 %co = parse_commit
($set{'commit'});
7518 my $author = chop_and_escape_str
($co{'author_name'}, 15, 5);
7519 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
7520 "<td><i>$author</i></td>\n" .
7522 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$co{'id'}),
7523 -class => "list subject"},
7524 chop_and_escape_str
($co{'title'}, 50) . "<br/>");
7525 } elsif (defined $set{'to_id'}) {
7526 next if ($set{'to_id'} =~ m/^0{40}$/);
7528 print $cgi->a({-href
=> href
(action
=>"blob", hash_base
=>$co{'id'},
7529 hash
=>$set{'to_id'}, file_name
=>$set{'to_file'}),
7531 "<span class=\"match\">" . esc_path
($set{'file'}) . "</span>") .
7537 # finish last commit (warning: repetition!)
7540 "<td class=\"link\">" .
7541 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$co{'id'})},
7544 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$co{'tree'},
7545 hash_base
=>$co{'id'})},
7556 sub git_search_files
{
7560 defined(my $fd = git_cmd_pipe
'grep', '-n', '-z',
7561 $search_use_regexp ?
('-E', '-i') : '-F',
7562 $searchtext, $co{'tree'})
7563 or die_error
(500, "Open git-grep failed");
7567 git_print_page_nav
('','', $hash,$co{'tree'},$hash);
7568 git_print_header_div
('commit', esc_html
($co{'title'}), $hash);
7570 print "<table class=\"grep_search\">\n";
7575 while (my $line = to_utf8
(scalar <$fd>)) {
7577 my ($file, $lno, $ltext, $binary);
7578 last if ($matches++ > 1000);
7579 if ($line =~ /^Binary file (.+) matches$/) {
7583 ($file, $lno, $ltext) = split(/\0/, $line, 3);
7584 $file =~ s/^$co{'tree'}://;
7586 if ($file ne $lastfile) {
7587 $lastfile and print "</td></tr>\n";
7589 print "<tr class=\"dark\">\n";
7591 print "<tr class=\"light\">\n";
7593 $file_href = href
(action
=>"blob", hash_base
=>$co{'id'},
7595 print "<td class=\"list\">".
7596 $cgi->a({-href
=> $file_href, -class => "list"}, esc_path
($file));
7597 print "</td><td>\n";
7601 print "<div class=\"binary\">Binary file</div>\n";
7603 $ltext = untabify
($ltext);
7604 if ($ltext =~ m/^(.*)($search_regexp)(.*)$/i) {
7605 $ltext = esc_html
($1, -nbsp
=>1);
7606 $ltext .= '<span class="match">';
7607 $ltext .= esc_html
($2, -nbsp
=>1);
7608 $ltext .= '</span>';
7609 $ltext .= esc_html
($3, -nbsp
=>1);
7611 $ltext = esc_html
($ltext, -nbsp
=>1);
7613 print "<div class=\"pre\">" .
7614 $cgi->a({-href
=> $file_href.'#l'.$lno,
7615 -class => "linenr"}, sprintf('%4i ', $lno)) .
7616 $ltext . "</div>\n";
7620 print "</td></tr>\n";
7621 if ($matches > 1000) {
7622 print "<div class=\"diff nodifferences\">Too many matches, listing trimmed</div>\n";
7625 print "<div class=\"diff nodifferences\">No matches found</div>\n";
7634 sub git_search_grep_body
{
7635 my ($commitlist, $from, $to, $extra) = @_;
7636 $from = 0 unless defined $from;
7637 $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
7639 print "<table class=\"commit_search\">\n";
7641 for (my $i = $from; $i <= $to; $i++) {
7642 my %co = %{$commitlist->[$i]};
7646 my $commit = $co{'id'};
7648 print "<tr class=\"dark\">\n";
7650 print "<tr class=\"light\">\n";
7653 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
7654 format_author_html
('td', \
%co, 15, 5) .
7656 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$co{'id'}),
7657 -class => "list subject"},
7658 chop_and_escape_str
($co{'title'}, 50) . "<br/>");
7659 my $comment = $co{'comment'};
7660 foreach my $line (@
$comment) {
7661 if ($line =~ m/^(.*?)($search_regexp)(.*)$/i) {
7662 my ($lead, $match, $trail) = ($1, $2, $3);
7663 $match = chop_str
($match, 70, 5, 'center');
7664 my $contextlen = int((80 - length($match))/2);
7665 $contextlen = 30 if ($contextlen > 30);
7666 $lead = chop_str
($lead, $contextlen, 10, 'left');
7667 $trail = chop_str
($trail, $contextlen, 10, 'right');
7669 $lead = esc_html
($lead);
7670 $match = esc_html
($match);
7671 $trail = esc_html
($trail);
7673 print "$lead<span class=\"match\">$match</span>$trail<br />";
7677 "<td class=\"link\">" .
7678 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$co{'id'})}, "commit") .
7680 $cgi->a({-href
=> href
(action
=>"commitdiff", hash
=>$co{'id'})}, "commitdiff") .
7682 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$co{'tree'}, hash_base
=>$co{'id'})}, "tree");
7686 if (defined $extra) {
7687 print "<tr class=\"extra\">\n" .
7688 "<td colspan=\"3\" class=\"extra\">$extra</td>\n" .
7694 ## ======================================================================
7695 ## ======================================================================
7698 sub git_project_list_load
{
7699 my $empty_list_ok = shift;
7700 my $order = $input_params{'order'};
7701 if (defined $order && $order !~ m/none|project|descr|owner|age/) {
7702 die_error
(400, "Unknown order parameter");
7705 my @list = git_get_projects_list
($project_filter, $strict_export);
7706 if ($project_filter && (!@list || !gitweb_check_feature
('forks'))) {
7707 push @list, { 'path' => "$project_filter.git" }
7708 if is_valid_project
("$project_filter.git");
7711 die_error
(404, "No projects found") unless $empty_list_ok;
7714 return (\
@list, $order);
7718 my ($projlist, $order);
7720 if ($frontpage_no_project_list) {
7722 $project_filter = undef;
7724 ($projlist, $order) = git_project_list_load
(1);
7727 if (defined $home_text && -f
$home_text) {
7728 print "<div class=\"index_include\">\n";
7729 insert_file
($home_text);
7732 git_project_search_form
($searchtext, $search_use_regexp);
7733 if ($frontpage_no_project_list) {
7734 my $show_ctags = gitweb_check_feature
('ctags');
7735 if ($frontpage_no_project_list == 1 and $show_ctags) {
7736 my @projects = git_get_projects_list
($project_filter, $strict_export);
7737 @projects = filter_forks_from_projects_list
(\
@projects) if gitweb_check_feature
('forks');
7738 @projects = fill_project_list_info
(\
@projects, 'ctags');
7739 my $ctags = git_gather_all_ctags
(\
@projects);
7740 my $cloud = git_populate_project_tagcloud
($ctags, 'project_list');
7741 print git_show_project_tagcloud
($cloud, 64);
7744 git_project_list_body
($projlist, $order, undef, undef, undef, undef, undef, 1);
7749 sub git_project_list
{
7750 my ($projlist, $order) = git_project_list_load
();
7752 if (!$frontpage_no_project_list && defined $home_text && -f
$home_text) {
7753 print "<div class=\"index_include\">\n";
7754 insert_file
($home_text);
7757 git_project_search_form
();
7758 git_project_list_body
($projlist, $order, undef, undef, undef, undef, undef, 1);
7763 my $order = $input_params{'order'};
7764 if (defined $order && $order !~ m/none|project|descr|owner|age/) {
7765 die_error
(400, "Unknown order parameter");
7768 my $filter = $project;
7769 $filter =~ s/\.git$//;
7770 my @list = git_get_projects_list
($filter);
7772 die_error
(404, "No forks found");
7776 git_print_page_nav
('','');
7777 git_print_header_div
('summary', "$project forks");
7778 git_project_list_body
(\
@list, $order, undef, undef, undef, undef, 'forks');
7782 sub git_project_index
{
7783 my @projects = git_get_projects_list
($project_filter, $strict_export);
7785 die_error
(404, "No projects found");
7789 -type
=> 'text/plain',
7790 -charset
=> 'utf-8',
7791 -content_disposition
=> 'inline; filename="index.aux"');
7793 foreach my $pr (@projects) {
7794 if (!exists $pr->{'owner'}) {
7795 $pr->{'owner'} = git_get_project_owner
("$pr->{'path'}");
7798 my ($path, $owner) = ($pr->{'path'}, $pr->{'owner'});
7799 # quote as in CGI::Util::encode, but keep the slash, and use '+' for ' '
7800 $path =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf
("%%%02X", ord($1))/eg
;
7801 $owner =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf
("%%%02X", ord($1))/eg
;
7805 print "$path $owner\n";
7810 my $descr = git_get_project_description
($project) || "none";
7811 my %co = parse_commit
("HEAD");
7812 my %cd = %co ? parse_date
($co{'committer_epoch'}, $co{'committer_tz'}) : ();
7813 my $head = $co{'id'};
7814 my $remote_heads = gitweb_check_feature
('remote_heads');
7816 my $owner = git_get_project_owner
($project);
7817 my $homepage = git_get_project_config
('homepage');
7818 my $base_url = git_get_project_config
('baseurl');
7820 my $refs = git_get_references
();
7821 # These get_*_list functions return one more to allow us to see if
7822 # there are more ...
7823 my @taglist = git_get_tags_list
(16);
7824 my @headlist = git_get_heads_list
(16);
7825 my %remotedata = $remote_heads ? git_get_remotes_list
() : ();
7827 my $check_forks = gitweb_check_feature
('forks');
7830 # find forks of a project
7831 my $filter = $project;
7832 $filter =~ s/\.git$//;
7833 @forklist = git_get_projects_list
($filter);
7834 # filter out forks of forks
7835 @forklist = filter_forks_from_projects_list
(\
@forklist)
7840 git_print_page_nav
('summary','', $head);
7842 if ($check_forks and $project =~ m
#/#) {
7843 my $xproject = $project; $xproject =~ s
#/[^/]+$#.git#; #
7844 my $r = $cgi->a({-href
=> href
(project
=> $xproject, action
=> 'summary')}, $xproject);
7846 <div class="forkinfo">
7847 This project is a fork of the $r project. If you have that one
7848 already cloned locally, you can use
7849 <pre>git clone --reference /path/to/your/$xproject/incarnation mirror_URL</pre>
7850 to save bandwidth during cloning.
7855 print "<div class=\"title\"> </div>\n";
7856 print "<table class=\"projects_list\">\n" .
7857 "<tr id=\"metadata_desc\"><td>description</td><td>" . esc_html
($descr) . "</td></tr>\n";
7859 print "<tr id=\"metadata_homepage\"><td>homepage URL</td><td>" . $cgi->a({-href
=> $homepage}, $homepage) . "</td></tr>\n";
7862 print "<tr id=\"metadata_baseurl\"><td>repository URL</td><td>" . esc_html
($base_url) . "</td></tr>\n";
7864 if ($owner and not $omit_owner) {
7865 print "<tr id=\"metadata_owner\"><td>owner</td><td>" . ($owner_link_hook
7866 ?
$cgi->a({-href
=> $owner_link_hook->($owner)}, email_obfuscate
($owner))
7867 : email_obfuscate
($owner)) . "</td></tr>\n";
7869 if (defined $cd{'rfc2822'}) {
7870 print "<tr id=\"metadata_lchange\"><td>last change</td>" .
7871 "<td>".format_timestamp_html
(\
%cd)."</td></tr>\n";
7873 print format_lastrefresh_row
(), "\n";
7875 # use per project git URL list in $projectroot/$project/cloneurl
7876 # or make project git URL from git base URL and project name
7877 my $url_tag = $base_url ?
"mirror URL" : "URL";
7878 my @url_list = git_get_project_url_list
($project);
7879 @url_list = map { "$_/$project" } @git_base_url_list unless @url_list;
7880 foreach my $git_url (@url_list) {
7881 next unless $git_url;
7882 print format_repo_url
($url_tag, $git_url);
7885 @url_list = map { "$_/$project" } @git_base_push_urls;
7886 if ((git_get_project_config
("showpush", '--bool')||'false') eq "true" ||
7887 -f
"$projectroot/$project/.nofetch") {
7888 $url_tag = "push URL";
7889 foreach my $git_push_url (@url_list) {
7890 next unless $git_push_url;
7891 my $hint = $https_hint_html && $git_push_url =~ /^https:/i ?
7892 " $https_hint_html" : '';
7893 print "<tr class=\"metadata_pushurl\"><td>$url_tag</td><td>$git_push_url$hint</td></tr>\n";
7898 if (defined($git_base_bundles_url) && -d
"$projectroot/$project/bundles") {
7899 my $projname = $project;
7900 $projname =~ s
|^.*/||;
7901 my $url = "$git_base_bundles_url/$project/bundles";
7902 print format_repo_url
(
7904 "<a rel='nofollow' href='$url'>$projname downloadable bundles</a>");
7908 my $show_ctags = gitweb_check_feature
('ctags');
7910 my $ctags = git_get_project_ctags
($project);
7911 if (%$ctags || $show_ctags !~ /^\d+$/) {
7912 # without ability to add tags, don't show if there are none
7913 my $cloud = git_populate_project_tagcloud
($ctags, 'project_list');
7914 print "<tr id=\"metadata_ctags\">" .
7915 "<td style=\"vertical-align:middle\">content tags<br />";
7916 print "</td>\n<td>" unless %$ctags;
7917 print "<form action=\"$show_ctags\" method=\"post\" style=\"white-space:nowrap\">" .
7918 "<input type=\"hidden\" name=\"p\" value=\"$project\"/>" .
7919 "add: <input type=\"text\" name=\"t\" size=\"8\" /></form>"
7920 unless $show_ctags =~ /^\d+$/;
7921 print "</td>\n<td>" if %$ctags;
7922 print git_show_project_tagcloud
($cloud, 48)."</td>" .
7929 # If XSS prevention is on, we don't include README.html.
7930 # TODO: Allow a readme in some safe format.
7931 if (!$prevent_xss) {
7932 my $readme_name = "readme";
7934 if (-s
"$projectroot/$project/README.html") {
7935 $readme = collect_html_file
("$projectroot/$project/README.html");
7937 $readme = collect_output
($git_automatic_readme_html, "$projectroot/$project");
7938 if ($readme && $readme =~ /^<!-- README NAME: ((?:[^-]|(?:-(?!-)))+) -->/) {
7940 $readme =~ s/^<!--(?:[^-]|(?:-(?!-)))*-->\n?//;
7943 if (defined($readme)) {
7944 $readme =~ s/^\s+//s;
7945 $readme =~ s/\s+$//s;
7946 print "<div class=\"title\">$readme_name</div>\n",
7947 "<div id=\"readme\" class=\"readme\">\n",
7954 # we need to request one more than 16 (0..15) to check if
7956 my @commitlist = $head ? parse_commits
($head, 17) : ();
7958 git_print_header_div
('shortlog');
7959 git_shortlog_body
(\
@commitlist, 0, 15, $refs,
7960 $#commitlist <= 15 ?
undef :
7961 $cgi->a({-href
=> href
(action
=>"shortlog")}, "..."));
7965 git_print_header_div
('tags');
7966 git_tags_body
(\
@taglist, 0, 15,
7967 $#taglist <= 15 ?
undef :
7968 $cgi->a({-href
=> href
(action
=>"tags")}, "..."));
7972 git_print_header_div
('heads');
7973 git_heads_body
(\
@headlist, $head, 0, 15,
7974 $#headlist <= 15 ?
undef :
7975 $cgi->a({-href
=> href
(action
=>"heads")}, "..."));
7979 git_print_header_div
('remotes');
7980 git_remotes_body
(\
%remotedata, 15, $head);
7984 git_print_header_div
('forks');
7985 git_project_list_body
(\
@forklist, 'age', 0, 15,
7986 $#forklist <= 15 ?
undef :
7987 $cgi->a({-href
=> href
(action
=>"forks")}, "..."),
7988 'no_header', 'forks');
7995 my %tag = parse_tag
($hash);
7998 die_error
(404, "Unknown tag object");
8002 $fullhash = $hash if $hash =~ m/^[0-9a-fA-F]{40}$/;
8003 $fullhash = git_get_full_hash
($project, $hash) unless $fullhash;
8005 my $obj = $tag{'object'};
8007 if ($tag{'type'} eq 'commit') {
8008 git_print_page_nav
('','', $obj,undef,$obj);
8009 git_print_header_div
('commit', esc_html
($tag{'name'}), $hash);
8011 if ($tag{'type'} eq 'tree') {
8012 git_print_page_nav
('',['commit','commitdiff'], undef,undef,$obj);
8014 git_print_page_nav
('',['commit','commitdiff','tree'], undef,undef,undef);
8016 print "<div class=\"title\">".esc_html
($hash)."</div>\n";
8018 print "<div class=\"title_text\">\n" .
8019 "<table class=\"object_header\">\n" .
8020 "<tr><td>tag</td><td class=\"sha1\">$fullhash</td></tr>\n" .
8022 "<td>object</td>\n" .
8023 "<td>" . $cgi->a({-class => "list", -href
=> href
(action
=>$tag{'type'}, hash
=>$tag{'object'})},
8024 $tag{'object'}) . "</td>\n" .
8025 "<td class=\"link\">" . $cgi->a({-href
=> href
(action
=>$tag{'type'}, hash
=>$tag{'object'})},
8026 $tag{'type'}) . "</td>\n" .
8028 if (defined($tag{'author'})) {
8029 git_print_authorship_rows
(\
%tag, 'author');
8031 print "</table>\n\n" .
8033 print "<div class=\"page_body\">";
8034 my $comment = $tag{'comment'};
8035 foreach my $line (@
$comment) {
8037 print esc_html
($line, -nbsp
=>1) . "<br/>\n";
8043 sub git_blame_common
{
8044 my $format = shift || 'porcelain';
8045 if ($format eq 'porcelain' && $input_params{'javascript'}) {
8046 $format = 'incremental';
8047 $action = 'blame_incremental'; # for page title etc
8051 gitweb_check_feature
('blame')
8052 or die_error
(403, "Blame view not allowed");
8055 die_error
(400, "No file name given") unless $file_name;
8056 $hash_base ||= git_get_head_hash
($project);
8057 die_error
(404, "Couldn't find base commit") unless $hash_base;
8058 my %co = parse_commit
($hash_base)
8059 or die_error
(404, "Commit not found");
8061 if (!defined $hash) {
8062 $hash = git_get_hash_by_path
($hash_base, $file_name, "blob")
8063 or die_error
(404, "Error looking up file");
8065 $ftype = git_get_type
($hash);
8066 if ($ftype !~ "blob") {
8067 die_error
(400, "Object is not a blob");
8072 if ($format eq 'incremental') {
8073 # get file contents (as base)
8074 defined($fd = git_cmd_pipe
'cat-file', 'blob', $hash)
8075 or die_error
(500, "Open git-cat-file failed");
8076 } elsif ($format eq 'data') {
8077 # run git-blame --incremental
8078 defined($fd = git_cmd_pipe
"blame", "--incremental",
8079 $hash_base, "--", $file_name)
8080 or die_error
(500, "Open git-blame --incremental failed");
8082 # run git-blame --porcelain
8083 defined($fd = git_cmd_pipe
"blame", '-p',
8084 $hash_base, '--', $file_name)
8085 or die_error
(500, "Open git-blame --porcelain failed");
8088 # incremental blame data returns early
8089 if ($format eq 'data') {
8091 -type
=>"text/plain", -charset
=> "utf-8",
8092 -status
=> "200 OK");
8093 local $| = 1; # output autoflush
8098 or print "ERROR $!\n";
8101 if (defined $t0 && gitweb_check_feature
('timed')) {
8103 tv_interval
($t0, [ gettimeofday
() ]).
8104 ' '.$number_of_git_cmds;
8113 my $formats_nav = tabspan
(
8114 $cgi->a({-href
=> href
(action
=>"blob", -replay
=>1)},
8118 $cgi->a({-href
=> href
(action
=>"history", -replay
=>1)},
8121 $cgi->a({-href
=> href
(action
=>$action, file_name
=>$file_name)},
8123 git_print_page_nav
('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
8124 git_print_header_div
('commit', esc_html
($co{'title'}), $hash_base);
8125 git_print_page_path
($file_name, $ftype, $hash_base);
8128 if ($format eq 'incremental') {
8129 print "<noscript>\n<div class=\"error\"><center><b>\n".
8130 "This page requires JavaScript to run.\n Use ".
8131 $cgi->a({-href
=> href
(action
=>'blame',javascript
=>0,-replay
=>1)},
8134 "</b></center></div>\n</noscript>\n";
8136 print qq!<div id
="progress_bar" style
="width: 100%; background-color: yellow"></div
>\n!;
8139 print qq!<div
class="page_body">\n!;
8140 print qq!<div id
="progress_info">... / ...</div
>\n!
8141 if ($format eq 'incremental');
8142 print qq!<table id
="blame_table" class="blame" width
="100%">\n!.
8143 #qq!<col width="5.5em" /><col width="2.5em" /><col width="*" />\n!.
8145 qq!<tr
><th nowrap
="nowrap" style
="white-space:nowrap">!.
8146 qq!Commit
 <a href="javascript:extra_blame_columns()" id="columns_expander" !.
8147 qq!title
="toggles blame author information display">[+]</a></th
>!.
8148 qq!<th
class="extra_column">Author
</th><th class="extra_column">Date</th
>!.
8149 qq!<th
>Line
</th><th width="100%">Data</th
></tr
>\n!.
8153 my @rev_color = qw(light dark);
8154 my $num_colors = scalar(@rev_color);
8155 my $current_color = 0;
8157 if ($format eq 'incremental') {
8158 my $color_class = $rev_color[$current_color];
8163 while (my $line = to_utf8
(scalar <$fd>)) {
8167 print qq!<tr id
="l$linenr" class="$color_class">!.
8168 qq!<td
class="sha1"><a href
=""> </a></td
>!.
8169 qq!<td
class="extra_column" nowrap
="nowrap"></td
>!.
8170 qq!<td
class="extra_column" nowrap
="nowrap"></td
>!.
8171 qq!<td
class="linenr">!.
8172 qq!<a
class="linenr" href
="">$linenr</a></td
>!;
8173 print qq!<td
class="pre">! . esc_html
($line) . "</td>\n";
8177 } else { # porcelain, i.e. ordinary blame
8178 my %metainfo = (); # saves information about commits
8182 while (my $line = to_utf8
(scalar <$fd>)) {
8184 # the header: <SHA-1> <src lineno> <dst lineno> [<lines in group>]
8185 # no <lines in group> for subsequent lines in group of lines
8186 my ($full_rev, $orig_lineno, $lineno, $group_size) =
8187 ($line =~ /^([0-9a-f]{40}) (\d+) (\d+)(?: (\d+))?$/);
8188 if (!exists $metainfo{$full_rev}) {
8189 $metainfo{$full_rev} = { 'nprevious' => 0 };
8191 my $meta = $metainfo{$full_rev};
8193 while ($data = to_utf8
(scalar <$fd>)) {
8195 last if ($data =~ s/^\t//); # contents of line
8196 if ($data =~ /^(\S+)(?: (.*))?$/) {
8197 $meta->{$1} = $2 unless exists $meta->{$1};
8199 if ($data =~ /^previous /) {
8200 $meta->{'nprevious'}++;
8203 my $short_rev = substr($full_rev, 0, 8);
8204 my $author = $meta->{'author'};
8206 parse_date
($meta->{'author-time'}, $meta->{'author-tz'});
8207 my $date = $date{'iso-tz'};
8209 $current_color = ($current_color + 1) % $num_colors;
8211 my $tr_class = $rev_color[$current_color];
8212 $tr_class .= ' boundary' if (exists $meta->{'boundary'});
8213 $tr_class .= ' no-previous' if ($meta->{'nprevious'} == 0);
8214 $tr_class .= ' multiple-previous' if ($meta->{'nprevious'} > 1);
8215 print "<tr id=\"l$lineno\" class=\"$tr_class\">\n";
8217 my $rowspan = $group_size > 1 ?
" rowspan=\"$group_size\"" : "";
8218 print "<td class=\"sha1\"";
8219 print " title=\"". esc_html
($author) . ", $date\"";
8221 print $cgi->a({-href
=> href
(action
=>"commit",
8223 file_name
=>$file_name)},
8224 esc_html
($short_rev));
8225 if ($group_size >= 2) {
8226 my @author_initials = ($author =~ /\b([[:upper:]])\B/g);
8227 if (@author_initials) {
8229 esc_html
(join('', @author_initials));
8234 print "<td class=\"extra_column\" nowrap=\"nowrap\"$rowspan>". esc_html
($author) . "</td>";
8235 print "<td class=\"extra_column\" nowrap=\"nowrap\"$rowspan>". $date . "</td>";
8237 # 'previous' <sha1 of parent commit> <filename at commit>
8238 if (exists $meta->{'previous'} &&
8239 $meta->{'previous'} =~ /^([a-fA-F0-9]{40}) (.*)$/) {
8240 $meta->{'parent'} = $1;
8241 $meta->{'file_parent'} = unquote
($2);
8244 exists($meta->{'parent'}) ?
8245 $meta->{'parent'} : $full_rev;
8246 my $linenr_filename =
8247 exists($meta->{'file_parent'}) ?
8248 $meta->{'file_parent'} : unquote
($meta->{'filename'});
8249 my $blamed = href
(action
=> 'blame',
8250 file_name
=> $linenr_filename,
8251 hash_base
=> $linenr_commit);
8252 print "<td class=\"linenr\">";
8253 print $cgi->a({ -href
=> "$blamed#l$orig_lineno",
8254 -class => "linenr" },
8257 print "<td class=\"pre\">" . esc_html
($data) . "</td>\n";
8265 "</table>\n"; # class="blame"
8266 print "</div>\n"; # class="blame_body"
8268 or print "Reading blob failed\n";
8277 sub git_blame_incremental
{
8278 git_blame_common
('incremental');
8281 sub git_blame_data
{
8282 git_blame_common
('data');
8286 my $head = git_get_head_hash
($project);
8288 git_print_page_nav
('','', $head,undef,$head,format_ref_views
('tags'));
8289 git_print_header_div
('summary', $project);
8291 my @tagslist = git_get_tags_list
();
8293 git_tags_body
(\
@tagslist);
8299 my $order = $input_params{'order'};
8300 if (defined $order && $order !~ m/age|name/) {
8301 die_error
(400, "Unknown order parameter");
8304 my $head = git_get_head_hash
($project);
8306 git_print_page_nav
('','', $head,undef,$head,format_ref_views
('refs'));
8307 git_print_header_div
('summary', $project);
8309 my @refslist = git_get_tags_list
(undef, 1, $order);
8311 git_tags_body
(\
@refslist, undef, undef, undef, $head, 1, $order);
8317 my $head = git_get_head_hash
($project);
8319 git_print_page_nav
('','', $head,undef,$head,format_ref_views
('heads'));
8320 git_print_header_div
('summary', $project);
8322 my @headslist = git_get_heads_list
();
8324 git_heads_body
(\
@headslist, $head);
8329 # used both for single remote view and for list of all the remotes
8331 gitweb_check_feature
('remote_heads')
8332 or die_error
(403, "Remote heads view is disabled");
8334 my $head = git_get_head_hash
($project);
8335 my $remote = $input_params{'hash'};
8337 my $remotedata = git_get_remotes_list
($remote);
8338 die_error
(500, "Unable to get remote information") unless defined $remotedata;
8340 unless (%$remotedata) {
8341 die_error
(404, defined $remote ?
8342 "Remote $remote not found" :
8343 "No remotes found");
8346 git_header_html
(undef, undef, -action_extra
=> $remote);
8347 git_print_page_nav
('', '', $head, undef, $head,
8348 format_ref_views
($remote ?
'' : 'remotes'));
8350 fill_remote_heads
($remotedata);
8351 if (defined $remote) {
8352 git_print_header_div
('remotes', "$remote remote for $project");
8353 git_remote_block
($remote, $remotedata->{$remote}, undef, $head);
8355 git_print_header_div
('summary', "$project remotes");
8356 git_remotes_body
($remotedata, undef, $head);
8362 sub git_blob_plain
{
8366 if (!defined $hash) {
8367 if (defined $file_name) {
8368 my $base = $hash_base || git_get_head_hash
($project);
8369 $hash = git_get_hash_by_path
($base, $file_name, "blob")
8370 or die_error
(404, "Cannot find file");
8372 die_error
(400, "No file name defined");
8374 } elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) {
8375 # blobs defined by non-textual hash id's can be cached
8379 defined(my $fd = git_cmd_pipe
"cat-file", "blob", $hash)
8380 or die_error
(500, "Open git-cat-file blob '$hash' failed");
8383 # content-type (can include charset)
8385 ($type, $leader) = blob_contenttype
($fd, $file_name, $type);
8387 # "save as" filename, even when no $file_name is given
8388 my $save_as = "$hash";
8389 if (defined $file_name) {
8390 $save_as = $file_name;
8391 } elsif ($type =~ m/^text\//) {
8395 # With XSS prevention on, blobs of all types except a few known safe
8396 # ones are served with "Content-Disposition: attachment" to make sure
8397 # they don't run in our security domain. For certain image types,
8398 # blob view writes an <img> tag referring to blob_plain view, and we
8399 # want to be sure not to break that by serving the image as an
8400 # attachment (though Firefox 3 doesn't seem to care).
8401 my $sandbox = $prevent_xss &&
8402 $type !~ m!^(?:text/[a-z]+|image/(?:gif|png|jpeg))(?:[ ;]|$)!;
8404 # serve text/* as text/plain
8406 ($type =~ m!^text/[a-z]+\b(.*)$! ||
8407 ($type =~ m!^[a-z]+/[a-z]\+xml\b(.*)$! && -T
$fd))) {
8409 $rest = defined $rest ?
$rest : '';
8410 $type = "text/plain$rest";
8415 -expires
=> $expires,
8416 -content_disposition
=>
8417 ($sandbox ?
'attachment' : 'inline')
8418 . '; filename="' . $save_as . '"');
8419 binmode STDOUT
, ':raw';
8421 print $leader if defined $leader;
8423 while (read($fd, $buf, 32768)) {
8426 binmode STDOUT
, ':utf8'; # as set at the beginning of gitweb.cgi
8434 if (!defined $hash) {
8435 if (defined $file_name) {
8436 my $base = $hash_base || git_get_head_hash
($project);
8437 $hash = git_get_hash_by_path
($base, $file_name, "blob")
8438 or die_error
(404, "Cannot find file");
8440 die_error
(400, "No file name defined");
8442 } elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) {
8443 # blobs defined by non-textual hash id's can be cached
8446 my $fullhash = git_get_full_hash
($project, "$hash^{blob}");
8447 die_error
(404, "No such blob") unless defined($fullhash);
8449 my $have_blame = gitweb_check_feature
('blame');
8450 defined(my $fd = git_cmd_pipe
"cat-file", "blob", $fullhash)
8451 or die_error
(500, "Couldn't cat $file_name, $hash");
8453 my $mimetype = blob_mimetype
($fd, $file_name);
8454 # use 'blob_plain' (aka 'raw') view for files that cannot be displayed
8455 if ($mimetype !~ m!^(?:text/|image/(?:gif|png|jpeg)$)! && -B
$fd) {
8457 return git_blob_plain
($mimetype);
8459 # we can have blame only for text/* mimetype
8460 $have_blame &&= ($mimetype =~ m!^text/!);
8462 my $highlight = gitweb_check_feature
('highlight') && defined $highlight_bin;
8463 my $syntax = guess_file_syntax
($fd, $mimetype, $file_name) if $highlight;
8464 my $highlight_mode_active;
8465 ($fd, $highlight_mode_active) = run_highlighter
($fd, $syntax) if $syntax;
8467 git_header_html
(undef, $expires);
8468 my $formats_nav = '';
8469 if (defined $hash_base && (my %co = parse_commit
($hash_base))) {
8470 if (defined $file_name) {
8472 $formats_nav .= tabspan
(
8473 $cgi->a({-href
=> href
(action
=>"blame", -replay
=>1),
8474 -class => "blamelink"},
8478 $formats_nav .= tabspan
(
8479 $cgi->a({-href
=> href
(action
=>"history", -replay
=>1)},
8482 $cgi->a({-href
=> href
(action
=>"blob_plain", -replay
=>1)},
8485 $cgi->a({-href
=> href
(action
=>"blob",
8486 hash_base
=>"HEAD", file_name
=>$file_name)},
8489 $formats_nav .= tabspan
(
8490 $cgi->a({-href
=> href
(action
=>"blob_plain", -replay
=>1)},
8493 git_print_page_nav
('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
8494 git_print_header_div
('commit', esc_html
($co{'title'}), $hash_base);
8496 git_print_page_nav
('',['commit','commitdiff','tree'], undef,undef,undef);
8497 print "<div class=\"title\">".esc_html
($hash)."</div>\n";
8499 git_print_page_path
($file_name, "blob", $hash_base);
8500 print "<div class=\"title_text\">\n" .
8501 "<table class=\"object_header\">\n";
8502 print "<tr><td>blob</td><td class=\"sha1\">$fullhash</td></tr>\n";
8505 print "<div class=\"page_body\">\n";
8506 if ($mimetype =~ m!^image/!) {
8507 print qq!<img
class="blob" type
="!.esc_attr($mimetype).qq!"!;
8509 print qq! alt
="!.esc_attr($file_name).qq!" title
="!.esc_attr($file_name).qq!"!;
8512 href(action=>"blob_plain
", hash=>$hash,
8513 hash_base=>$hash_base, file_name=>$file_name) .
8515 close $fd; # ignore likely EPIPE error from child
8518 while (my $line = to_utf8
(scalar <$fd>)) {
8521 $line = untabify
($line);
8522 printf qq!<div
class="pre"><a id
="l%i" href
="%s#l%i" class="linenr">%4i </a>%s</div
>\n!,
8523 $nr, esc_attr
(href
(-replay
=> 1)), $nr, $nr,
8524 $highlight_mode_active ? sanitize
($line) : esc_html
($line, -nbsp
=>1);
8527 or print "Reading blob failed.\n";
8534 if (!defined $hash_base) {
8535 $hash_base = "HEAD";
8537 if (!defined $hash) {
8538 if (defined $file_name) {
8539 $hash = git_get_hash_by_path
($hash_base, $file_name, "tree");
8544 die_error
(404, "No such tree") unless defined($hash);
8545 my $fullhash = git_get_full_hash
($project, "$hash^{tree}");
8546 die_error
(404, "No such tree") unless defined($fullhash);
8548 my $show_sizes = gitweb_check_feature
('show-sizes');
8549 my $have_blame = gitweb_check_feature
('blame');
8554 defined(my $fd = git_cmd_pipe
"ls-tree", '-z',
8555 ($show_sizes ?
'-l' : ()), @extra_options, $fullhash)
8556 or die_error
(500, "Open git-ls-tree failed");
8557 @entries = map { chomp; to_utf8
($_) } <$fd>;
8559 or die_error
(404, "Reading tree failed");
8564 if (defined $hash_base && (my %co = parse_commit
($hash_base))) {
8565 my $refs = git_get_references
();
8566 my $ref = format_ref_marker
($refs, $co{'id'});
8568 if (defined $file_name) {
8570 tabspan
($cgi->a({-href
=> href
(action
=>"history", -replay
=>1)},
8572 tabspan
($cgi->a({-href
=> href
(action
=>"tree",
8573 hash_base
=>"HEAD", file_name
=>$file_name)},
8576 my $snapshot_links = format_snapshot_links
($hash);
8577 if (defined $snapshot_links) {
8578 # FIXME: Should be available when we have no hash base as well.
8579 push @views_nav, $snapshot_links;
8581 git_print_page_nav
('tree','', $hash_base, undef, undef,
8582 join($barsep, @views_nav));
8583 git_print_header_div
('commit', esc_html
($co{'title'}), $hash_base, undef, $ref);
8585 git_print_page_nav
('tree',['commit','commitdiff'], undef,undef,$hash_base);
8587 print "<div class=\"title\">".esc_html
($hash)."</div>\n";
8589 if (defined $file_name) {
8590 $basedir = $file_name;
8591 if ($basedir ne '' && substr($basedir, -1) ne '/') {
8594 git_print_page_path
($file_name, 'tree', $hash_base);
8596 print "<div class=\"title_text\">\n" .
8597 "<table class=\"object_header\">\n";
8598 print "<tr><td>tree</td><td class=\"sha1\">$fullhash</td></tr>\n";
8601 print "<div class=\"page_body\">\n";
8602 print "<table class=\"tree\">\n";
8604 # '..' (top directory) link if possible
8605 if (defined $hash_base &&
8606 defined $file_name && $file_name =~ m![^/]+$!) {
8608 print "<tr class=\"dark\">\n";
8610 print "<tr class=\"light\">\n";
8614 my $up = $file_name;
8615 $up =~ s!/?[^/]+$!!;
8616 undef $up unless $up;
8617 # based on git_print_tree_entry
8618 print '<td class="mode">' . mode_str
('040000') . "</td>\n";
8619 print '<td class="size"> </td>'."\n" if $show_sizes;
8620 print '<td class="list">';
8621 print $cgi->a({-href
=> href
(action
=>"tree",
8622 hash_base
=>$hash_base,
8626 print "<td class=\"link\"></td>\n";
8630 foreach my $line (@entries) {
8631 my %t = parse_ls_tree_line
($line, -z
=> 1, -l
=> $show_sizes);
8634 print "<tr class=\"dark\">\n";
8636 print "<tr class=\"light\">\n";
8640 git_print_tree_entry
(\
%t, $basedir, $hash_base, $have_blame);
8644 print "</table>\n" .
8649 sub sanitize_for_filename
{
8653 $name =~ s/[^[:alnum:]_.-]//g;
8659 my ($project, $hash) = @_;
8661 # path/to/project.git -> project
8662 # path/to/project/.git -> project
8663 my $name = to_utf8
($project);
8664 $name =~ s
,([^/])/*\
.git
$,$1,;
8665 $name = sanitize_for_filename
(basename
($name));
8668 if ($hash =~ /^[0-9a-fA-F]+$/) {
8669 # shorten SHA-1 hash
8670 my $full_hash = git_get_full_hash
($project, $hash);
8671 if ($full_hash =~ /^$hash/ && length($hash) > 7) {
8672 $ver = git_get_short_hash
($project, $hash);
8674 } elsif ($hash =~ m!^refs/tags/(.*)$!) {
8675 # tags don't need shortened SHA-1 hash
8678 # branches and other need shortened SHA-1 hash
8679 my $strip_refs = join '|', map { quotemeta } get_branch_refs
();
8680 if ($hash =~ m!^refs/($strip_refs|remotes)/(.*)$!) {
8681 my $ref_dir = (defined $1) ?
$1 : '';
8684 $ref_dir = sanitize_for_filename
($ref_dir);
8685 # for refs neither in heads nor remotes we want to
8686 # add a ref dir to archive name
8687 if ($ref_dir ne '' and $ref_dir ne 'heads' and $ref_dir ne 'remotes') {
8688 $ver = $ref_dir . '-' . $ver;
8691 $ver .= '-' . git_get_short_hash
($project, $hash);
8693 # special case of sanitization for filename - we change
8694 # slashes to dots instead of dashes
8695 # in case of hierarchical branch names
8697 $ver =~ s/[^[:alnum:]_.-]//g;
8699 # name = project-version_string
8700 $name = "$name-$ver";
8702 return wantarray ?
($name, $name) : $name;
8705 sub exit_if_unmodified_since
{
8706 my ($latest_epoch) = @_;
8709 my $if_modified = $cgi->http('IF_MODIFIED_SINCE');
8710 if (defined $if_modified) {
8712 if (eval { require HTTP
::Date
; 1; }) {
8713 $since = HTTP
::Date
::str2time
($if_modified);
8714 } elsif (eval { require Time
::ParseDate
; 1; }) {
8715 $since = Time
::ParseDate
::parsedate
($if_modified, GMT
=> 1);
8717 if (defined $since && $latest_epoch <= $since) {
8718 my %latest_date = parse_date
($latest_epoch);
8720 -last_modified
=> $latest_date{'rfc2822'},
8721 -status
=> '304 Not Modified');
8728 my $format = $input_params{'snapshot_format'};
8729 if (!@snapshot_fmts) {
8730 die_error
(403, "Snapshots not allowed");
8732 # default to first supported snapshot format
8733 $format ||= $snapshot_fmts[0];
8734 if ($format !~ m/^[a-z0-9]+$/) {
8735 die_error
(400, "Invalid snapshot format parameter");
8736 } elsif (!exists($known_snapshot_formats{$format})) {
8737 die_error
(400, "Unknown snapshot format");
8738 } elsif ($known_snapshot_formats{$format}{'disabled'}) {
8739 die_error
(403, "Snapshot format not allowed");
8740 } elsif (!grep($_ eq $format, @snapshot_fmts)) {
8741 die_error
(403, "Unsupported snapshot format");
8744 my $type = git_get_type
("$hash^{}");
8746 die_error
(404, 'Object does not exist');
8747 } elsif ($type eq 'blob') {
8748 die_error
(400, 'Object is not a tree-ish');
8751 my ($name, $prefix) = snapshot_name
($project, $hash);
8752 my $filename = "$name$known_snapshot_formats{$format}{'suffix'}";
8754 my %co = parse_commit
($hash);
8755 exit_if_unmodified_since
($co{'committer_epoch'}) if %co;
8758 git_cmd
(), 'archive',
8759 "--format=$known_snapshot_formats{$format}{'format'}",
8760 "--prefix=$prefix/", $hash);
8761 if (exists $known_snapshot_formats{$format}{'compressor'}) {
8762 @cmd = ($posix_shell_bin, '-c', quote_command
(@cmd) .
8763 ' | ' . quote_command
(@
{$known_snapshot_formats{$format}{'compressor'}}));
8766 $filename =~ s/(["\\])/\\$1/g;
8769 %latest_date = parse_date
($co{'committer_epoch'}, $co{'committer_tz'});
8773 -type
=> $known_snapshot_formats{$format}{'type'},
8774 -content_disposition
=> 'inline; filename="' . $filename . '"',
8775 %co ?
(-last_modified
=> $latest_date{'rfc2822'}) : (),
8776 -status
=> '200 OK');
8778 defined(my $fd = cmd_pipe
@cmd)
8779 or die_error
(500, "Execute git-archive failed");
8781 binmode STDOUT
, ':raw';
8784 while (read($fd, $buf, 32768)) {
8787 binmode STDOUT
, ':utf8'; # as set at the beginning of gitweb.cgi
8792 sub git_log_generic
{
8793 my ($fmt_name, $body_subr, $base, $parent, $file_name, $file_hash) = @_;
8795 my $head = git_get_head_hash
($project);
8796 if (!defined $base) {
8799 if (!defined $page) {
8802 my $refs = git_get_references
();
8804 my $commit_hash = $base;
8805 if (defined $parent) {
8806 $commit_hash = "$parent..$base";
8809 parse_commits
($commit_hash, 101, (100 * $page),
8810 defined $file_name ?
($file_name, "--full-history") : ());
8813 if (!defined $file_hash && defined $file_name) {
8814 # some commits could have deleted file in question,
8815 # and not have it in tree, but one of them has to have it
8816 for (my $i = 0; $i < @commitlist; $i++) {
8817 $file_hash = git_get_hash_by_path
($commitlist[$i]{'id'}, $file_name);
8818 last if defined $file_hash;
8821 if (defined $file_hash) {
8822 $ftype = git_get_type
($file_hash);
8824 if (defined $file_name && !defined $ftype) {
8825 die_error
(500, "Unknown type of object");
8828 if (defined $file_name) {
8829 %co = parse_commit
($base)
8830 or die_error
(404, "Unknown commit object");
8835 if ($#commitlist >= 100) {
8837 $cgi->a({-href
=> href
(-replay
=>1, page
=>$page+1),
8838 -accesskey
=> "n", -title
=> "Alt-n"}, "next");
8841 my ($patch_max) = gitweb_get_feature
('patches');
8842 if ($patch_max && !defined $file_name) {
8843 if ($patch_max < 0 || @commitlist <= $patch_max) {
8844 $extra = $cgi->a({-href
=> href
(action
=>"patches", -replay
=>1)},
8848 my $paging_nav = format_log_nav
($fmt_name, $page, $#commitlist >= 100, $extra);
8851 local $action = 'log';
8854 git_print_page_nav
($fmt_name,'', $hash,$hash,$hash, $paging_nav);
8855 if (defined $file_name) {
8856 git_print_header_div
('commit', esc_html
($co{'title'}), $base);
8858 git_print_header_div
('summary', $project)
8860 git_print_page_path
($file_name, $ftype, $hash_base)
8861 if (defined $file_name);
8863 $body_subr->(\
@commitlist, 0, 99, $refs, $next_link,
8864 $file_name, $file_hash, $ftype);
8870 git_log_generic
('log', \
&git_log_body
,
8871 $hash, $hash_parent);
8875 $hash ||= $hash_base || "HEAD";
8876 my %co = parse_commit
($hash)
8877 or die_error
(404, "Unknown commit object");
8879 my $parent = $co{'parent'};
8880 my $parents = $co{'parents'}; # listref
8882 # we need to prepare $formats_nav before any parameter munging
8884 if (!defined $parent) {
8886 $formats_nav .= '<span class="parents none">(initial)</span>';
8887 } elsif (@
$parents == 1) {
8888 # single parent commit
8890 '<span class="parents single">(parent: ' .
8891 $cgi->a({-href
=> href
(action
=>"commit",
8893 esc_html
(substr($parent, 0, 7))) .
8898 '<span class="parents multiple">(merge: ' .
8900 $cgi->a({-href
=> href
(action
=>"commit",
8902 esc_html
(substr($_, 0, 7)));
8906 if (gitweb_check_feature
('patches') && @
$parents <= 1) {
8907 $formats_nav .= $barsep . tabspan
(
8908 $cgi->a({-href
=> href
(action
=>"patch", -replay
=>1)},
8912 if (!defined $parent) {
8916 defined(my $fd = git_cmd_pipe
"diff-tree", '-r', "--no-commit-id",
8918 (@
$parents <= 1 ?
$parent : '-c'),
8920 or die_error
(500, "Open git-diff-tree failed");
8921 @difftree = map { chomp; to_utf8
($_) } <$fd>;
8922 close $fd or die_error
(404, "Reading git-diff-tree failed");
8924 # non-textual hash id's can be cached
8926 if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
8929 my $refs = git_get_references
();
8930 my $ref = format_ref_marker
($refs, $co{'id'});
8932 git_header_html
(undef, $expires);
8933 git_print_page_nav
('commit', '',
8934 $hash, $co{'tree'}, $hash,
8937 if (defined $co{'parent'}) {
8938 git_print_header_div
('commitdiff', esc_html
($co{'title'}), $hash, undef, $ref);
8940 git_print_header_div
('tree', esc_html
($co{'title'}), $co{'tree'}, $hash, $ref);
8942 print "<div class=\"title_text\">\n" .
8943 "<table class=\"object_header\">\n";
8944 print "<tr><td>commit</td><td class=\"sha1\">$co{'id'}</td></tr>\n";
8945 git_print_authorship_rows
(\
%co);
8948 "<td class=\"sha1\">" .
8949 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$co{'tree'}, hash_base
=>$hash),
8950 class => "list"}, $co{'tree'}) .
8952 "<td class=\"link\">" .
8953 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$co{'tree'}, hash_base
=>$hash)},
8955 my $snapshot_links = format_snapshot_links
($hash);
8956 if (defined $snapshot_links) {
8957 print $barsep . $snapshot_links;
8962 foreach my $par (@
$parents) {
8965 "<td class=\"sha1\">" .
8966 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$par),
8967 class => "list"}, $par) .
8969 "<td class=\"link\">" .
8970 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$par)}, "commit") .
8972 $cgi->a({-href
=> href
(action
=>"commitdiff", hash
=>$hash, hash_parent
=>$par)}, "diff") .
8979 print "<div class=\"page_body\">\n";
8980 git_print_log
($co{'comment'});
8983 git_difftree_body
(\
@difftree, $hash, @
$parents);
8989 # object is defined by:
8990 # - hash or hash_base alone
8991 # - hash_base and file_name
8994 # - hash or hash_base alone
8995 if ($hash || ($hash_base && !defined $file_name)) {
8996 my $object_id = $hash || $hash_base;
8998 defined(my $fd = git_cmd_pipe
'cat-file', '-t', $object_id)
8999 or die_error
(404, "Object does not exist");
9001 defined $type && chomp $type;
9003 or die_error
(404, "Object does not exist");
9005 # - hash_base and file_name
9006 } elsif ($hash_base && defined $file_name) {
9007 $file_name =~ s
,/+$,,;
9009 system(git_cmd
(), "cat-file", '-e', $hash_base) == 0
9010 or die_error
(404, "Base object does not exist");
9012 # here errors should not happen
9013 defined(my $fd = git_cmd_pipe
"ls-tree", $hash_base, "--", $file_name)
9014 or die_error
(500, "Open git-ls-tree failed");
9015 my $line = to_utf8
(scalar <$fd>);
9018 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'
9019 unless ($line && $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t/) {
9020 die_error
(404, "File or directory for given base does not exist");
9025 die_error
(400, "Not enough information to find object");
9028 print $cgi->redirect(-uri
=> href
(action
=>$type, -full
=>1,
9029 hash
=>$hash, hash_base
=>$hash_base,
9030 file_name
=>$file_name),
9031 -status
=> '302 Found');
9035 my $format = shift || 'html';
9036 my $diff_style = $input_params{'diff_style'} || 'inline';
9043 # preparing $fd and %diffinfo for git_patchset_body
9045 if (defined $hash_base && defined $hash_parent_base) {
9046 if (defined $file_name) {
9048 defined($fd = git_cmd_pipe
"diff-tree", '-r', @diff_opts,
9049 $hash_parent_base, $hash_base,
9050 "--", (defined $file_parent ?
$file_parent : ()), $file_name)
9051 or die_error
(500, "Open git-diff-tree failed");
9052 @difftree = map { chomp; to_utf8
($_) } <$fd>;
9054 or die_error
(404, "Reading git-diff-tree failed");
9056 or die_error
(404, "Blob diff not found");
9058 } elsif (defined $hash &&
9059 $hash =~ /[0-9a-fA-F]{40}/) {
9060 # try to find filename from $hash
9062 # read filtered raw output
9063 defined($fd = git_cmd_pipe
"diff-tree", '-r', @diff_opts,
9064 $hash_parent_base, $hash_base, "--")
9065 or die_error
(500, "Open git-diff-tree failed");
9067 # ':100644 100644 03b21826... 3b93d5e7... M ls-files.c'
9069 grep { /^:[0-7]{6} [0-7]{6} [0-9a-fA-F]{40} $hash/ }
9070 map { chomp; to_utf8
($_) } <$fd>;
9072 or die_error
(404, "Reading git-diff-tree failed");
9074 or die_error
(404, "Blob diff not found");
9077 die_error
(400, "Missing one of the blob diff parameters");
9080 if (@difftree > 1) {
9081 die_error
(400, "Ambiguous blob diff specification");
9084 %diffinfo = parse_difftree_raw_line
($difftree[0]);
9085 $file_parent ||= $diffinfo{'from_file'} || $file_name;
9086 $file_name ||= $diffinfo{'to_file'};
9088 $hash_parent ||= $diffinfo{'from_id'};
9089 $hash ||= $diffinfo{'to_id'};
9091 # non-textual hash id's can be cached
9092 if ($hash_base =~ m/^[0-9a-fA-F]{40}$/ &&
9093 $hash_parent_base =~ m/^[0-9a-fA-F]{40}$/) {
9098 defined($fd = git_cmd_pipe
"diff-tree", '-r', @diff_opts,
9099 '-p', ($format eq 'html' ?
"--full-index" : ()),
9100 $hash_parent_base, $hash_base,
9101 "--", (defined $file_parent ?
$file_parent : ()), $file_name)
9102 or die_error
(500, "Open git-diff-tree failed");
9105 # old/legacy style URI -- not generated anymore since 1.4.3.
9107 die_error
('404 Not Found', "Missing one of the blob diff parameters")
9111 if ($format eq 'html') {
9113 $cgi->a({-href
=> href
(action
=>"blobdiff_plain", -replay
=>1)},
9115 $formats_nav .= diff_style_nav
($diff_style);
9116 git_header_html
(undef, $expires);
9117 if (defined $hash_base && (my %co = parse_commit
($hash_base))) {
9118 git_print_page_nav
('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
9119 git_print_header_div
('commit', esc_html
($co{'title'}), $hash_base);
9121 print "<div class=\"page_nav\"><br/>$formats_nav<br/></div>\n";
9122 print "<div class=\"title\">".esc_html
("$hash vs $hash_parent")."</div>\n";
9124 if (defined $file_name) {
9125 git_print_page_path
($file_name, "blob", $hash_base);
9127 print "<div class=\"page_path\"></div>\n";
9130 } elsif ($format eq 'plain') {
9132 -type
=> 'text/plain',
9133 -charset
=> 'utf-8',
9134 -expires
=> $expires,
9135 -content_disposition
=> 'inline; filename="' . "$file_name" . '.patch"');
9137 print "X-Git-Url: " . $cgi->self_url() . "\n\n";
9140 die_error
(400, "Unknown blobdiff format");
9144 if ($format eq 'html') {
9145 print "<div class=\"page_body\">\n";
9147 git_patchset_body
($fd, $diff_style,
9148 [ \
%diffinfo ], $hash_base, $hash_parent_base);
9151 print "</div>\n"; # class="page_body"
9155 while (my $line = to_utf8
(scalar <$fd>)) {
9156 $line =~ s!a/($hash|$hash_parent)!'a/'.esc_path($diffinfo{'from_file'})!eg;
9157 $line =~ s!b/($hash|$hash_parent)!'b/'.esc_path($diffinfo{'to_file'})!eg;
9161 last if $line =~ m!^\+\+\+!;
9170 sub git_blobdiff_plain
{
9171 git_blobdiff
('plain');
9174 # assumes that it is added as later part of already existing navigation,
9175 # so it returns "| foo | bar" rather than just "foo | bar"
9176 sub diff_style_nav
{
9177 my ($diff_style, $is_combined) = @_;
9178 $diff_style ||= 'inline';
9180 return "" if ($is_combined);
9182 my @styles = (inline
=> 'inline', 'sidebyside' => 'side by side');
9183 my %styles = @styles;
9185 @styles[ map { $_ * 2 } 0..$#styles/2 ];
9187 return $barsep . '<span class="diffstyles">' . join($barsep,
9190 '<span class="diffstyle selected">' . $styles{$_} . '</span>' :
9191 '<span class="diffstyle">' .
9192 $cgi->a({-href
=> href
(-replay
=>1, diff_style
=> $_)}, $styles{$_}) .
9194 } @styles) . '</span>';
9197 sub git_commitdiff
{
9199 my $format = $params{-format
} || 'html';
9200 my $diff_style = $input_params{'diff_style'} || 'inline';
9202 my ($patch_max) = gitweb_get_feature
('patches');
9203 if ($format eq 'patch') {
9204 die_error
(403, "Patch view not allowed") unless $patch_max;
9207 $hash ||= $hash_base || "HEAD";
9208 my %co = parse_commit
($hash)
9209 or die_error
(404, "Unknown commit object");
9211 # choose format for commitdiff for merge
9212 if (! defined $hash_parent && @
{$co{'parents'}} > 1) {
9213 $hash_parent = '--cc';
9215 # we need to prepare $formats_nav before almost any parameter munging
9217 if ($format eq 'html') {
9218 $formats_nav = tabspan
(
9219 $cgi->a({-href
=> href
(action
=>"commitdiff_plain", -replay
=>1)},
9221 if ($patch_max && @
{$co{'parents'}} <= 1) {
9222 $formats_nav .= $barsep . tabspan
(
9223 $cgi->a({-href
=> href
(action
=>"patch", -replay
=>1)},
9226 $formats_nav .= diff_style_nav
($diff_style, @
{$co{'parents'}} > 1);
9228 if (defined $hash_parent &&
9229 $hash_parent ne '-c' && $hash_parent ne '--cc') {
9230 # commitdiff with two commits given
9231 my $hash_parent_short = $hash_parent;
9232 if ($hash_parent =~ m/^[0-9a-fA-F]{40}$/) {
9233 $hash_parent_short = substr($hash_parent, 0, 7);
9235 $formats_nav .= $spcsep . '<span class="parents multiple">' .
9237 for (my $i = 0; $i < @
{$co{'parents'}}; $i++) {
9238 if ($co{'parents'}[$i] eq $hash_parent) {
9239 $formats_nav .= ' parent ' . ($i+1);
9243 $formats_nav .= ': ' .
9244 $cgi->a({-href
=> href
(-replay
=>1,
9245 hash
=>$hash_parent, hash_base
=>undef)},
9246 esc_html
($hash_parent_short)) .
9248 } elsif (!$co{'parent'}) {
9250 $formats_nav .= $spcsep . '<span class="parents none">(initial)</span>';
9251 } elsif (scalar @
{$co{'parents'}} == 1) {
9252 # single parent commit
9253 $formats_nav .= $spcsep .
9254 '<span class="parents single">(parent: ' .
9255 $cgi->a({-href
=> href
(-replay
=>1,
9256 hash
=>$co{'parent'}, hash_base
=>undef)},
9257 esc_html
(substr($co{'parent'}, 0, 7))) .
9261 if ($hash_parent eq '--cc') {
9262 $formats_nav .= $barsep . tabspan
(
9263 $cgi->a({-href
=> href
(-replay
=>1,
9264 hash
=>$hash, hash_parent
=>'-c')},
9266 } else { # $hash_parent eq '-c'
9267 $formats_nav .= $barsep . tabspan
(
9268 $cgi->a({-href
=> href
(-replay
=>1,
9269 hash
=>$hash, hash_parent
=>'--cc')},
9272 $formats_nav .= $spcsep .
9273 '<span class="parents multiple">(merge: ' .
9275 $cgi->a({-href
=> href
(-replay
=>1,
9276 hash
=>$_, hash_base
=>undef)},
9277 esc_html
(substr($_, 0, 7)));
9278 } @
{$co{'parents'}} ) .
9283 my $hash_parent_param = $hash_parent;
9284 if (!defined $hash_parent_param) {
9285 # --cc for multiple parents, --root for parentless
9286 $hash_parent_param =
9287 @
{$co{'parents'}} > 1 ?
'--cc' : $co{'parent'} || '--root';
9293 if ($format eq 'html') {
9294 defined($fd = git_cmd_pipe
"diff-tree", '-r', @diff_opts,
9295 "--no-commit-id", "--patch-with-raw", "--full-index",
9296 $hash_parent_param, $hash, "--")
9297 or die_error
(500, "Open git-diff-tree failed");
9299 while (my $line = to_utf8
(scalar <$fd>)) {
9301 # empty line ends raw part of diff-tree output
9303 push @difftree, scalar parse_difftree_raw_line
($line);
9306 } elsif ($format eq 'plain') {
9307 defined($fd = git_cmd_pipe
"diff-tree", '-r', @diff_opts,
9308 '-p', $hash_parent_param, $hash, "--")
9309 or die_error
(500, "Open git-diff-tree failed");
9310 } elsif ($format eq 'patch') {
9311 # For commit ranges, we limit the output to the number of
9312 # patches specified in the 'patches' feature.
9313 # For single commits, we limit the output to a single patch,
9314 # diverging from the git-format-patch default.
9315 my @commit_spec = ();
9317 if ($patch_max > 0) {
9318 push @commit_spec, "-$patch_max";
9320 push @commit_spec, '-n', "$hash_parent..$hash";
9322 if ($params{-single
}) {
9323 push @commit_spec, '-1';
9325 if ($patch_max > 0) {
9326 push @commit_spec, "-$patch_max";
9328 push @commit_spec, "-n";
9330 push @commit_spec, '--root', $hash;
9332 defined($fd = git_cmd_pipe
"format-patch", @diff_opts,
9333 '--encoding=utf8', '--stdout', @commit_spec)
9334 or die_error
(500, "Open git-format-patch failed");
9336 die_error
(400, "Unknown commitdiff format");
9339 # non-textual hash id's can be cached
9341 if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
9345 # write commit message
9346 if ($format eq 'html') {
9347 my $refs = git_get_references
();
9348 my $ref = format_ref_marker
($refs, $co{'id'});
9350 git_header_html
(undef, $expires);
9351 git_print_page_nav
('commitdiff','', $hash,$co{'tree'},$hash, $formats_nav);
9352 git_print_header_div
('commit', esc_html
($co{'title'}), $hash, undef, $ref);
9353 print "<div class=\"title_text\">\n" .
9354 "<table class=\"object_header\">\n";
9355 git_print_authorship_rows
(\
%co);
9358 print "<div class=\"page_body\">\n";
9359 if (@
{$co{'comment'}} > 1) {
9360 print "<div class=\"log\">\n";
9361 git_print_log
($co{'comment'}, -final_empty_line
=> 1, -remove_title
=> 1);
9362 print "</div>\n"; # class="log"
9365 } elsif ($format eq 'plain') {
9366 my $refs = git_get_references
("tags");
9367 my $tagname = git_get_rev_name_tags
($hash);
9368 my $filename = basename
($project) . "-$hash.patch";
9371 -type
=> 'text/plain',
9372 -charset
=> 'utf-8',
9373 -expires
=> $expires,
9374 -content_disposition
=> 'inline; filename="' . "$filename" . '"');
9375 my %ad = parse_date
($co{'author_epoch'}, $co{'author_tz'});
9376 print "From: " . to_utf8
($co{'author'}) . "\n";
9377 print "Date: $ad{'rfc2822'} ($ad{'tz_local'})\n";
9378 print "Subject: " . to_utf8
($co{'title'}) . "\n";
9380 print "X-Git-Tag: $tagname\n" if $tagname;
9381 print "X-Git-Url: " . $cgi->self_url() . "\n\n";
9383 foreach my $line (@
{$co{'comment'}}) {
9384 print to_utf8
($line) . "\n";
9387 } elsif ($format eq 'patch') {
9388 my $filename = basename
($project) . "-$hash.patch";
9391 -type
=> 'text/plain',
9392 -charset
=> 'utf-8',
9393 -expires
=> $expires,
9394 -content_disposition
=> 'inline; filename="' . "$filename" . '"');
9398 if ($format eq 'html') {
9399 my $use_parents = !defined $hash_parent ||
9400 $hash_parent eq '-c' || $hash_parent eq '--cc';
9401 git_difftree_body
(\
@difftree, $hash,
9402 $use_parents ? @
{$co{'parents'}} : $hash_parent);
9405 git_patchset_body
($fd, $diff_style,
9407 $use_parents ? @
{$co{'parents'}} : $hash_parent);
9409 print "</div>\n"; # class="page_body"
9412 } elsif ($format eq 'plain') {
9417 or print "Reading git-diff-tree failed\n";
9418 } elsif ($format eq 'patch') {
9423 or print "Reading git-format-patch failed\n";
9427 sub git_commitdiff_plain
{
9428 git_commitdiff
(-format
=> 'plain');
9431 # format-patch-style patches
9433 git_commitdiff
(-format
=> 'patch', -single
=> 1);
9437 git_commitdiff
(-format
=> 'patch');
9441 git_log_generic
('history', \
&git_history_body
,
9442 $hash_base, $hash_parent_base,
9447 $searchtype ||= 'commit';
9449 # check if appropriate features are enabled
9450 gitweb_check_feature
('search')
9451 or die_error
(403, "Search is disabled");
9452 if ($searchtype eq 'pickaxe') {
9453 # pickaxe may take all resources of your box and run for several minutes
9454 # with every query - so decide by yourself how public you make this feature
9455 gitweb_check_feature
('pickaxe')
9456 or die_error
(403, "Pickaxe search is disabled");
9458 if ($searchtype eq 'grep') {
9459 # grep search might be potentially CPU-intensive, too
9460 gitweb_check_feature
('grep')
9461 or die_error
(403, "Grep search is disabled");
9464 if (!defined $searchtext) {
9465 die_error
(400, "Text field is empty");
9467 if (!defined $hash) {
9468 $hash = git_get_head_hash
($project);
9470 my %co = parse_commit
($hash);
9472 die_error
(404, "Unknown commit object");
9474 if (!defined $page) {
9478 if ($searchtype eq 'commit' ||
9479 $searchtype eq 'author' ||
9480 $searchtype eq 'committer') {
9481 git_search_message
(%co);
9482 } elsif ($searchtype eq 'pickaxe') {
9483 git_search_changes
(%co);
9484 } elsif ($searchtype eq 'grep') {
9485 git_search_files
(%co);
9487 die_error
(400, "Unknown search type");
9491 sub git_search_help
{
9493 git_print_page_nav
('','', $hash,$hash,$hash);
9495 <div class="search_help">
9496 <p><strong>Pattern</strong> is by default a normal string that is matched precisely (but without
9497 regard to case, except in the case of pickaxe). However, when you check the <em>re</em> checkbox,
9498 the pattern entered is recognized as the POSIX extended
9499 <a href="http://en.wikipedia.org/wiki/Regular_expression">regular expression</a> (also case
9502 <dt><b>commit</b></dt>
9503 <dd>The commit messages and authorship information will be scanned for the given pattern.</dd>
9505 my $have_grep = gitweb_check_feature
('grep');
9508 <dt><b>grep</b></dt>
9509 <dd>All files in the currently selected tree (HEAD unless you are explicitly browsing
9510 a different one) are searched for the given pattern. On large trees, this search can take
9511 a while and put some strain on the server, so please use it with some consideration. Note that
9512 due to git-grep peculiarity, currently if regexp mode is turned off, the matches are
9513 case-sensitive.</dd>
9517 <dt><b>author</b></dt>
9518 <dd>Name and e-mail of the change author and date of birth of the patch will be scanned for the given pattern.</dd>
9519 <dt><b>committer</b></dt>
9520 <dd>Name and e-mail of the committer and date of commit will be scanned for the given pattern.</dd>
9522 my $have_pickaxe = gitweb_check_feature
('pickaxe');
9523 if ($have_pickaxe) {
9525 <dt><b>pickaxe</b></dt>
9526 <dd>All commits that caused the string to appear or disappear from any file (changes that
9527 added, removed or "modified" the string) will be listed. This search can take a while and
9528 takes a lot of strain on the server, so please use it wisely. Note that since you may be
9529 interested even in changes just changing the case as well, this search is case sensitive.</dd>
9532 print "</dl>\n</div>\n";
9537 git_log_generic
('shortlog', \
&git_shortlog_body
,
9538 $hash, $hash_parent);
9541 ## ......................................................................
9542 ## feeds (RSS, Atom; OPML)
9545 my $format = shift || 'atom';
9546 my $have_blame = gitweb_check_feature
('blame');
9548 # Atom: http://www.atomenabled.org/developers/syndication/
9549 # RSS: http://www.notestips.com/80256B3A007F2692/1/NAMO5P9UPQ
9550 if ($format ne 'rss' && $format ne 'atom') {
9551 die_error
(400, "Unknown web feed format");
9554 # log/feed of current (HEAD) branch, log of given branch, history of file/directory
9555 my $head = $hash || 'HEAD';
9556 my @commitlist = parse_commits
($head, 150, 0, $file_name);
9560 my $content_type = "application/$format+xml";
9561 if (defined $cgi->http('HTTP_ACCEPT') &&
9562 $cgi->Accept('text/xml') > $cgi->Accept($content_type)) {
9563 # browser (feed reader) prefers text/xml
9564 $content_type = 'text/xml';
9566 if (defined($commitlist[0])) {
9567 %latest_commit = %{$commitlist[0]};
9568 my $latest_epoch = $latest_commit{'committer_epoch'};
9569 exit_if_unmodified_since
($latest_epoch);
9570 %latest_date = parse_date
($latest_epoch, $latest_commit{'committer_tz'});
9573 -type
=> $content_type,
9574 -charset
=> 'utf-8',
9575 %latest_date ?
(-last_modified
=> $latest_date{'rfc2822'}) : (),
9576 -status
=> '200 OK');
9578 # Optimization: skip generating the body if client asks only
9579 # for Last-Modified date.
9580 return if ($cgi->request_method() eq 'HEAD');
9583 my $title = "$site_name - $project/$action";
9584 my $feed_type = 'log';
9585 if (defined $hash) {
9586 $title .= " - '$hash'";
9587 $feed_type = 'branch log';
9588 if (defined $file_name) {
9589 $title .= " :: $file_name";
9590 $feed_type = 'history';
9592 } elsif (defined $file_name) {
9593 $title .= " - $file_name";
9594 $feed_type = 'history';
9596 $title .= " $feed_type";
9597 $title = esc_html
($title);
9598 my $descr = git_get_project_description
($project);
9599 if (defined $descr) {
9600 $descr = esc_html
($descr);
9602 $descr = "$project " .
9603 ($format eq 'rss' ?
'RSS' : 'Atom') .
9606 my $owner = git_get_project_owner
($project);
9607 $owner = esc_html
($owner);
9611 if (defined $file_name) {
9612 $alt_url = href
(-full
=>1, action
=>"history", hash
=>$hash, file_name
=>$file_name);
9613 } elsif (defined $hash) {
9614 $alt_url = href
(-full
=>1, action
=>"log", hash
=>$hash);
9616 $alt_url = href
(-full
=>1, action
=>"summary");
9618 print qq!<?xml version
="1.0" encoding
="utf-8"?
>\n!;
9619 if ($format eq 'rss') {
9621 <rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
9624 print "<title>$title</title>\n" .
9625 "<link>$alt_url</link>\n" .
9626 "<description>$descr</description>\n" .
9627 "<language>en</language>\n" .
9628 # project owner is responsible for 'editorial' content
9629 "<managingEditor>$owner</managingEditor>\n";
9630 if (defined $logo || defined $favicon) {
9631 # prefer the logo to the favicon, since RSS
9632 # doesn't allow both
9633 my $img = esc_url
($logo || $favicon);
9635 "<url>$img</url>\n" .
9636 "<title>$title</title>\n" .
9637 "<link>$alt_url</link>\n" .
9641 print "<pubDate>$latest_date{'rfc2822'}</pubDate>\n";
9642 print "<lastBuildDate>$latest_date{'rfc2822'}</lastBuildDate>\n";
9644 print "<generator>gitweb v.$version/$git_version</generator>\n";
9645 } elsif ($format eq 'atom') {
9647 <feed xmlns="http://www.w3.org/2005/Atom">
9649 print "<title>$title</title>\n" .
9650 "<subtitle>$descr</subtitle>\n" .
9651 '<link rel="alternate" type="text/html" href="' .
9652 $alt_url . '" />' . "\n" .
9653 '<link rel="self" type="' . $content_type . '" href="' .
9654 $cgi->self_url() . '" />' . "\n" .
9655 "<id>" . href
(-full
=>1) . "</id>\n" .
9656 # use project owner for feed author
9657 '<author><name>'. email_obfuscate
($owner) . '</name></author>\n';
9658 if (defined $favicon) {
9659 print "<icon>" . esc_url
($favicon) . "</icon>\n";
9661 if (defined $logo) {
9662 # not twice as wide as tall: 72 x 27 pixels
9663 print "<logo>" . esc_url
($logo) . "</logo>\n";
9665 if (! %latest_date) {
9666 # dummy date to keep the feed valid until commits trickle in:
9667 print "<updated>1970-01-01T00:00:00Z</updated>\n";
9669 print "<updated>$latest_date{'iso-8601'}</updated>\n";
9671 print "<generator version='$version/$git_version'>gitweb</generator>\n";
9675 for (my $i = 0; $i <= $#commitlist; $i++) {
9676 my %co = %{$commitlist[$i]};
9677 my $commit = $co{'id'};
9678 # we read 150, we always show 30 and the ones more recent than 48 hours
9679 if (($i >= 20) && ((time - $co{'author_epoch'}) > 48*60*60)) {
9682 my %cd = parse_date
($co{'author_epoch'}, $co{'author_tz'});
9684 # get list of changed files
9685 defined(my $fd = git_cmd_pipe
"diff-tree", '-r', @diff_opts,
9686 $co{'parent'} || "--root",
9687 $co{'id'}, "--", (defined $file_name ?
$file_name : ()))
9689 my @difftree = map { chomp; to_utf8
($_) } <$fd>;
9693 # print element (entry, item)
9694 my $co_url = href
(-full
=>1, action
=>"commitdiff", hash
=>$commit);
9695 if ($format eq 'rss') {
9697 "<title>" . esc_html
($co{'title'}) . "</title>\n" .
9698 "<author>" . esc_html
($co{'author'}) . "</author>\n" .
9699 "<pubDate>$cd{'rfc2822'}</pubDate>\n" .
9700 "<guid isPermaLink=\"true\">$co_url</guid>\n" .
9701 "<link>$co_url</link>\n" .
9702 "<description>" . esc_html
($co{'title'}) . "</description>\n" .
9703 "<content:encoded>" .
9705 } elsif ($format eq 'atom') {
9707 "<title type=\"html\">" . esc_html
($co{'title'}) . "</title>\n" .
9708 "<updated>$cd{'iso-8601'}</updated>\n" .
9710 " <name>" . esc_html
($co{'author_name'}) . "</name>\n";
9711 if ($co{'author_email'}) {
9712 print " <email>" . esc_html
($co{'author_email'}) . "</email>\n";
9714 print "</author>\n" .
9715 # use committer for contributor
9717 " <name>" . esc_html
($co{'committer_name'}) . "</name>\n";
9718 if ($co{'committer_email'}) {
9719 print " <email>" . esc_html
($co{'committer_email'}) . "</email>\n";
9721 print "</contributor>\n" .
9722 "<published>$cd{'iso-8601'}</published>\n" .
9723 "<link rel=\"alternate\" type=\"text/html\" href=\"$co_url\" />\n" .
9724 "<id>$co_url</id>\n" .
9725 "<content type=\"xhtml\" xml:base=\"" . esc_url
($my_url) . "\">\n" .
9726 "<div xmlns=\"http://www.w3.org/1999/xhtml\">\n";
9728 my $comment = $co{'comment'};
9730 foreach my $line (@
$comment) {
9731 $line = esc_html
($line);
9734 print "</pre><ul>\n";
9735 foreach my $difftree_line (@difftree) {
9736 my %difftree = parse_difftree_raw_line
($difftree_line);
9737 next if !$difftree{'from_id'};
9739 my $file = $difftree{'file'} || $difftree{'to_file'};
9743 $cgi->a({-href
=> href
(-full
=>1, action
=>"blobdiff",
9744 hash
=>$difftree{'to_id'}, hash_parent
=>$difftree{'from_id'},
9745 hash_base
=>$co{'id'}, hash_parent_base
=>$co{'parent'},
9746 file_name
=>$file, file_parent
=>$difftree{'from_file'}),
9747 -title
=> "diff"}, 'D');
9749 print $cgi->a({-href
=> href
(-full
=>1, action
=>"blame",
9750 file_name
=>$file, hash_base
=>$commit),
9751 -class => "blamelink",
9752 -title
=> "blame"}, 'B');
9754 # if this is not a feed of a file history
9755 if (!defined $file_name || $file_name ne $file) {
9756 print $cgi->a({-href
=> href
(-full
=>1, action
=>"history",
9757 file_name
=>$file, hash
=>$commit),
9758 -title
=> "history"}, 'H');
9760 $file = esc_path
($file);
9764 if ($format eq 'rss') {
9765 print "</ul>]]>\n" .
9766 "</content:encoded>\n" .
9768 } elsif ($format eq 'atom') {
9769 print "</ul>\n</div>\n" .
9776 if ($format eq 'rss') {
9777 print "</channel>\n</rss>\n";
9778 } elsif ($format eq 'atom') {
9792 my @list = git_get_projects_list
($project_filter, $strict_export);
9794 die_error
(404, "No projects found");
9798 -type
=> 'text/xml',
9799 -charset
=> 'utf-8',
9800 -content_disposition
=> 'inline; filename="opml.xml"');
9802 my $title = esc_html
($site_name);
9803 my $filter = " within subdirectory ";
9804 if (defined $project_filter) {
9805 $filter .= esc_html
($project_filter);
9810 <?xml version="1.0" encoding="utf-8"?>
9811 <opml version="1.0">
9813 <title>$title OPML Export$filter</title>
9816 <outline text="git RSS feeds">
9819 foreach my $pr (@list) {
9821 my $head = git_get_head_hash
($proj{'path'});
9822 if (!defined $head) {
9825 $git_dir = "$projectroot/$proj{'path'}";
9826 my %co = parse_commit
($head);
9831 my $path = esc_html
(chop_str
($proj{'path'}, 25, 5));
9832 my $rss = href
('project' => $proj{'path'}, 'action' => 'rss', -full
=> 1);
9833 my $html = href
('project' => $proj{'path'}, 'action' => 'summary', -full
=> 1);
9834 print "<outline type=\"rss\" text=\"$path\" title=\"$path\" xmlUrl=\"$rss\" htmlUrl=\"$html\"/>\n";