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;
34 CGI
->compile() if $ENV{'MOD_PERL'};
37 our $version = "++GIT_VERSION++";
39 our ($my_url, $my_uri, $base_url, $path_info, $home_link);
44 local $ENV{PATH_INFO
} = ""
45 if exists($ENV{PATH_INFO
}) && (!defined($CGI::VERSION
) || $CGI::VERSION
< 3.34);
46 our $my_url = $cgi->url();
47 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;
191 ## Any of the urls in @git_base_url_list, @git_base_mirror_urls or
192 ## @git_base_push_urls may be an array ref instead of a scalar in which
193 ## case ${}[0] is the url and ${}[1] is an html fragment "hint" to display
194 ## right after the URL.
196 # list of git base URLs used for URL to where fetch project from,
197 # i.e. full URL is "$git_base_url/$project"
198 our @git_base_url_list = grep { $_ ne '' } ("++GITWEB_BASE_URL++");
200 ## For push projects (a .nofetch file exists OR gitweb.showpush is true)
201 ## @git_base_url_list entries are shown as "URL" and @git_base_push_urls
202 ## are shown as "push URL" and @git_base_mirror_urls are ignored.
203 ## For non-push projects, @git_base_url_list and @git_base_mirror_urls are shown
204 ## as "URL" and @git_base_push_urls are ignored.
206 # URLs shown for mirrors but not for push projects in addition to base_url_list,
207 # extended by the project name (i.e. full URL is "$git_mirror_url/$project")
208 our @git_base_mirror_urls = ();
210 # URLs designated for pushing new changes, extended by the
211 # project name (i.e. "$git_base_push_url[0]/$project")
212 our @git_base_push_urls = ();
214 # https hint html inserted right after any https push URL (undef for none)
215 # ignored if the url already has its own hint
216 # this is supported for backwards compatibility but is now deprecated in favor
217 # of using an array ref in the @git_base_push_urls list instead
218 our $https_hint_html = undef;
220 # default blob_plain mimetype and default charset for text/plain blob
221 our $default_blob_plain_mimetype = 'application/octet-stream';
222 our $default_text_plain_charset = undef;
224 # file to use for guessing MIME types before trying /etc/mime.types
225 # (relative to the current git repository)
226 our $mimetypes_file = undef;
228 # assume this charset if line contains non-UTF-8 characters;
229 # it should be valid encoding (see Encoding::Supported(3pm) for list),
230 # for which encoding all byte sequences are valid, for example
231 # 'iso-8859-1' aka 'latin1' (it is decoded without checking, so it
232 # could be even 'utf-8' for the old behavior)
233 our $fallback_encoding = 'latin1';
235 # rename detection options for git-diff and git-diff-tree
236 # - default is '-M', with the cost proportional to
237 # (number of removed files) * (number of new files).
238 # - more costly is '-C' (which implies '-M'), with the cost proportional to
239 # (number of changed files + number of removed files) * (number of new files)
240 # - even more costly is '-C', '--find-copies-harder' with cost
241 # (number of files in the original tree) * (number of new files)
242 # - one might want to include '-B' option, e.g. '-B', '-M'
243 our @diff_opts = ('-M'); # taken from git_commit
245 # cache directory (relative to $GIT_DIR) for project-specific html page caches.
246 # the directory must exist and be writable by the process running gitweb.
247 # additionally some actions must be selected for caching in %html_cache_actions
248 # - default is 'htmlcache'
249 our $html_cache_dir = 'htmlcache';
251 # which actions to cache in $html_cache_dir
252 # if $html_cache_dir exists (relative to $GIT_DIR) and is writable by the
253 # process running gitweb, then any actions selected here will have their output
254 # cached and the cache file will be returned instead of regenerating the page
255 # if it exists. For this to be useful, an external process must create the
256 # 'changed' file (if it does not already exist) in the $html_cache_dir whenever
257 # the project information has been changed. Alternatively it may create a
258 # "$action.changed" file (if it does not exist) instead to limit the changes
259 # to just "$action" instead of any action. If 'changed' or "$action.changed"
260 # exist, then the cached version will never be used for "$action" and a new
261 # cache page will be regenerated (and the "changed" files removed as appropriate).
263 # Additionally if $projlist_cache_lifetime is > 0 AND the forks feature has been
264 # enabled ('$feature{'forks'}{'default'} = [1];') then additionally an external
265 # process must create the 'forkchange' file or update its timestamp if it already
266 # exists whenever a fork is added to or removed from the project (as well as
267 # create the 'changed' or "$action.changed" file). Otherwise the "forks"
268 # section on the summary page may remain out-of-date indefinately.
271 # currently only caching of the summary page is supported
272 # - to enable caching of the summary page use:
273 # $html_cache_actions{'summary'} = 1;
274 our %html_cache_actions = ();
276 # utility to automatically produce a default README.html if README.html is
277 # enabled and it does not exist or is 0 bytes in length. If this is set to an
278 # executable utility that takes an absolute path to a .git directory as its
279 # first argument and outputs an HTML fragment to use for README.html, then
280 # it will be called when README.html is enabled but empty or missing.
281 our $git_automatic_readme_html = undef;
283 # Disables features that would allow repository owners to inject script into
285 our $prevent_xss = 0;
287 # Path to a POSIX shell. Needed to run $highlight_bin and a snapshot compressor.
288 # Only used when highlight is enabled or snapshots with compressors are enabled.
289 our $posix_shell_bin = "++POSIX_SHELL_BIN++";
291 # Path to the highlight executable to use (must be the one from
292 # http://www.andre-simon.de due to assumptions about parameters and output).
293 # Useful if highlight is not installed on your webserver's PATH.
294 # [Default: highlight]
295 our $highlight_bin = "++HIGHLIGHT_BIN++";
297 # Whether to include project list on the gitweb front page; 0 means yes,
298 # 1 means no list but show tag cloud if enabled (all projects still need
299 # to be scanned, unless the info is cached), 2 means no list and no tag cloud
301 our $frontpage_no_project_list = 0;
303 # projects list cache for busy sites with many projects;
304 # if you set this to non-zero, it will be used as the cached
305 # index lifetime in minutes
307 # the cached list version is stored in $cache_dir/$cache_name and can
308 # be tweaked by other scripts running with the same uid as gitweb -
309 # use this ONLY at secure installations; only single gitweb project
310 # root per system is supported, unless you tweak configuration!
311 our $projlist_cache_lifetime = 0; # in minutes
312 # FHS compliant $cache_dir would be "/var/cache/gitweb"
314 (defined $ENV{'TMPDIR'} ?
$ENV{'TMPDIR'} : '/tmp').'/gitweb';
315 our $projlist_cache_name = 'gitweb.index.cache';
316 our $cache_grpshared = 0;
318 # information about snapshot formats that gitweb is capable of serving
319 our %known_snapshot_formats = (
321 # 'display' => display name,
322 # 'type' => mime type,
323 # 'suffix' => filename suffix,
324 # 'format' => --format for git-archive,
325 # 'compressor' => [compressor command and arguments]
326 # (array reference, optional)
327 # 'disabled' => boolean (optional)}
330 'display' => 'tar.gz',
331 'type' => 'application/x-gzip',
332 'suffix' => '.tar.gz',
334 'compressor' => ['gzip', '-n']},
337 'display' => 'tar.bz2',
338 'type' => 'application/x-bzip2',
339 'suffix' => '.tar.bz2',
341 'compressor' => ['bzip2']},
344 'display' => 'tar.xz',
345 'type' => 'application/x-xz',
346 'suffix' => '.tar.xz',
348 'compressor' => ['xz'],
353 'type' => 'application/x-zip',
358 # Aliases so we understand old gitweb.snapshot values in repository
360 our %known_snapshot_format_aliases = (
365 # backward compatibility: legacy gitweb config support
366 'x-gzip' => undef, 'gz' => undef,
367 'x-bzip2' => undef, 'bz2' => undef,
368 'x-zip' => undef, '' => undef,
371 # Pixel sizes for icons and avatars. If the default font sizes or lineheights
372 # are changed, it may be appropriate to change these values too via
379 # Used to set the maximum load that we will still respond to gitweb queries.
380 # If server load exceed this value then return "503 server busy" error.
381 # If gitweb cannot determined server load, it is taken to be 0.
382 # Leave it undefined (or set to 'undef') to turn off load checking.
385 # configuration for 'highlight' (http://www.andre-simon.de/)
387 our %highlight_basename = (
390 'SConstruct' => 'py', # SCons equivalent of Makefile
391 'Makefile' => 'make',
392 'makefile' => 'make',
393 'GNUmakefile' => 'make',
394 'BSDmakefile' => 'make',
396 # match by shebang regex
397 our %highlight_shebang = (
398 # Each entry has a key which is the syntax to use and
399 # a value which is either a qr regex or an array of qr regexs to match
400 # against the first 128 (less if the blob is shorter) BYTES of the blob.
401 # We match /usr/bin/env items separately to require "/usr/bin/env" and
402 # allow a limited subset of NAME=value items to appear.
403 'awk' => [ qr
,^#!\s*/(?:\w+/)*(?:[gnm]?awk)(?:\s|$),mo,
404 qr
,^#!\s*/usr/bin/env\s+(?:\w+=\w*\s+)*(?:[gnm]?awk)(?:\s|$),mo ],
405 'make' => [ qr
,^#!\s*/(?:\w+/)*(?:g?make)(?:\s|$),mo,
406 qr
,^#!\s*/usr/bin/env\s+(?:\w+=\w*\s+)*(?:g?make)(?:\s|$),mo ],
407 'php' => [ qr
,^#!\s*/(?:\w+/)*(?:php)(?:\s|$),mo,
408 qr
,^#!\s*/usr/bin/env\s+(?:\w+=\w*\s+)*(?:php)(?:\s|$),mo ],
409 'pl' => [ qr
,^#!\s*/(?:\w+/)*(?:perl)(?:\s|$),mo,
410 qr
,^#!\s*/usr/bin/env\s+(?:\w+=\w*\s+)*(?:perl)(?:\s|$),mo ],
411 'py' => [ qr
,^#!\s*/(?:\w+/)*(?:python)(?:\s|$),mo,
412 qr
,^#!\s*/usr/bin/env\s+(?:\w+=\w*\s+)*(?:python)(?:\s|$),mo ],
413 'sh' => [ qr
,^#!\s*/(?:\w+/)*(?:[bd]ash|t?csh|[akz]?sh)(?:\s|$),mo,
414 qr
,^#!\s*/usr/bin/env\s+(?:\w+=\w*\s+)*(?:[bd]ash|t?csh|[akz]?sh)(?:\s|$),mo ],
415 'rb' => [ qr
,^#!\s*/(?:\w+/)*(?:ruby)(?:\s|$),mo,
416 qr
,^#!\s*/usr/bin/env\s+(?:\w+=\w*\s+)*(?:ruby)(?:\s|$),mo ],
419 our %highlight_ext = (
420 # main extensions, defining name of syntax;
421 # see files in /usr/share/highlight/langDefs/ directory
422 (map { $_ => $_ } qw(
423 4gl a4c abnf abp ada agda ahk ampl amtrix applescript arc
424 arm as asm asp aspect ats au3 avenue awk bat bb bbcode bib
425 bms bnf boo c cb cfc chl clipper clojure clp cob cs css d
426 diff dot dylan e ebnf erl euphoria exp f90 flx for frink fs
427 go haskell hcl html httpd hx icl icn idl idlang ili
428 inc_luatex ini inp io iss j java js jsp lbn ldif lgt lhs
429 lisp lotos ls lsl lua ly make mel mercury mib miranda ml mo
430 mod2 mod3 mpl ms mssql n nas nbc nice nrx nsi nut nxc oberon
431 objc octave oorexx os oz pas php pike pl pl1 pov pro
432 progress ps ps1 psl pure py pyx q qmake qu r rb rebol rexx
433 rnc s sas sc scala scilab sh sma smalltalk sml sno spec spn
434 sql sybase tcl tcsh tex ttcn3 vala vb verilog vhd xml xpp y
436 # alternate extensions, see /etc/highlight/filetypes.conf
437 (map { $_ => '4gl' } qw(informix)),
438 (map { $_ => 'a4c' } qw(ascend)),
439 (map { $_ => 'abp' } qw(abp4)),
440 (map { $_ => 'ada' } qw(a adb ads gnad)),
441 (map { $_ => 'ahk' } qw(autohotkey)),
442 (map { $_ => 'ampl' } qw(dat run)),
443 (map { $_ => 'amtrix' } qw(hnd s4 s4h s4t t4)),
444 (map { $_ => 'as' } qw(actionscript)),
445 (map { $_ => 'asm' } qw(29k 68s 68x a51 assembler x68 x86)),
446 (map { $_ => 'asp' } qw(asa)),
447 (map { $_ => 'aspect' } qw(was wud)),
448 (map { $_ => 'ats' } qw(dats)),
449 (map { $_ => 'au3' } qw(autoit)),
450 (map { $_ => 'bat' } qw(cmd)),
451 (map { $_ => 'bb' } qw(blitzbasic)),
452 (map { $_ => 'bib' } qw(bibtex)),
453 (map { $_ => 'c' } qw(c++ cc cpp cu cxx h hh hpp hxx)),
454 (map { $_ => 'cb' } qw(clearbasic)),
455 (map { $_ => 'cfc' } qw(cfm coldfusion)),
456 (map { $_ => 'chl' } qw(chill)),
457 (map { $_ => 'cob' } qw(cbl cobol)),
458 (map { $_ => 'cs' } qw(csharp)),
459 (map { $_ => 'diff' } qw(patch)),
460 (map { $_ => 'dot' } qw(graphviz)),
461 (map { $_ => 'e' } qw(eiffel se)),
462 (map { $_ => 'erl' } qw(erlang hrl)),
463 (map { $_ => 'euphoria' } qw(eu ew ex exu exw wxu)),
464 (map { $_ => 'exp' } qw(express)),
465 (map { $_ => 'f90' } qw(f95)),
466 (map { $_ => 'flx' } qw(felix)),
467 (map { $_ => 'for' } qw(f f77 ftn)),
468 (map { $_ => 'fs' } qw(fsharp fsx)),
469 (map { $_ => 'haskell' } qw(hs)),
470 (map { $_ => 'html' } qw(htm xhtml)),
471 (map { $_ => 'hx' } qw(haxe)),
472 (map { $_ => 'icl' } qw(clean)),
473 (map { $_ => 'icn' } qw(icon)),
474 (map { $_ => 'ili' } qw(interlis)),
475 (map { $_ => 'inp' } qw(fame)),
476 (map { $_ => 'iss' } qw(innosetup)),
477 (map { $_ => 'j' } qw(jasmin)),
478 (map { $_ => 'java' } qw(groovy grv)),
479 (map { $_ => 'lbn' } qw(luban)),
480 (map { $_ => 'lgt' } qw(logtalk)),
481 (map { $_ => 'lisp' } qw(cl clisp el lsp sbcl scom)),
482 (map { $_ => 'ls' } qw(lotus)),
483 (map { $_ => 'lsl' } qw(lindenscript)),
484 (map { $_ => 'ly' } qw(lilypond)),
485 (map { $_ => 'make' } qw(mak mk kmk)),
486 (map { $_ => 'mel' } qw(maya)),
487 (map { $_ => 'mib' } qw(smi snmp)),
488 (map { $_ => 'ml' } qw(mli ocaml)),
489 (map { $_ => 'mo' } qw(modelica)),
490 (map { $_ => 'mod2' } qw(def mod)),
491 (map { $_ => 'mod3' } qw(i3 m3)),
492 (map { $_ => 'mpl' } qw(maple)),
493 (map { $_ => 'n' } qw(nemerle)),
494 (map { $_ => 'nas' } qw(nasal)),
495 (map { $_ => 'nrx' } qw(netrexx)),
496 (map { $_ => 'nsi' } qw(nsis)),
497 (map { $_ => 'nut' } qw(squirrel)),
498 (map { $_ => 'oberon' } qw(ooc)),
499 (map { $_ => 'objc' } qw(M m mm)),
500 (map { $_ => 'php' } qw(php3 php4 php5 php6)),
501 (map { $_ => 'pike' } qw(pmod)),
502 (map { $_ => 'pl' } qw(perl plex plx pm)),
503 (map { $_ => 'pl1' } qw(bdy ff fp fpp rpp sf sp spb spe spp sps wf wp wpb wpp wps)),
504 (map { $_ => 'progress' } qw(i p w)),
505 (map { $_ => 'py' } qw(python)),
506 (map { $_ => 'pyx' } qw(pyrex)),
507 (map { $_ => 'rb' } qw(pp rjs ruby)),
508 (map { $_ => 'rexx' } qw(rex rx the)),
509 (map { $_ => 'sc' } qw(paradox)),
510 (map { $_ => 'scilab' } qw(sce sci)),
511 (map { $_ => 'sh' } qw(bash ebuild eclass ksh zsh)),
512 (map { $_ => 'sma' } qw(small)),
513 (map { $_ => 'smalltalk' } qw(gst sq st)),
514 (map { $_ => 'sno' } qw(snobal)),
515 (map { $_ => 'sybase' } qw(sp)),
516 (map { $_ => 'tcl' } qw(itcl wish)),
517 (map { $_ => 'tex' } qw(cls sty)),
518 (map { $_ => 'vb' } qw(bas basic bi vbs)),
519 (map { $_ => 'verilog' } qw(v)),
520 (map { $_ => 'xml' } qw(dtd ecf ent hdr hub jnlp nrm plist resx sgm sgml svg tld vxml wml xsd xsl)),
521 (map { $_ => 'y' } qw(bison)),
524 # You define site-wide feature defaults here; override them with
525 # $GITWEB_CONFIG as necessary.
528 # 'sub' => feature-sub (subroutine),
529 # 'override' => allow-override (boolean),
530 # 'default' => [ default options...] (array reference)}
532 # if feature is overridable (it means that allow-override has true value),
533 # then feature-sub will be called with default options as parameters;
534 # return value of feature-sub indicates if to enable specified feature
536 # if there is no 'sub' key (no feature-sub), then feature cannot be
539 # use gitweb_get_feature(<feature>) to retrieve the <feature> value
540 # (an array) or gitweb_check_feature(<feature>) to check if <feature>
543 # Enable the 'blame' blob view, showing the last commit that modified
544 # each line in the file. This can be very CPU-intensive.
546 # To enable system wide have in $GITWEB_CONFIG
547 # $feature{'blame'}{'default'} = [1];
548 # To have project specific config enable override in $GITWEB_CONFIG
549 # $feature{'blame'}{'override'} = 1;
550 # and in project config gitweb.blame = 0|1;
552 'sub' => sub { feature_bool
('blame', @_) },
556 # Enable the 'incremental blame' blob view, which uses javascript to
557 # incrementally show the revisions of lines as they are discovered
558 # in the history. It is better for large histories, files and slow
559 # servers, but requires javascript in the client and can slow down the
560 # browser on large files.
562 # To enable system wide have in $GITWEB_CONFIG
563 # $feature{'blame_incremental'}{'default'} = [1];
564 # To have project specific config enable override in $GITWEB_CONFIG
565 # $feature{'blame_incremental'}{'override'} = 1;
566 # and in project config gitweb.blame_incremental = 0|1;
567 'blame_incremental' => {
568 'sub' => sub { feature_bool
('blame_incremental', @_) },
572 # Enable the 'snapshot' link, providing a compressed archive of any
573 # tree. This can potentially generate high traffic if you have large
576 # Value is a list of formats defined in %known_snapshot_formats that
578 # To disable system wide have in $GITWEB_CONFIG
579 # $feature{'snapshot'}{'default'} = [];
580 # To have project specific config enable override in $GITWEB_CONFIG
581 # $feature{'snapshot'}{'override'} = 1;
582 # and in project config, a comma-separated list of formats or "none"
583 # to disable. Example: gitweb.snapshot = tbz2,zip;
585 'sub' => \
&feature_snapshot
,
587 'default' => ['tgz']},
589 # Enable text search, which will list the commits which match author,
590 # committer or commit text to a given string. Enabled by default.
591 # Project specific override is not supported.
593 # Note that this controls all search features, which means that if
594 # it is disabled, then 'grep' and 'pickaxe' search would also be
600 # Enable regular expression search. Enabled by default.
601 # Note that you need to have 'search' feature enabled too.
603 # Note that this affects all git search features, which means that if
604 # it is disabled, none of the git search options will allow a regular
605 # expression (the "RE" checkbox) to be used. However, the project
606 # list search is unaffected by this setting (it uses Perl to do the
607 # matching not Git) and will always allow a regular expression to
608 # be used (by checking the box) regardless of this setting.
610 'sub' => sub { feature_bool
('regexp', @_) },
614 # Enable grep search, which will list the files in currently selected
615 # tree containing the given string. Enabled by default. This can be
616 # potentially CPU-intensive, of course.
617 # Note that you need to have 'search' feature enabled too.
619 # To enable system wide have in $GITWEB_CONFIG
620 # $feature{'grep'}{'default'} = [1];
621 # To have project specific config enable override in $GITWEB_CONFIG
622 # $feature{'grep'}{'override'} = 1;
623 # and in project config gitweb.grep = 0|1;
625 'sub' => sub { feature_bool
('grep', @_) },
629 # Enable the pickaxe search, which will list the commits that modified
630 # a given string in a file. This can be practical and quite faster
631 # alternative to 'blame', but still potentially CPU-intensive.
632 # Note that you need to have 'search' feature enabled too.
634 # To enable system wide have in $GITWEB_CONFIG
635 # $feature{'pickaxe'}{'default'} = [1];
636 # To have project specific config enable override in $GITWEB_CONFIG
637 # $feature{'pickaxe'}{'override'} = 1;
638 # and in project config gitweb.pickaxe = 0|1;
640 'sub' => sub { feature_bool
('pickaxe', @_) },
644 # Enable showing size of blobs in a 'tree' view, in a separate
645 # column, similar to what 'ls -l' does. This cost a bit of IO.
647 # To disable system wide have in $GITWEB_CONFIG
648 # $feature{'show-sizes'}{'default'} = [0];
649 # To have project specific config enable override in $GITWEB_CONFIG
650 # $feature{'show-sizes'}{'override'} = 1;
651 # and in project config gitweb.showsizes = 0|1;
653 'sub' => sub { feature_bool
('showsizes', @_) },
657 # Make gitweb use an alternative format of the URLs which can be
658 # more readable and natural-looking: project name is embedded
659 # directly in the path and the query string contains other
660 # auxiliary information. All gitweb installations recognize
661 # URL in either format; this configures in which formats gitweb
664 # To enable system wide have in $GITWEB_CONFIG
665 # $feature{'pathinfo'}{'default'} = [1];
666 # Project specific override is not supported.
668 # Note that you will need to change the default location of CSS,
669 # favicon, logo and possibly other files to an absolute URL. Also,
670 # if gitweb.cgi serves as your indexfile, you will need to force
671 # $my_uri to contain the script name in your $GITWEB_CONFIG (and you
672 # will also likely want to set $home_link if you're setting $my_uri).
677 # Make gitweb consider projects in project root subdirectories
678 # to be forks of existing projects. Given project $projname.git,
679 # projects matching $projname/*.git will not be shown in the main
680 # projects list, instead a '+' mark will be added to $projname
681 # there and a 'forks' view will be enabled for the project, listing
682 # all the forks. If project list is taken from a file, forks have
683 # to be listed after the main project.
685 # To enable system wide have in $GITWEB_CONFIG
686 # $feature{'forks'}{'default'} = [1];
687 # Project specific override is not supported.
692 # Insert custom links to the action bar of all project pages.
693 # This enables you mainly to link to third-party scripts integrating
694 # into gitweb; e.g. git-browser for graphical history representation
695 # or custom web-based repository administration interface.
697 # The 'default' value consists of a list of triplets in the form
698 # (label, link, position) where position is the label after which
699 # to insert the link and link is a format string where %n expands
700 # to the project name, %f to the project path within the filesystem,
701 # %h to the current hash (h gitweb parameter) and %b to the current
702 # hash base (hb gitweb parameter); %% expands to %. %e expands to the
703 # project name where all needed characters have been %-escaped.
705 # To enable system wide have in $GITWEB_CONFIG e.g.
706 # $feature{'actions'}{'default'} = [('graphiclog',
707 # '/git-browser/by-commit.html?r=%n', 'summary')];
708 # Project specific override is not supported.
713 # Allow gitweb scan project content tags of project repository,
714 # and display the popular Web 2.0-ish "tag cloud" near the projects
715 # list. Note that this is something COMPLETELY different from the
718 # gitweb by itself can show existing tags, but it does not handle
719 # tagging itself; you need to do it externally, outside gitweb.
720 # The format is described in git_get_project_ctags() subroutine.
721 # You may want to install the HTML::TagCloud Perl module to get
722 # a pretty tag cloud instead of just a list of tags.
724 # To enable system wide have in $GITWEB_CONFIG
725 # $feature{'ctags'}{'default'} = [1];
726 # Project specific override is not supported.
728 # A value of 0 means no ctags display or editing. A value of
729 # 1 enables ctags display but never editing. A non-empty value
730 # that is not a string of digits enables ctags display AND the
731 # ability to add tags using a form that uses method POST and
732 # an action value set to the configured 'ctags' value.
737 # The maximum number of patches in a patchset generated in patch
738 # view. Set this to 0 or undef to disable patch view, or to a
739 # negative number to remove any limit.
741 # To disable system wide have in $GITWEB_CONFIG
742 # $feature{'patches'}{'default'} = [0];
743 # To have project specific config enable override in $GITWEB_CONFIG
744 # $feature{'patches'}{'override'} = 1;
745 # and in project config gitweb.patches = 0|n;
746 # where n is the maximum number of patches allowed in a patchset.
748 'sub' => \
&feature_patches
,
752 # Avatar support. When this feature is enabled, views such as
753 # shortlog or commit will display an avatar associated with
754 # the email of the committer(s) and/or author(s).
756 # Currently available providers are gravatar and picon.
757 # If an unknown provider is specified, the feature is disabled.
759 # Gravatar depends on Digest::MD5.
760 # Picon currently relies on the indiana.edu database.
762 # To enable system wide have in $GITWEB_CONFIG
763 # $feature{'avatar'}{'default'} = ['<provider>'];
764 # where <provider> is either gravatar or picon.
765 # To have project specific config enable override in $GITWEB_CONFIG
766 # $feature{'avatar'}{'override'} = 1;
767 # and in project config gitweb.avatar = <provider>;
769 'sub' => \
&feature_avatar
,
773 # Enable displaying how much time and how many git commands
774 # it took to generate and display page. Disabled by default.
775 # Project specific override is not supported.
780 # Enable turning some links into links to actions which require
781 # JavaScript to run (like 'blame_incremental'). Not enabled by
782 # default. Project specific override is currently not supported.
783 'javascript-actions' => {
787 # Enable and configure ability to change common timezone for dates
788 # in gitweb output via JavaScript. Enabled by default.
789 # Project specific override is not supported.
790 'javascript-timezone' => {
793 'local', # default timezone: 'utc', 'local', or '(-|+)HHMM' format,
794 # or undef to turn off this feature
795 'gitweb_tz', # name of cookie where to store selected timezone
796 'datetime', # CSS class used to mark up dates for manipulation
799 # Syntax highlighting support. This is based on Daniel Svensson's
800 # and Sham Chukoury's work in gitweb-xmms2.git.
801 # It requires the 'highlight' program present in $PATH,
802 # and therefore is disabled by default.
804 # To enable system wide have in $GITWEB_CONFIG
805 # $feature{'highlight'}{'default'} = [1];
808 'sub' => sub { feature_bool
('highlight', @_) },
812 # Enable displaying of remote heads in the heads list
814 # To enable system wide have in $GITWEB_CONFIG
815 # $feature{'remote_heads'}{'default'} = [1];
816 # To have project specific config enable override in $GITWEB_CONFIG
817 # $feature{'remote_heads'}{'override'} = 1;
818 # and in project config gitweb.remoteheads = 0|1;
820 'sub' => sub { feature_bool
('remote_heads', @_) },
824 # Enable showing branches under other refs in addition to heads
826 # To set system wide extra branch refs have in $GITWEB_CONFIG
827 # $feature{'extra-branch-refs'}{'default'} = ['dirs', 'of', 'choice'];
828 # To have project specific config enable override in $GITWEB_CONFIG
829 # $feature{'extra-branch-refs'}{'override'} = 1;
830 # and in project config gitweb.extrabranchrefs = dirs of choice
831 # Every directory is separated with whitespace.
833 'extra-branch-refs' => {
834 'sub' => \
&feature_extra_branch_refs
,
839 sub gitweb_get_feature
{
841 return unless exists $feature{$name};
842 my ($sub, $override, @defaults) = (
843 $feature{$name}{'sub'},
844 $feature{$name}{'override'},
845 @
{$feature{$name}{'default'}});
846 # project specific override is possible only if we have project
847 our $git_dir; # global variable, declared later
848 if (!$override || !defined $git_dir) {
852 warn "feature $name is not overridable";
855 return $sub->(@defaults);
858 # A wrapper to check if a given feature is enabled.
859 # With this, you can say
861 # my $bool_feat = gitweb_check_feature('bool_feat');
862 # gitweb_check_feature('bool_feat') or somecode;
866 # my ($bool_feat) = gitweb_get_feature('bool_feat');
867 # (gitweb_get_feature('bool_feat'))[0] or somecode;
869 sub gitweb_check_feature
{
870 return (gitweb_get_feature
(@_))[0];
876 my ($val) = git_get_project_config
($key, '--bool');
880 } elsif ($val eq 'true') {
882 } elsif ($val eq 'false') {
887 sub feature_snapshot
{
890 my ($val) = git_get_project_config
('snapshot');
893 @fmts = ($val eq 'none' ?
() : split /\s*[,\s]\s*/, $val);
899 sub feature_patches
{
900 my @val = (git_get_project_config
('patches', '--int'));
910 my @val = (git_get_project_config
('avatar'));
912 return @val ?
@val : @_;
915 sub feature_extra_branch_refs
{
916 my (@branch_refs) = @_;
917 my $values = git_get_project_config
('extrabranchrefs');
920 $values = config_to_multi
($values);
922 foreach my $value (@
{$values}) {
923 push @branch_refs, split /\s+/, $value;
930 # checking HEAD file with -e is fragile if the repository was
931 # initialized long time ago (i.e. symlink HEAD) and was pack-ref'ed
933 sub check_head_link
{
935 return 0 unless -d
"$dir/objects" && -x _
;
936 return 0 unless -d
"$dir/refs" && -x _
;
937 my $headfile = "$dir/HEAD";
938 return -l
$headfile ?
939 readlink($headfile) =~ /^refs\/heads\
// : -f
$headfile;
942 sub check_export_ok
{
944 return (check_head_link
($dir) &&
945 (!$export_ok || -e
"$dir/$export_ok") &&
946 (!$export_auth_hook || $export_auth_hook->($dir)));
949 # process alternate names for backward compatibility
950 # filter out unsupported (unknown) snapshot formats
951 sub filter_snapshot_fmts
{
955 exists $known_snapshot_format_aliases{$_} ?
956 $known_snapshot_format_aliases{$_} : $_} @fmts;
958 exists $known_snapshot_formats{$_} &&
959 !$known_snapshot_formats{$_}{'disabled'}} @fmts;
962 sub filter_and_validate_refs
{
964 my %unique_refs = ();
966 foreach my $ref (@refs) {
967 die_error
(500, "Invalid ref '$ref' in 'extra-branch-refs' feature") unless (is_valid_ref_format
($ref));
968 # 'heads' are added implicitly in get_branch_refs().
969 $unique_refs{$ref} = 1 if ($ref ne 'heads');
971 return sort keys %unique_refs;
974 # If it is set to code reference, it is code that it is to be run once per
975 # request, allowing updating configurations that change with each request,
976 # while running other code in config file only once.
978 # Otherwise, if it is false then gitweb would process config file only once;
979 # if it is true then gitweb config would be run for each request.
980 our $per_request_config = 1;
982 # If true and fileno STDIN is 0 and getsockname succeeds and getpeername fails
983 # with ENOTCONN, then FCGI mode will be activated automatically in just the
984 # same way as though the --fcgi option had been given instead.
987 # read and parse gitweb config file given by its parameter.
988 # returns true on success, false on recoverable error, allowing
989 # to chain this subroutine, using first file that exists.
990 # dies on errors during parsing config file, as it is unrecoverable.
991 sub read_config_file
{
992 my $filename = shift;
993 return unless defined $filename;
994 # die if there are errors parsing config file
1003 our ($GITWEB_CONFIG, $GITWEB_CONFIG_SYSTEM, $GITWEB_CONFIG_COMMON);
1004 sub evaluate_gitweb_config
{
1005 our $GITWEB_CONFIG = $ENV{'GITWEB_CONFIG'} || "++GITWEB_CONFIG++";
1006 our $GITWEB_CONFIG_SYSTEM = $ENV{'GITWEB_CONFIG_SYSTEM'} || "++GITWEB_CONFIG_SYSTEM++";
1007 our $GITWEB_CONFIG_COMMON = $ENV{'GITWEB_CONFIG_COMMON'} || "++GITWEB_CONFIG_COMMON++";
1009 # Protect against duplications of file names, to not read config twice.
1010 # Only one of $GITWEB_CONFIG and $GITWEB_CONFIG_SYSTEM is used, so
1011 # there possibility of duplication of filename there doesn't matter.
1012 $GITWEB_CONFIG = "" if ($GITWEB_CONFIG eq $GITWEB_CONFIG_COMMON);
1013 $GITWEB_CONFIG_SYSTEM = "" if ($GITWEB_CONFIG_SYSTEM eq $GITWEB_CONFIG_COMMON);
1015 # Common system-wide settings for convenience.
1016 # Those settings can be ovverriden by GITWEB_CONFIG or GITWEB_CONFIG_SYSTEM.
1017 read_config_file
($GITWEB_CONFIG_COMMON);
1019 # Use first config file that exists. This means use the per-instance
1020 # GITWEB_CONFIG if exists, otherwise use GITWEB_SYSTEM_CONFIG.
1021 read_config_file
($GITWEB_CONFIG) and return;
1022 read_config_file
($GITWEB_CONFIG_SYSTEM);
1026 our $to_utf8_pipe_command = '';
1028 sub evaluate_encoding
{
1029 my $requested = $fallback_encoding || 'ISO-8859-1';
1030 my $obj = Encode
::find_encoding
($requested) or
1031 die_error
(400, "Requested fallback encoding not found");
1032 if ($obj->name eq 'iso-8859-1') {
1033 # Use Windows-1252 instead as required by the HTML 5 standard
1034 my $altobj = Encode
::find_encoding
('Windows-1252');
1035 $obj = $altobj if $altobj;
1037 $encode_object = $obj;
1038 my $nm = lc($encode_object->name);
1039 unless ($nm eq 'cp1252' || $nm eq 'ascii' || $nm eq 'utf8' ||
1040 $nm =~ /^utf-8/ || $nm =~ /^iso-8859-/) {
1041 $to_utf8_pipe_command =
1042 quote_command
($^X
, '-CO', '-MEncode=decode,FB_DEFAULT', '-pse',
1043 '$_ = decode($fe, $_, FB_DEFAULT) if !utf8::decode($_);',
1044 '--', "-fe=$fallback_encoding")." | ";
1048 sub evaluate_email_obfuscate
{
1051 if (!$email && eval { require HTML
::Email
::Obfuscate
; 1 }) {
1052 $email = HTML
::Email
::Obfuscate
->new(lite
=> 1);
1056 # Get loadavg of system, to compare against $maxload.
1057 # Currently it requires '/proc/loadavg' present to get loadavg;
1058 # if it is not present it returns 0, which means no load checking.
1060 if( -e
'/proc/loadavg' ){
1061 open my $fd, '<', '/proc/loadavg'
1063 my @load = split(/\s+/, scalar <$fd>);
1066 # The first three columns measure CPU and IO utilization of the last one,
1067 # five, and 10 minute periods. The fourth column shows the number of
1068 # currently running processes and the total number of processes in the m/n
1069 # format. The last column displays the last process ID used.
1070 return $load[0] || 0;
1072 # additional checks for load average should go here for things that don't export
1078 # version of the core git binary
1080 our $git_vernum = "0"; # guaranteed to always match /^\d+(\.\d+)*$/
1081 sub evaluate_git_version
{
1082 $git_version = $version; # don't leak system information to attackers
1083 $git_vernum eq "0" or return; # don't run it again
1086 if (defined(my $fd = cmd_pipe
$GIT, '--version')) {
1089 $number_of_git_cmds++;
1091 $git_vernum = $1 if defined($vers) && $vers =~ /git\s+version\s+(\d+(?:\.\d+)*)$/io;
1095 if (defined $maxload && get_loadavg
() > $maxload) {
1096 die_error
(503, "The load average on the server is too high");
1100 # ======================================================================
1101 # input validation and dispatch
1103 # input parameters can be collected from a variety of sources (presently, CGI
1104 # and PATH_INFO), so we define an %input_params hash that collects them all
1105 # together during validation: this allows subsequent uses (e.g. href()) to be
1106 # agnostic of the parameter origin
1108 our %input_params = ();
1110 # input parameters are stored with the long parameter name as key. This will
1111 # also be used in the href subroutine to convert parameters to their CGI
1112 # equivalent, and since the href() usage is the most frequent one, we store
1113 # the name -> CGI key mapping here, instead of the reverse.
1115 # XXX: Warning: If you touch this, check the search form for updating,
1118 our @cgi_param_mapping = (
1122 file_parent
=> "fp",
1124 hash_parent
=> "hp",
1126 hash_parent_base
=> "hpb",
1131 snapshot_format
=> "sf",
1133 extra_options
=> "opt",
1134 search_use_regexp
=> "sr",
1137 project_filter
=> "pf",
1138 # this must be last entry (for manipulation from JavaScript)
1141 our %cgi_param_mapping = @cgi_param_mapping;
1143 # we will also need to know the possible actions, for validation
1145 "blame" => \
&git_blame
,
1146 "blame_incremental" => \
&git_blame_incremental
,
1147 "blame_data" => \
&git_blame_data
,
1148 "blobdiff" => \
&git_blobdiff
,
1149 "blobdiff_plain" => \
&git_blobdiff_plain
,
1150 "blob" => \
&git_blob
,
1151 "blob_plain" => \
&git_blob_plain
,
1152 "commitdiff" => \
&git_commitdiff
,
1153 "commitdiff_plain" => \
&git_commitdiff_plain
,
1154 "commit" => \
&git_commit
,
1155 "forks" => \
&git_forks
,
1156 "heads" => \
&git_heads
,
1157 "history" => \
&git_history
,
1159 "patch" => \
&git_patch
,
1160 "patches" => \
&git_patches
,
1161 "refs" => \
&git_refs
,
1162 "remotes" => \
&git_remotes
,
1164 "atom" => \
&git_atom
,
1165 "search" => \
&git_search
,
1166 "search_help" => \
&git_search_help
,
1167 "shortlog" => \
&git_shortlog
,
1168 "summary" => \
&git_summary
,
1170 "tags" => \
&git_tags
,
1171 "tree" => \
&git_tree
,
1172 "snapshot" => \
&git_snapshot
,
1173 "object" => \
&git_object
,
1174 # those below don't need $project
1175 "opml" => \
&git_opml
,
1176 "frontpage" => \
&git_frontpage
,
1177 "project_list" => \
&git_project_list
,
1178 "project_index" => \
&git_project_index
,
1181 # the only actions we will allow to be cached
1182 my %supported_cache_actions;
1183 BEGIN {%supported_cache_actions = map {( $_ => 1 )} qw(summary)}
1185 # finally, we have the hash of allowed extra_options for the commands that
1187 our %allowed_options = (
1188 "--no-merges" => [ qw(rss atom log shortlog history) ],
1191 # fill %input_params with the CGI parameters. All values except for 'opt'
1192 # should be single values, but opt can be an array. We should probably
1193 # build an array of parameters that can be multi-valued, but since for the time
1194 # being it's only this one, we just single it out
1195 sub evaluate_query_params
{
1198 while (my ($name, $symbol) = each %cgi_param_mapping) {
1199 if ($symbol eq 'opt') {
1200 $input_params{$name} = [ map { decode_utf8
($_) } $cgi->multi_param($symbol) ];
1202 $input_params{$name} = decode_utf8
($cgi->param($symbol));
1206 # Backwards compatibility - by_tag= <=> t=
1207 if ($input_params{'ctag'}) {
1208 $input_params{'ctag_filter'} = $input_params{'ctag'};
1212 # now read PATH_INFO and update the parameter list for missing parameters
1213 sub evaluate_path_info
{
1214 return if defined $input_params{'project'};
1215 return if !$path_info;
1216 $path_info =~ s
,^/+,,;
1217 return if !$path_info;
1219 # find which part of PATH_INFO is project
1220 my $project = $path_info;
1221 $project =~ s
,/+$,,;
1222 while ($project && !check_head_link
("$projectroot/$project")) {
1223 $project =~ s
,/*[^/]*$,,;
1225 return unless $project;
1226 $input_params{'project'} = $project;
1228 # do not change any parameters if an action is given using the query string
1229 return if $input_params{'action'};
1230 $path_info =~ s
,^\Q
$project\E
/*,,;
1232 # next, check if we have an action
1233 my $action = $path_info;
1234 $action =~ s
,/.*$,,;
1235 if (exists $actions{$action}) {
1236 $path_info =~ s
,^$action/*,,;
1237 $input_params{'action'} = $action;
1240 # list of actions that want hash_base instead of hash, but can have no
1241 # pathname (f) parameter
1247 # we want to catch, among others
1248 # [$hash_parent_base[:$file_parent]..]$hash_parent[:$file_name]
1249 my ($parentrefname, $parentpathname, $refname, $pathname) =
1250 ($path_info =~ /^(?:(.+?)(?::(.+))?\.\.)?([^:]+?)?(?::(.+))?$/);
1252 # first, analyze the 'current' part
1253 if (defined $pathname) {
1254 # we got "branch:filename" or "branch:dir/"
1255 # we could use git_get_type(branch:pathname), but:
1256 # - it needs $git_dir
1257 # - it does a git() call
1258 # - the convention of terminating directories with a slash
1259 # makes it superfluous
1260 # - embedding the action in the PATH_INFO would make it even
1262 $pathname =~ s
,^/+,,;
1263 if (!$pathname || substr($pathname, -1) eq "/") {
1264 $input_params{'action'} ||= "tree";
1265 $pathname =~ s
,/$,,;
1267 # the default action depends on whether we had parent info
1269 if ($parentrefname) {
1270 $input_params{'action'} ||= "blobdiff_plain";
1272 $input_params{'action'} ||= "blob_plain";
1275 $input_params{'hash_base'} ||= $refname;
1276 $input_params{'file_name'} ||= $pathname;
1277 } elsif (defined $refname) {
1278 # we got "branch". In this case we have to choose if we have to
1279 # set hash or hash_base.
1281 # Most of the actions without a pathname only want hash to be
1282 # set, except for the ones specified in @wants_base that want
1283 # hash_base instead. It should also be noted that hand-crafted
1284 # links having 'history' as an action and no pathname or hash
1285 # set will fail, but that happens regardless of PATH_INFO.
1286 if (defined $parentrefname) {
1287 # if there is parent let the default be 'shortlog' action
1288 # (for http://git.example.com/repo.git/A..B links); if there
1289 # is no parent, dispatch will detect type of object and set
1290 # action appropriately if required (if action is not set)
1291 $input_params{'action'} ||= "shortlog";
1293 if ($input_params{'action'} &&
1294 grep { $_ eq $input_params{'action'} } @wants_base) {
1295 $input_params{'hash_base'} ||= $refname;
1297 $input_params{'hash'} ||= $refname;
1301 # next, handle the 'parent' part, if present
1302 if (defined $parentrefname) {
1303 # a missing pathspec defaults to the 'current' filename, allowing e.g.
1304 # someproject/blobdiff/oldrev..newrev:/filename
1305 if ($parentpathname) {
1306 $parentpathname =~ s
,^/+,,;
1307 $parentpathname =~ s
,/$,,;
1308 $input_params{'file_parent'} ||= $parentpathname;
1310 $input_params{'file_parent'} ||= $input_params{'file_name'};
1312 # we assume that hash_parent_base is wanted if a path was specified,
1313 # or if the action wants hash_base instead of hash
1314 if (defined $input_params{'file_parent'} ||
1315 grep { $_ eq $input_params{'action'} } @wants_base) {
1316 $input_params{'hash_parent_base'} ||= $parentrefname;
1318 $input_params{'hash_parent'} ||= $parentrefname;
1322 # for the snapshot action, we allow URLs in the form
1323 # $project/snapshot/$hash.ext
1324 # where .ext determines the snapshot and gets removed from the
1325 # passed $refname to provide the $hash.
1327 # To be able to tell that $refname includes the format extension, we
1328 # require the following two conditions to be satisfied:
1329 # - the hash input parameter MUST have been set from the $refname part
1330 # of the URL (i.e. they must be equal)
1331 # - the snapshot format MUST NOT have been defined already (e.g. from
1333 # It's also useless to try any matching unless $refname has a dot,
1334 # so we check for that too
1335 if (defined $input_params{'action'} &&
1336 $input_params{'action'} eq 'snapshot' &&
1337 defined $refname && index($refname, '.') != -1 &&
1338 $refname eq $input_params{'hash'} &&
1339 !defined $input_params{'snapshot_format'}) {
1340 # We loop over the known snapshot formats, checking for
1341 # extensions. Allowed extensions are both the defined suffix
1342 # (which includes the initial dot already) and the snapshot
1343 # format key itself, with a prepended dot
1344 while (my ($fmt, $opt) = each %known_snapshot_formats) {
1345 my $hash = $refname;
1346 unless ($hash =~ s/(\Q$opt->{'suffix'}\E|\Q.$fmt\E)$//) {
1350 # a valid suffix was found, so set the snapshot format
1351 # and reset the hash parameter
1352 $input_params{'snapshot_format'} = $fmt;
1353 $input_params{'hash'} = $hash;
1354 # we also set the format suffix to the one requested
1355 # in the URL: this way a request for e.g. .tgz returns
1356 # a .tgz instead of a .tar.gz
1357 $known_snapshot_formats{$fmt}{'suffix'} = $sfx;
1363 our ($action, $project, $file_name, $file_parent, $hash, $hash_parent, $hash_base,
1364 $hash_parent_base, @extra_options, $page, $searchtype, $search_use_regexp,
1365 $searchtext, $search_regexp, $project_filter);
1366 sub evaluate_and_validate_params
{
1367 our $action = $input_params{'action'};
1368 if (defined $action) {
1369 if (!is_valid_action
($action)) {
1370 die_error
(400, "Invalid action parameter");
1374 # parameters which are pathnames
1375 our $project = $input_params{'project'};
1376 if (defined $project) {
1377 if (!is_valid_project
($project)) {
1379 die_error
(404, "No such project");
1383 our $project_filter = $input_params{'project_filter'};
1384 if (defined $project_filter) {
1385 if (!is_valid_pathname
($project_filter)) {
1386 die_error
(404, "Invalid project_filter parameter");
1390 our $file_name = $input_params{'file_name'};
1391 if (defined $file_name) {
1392 if (!is_valid_pathname
($file_name)) {
1393 die_error
(400, "Invalid file parameter");
1397 our $file_parent = $input_params{'file_parent'};
1398 if (defined $file_parent) {
1399 if (!is_valid_pathname
($file_parent)) {
1400 die_error
(400, "Invalid file parent parameter");
1404 # parameters which are refnames
1405 our $hash = $input_params{'hash'};
1406 if (defined $hash) {
1407 if (!is_valid_refname
($hash)) {
1408 die_error
(400, "Invalid hash parameter");
1412 our $hash_parent = $input_params{'hash_parent'};
1413 if (defined $hash_parent) {
1414 if (!is_valid_refname
($hash_parent)) {
1415 die_error
(400, "Invalid hash parent parameter");
1419 our $hash_base = $input_params{'hash_base'};
1420 if (defined $hash_base) {
1421 if (!is_valid_refname
($hash_base)) {
1422 die_error
(400, "Invalid hash base parameter");
1426 our @extra_options = @
{$input_params{'extra_options'}};
1427 # @extra_options is always defined, since it can only be (currently) set from
1428 # CGI, and $cgi->param() returns the empty array in array context if the param
1430 foreach my $opt (@extra_options) {
1431 if (not exists $allowed_options{$opt}) {
1432 die_error
(400, "Invalid option parameter");
1434 if (not grep(/^$action$/, @
{$allowed_options{$opt}})) {
1435 die_error
(400, "Invalid option parameter for this action");
1439 our $hash_parent_base = $input_params{'hash_parent_base'};
1440 if (defined $hash_parent_base) {
1441 if (!is_valid_refname
($hash_parent_base)) {
1442 die_error
(400, "Invalid hash parent base parameter");
1447 our $page = $input_params{'page'};
1448 if (defined $page) {
1449 if ($page =~ m/[^0-9]/) {
1450 die_error
(400, "Invalid page parameter");
1454 our $searchtype = $input_params{'searchtype'};
1455 if (defined $searchtype) {
1456 if ($searchtype =~ m/[^a-z]/) {
1457 die_error
(400, "Invalid searchtype parameter");
1461 our $search_use_regexp = $input_params{'search_use_regexp'};
1463 our $searchtext = $input_params{'searchtext'};
1464 our $search_regexp = undef;
1465 if (defined $searchtext) {
1466 if (length($searchtext) < 2) {
1467 die_error
(403, "At least two characters are required for search parameter");
1469 if ($search_use_regexp) {
1470 $search_regexp = $searchtext;
1471 if (!eval { qr/$search_regexp/; 1; }) {
1472 (my $error = $@
) =~ s/ at \S+ line \d+.*\n?//;
1473 die_error
(400, "Invalid search regexp '$search_regexp'",
1477 $search_regexp = quotemeta $searchtext;
1482 # path to the current git repository
1484 sub evaluate_git_dir
{
1485 our $git_dir = $project ?
"$projectroot/$project" : undef;
1488 our (@snapshot_fmts, $git_avatar, @extra_branch_refs);
1489 sub configure_gitweb_features
{
1490 # list of supported snapshot formats
1491 our @snapshot_fmts = gitweb_get_feature
('snapshot');
1492 @snapshot_fmts = filter_snapshot_fmts
(@snapshot_fmts);
1494 # check that the avatar feature is set to a known provider name,
1495 # and for each provider check if the dependencies are satisfied.
1496 # if the provider name is invalid or the dependencies are not met,
1497 # reset $git_avatar to the empty string.
1498 our ($git_avatar) = gitweb_get_feature
('avatar');
1499 if ($git_avatar eq 'gravatar') {
1500 $git_avatar = '' unless (eval { require Digest
::MD5
; 1; });
1501 } elsif ($git_avatar eq 'picon') {
1507 our @extra_branch_refs = gitweb_get_feature
('extra-branch-refs');
1508 @extra_branch_refs = filter_and_validate_refs
(@extra_branch_refs);
1511 sub get_branch_refs
{
1512 return ('heads', @extra_branch_refs);
1515 # custom error handler: 'die <message>' is Internal Server Error
1516 sub handle_errors_html
{
1517 my $msg = shift; # it is already HTML escaped
1519 # to avoid infinite loop where error occurs in die_error,
1520 # change handler to default handler, disabling handle_errors_html
1521 set_message
("Error occurred when inside die_error:\n$msg");
1523 # you cannot jump out of die_error when called as error handler;
1524 # the subroutine set via CGI::Carp::set_message is called _after_
1525 # HTTP headers are already written, so it cannot write them itself
1526 die_error
(undef, undef, $msg, -error_handler
=> 1, -no_http_header
=> 1);
1528 set_message
(\
&handle_errors_html
);
1530 our $shown_stale_message = 0;
1531 our $cache_dump = undef;
1532 our $cache_dump_mtime = undef;
1535 my $cache_mode_active;
1537 if (!defined $action) {
1538 if (defined $hash) {
1539 $action = git_get_type
($hash);
1540 $action or die_error
(404, "Object does not exist");
1541 } elsif (defined $hash_base && defined $file_name) {
1542 $action = git_get_type
("$hash_base:$file_name");
1543 $action or die_error
(404, "File or directory does not exist");
1544 } elsif (defined $project) {
1545 $action = 'summary';
1547 $action = 'frontpage';
1550 if (!defined($actions{$action})) {
1551 die_error
(400, "Unknown action");
1553 if ($action !~ m/^(?:opml|frontpage|project_list|project_index)$/ &&
1555 die_error
(400, "Project needed");
1558 my $cached_page = $supported_cache_actions{$action}
1559 ? cached_action_page
($action)
1561 goto DUMPCACHE
if $cached_page;
1562 local *SAVEOUT
= *STDOUT
;
1563 $cache_mode_active = $supported_cache_actions{$action}
1564 ? cached_action_start
($action)
1567 configure_gitweb_features
();
1568 $actions{$action}->();
1570 return unless $cache_mode_active;
1572 $cached_page = cached_action_finish
($action);
1577 $cache_mode_active = 0;
1578 # Avoid any extra unwanted encoding steps as $cached_page is raw bytes
1579 binmode STDOUT
, ':raw';
1580 our $fcgi_raw_mode = 1;
1581 print expand_gitweb_pi
($cached_page, time);
1582 binmode STDOUT
, ':utf8'; # as set at the beginning of gitweb.cgi
1587 our $t0 = [ gettimeofday
() ]
1589 our $number_of_git_cmds = 0;
1592 our $first_request = 1;
1593 our $evaluate_uri_force = undef;
1597 # Only allow GET and HEAD methods
1598 if (!$ENV{'REQUEST_METHOD'} || ($ENV{'REQUEST_METHOD'} ne 'GET' && $ENV{'REQUEST_METHOD'} ne 'HEAD')) {
1600 Status: 405 Method Not Allowed
1601 Content-Type: text/plain
1604 405 Method Not Allowed
1610 &$evaluate_uri_force() if $evaluate_uri_force;
1611 if ($per_request_config) {
1612 if (ref($per_request_config) eq 'CODE') {
1613 $per_request_config->();
1614 } elsif (!$first_request) {
1615 evaluate_gitweb_config
();
1616 evaluate_email_obfuscate
();
1621 # $projectroot and $projects_list might be set in gitweb config file
1622 $projects_list ||= $projectroot;
1624 evaluate_query_params
();
1625 evaluate_path_info
();
1626 evaluate_and_validate_params
();
1632 our $is_last_request = sub { 1 };
1633 our ($pre_dispatch_hook, $post_dispatch_hook, $pre_listen_hook);
1637 our $fcgi_nproc_active = 0;
1638 our $fcgi_raw_mode = 0;
1641 my $stdinfno = fileno STDIN
;
1642 return 0 unless defined $stdinfno && $stdinfno == 0;
1643 return 0 unless getsockname STDIN
;
1644 return 0 if getpeername STDIN
;
1645 return $!{ENOTCONN
}?
1:0;
1647 sub configure_as_fcgi
{
1648 return if $fcgi_mode;
1653 # We have gone to great effort to make sure that all incoming data has
1654 # been converted from whatever format it was in into UTF-8. We have
1655 # even taken care to make sure the output handle is in ':utf8' mode.
1656 # Now along comes FCGI and blows it with:
1658 # Use of wide characters in FCGI::Stream::PRINT is deprecated
1659 # and will stop wprking[sic] in a future version of FCGI
1661 # To fix this we replace FCGI::Stream::PRINT with our own routine that
1662 # first encodes everything and then calls the original routine, but
1663 # not if $fcgi_raw_mode is true (then we just call the original routine).
1665 # Note that we could do this by using utf8::is_utf8 to check instead
1666 # of having a $fcgi_raw_mode global, but that would be slower to run
1667 # the test on each element and much slower than skipping the conversion
1668 # entirely when we know we're outputting raw bytes.
1669 my $orig = \
&FCGI
::Stream
::PRINT
;
1670 undef *FCGI
::Stream
::PRINT
;
1671 *FCGI
::Stream
::PRINT
= sub {
1672 @_ = (shift, map {my $x=$_; utf8
::encode
($x); $x} @_)
1673 unless $fcgi_raw_mode;
1677 our $CGI = 'CGI::Fast';
1681 my $request_number = 0;
1682 # let each child service 100 requests
1683 our $is_last_request = sub { ++$request_number >= 100 };
1686 my $script_name = $ENV{'SCRIPT_NAME'} || $ENV{'SCRIPT_FILENAME'} || __FILE__
;
1688 if $script_name =~ /\.fcgi$/ || ($auto_fcgi && is_fcgi
());
1690 my $nproc_sub = sub {
1691 my ($arg, $val) = @_;
1692 return unless eval { require FCGI
::ProcManager
; 1; };
1693 $fcgi_nproc_active = 1;
1694 my $proc_manager = FCGI
::ProcManager
->new({
1695 n_processes
=> $val,
1697 our $pre_listen_hook = sub { $proc_manager->pm_manage() };
1698 our $pre_dispatch_hook = sub { $proc_manager->pm_pre_dispatch() };
1699 our $post_dispatch_hook = sub { $proc_manager->pm_post_dispatch() };
1702 require Getopt
::Long
;
1703 Getopt
::Long
::GetOptions
(
1704 'fastcgi|fcgi|f' => \
&configure_as_fcgi
,
1705 'nproc|n=i' => $nproc_sub,
1708 if (!$fcgi_nproc_active && defined $ENV{'GITWEB_FCGI_NPROC'} && $ENV{'GITWEB_FCGI_NPROC'} =~ /^\d+$/) {
1709 &$nproc_sub('nproc', $ENV{'GITWEB_FCGI_NPROC'});
1713 # Any "our" variable that could possibly influence correct handling of
1714 # a CGI request MUST be reset in this subroutine
1715 sub _reset_globals
{
1716 # Note that $t0 and $number_of_git_commands are handled by reset_timer
1717 our %input_params = ();
1718 our ($action, $project, $file_name, $file_parent, $hash, $hash_parent, $hash_base,
1719 $hash_parent_base, @extra_options, $page, $searchtype, $search_use_regexp,
1720 $searchtext, $search_regexp, $project_filter) = ();
1721 our $git_dir = undef;
1722 our (@snapshot_fmts, $git_avatar, @extra_branch_refs) = ();
1723 our %avatar_cache = ();
1724 our $config_file = '';
1726 our $gitweb_project_owner = undef;
1727 our $shown_stale_message = 0;
1728 our $fcgi_raw_mode = 0;
1729 keys %known_snapshot_formats; # reset 'each' iterator
1733 evaluate_gitweb_config
();
1734 evaluate_encoding
();
1735 evaluate_email_obfuscate
();
1736 evaluate_git_version
();
1737 my ($ml, $mi, $bu, $hl, $subroutine) = ($my_url, $my_uri, $base_url, $home_link, '');
1738 $subroutine .= '$my_url = $ml;' if defined $my_url && $my_url ne '';
1739 $subroutine .= '$my_uri = $mi;' if defined $my_uri; # this one can be ""
1740 $subroutine .= '$base_url = $bu;' if defined $base_url && $base_url ne '';
1741 $subroutine .= '$home_link = $hl;' if defined $home_link && $home_link ne '';
1742 $evaluate_uri_force = eval "sub {$subroutine}" if $subroutine;
1746 $pre_listen_hook->()
1747 if $pre_listen_hook;
1750 while ($cgi = $CGI->new()) {
1751 $pre_dispatch_hook->()
1752 if $pre_dispatch_hook;
1754 # most globals can simply be reset
1757 # evaluate_path_info corrupts %known_snapshot_formats
1758 # so we need a deepish copy of it -- note that
1759 # _reset_globals already took care of resetting its
1760 # hash iterator that evaluate_path_info also leaves
1761 # in an indeterminate state
1763 while (my ($k,$v) = each(%known_snapshot_formats)) {
1764 $formats{$k} = {%{$known_snapshot_formats{$k}}};
1766 local *known_snapshot_formats
= \
%formats;
1768 eval {run_request
()};
1770 $post_dispatch_hook->()
1771 if $post_dispatch_hook;
1774 last REQUEST
if ($is_last_request->());
1782 if (defined caller) {
1783 # wrapped in a subroutine processing requests,
1784 # e.g. mod_perl with ModPerl::Registry, or PSGI with Plack::App::WrapCGI
1787 # pure CGI script, serving single request
1791 ## ======================================================================
1794 # possible values of extra options
1795 # -full => 0|1 - use absolute/full URL ($my_uri/$my_url as base)
1796 # -replay => 1 - start from a current view (replay with modifications)
1797 # -path_info => 0|1 - don't use/use path_info URL (if possible)
1798 # -anchor => ANCHOR - add #ANCHOR to end of URL, implies -replay if used alone
1801 # default is to use -absolute url() i.e. $my_uri
1802 my $href = $params{-full
} ?
$my_url : $my_uri;
1804 # implicit -replay, must be first of implicit params
1805 $params{-replay
} = 1 if (keys %params == 1 && $params{-anchor
});
1807 $params{'project'} = $project unless exists $params{'project'};
1809 if ($params{-replay
}) {
1810 while (my ($name, $symbol) = each %cgi_param_mapping) {
1811 if (!exists $params{$name}) {
1812 $params{$name} = $input_params{$name};
1817 my $use_pathinfo = gitweb_check_feature
('pathinfo');
1818 if (defined $params{'project'} &&
1819 (exists $params{-path_info
} ?
$params{-path_info
} : $use_pathinfo)) {
1820 # try to put as many parameters as possible in PATH_INFO:
1823 # - hash_parent or hash_parent_base:/file_parent
1824 # - hash or hash_base:/filename
1825 # - the snapshot_format as an appropriate suffix
1827 # When the script is the root DirectoryIndex for the domain,
1828 # $href here would be something like http://gitweb.example.com/
1829 # Thus, we strip any trailing / from $href, to spare us double
1830 # slashes in the final URL
1833 # Then add the project name, if present
1834 $href .= "/".esc_path_info
($params{'project'});
1835 delete $params{'project'};
1837 # since we destructively absorb parameters, we keep this
1838 # boolean that remembers if we're handling a snapshot
1839 my $is_snapshot = $params{'action'} eq 'snapshot';
1841 # Summary just uses the project path URL, any other action is
1843 if (defined $params{'action'}) {
1844 $href .= "/".esc_path_info
($params{'action'})
1845 unless $params{'action'} eq 'summary';
1846 delete $params{'action'};
1849 # Next, we put hash_parent_base:/file_parent..hash_base:/file_name,
1850 # stripping nonexistent or useless pieces
1851 $href .= "/" if ($params{'hash_base'} || $params{'hash_parent_base'}
1852 || $params{'hash_parent'} || $params{'hash'});
1853 if (defined $params{'hash_base'}) {
1854 if (defined $params{'hash_parent_base'}) {
1855 $href .= esc_path_info
($params{'hash_parent_base'});
1856 # skip the file_parent if it's the same as the file_name
1857 if (defined $params{'file_parent'}) {
1858 if (defined $params{'file_name'} && $params{'file_parent'} eq $params{'file_name'}) {
1859 delete $params{'file_parent'};
1860 } elsif ($params{'file_parent'} !~ /\.\./) {
1861 $href .= ":/".esc_path_info
($params{'file_parent'});
1862 delete $params{'file_parent'};
1866 delete $params{'hash_parent'};
1867 delete $params{'hash_parent_base'};
1868 } elsif (defined $params{'hash_parent'}) {
1869 $href .= esc_path_info
($params{'hash_parent'}). "..";
1870 delete $params{'hash_parent'};
1873 $href .= esc_path_info
($params{'hash_base'});
1874 if (defined $params{'file_name'} && $params{'file_name'} !~ /\.\./) {
1875 $href .= ":/".esc_path_info
($params{'file_name'});
1876 delete $params{'file_name'};
1878 delete $params{'hash'};
1879 delete $params{'hash_base'};
1880 } elsif (defined $params{'hash'}) {
1881 $href .= esc_path_info
($params{'hash'});
1882 delete $params{'hash'};
1885 # If the action was a snapshot, we can absorb the
1886 # snapshot_format parameter too
1888 my $fmt = $params{'snapshot_format'};
1889 # snapshot_format should always be defined when href()
1890 # is called, but just in case some code forgets, we
1891 # fall back to the default
1892 $fmt ||= $snapshot_fmts[0];
1893 $href .= $known_snapshot_formats{$fmt}{'suffix'};
1894 delete $params{'snapshot_format'};
1898 # now encode the parameters explicitly
1900 for (my $i = 0; $i < @cgi_param_mapping; $i += 2) {
1901 my ($name, $symbol) = ($cgi_param_mapping[$i], $cgi_param_mapping[$i+1]);
1902 if (defined $params{$name}) {
1903 if (ref($params{$name}) eq "ARRAY") {
1904 foreach my $par (@
{$params{$name}}) {
1905 push @result, $symbol . "=" . esc_param
($par);
1908 push @result, $symbol . "=" . esc_param
($params{$name});
1912 $href .= "?" . join(';', @result) if scalar @result;
1914 # final transformation: trailing spaces must be escaped (URI-encoded)
1915 $href =~ s/(\s+)$/CGI::escape($1)/e;
1917 if ($params{-anchor
}) {
1918 $href .= "#".esc_param
($params{-anchor
});
1925 ## ======================================================================
1926 ## validation, quoting/unquoting and escaping
1928 sub is_valid_action
{
1930 return undef unless exists $actions{$input};
1934 sub is_valid_project
{
1937 return unless defined $input;
1938 if (!is_valid_pathname
($input) ||
1939 !(-d
"$projectroot/$input") ||
1940 !check_export_ok
("$projectroot/$input") ||
1941 ($strict_export && !project_in_list
($input))) {
1948 sub is_valid_pathname
{
1951 return undef unless defined $input;
1952 # no '.' or '..' as elements of path, i.e. no '.' or '..'
1953 # at the beginning, at the end, and between slashes.
1954 # also this catches doubled slashes
1955 if ($input =~ m!(^|/)(|\.|\.\.)(/|$)!) {
1958 # no null characters
1959 if ($input =~ m!\0!) {
1965 sub is_valid_ref_format
{
1968 return undef unless defined $input;
1969 # restrictions on ref name according to git-check-ref-format
1970 if ($input =~ m!(/\.|\.\.|[\000-\040\177 ~^:?*\[]|/$)!) {
1976 sub is_valid_refname
{
1979 return undef unless defined $input;
1980 # textual hashes are O.K.
1981 if ($input =~ m/^[0-9a-fA-F]{40}$/) {
1984 # allow repeated trailing '[~^]n*' suffix(es)
1985 $input =~ s/^([^~^]+)(?:[~^]\d*)+$/$1/;
1986 # it must be correct pathname
1987 is_valid_pathname
($input) or return undef;
1988 # check git-check-ref-format restrictions
1989 is_valid_ref_format
($input) or return undef;
1993 # decode sequences of octets in utf8 into Perl's internal form,
1994 # which is utf-8 with utf8 flag set if needed. gitweb writes out
1995 # in utf-8 thanks to "binmode STDOUT, ':utf8'" at beginning
1998 return undef unless defined $str;
2000 if (utf8
::is_utf8
($str) || utf8
::decode
($str)) {
2003 return $encode_object->decode($str, Encode
::FB_DEFAULT
);
2007 # quote unsafe chars, but keep the slash, even when it's not
2008 # correct, but quoted slashes look too horrible in bookmarks
2011 return undef unless defined $str;
2012 $str =~ s/([^A-Za-z0-9\-_.~()\/:@ ]+)/CGI
::escape
($1)/eg
;
2017 # the quoting rules for path_info fragment are slightly different
2020 return undef unless defined $str;
2022 # path_info doesn't treat '+' as space (specially), but '?' must be escaped
2023 $str =~ s/([^A-Za-z0-9\-_.~();\/;:@&= +]+)/CGI
::escape
($1)/eg
;
2028 # quote unsafe chars in whole URL, so some characters cannot be quoted
2031 return undef unless defined $str;
2032 $str =~ s/([^A-Za-z0-9\-_.~();\/;?:@&= ]+)/CGI
::escape
($1)/eg
;
2037 # quote unsafe characters in HTML attributes
2040 # for XHTML conformance escaping '"' to '"' is not enough
2041 return esc_html
(@_);
2044 # replace invalid utf8 character with SUBSTITUTION sequence
2049 return undef unless defined $str;
2051 $str = to_utf8
($str);
2052 $str = $cgi->escapeHTML($str);
2053 if ($opts{'-nbsp'}) {
2054 $str =~ s/ / /g;
2057 $str =~ s
|([[:cntrl
:]])|(($1 ne "\t") ? quot_cec
($1) : $1)|eg
;
2061 # quote control characters and escape filename to HTML
2066 return undef unless defined $str;
2068 $str = to_utf8
($str);
2069 $str = $cgi->escapeHTML($str);
2070 if ($opts{'-nbsp'}) {
2071 $str =~ s/ / /g;
2074 $str =~ s
|([[:cntrl
:]])|quot_cec
($1)|eg
;
2078 # Sanitize for use in XHTML + application/xml+xhtml (valid XML 1.0)
2082 return undef unless defined $str;
2084 $str = to_utf8
($str);
2086 $str =~ s
|([[:cntrl
:]])|(index("\t\n\r", $1) != -1 ?
$1 : quot_cec
($1))|eg
;
2090 # Make control characters "printable", using character escape codes (CEC)
2094 my %es = ( # character escape codes, aka escape sequences
2095 "\t" => '\t', # tab (HT)
2096 "\n" => '\n', # line feed (LF)
2097 "\r" => '\r', # carrige return (CR)
2098 "\f" => '\f', # form feed (FF)
2099 "\b" => '\b', # backspace (BS)
2100 "\a" => '\a', # alarm (bell) (BEL)
2101 "\e" => '\e', # escape (ESC)
2102 "\013" => '\v', # vertical tab (VT)
2103 "\000" => '\0', # nul character (NUL)
2105 my $chr = ( (exists $es{$cntrl})
2107 : sprintf('\x%02x', ord($cntrl)) );
2108 if ($opts{-nohtml
}) {
2111 return "<span class=\"cntrl\">$chr</span>";
2115 # Alternatively use unicode control pictures codepoints,
2116 # Unicode "printable representation" (PR)
2121 my $chr = sprintf('&#%04d;', 0x2400+ord($cntrl));
2122 if ($opts{-nohtml
}) {
2125 return "<span class=\"cntrl\">$chr</span>";
2129 # git may return quoted and escaped filenames
2135 my %es = ( # character escape codes, aka escape sequences
2136 't' => "\t", # tab (HT, TAB)
2137 'n' => "\n", # newline (NL)
2138 'r' => "\r", # return (CR)
2139 'f' => "\f", # form feed (FF)
2140 'b' => "\b", # backspace (BS)
2141 'a' => "\a", # alarm (bell) (BEL)
2142 'e' => "\e", # escape (ESC)
2143 'v' => "\013", # vertical tab (VT)
2146 if ($seq =~ m/^[0-7]{1,3}$/) {
2147 # octal char sequence
2148 return chr(oct($seq));
2149 } elsif (exists $es{$seq}) {
2150 # C escape sequence, aka character escape code
2153 # quoted ordinary character
2157 if ($str =~ m/^"(.*)"$/) {
2160 $str =~ s/\\([^0-7]|[0-7]{1,3})/unq($1)/eg;
2165 # escape tabs (convert tabs to spaces)
2169 while ((my $pos = index($line, "\t")) != -1) {
2170 if (my $count = (8 - ($pos % 8))) {
2171 my $spaces = ' ' x
$count;
2172 $line =~ s/\t/$spaces/;
2179 sub project_in_list
{
2180 my $project = shift;
2181 my @list = git_get_projects_list
();
2182 return @list && scalar(grep { $_->{'path'} eq $project } @list);
2185 sub cached_page_precondition_check
{
2188 $action eq 'summary' &&
2189 $projlist_cache_lifetime > 0 &&
2190 gitweb_check_feature
('forks');
2192 # Note that ALL the 'forkchange' logic is in this function.
2193 # It does NOT belong in cached_action_page NOR in cached_action_start
2194 # NOR in cached_action_finish. None of those functions should know anything
2195 # about nor do anything to any of the 'forkchange'/"$action.forkchange" files.
2197 # besides the basic 'changed' "$action.changed" check, we may only use
2198 # a summary cache if:
2200 # 1) we are not using a project list cache file
2202 # 2) we are not using the 'forks' feature
2204 # 3) there is no 'forkchange' nor "$action.forkchange" file in $html_cache_dir
2206 # 4) there is no cache file ("$cache_dir/$projlist_cache_name")
2208 # 5) the OLDER of 'forkchange'/"$action.forkchange" is NEWER than the cache file
2210 # Otherwise we must re-generate the cache because we've had a fork change
2211 # (either a fork was added or a fork was removed) AND the change has been
2212 # picked up in the cache file AND we've not got that in our cached copy
2214 # For (5) regenerating the cached page wouldn't get us anything if the project
2215 # cache file is older than the 'forkchange'/"$action.forkchange" because the
2216 # forks information comes from the project cache file and it's clearly not
2217 # picked up the changes yet so we may continue to use a cached page until it does.
2219 my $htmlcd = "$projectroot/$project/$html_cache_dir";
2220 my $fc_mt = (stat("$htmlcd/forkchange"))[9];
2221 my $afc_mt = (stat("$htmlcd/$action.forkchange"))[9];
2222 return 1 unless defined($fc_mt) || defined($afc_mt);
2223 my $prj_mt = (stat("$cache_dir/$projlist_cache_name"))[9];
2224 return 1 unless $prj_mt;
2225 my $old_mt = $fc_mt;
2226 $old_mt = $afc_mt if !defined($old_mt) || (defined($afc_mt) && $afc_mt < $old_mt);
2227 return 1 if $old_mt > $prj_mt;
2229 # We're going to regenerate the cached page because we know the project cache
2230 # has new fork information that we cannot possibly have in our cached copy.
2232 # However, if both 'forkchange' and "$action.forkchange" exist and one of
2233 # them is older than the project cache and one of them is newer, we still
2234 # need to regenerate the page cache, but we will also need to do it again
2235 # in the future because there's yet another fork update not yet in the cache.
2237 # So we make sure to touch "$action.changed" to force a cache regeneration
2238 # and then we remove either or both of 'forkchange'/"$action.forkchange" if
2239 # they're older than the project cache (they've served their purpose, we're
2240 # forcing a page regeneration by touching "$action.changed" but the project
2241 # cache was rebuilt since then so there are no more pending fork updates to
2242 # pick up in the future and they need to go).
2244 # For best results, the external code that touches 'forkchange' should always
2245 # touch 'forkchange' and additionally touch 'summary.forkchange' but only
2246 # if it does not already exist. That way the cached page will be regenerated
2247 # each time it's requested and ANY fork updates are available in the proj
2248 # cache rather than waiting until they all are before updating.
2250 # Note that we take a shortcut here and will zap 'forkchange' since we know
2251 # that it only affects the 'summary' cache. If, in the future, it affects
2252 # other cache types, it will first need to be propogated down to
2253 # "$action.forkchange" for those types before we zap it.
2256 open $fd, '>', "$htmlcd/$action.changed" and close $fd;
2257 $fc_mt=undef, unlink "$htmlcd/forkchange" if defined $fc_mt && $fc_mt < $prj_mt;
2258 $afc_mt=undef, unlink "$htmlcd/$action.forkchange" if defined $afc_mt && $afc_mt < $prj_mt;
2260 # Now we propagate 'forkchange' to "$action.forkchange" if we have the
2261 # one and not the other.
2263 if (defined $fc_mt && ! defined $afc_mt) {
2264 open $fd, '>', "$htmlcd/$action.forkchange" and close $fd;
2265 -e
"$htmlcd/$action.forkchange" and
2266 utime($fc_mt, $fc_mt, "$htmlcd/$action.forkchange") and
2267 unlink "$htmlcd/forkchange";
2273 sub cached_action_page
{
2276 return undef unless $action && $html_cache_actions{$action} && $html_cache_dir;
2277 my $htmlcd = "$projectroot/$project/$html_cache_dir";
2278 return undef if -e
"$htmlcd/changed" || -e
"$htmlcd/$action.changed";
2279 return undef unless cached_page_precondition_check
($action);
2280 open my $fd, '<', "$htmlcd/$action" or return undef;
2283 my $cached_page = <$fd>;
2284 close $fd or return undef;
2285 return $cached_page;
2288 package Git
::Gitweb
::CacheFile
;
2291 use POSIX
qw(:fcntl_h);
2293 my $cachefile = shift;
2295 sysopen(my $self, $cachefile, O_WRONLY
|O_CREAT
|O_EXCL
, 0664)
2297 $$self->{'cachefile'} = $cachefile;
2298 $$self->{'opened'} = 1;
2299 $$self->{'contents'} = '';
2300 return bless $self, $class;
2305 if ($$self->{'opened'}) {
2306 $$self->{'opened'} = 0;
2307 my $result = close $self;
2308 unlink $$self->{'cachefile'} unless $result;
2316 if ($$self->{'opened'}) {
2317 $self->CLOSE() and unlink $$self->{'cachefile'};
2323 @_ = (map {my $x=$_; utf8
::encode
($x); $x} @_) unless $fcgi_raw_mode;
2324 print $self @_ if $$self->{'opened'};
2325 $$self->{'contents'} .= join('', @_);
2331 my $template = shift;
2332 return $self->PRINT(sprintf $template, @_);
2337 return $$self->{'contents'};
2342 # Caller is responsible for preserving STDOUT beforehand if needed
2343 sub cached_action_start
{
2346 return undef unless $html_cache_actions{$action} && $html_cache_dir;
2347 my $htmlcd = "$projectroot/$project/$html_cache_dir";
2348 return undef unless -d
$htmlcd;
2349 if (-e
"$htmlcd/changed") {
2350 foreach my $cacheable (keys(%html_cache_actions)) {
2351 next unless $supported_cache_actions{$cacheable} &&
2352 $html_cache_actions{$cacheable};
2354 open $fd, '>', "$htmlcd/$cacheable.changed"
2357 unlink "$htmlcd/changed";
2360 tie
*CACHEFILE
, 'Git::Gitweb::CacheFile', "$htmlcd/$action.lock" or return undef;
2361 *STDOUT
= *CACHEFILE
;
2362 unlink "$htmlcd/$action", "$htmlcd/$action.changed";
2366 # Caller is responsible for restoring STDOUT afterward if needed
2367 sub cached_action_finish
{
2372 my $obj = tied *STDOUT
;
2373 return undef unless ref($obj) eq 'Git::Gitweb::CacheFile';
2374 my $cached_page = $obj->contents;
2375 (my $result = close(STDOUT
)) or warn "couldn't close cache file on STDOUT: $!";
2376 # Do not leave STDOUT file descriptor invalid!
2378 open(NULL
, '>', File
::Spec
->devnull) or die "couldn't open NULL to devnull: $!";
2380 return $cached_page unless $result;
2381 my $htmlcd = "$projectroot/$project/$html_cache_dir";
2382 return $cached_page unless -d
$htmlcd;
2383 unlink "$htmlcd/$action.lock" unless rename "$htmlcd/$action.lock", "$htmlcd/$action";
2384 return $cached_page;
2388 BEGIN {%expand_pi_subs = (
2389 'age_string' => \
&age_string
,
2390 'age_string_date' => \
&age_string_date
,
2391 'age_string_age' => \
&age_string_age
,
2392 'compute_timed_interval' => \
&compute_timed_interval
,
2393 'compute_commands_count' => \
&compute_commands_count
,
2394 'format_lastrefresh_row' => \
&format_lastrefresh_row
,
2397 # Expands any <?gitweb...> processing instructions and returns the result
2398 sub expand_gitweb_pi
{
2401 my @time_now = gettimeofday
();
2402 $page =~ s
{<\?gitweb
(?
:\s
+([^\s
>]+)([^>]*))?\s
*\?>}
2404 (ref($expand_pi_subs{$1}) eq 'CODE' ?
2405 $expand_pi_subs{$1}->(split(' ',$2), @time_now) :
2411 ## ----------------------------------------------------------------------
2412 ## HTML aware string manipulation
2414 # Try to chop given string on a word boundary between position
2415 # $len and $len+$add_len. If there is no word boundary there,
2416 # chop at $len+$add_len. Do not chop if chopped part plus ellipsis
2417 # (marking chopped part) would be longer than given string.
2421 my $add_len = shift || 10;
2422 my $where = shift || 'right'; # 'left' | 'center' | 'right'
2424 # Make sure perl knows it is utf8 encoded so we don't
2425 # cut in the middle of a utf8 multibyte char.
2426 $str = to_utf8
($str);
2428 # allow only $len chars, but don't cut a word if it would fit in $add_len
2429 # if it doesn't fit, cut it if it's still longer than the dots we would add
2430 # remove chopped character entities entirely
2432 # when chopping in the middle, distribute $len into left and right part
2433 # return early if chopping wouldn't make string shorter
2434 if ($where eq 'center') {
2435 return $str if ($len + 5 >= length($str)); # filler is length 5
2438 return $str if ($len + 4 >= length($str)); # filler is length 4
2441 # regexps: ending and beginning with word part up to $add_len
2442 my $endre = qr/.{$len}\w{0,$add_len}/;
2443 my $begre = qr/\w{0,$add_len}.{$len}/;
2445 if ($where eq 'left') {
2446 $str =~ m/^(.*?)($begre)$/;
2447 my ($lead, $body) = ($1, $2);
2448 if (length($lead) > 4) {
2451 return "$lead$body";
2453 } elsif ($where eq 'center') {
2454 $str =~ m/^($endre)(.*)$/;
2455 my ($left, $str) = ($1, $2);
2456 $str =~ m/^(.*?)($begre)$/;
2457 my ($mid, $right) = ($1, $2);
2458 if (length($mid) > 5) {
2461 return "$left$mid$right";
2464 $str =~ m/^($endre)(.*)$/;
2467 if (length($tail) > 4) {
2470 return "$body$tail";
2474 # pass-through email filter, obfuscating it when possible
2475 sub email_obfuscate
{
2479 $str = $email->escape_html($str);
2480 # Stock HTML::Email::Obfuscate version likes to produce
2482 $str =~ s
#<(/?)B>#<$1b>#g;
2485 $str = esc_html
($str);
2486 $str =~ s/@/@/;
2491 # takes the same arguments as chop_str, but also wraps a <span> around the
2492 # result with a title attribute if it does get chopped. Additionally, the
2493 # string is HTML-escaped.
2494 sub chop_and_escape_str
{
2497 my $chopped = chop_str
(@_);
2498 $str = to_utf8
($str);
2499 if ($chopped eq $str) {
2500 return email_obfuscate
($chopped);
2504 $str =~ s/[[:cntrl:]]/?/g;
2506 return $cgi->span({-title
=>$str}, email_obfuscate
($chopped));
2510 # Highlight selected fragments of string, using given CSS class,
2511 # and escape HTML. It is assumed that fragments do not overlap.
2512 # Regions are passed as list of pairs (array references).
2514 # Example: esc_html_hl_regions("foobar", "mark", [ 0, 3 ]) returns
2515 # '<span class="mark">foo</span>bar'
2516 sub esc_html_hl_regions
{
2517 my ($str, $css_class, @sel) = @_;
2518 my %opts = grep { ref($_) ne 'ARRAY' } @sel;
2519 @sel = grep { ref($_) eq 'ARRAY' } @sel;
2520 return esc_html
($str, %opts) unless @sel;
2526 my ($begin, $end) = @
$s;
2528 # Don't create empty <span> elements.
2529 next if $end <= $begin;
2531 my $escaped = esc_html
(substr($str, $begin, $end - $begin),
2534 $out .= esc_html
(substr($str, $pos, $begin - $pos), %opts)
2535 if ($begin - $pos > 0);
2536 $out .= $cgi->span({-class => $css_class}, $escaped);
2540 $out .= esc_html
(substr($str, $pos), %opts)
2541 if ($pos < length($str));
2546 # return positions of beginning and end of each match
2548 my ($str, $regexp) = @_;
2549 return unless (defined $str && defined $regexp);
2552 while ($str =~ /$regexp/g) {
2553 push @matches, [$-[0], $+[0]];
2558 # highlight match (if any), and escape HTML
2559 sub esc_html_match_hl
{
2560 my ($str, $regexp) = @_;
2561 return esc_html
($str) unless defined $regexp;
2563 my @matches = matchpos_list
($str, $regexp);
2564 return esc_html
($str) unless @matches;
2566 return esc_html_hl_regions
($str, 'match', @matches);
2570 # highlight match (if any) of shortened string, and escape HTML
2571 sub esc_html_match_hl_chopped
{
2572 my ($str, $chopped, $regexp) = @_;
2573 return esc_html_match_hl
($str, $regexp) unless defined $chopped;
2575 my @matches = matchpos_list
($str, $regexp);
2576 return esc_html
($chopped) unless @matches;
2578 # filter matches so that we mark chopped string
2579 my $tail = "... "; # see chop_str
2580 unless ($chopped =~ s/\Q$tail\E$//) {
2583 my $chop_len = length($chopped);
2584 my $tail_len = length($tail);
2587 for my $m (@matches) {
2588 if ($m->[0] > $chop_len) {
2589 push @filtered, [ $chop_len, $chop_len + $tail_len ] if ($tail_len > 0);
2591 } elsif ($m->[1] > $chop_len) {
2592 push @filtered, [ $m->[0], $chop_len + $tail_len ];
2598 return esc_html_hl_regions
($chopped . $tail, 'match', @filtered);
2601 ## ----------------------------------------------------------------------
2602 ## functions returning short strings
2604 # CSS class for given age epoch value (in seconds)
2605 # and reference time (optional, defaults to now) as second value
2607 my ($age_epoch, $time_now) = @_;
2608 return "noage" unless defined $age_epoch;
2609 defined $time_now or $time_now = time;
2610 my $age = $time_now - $age_epoch;
2612 if ($age < 60*60*2) {
2614 } elsif ($age < 60*60*24*2) {
2621 # convert age epoch in seconds to "nn units ago" string
2622 # reference time used is now unless second argument passed in
2623 # to get the old behavior, pass 0 as the first argument and
2624 # the time in seconds as the second
2626 my ($age_epoch, $time_now) = @_;
2627 return "unknown" unless defined $age_epoch;
2628 return "<?gitweb age_string $age_epoch?>" if $cache_mode_active;
2629 defined $time_now or $time_now = time;
2630 my $age = $time_now - $age_epoch;
2633 if ($age > 60*60*24*365*2) {
2634 $age_str = (int $age/60/60/24/365);
2635 $age_str .= " years ago";
2636 } elsif ($age > 60*60*24*(365/12)*2) {
2637 $age_str = int $age/60/60/24/(365/12);
2638 $age_str .= " months ago";
2639 } elsif ($age > 60*60*24*7*2) {
2640 $age_str = int $age/60/60/24/7;
2641 $age_str .= " weeks ago";
2642 } elsif ($age > 60*60*24*2) {
2643 $age_str = int $age/60/60/24;
2644 $age_str .= " days ago";
2645 } elsif ($age > 60*60*2) {
2646 $age_str = int $age/60/60;
2647 $age_str .= " hours ago";
2648 } elsif ($age > 60*2) {
2649 $age_str = int $age/60;
2650 $age_str .= " min ago";
2651 } elsif ($age > 2) {
2652 $age_str = int $age;
2653 $age_str .= " sec ago";
2655 $age_str .= " right now";
2660 # returns age_string if the age is <= 2 weeks otherwise an absolute date
2661 # this is typically shown to the user directly with the age_string_age as a title
2662 sub age_string_date
{
2663 my ($age_epoch, $time_now) = @_;
2664 return "unknown" unless defined $age_epoch;
2665 return "<?gitweb age_string_date $age_epoch?>" if $cache_mode_active;
2666 defined $time_now or $time_now = time;
2667 my $age = $time_now - $age_epoch;
2669 if ($age > 60*60*24*7*2) {
2670 my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($age_epoch);
2671 return sprintf "%4i-%02u-%02i", 1900 + $year, $mon+1, $mday;
2673 return age_string
($age_epoch, $time_now);
2677 # returns an absolute date if the age is <= 2 weeks otherwise age_string
2678 # this is typically used for the 'title' attribute so it will show as a tooltip
2679 sub age_string_age
{
2680 my ($age_epoch, $time_now) = @_;
2681 return "unknown" unless defined $age_epoch;
2682 return "<?gitweb age_string_age $age_epoch?>" if $cache_mode_active;
2683 defined $time_now or $time_now = time;
2684 my $age = $time_now - $age_epoch;
2686 if ($age > 60*60*24*7*2) {
2687 return age_string
($age_epoch, $time_now);
2689 my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($age_epoch);
2690 return sprintf "%4i-%02u-%02i", 1900 + $year, $mon+1, $mday;
2695 S_IFINVALID
=> 0030000,
2696 S_IFGITLINK
=> 0160000,
2699 # submodule/subproject, a commit object reference
2703 return (($mode & S_IFMT
) == S_IFGITLINK
)
2706 # convert file mode in octal to symbolic file mode string
2708 my $mode = oct shift;
2710 if (S_ISGITLINK
($mode)) {
2711 return 'm---------';
2712 } elsif (S_ISDIR
($mode & S_IFMT
)) {
2713 return 'drwxr-xr-x';
2714 } elsif (S_ISLNK
($mode)) {
2715 return 'lrwxrwxrwx';
2716 } elsif (S_ISREG
($mode)) {
2717 # git cares only about the executable bit
2718 if ($mode & S_IXUSR
) {
2719 return '-rwxr-xr-x';
2721 return '-rw-r--r--';
2724 return '----------';
2728 # convert file mode in octal to file type string
2732 if ($mode !~ m/^[0-7]+$/) {
2738 if (S_ISGITLINK
($mode)) {
2740 } elsif (S_ISDIR
($mode & S_IFMT
)) {
2742 } elsif (S_ISLNK
($mode)) {
2744 } elsif (S_ISREG
($mode)) {
2751 # convert file mode in octal to file type description string
2752 sub file_type_long
{
2755 if ($mode !~ m/^[0-7]+$/) {
2761 if (S_ISGITLINK
($mode)) {
2763 } elsif (S_ISDIR
($mode & S_IFMT
)) {
2765 } elsif (S_ISLNK
($mode)) {
2767 } elsif (S_ISREG
($mode)) {
2768 if ($mode & S_IXUSR
) {
2769 return "executable";
2779 ## ----------------------------------------------------------------------
2780 ## functions returning short HTML fragments, or transforming HTML fragments
2781 ## which don't belong to other sections
2783 # format line of commit message.
2784 sub format_log_line_html
{
2787 $line = esc_html
($line, -nbsp
=>1);
2791 # The output of "git describe", e.g. v2.10.0-297-gf6727b0
2792 # or hadoop-20160921-113441-20-g094fb7d
2793 (?
<!-) # see strbuf_check_tag_ref(). Tags can't start with -
2795 (?
!\
.) # refs can't end with ".", see check_refname_format()
2798 # Just a normal looking Git SHA1
2803 $cgi->a({-href
=> href
(action
=>"object", hash
=>$1),
2804 -class => "text"}, $1);
2805 }egx
unless $line =~ /^\s*git-svn-id:/;
2810 # format marker of refs pointing to given object
2812 # the destination action is chosen based on object type and current context:
2813 # - for annotated tags, we choose the tag view unless it's the current view
2814 # already, in which case we go to shortlog view
2815 # - for other refs, we keep the current view if we're in history, shortlog or
2816 # log view, and select shortlog otherwise
2817 sub format_ref_marker
{
2818 my ($refs, $id) = @_;
2821 if (defined $refs->{$id}) {
2822 foreach my $ref (@
{$refs->{$id}}) {
2823 # this code exploits the fact that non-lightweight tags are the
2824 # only indirect objects, and that they are the only objects for which
2825 # we want to use tag instead of shortlog as action
2826 my ($type, $name) = qw();
2827 my $indirect = ($ref =~ s/\^\{\}$//);
2828 # e.g. tags/v2.6.11 or heads/next
2829 if ($ref =~ m!^(.*?)s?/(.*)$!) {
2838 $class .= " indirect" if $indirect;
2840 my $dest_action = "shortlog";
2843 $dest_action = "tag" unless $action eq "tag";
2844 } elsif ($action =~ /^(history|(short)?log)$/) {
2845 $dest_action = $action;
2849 $dest .= "refs/" unless $ref =~ m
!^refs
/!;
2852 my $link = $cgi->a({
2854 action
=>$dest_action,
2856 )}, esc_html
($name));
2858 $markers .= "<span class=\"".esc_attr
($class)."\" title=\"".esc_attr
($ref)."\">" .
2864 return '<span class="refs">'. $markers . '</span>';
2870 # format, perhaps shortened and with markers, title line
2871 sub format_subject_html
{
2872 my ($long, $short, $href, $extra) = @_;
2873 $extra = '' unless defined($extra);
2875 if (length($short) < length($long)) {
2878 $long =~ s/[[:cntrl:]]/?/g;
2880 return $cgi->a({-href
=> $href, -class => "list subject",
2881 -title
=> to_utf8
($long)},
2882 esc_html
($short)) . $extra;
2884 return $cgi->a({-href
=> $href, -class => "list subject"},
2885 esc_html
($long)) . $extra;
2889 # Rather than recomputing the url for an email multiple times, we cache it
2890 # after the first hit. This gives a visible benefit in views where the avatar
2891 # for the same email is used repeatedly (e.g. shortlog).
2892 # The cache is shared by all avatar engines (currently gravatar only), which
2893 # are free to use it as preferred. Since only one avatar engine is used for any
2894 # given page, there's no risk for cache conflicts.
2895 our %avatar_cache = ();
2897 # Compute the picon url for a given email, by using the picon search service over at
2898 # http://www.cs.indiana.edu/picons/search.html
2900 my $email = lc shift;
2901 if (!$avatar_cache{$email}) {
2902 my ($user, $domain) = split('@', $email);
2903 $avatar_cache{$email} =
2904 "//www.cs.indiana.edu/cgi-pub/kinzler/piconsearch.cgi/" .
2906 "users+domains+unknown/up/single";
2908 return $avatar_cache{$email};
2911 # Compute the gravatar url for a given email, if it's not in the cache already.
2912 # Gravatar stores only the part of the URL before the size, since that's the
2913 # one computationally more expensive. This also allows reuse of the cache for
2914 # different sizes (for this particular engine).
2916 my $email = lc shift;
2918 $avatar_cache{$email} ||=
2919 "//www.gravatar.com/avatar/" .
2920 Digest
::MD5
::md5_hex
($email) . "?s=";
2921 return $avatar_cache{$email} . $size;
2924 # Insert an avatar for the given $email at the given $size if the feature
2926 sub git_get_avatar
{
2927 my ($email, %opts) = @_;
2928 my $pre_white = ($opts{-pad_before
} ?
" " : "");
2929 my $post_white = ($opts{-pad_after
} ?
" " : "");
2930 $opts{-size
} ||= 'default';
2931 my $size = $avatar_size{$opts{-size
}} || $avatar_size{'default'};
2933 if ($git_avatar eq 'gravatar') {
2934 $url = gravatar_url
($email, $size);
2935 } elsif ($git_avatar eq 'picon') {
2936 $url = picon_url
($email);
2938 # Other providers can be added by extending the if chain, defining $url
2939 # as needed. If no variant puts something in $url, we assume avatars
2940 # are completely disabled/unavailable.
2943 "<img width=\"$size\" " .
2944 "class=\"avatar\" " .
2945 "src=\"".esc_url
($url)."\" " .
2953 sub format_search_author
{
2954 my ($author, $searchtype, $displaytext) = @_;
2955 my $have_search = gitweb_check_feature
('search');
2959 if ($searchtype eq 'author') {
2960 $performed = "authored";
2961 } elsif ($searchtype eq 'committer') {
2962 $performed = "committed";
2965 return $cgi->a({-href
=> href
(action
=>"search", hash
=>$hash,
2966 searchtext
=>$author,
2967 searchtype
=>$searchtype), class=>"list",
2968 title
=>"Search for commits $performed by $author"},
2972 return $displaytext;
2976 # format the author name of the given commit with the given tag
2977 # the author name is chopped and escaped according to the other
2978 # optional parameters (see chop_str).
2979 sub format_author_html
{
2982 my $author = chop_and_escape_str
($co->{'author_name'}, @_);
2983 return "<$tag class=\"author\">" .
2984 format_search_author
($co->{'author_name'}, "author",
2985 git_get_avatar
($co->{'author_email'}, -pad_after
=> 1) .
2990 # format git diff header line, i.e. "diff --(git|combined|cc) ..."
2991 sub format_git_diff_header_line
{
2993 my $diffinfo = shift;
2994 my ($from, $to) = @_;
2996 if ($diffinfo->{'nparents'}) {
2998 $line =~ s!^(diff (.*?) )"?.*$!$1!;
2999 if ($to->{'href'}) {
3000 $line .= $cgi->a({-href
=> $to->{'href'}, -class => "path"},
3001 esc_path
($to->{'file'}));
3002 } else { # file was deleted (no href)
3003 $line .= esc_path
($to->{'file'});
3007 $line =~ s!^(diff (.*?) )"?a/.*$!$1!;
3008 if ($from->{'href'}) {
3009 $line .= $cgi->a({-href
=> $from->{'href'}, -class => "path"},
3010 'a/' . esc_path
($from->{'file'}));
3011 } else { # file was added (no href)
3012 $line .= 'a/' . esc_path
($from->{'file'});
3015 if ($to->{'href'}) {
3016 $line .= $cgi->a({-href
=> $to->{'href'}, -class => "path"},
3017 'b/' . esc_path
($to->{'file'}));
3018 } else { # file was deleted
3019 $line .= 'b/' . esc_path
($to->{'file'});
3023 return "<div class=\"diff header\">$line</div>\n";
3026 # format extended diff header line, before patch itself
3027 sub format_extended_diff_header_line
{
3029 my $diffinfo = shift;
3030 my ($from, $to) = @_;
3033 if ($line =~ s!^((copy|rename) from ).*$!$1! && $from->{'href'}) {
3034 $line .= $cgi->a({-href
=>$from->{'href'}, -class=>"path"},
3035 esc_path
($from->{'file'}));
3037 if ($line =~ s!^((copy|rename) to ).*$!$1! && $to->{'href'}) {
3038 $line .= $cgi->a({-href
=>$to->{'href'}, -class=>"path"},
3039 esc_path
($to->{'file'}));
3041 # match single <mode>
3042 if ($line =~ m/\s(\d{6})$/) {
3043 $line .= '<span class="info"> (' .
3044 file_type_long
($1) .
3048 if ($line =~ m/^index [0-9a-fA-F]{40},[0-9a-fA-F]{40}/) {
3049 # can match only for combined diff
3051 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
3052 if ($from->{'href'}[$i]) {
3053 $line .= $cgi->a({-href
=>$from->{'href'}[$i],
3055 substr($diffinfo->{'from_id'}[$i],0,7));
3060 $line .= ',' if ($i < $diffinfo->{'nparents'} - 1);
3063 if ($to->{'href'}) {
3064 $line .= $cgi->a({-href
=>$to->{'href'}, -class=>"hash"},
3065 substr($diffinfo->{'to_id'},0,7));
3070 } elsif ($line =~ m/^index [0-9a-fA-F]{40}..[0-9a-fA-F]{40}/) {
3071 # can match only for ordinary diff
3072 my ($from_link, $to_link);
3073 if ($from->{'href'}) {
3074 $from_link = $cgi->a({-href
=>$from->{'href'}, -class=>"hash"},
3075 substr($diffinfo->{'from_id'},0,7));
3077 $from_link = '0' x
7;
3079 if ($to->{'href'}) {
3080 $to_link = $cgi->a({-href
=>$to->{'href'}, -class=>"hash"},
3081 substr($diffinfo->{'to_id'},0,7));
3085 my ($from_id, $to_id) = ($diffinfo->{'from_id'}, $diffinfo->{'to_id'});
3086 $line =~ s!$from_id\.\.$to_id!$from_link..$to_link!;
3089 return $line . "<br/>\n";
3092 # format from-file/to-file diff header
3093 sub format_diff_from_to_header
{
3094 my ($from_line, $to_line, $diffinfo, $from, $to, @parents) = @_;
3099 #assert($line =~ m/^---/) if DEBUG;
3100 # no extra formatting for "^--- /dev/null"
3101 if (! $diffinfo->{'nparents'}) {
3102 # ordinary (single parent) diff
3103 if ($line =~ m!^--- "?a/!) {
3104 if ($from->{'href'}) {
3106 $cgi->a({-href
=>$from->{'href'}, -class=>"path"},
3107 esc_path
($from->{'file'}));
3110 esc_path
($from->{'file'});
3113 $result .= qq!<div
class="diff from_file">$line</div
>\n!;
3116 # combined diff (merge commit)
3117 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
3118 if ($from->{'href'}[$i]) {
3120 $cgi->a({-href
=>href
(action
=>"blobdiff",
3121 hash_parent
=>$diffinfo->{'from_id'}[$i],
3122 hash_parent_base
=>$parents[$i],
3123 file_parent
=>$from->{'file'}[$i],
3124 hash
=>$diffinfo->{'to_id'},
3126 file_name
=>$to->{'file'}),
3128 -title
=>"diff" . ($i+1)},
3131 $cgi->a({-href
=>$from->{'href'}[$i], -class=>"path"},
3132 esc_path
($from->{'file'}[$i]));
3134 $line = '--- /dev/null';
3136 $result .= qq!<div
class="diff from_file">$line</div
>\n!;
3141 #assert($line =~ m/^\+\+\+/) if DEBUG;
3142 # no extra formatting for "^+++ /dev/null"
3143 if ($line =~ m!^\+\+\+ "?b/!) {
3144 if ($to->{'href'}) {
3146 $cgi->a({-href
=>$to->{'href'}, -class=>"path"},
3147 esc_path
($to->{'file'}));
3150 esc_path
($to->{'file'});
3153 $result .= qq!<div
class="diff to_file">$line</div
>\n!;
3158 # create note for patch simplified by combined diff
3159 sub format_diff_cc_simplified
{
3160 my ($diffinfo, @parents) = @_;
3163 $result .= "<div class=\"diff header\">" .
3165 if (!is_deleted
($diffinfo)) {
3166 $result .= $cgi->a({-href
=> href
(action
=>"blob",
3168 hash
=>$diffinfo->{'to_id'},
3169 file_name
=>$diffinfo->{'to_file'}),
3171 esc_path
($diffinfo->{'to_file'}));
3173 $result .= esc_path
($diffinfo->{'to_file'});
3175 $result .= "</div>\n" . # class="diff header"
3176 "<div class=\"diff nodifferences\">" .
3178 "</div>\n"; # class="diff nodifferences"
3183 sub diff_line_class
{
3184 my ($line, $from, $to) = @_;
3189 if ($from && $to && ref($from->{'href'}) eq "ARRAY") {
3190 $num_sign = scalar @
{$from->{'href'}};
3193 my @diff_line_classifier = (
3194 { regexp
=> qr/^\@\@{$num_sign} /, class => "chunk_header"},
3195 { regexp
=> qr/^\\/, class => "incomplete" },
3196 { regexp
=> qr/^ {$num_sign}/, class => "ctx" },
3197 # classifier for context must come before classifier add/rem,
3198 # or we would have to use more complicated regexp, for example
3199 # qr/(?= {0,$m}\+)[+ ]{$num_sign}/, where $m = $num_sign - 1;
3200 { regexp
=> qr/^[+ ]{$num_sign}/, class => "add" },
3201 { regexp
=> qr/^[- ]{$num_sign}/, class => "rem" },
3203 for my $clsfy (@diff_line_classifier) {
3204 return $clsfy->{'class'}
3205 if ($line =~ $clsfy->{'regexp'});
3212 # assumes that $from and $to are defined and correctly filled,
3213 # and that $line holds a line of chunk header for unified diff
3214 sub format_unidiff_chunk_header
{
3215 my ($line, $from, $to) = @_;
3217 my ($from_text, $from_start, $from_lines, $to_text, $to_start, $to_lines, $section) =
3218 $line =~ m/^\@{2} (-(\d+)(?:,(\d+))?) (\+(\d+)(?:,(\d+))?) \@{2}(.*)$/;
3220 $from_lines = 0 unless defined $from_lines;
3221 $to_lines = 0 unless defined $to_lines;
3223 if ($from->{'href'}) {
3224 $from_text = $cgi->a({-href
=>"$from->{'href'}#l$from_start",
3225 -class=>"list"}, $from_text);
3227 if ($to->{'href'}) {
3228 $to_text = $cgi->a({-href
=>"$to->{'href'}#l$to_start",
3229 -class=>"list"}, $to_text);
3231 $line = "<span class=\"chunk_info\">@@ $from_text $to_text @@</span>" .
3232 "<span class=\"section\">" . esc_html
($section, -nbsp
=>1) . "</span>";
3236 # assumes that $from and $to are defined and correctly filled,
3237 # and that $line holds a line of chunk header for combined diff
3238 sub format_cc_diff_chunk_header
{
3239 my ($line, $from, $to) = @_;
3241 my ($prefix, $ranges, $section) = $line =~ m/^(\@+) (.*?) \@+(.*)$/;
3242 my (@from_text, @from_start, @from_nlines, $to_text, $to_start, $to_nlines);
3244 @from_text = split(' ', $ranges);
3245 for (my $i = 0; $i < @from_text; ++$i) {
3246 ($from_start[$i], $from_nlines[$i]) =
3247 (split(',', substr($from_text[$i], 1)), 0);
3250 $to_text = pop @from_text;
3251 $to_start = pop @from_start;
3252 $to_nlines = pop @from_nlines;
3254 $line = "<span class=\"chunk_info\">$prefix ";
3255 for (my $i = 0; $i < @from_text; ++$i) {
3256 if ($from->{'href'}[$i]) {
3257 $line .= $cgi->a({-href
=>"$from->{'href'}[$i]#l$from_start[$i]",
3258 -class=>"list"}, $from_text[$i]);
3260 $line .= $from_text[$i];
3264 if ($to->{'href'}) {
3265 $line .= $cgi->a({-href
=>"$to->{'href'}#l$to_start",
3266 -class=>"list"}, $to_text);
3270 $line .= " $prefix</span>" .
3271 "<span class=\"section\">" . esc_html
($section, -nbsp
=>1) . "</span>";
3275 # process patch (diff) line (not to be used for diff headers),
3276 # returning HTML-formatted (but not wrapped) line.
3277 # If the line is passed as a reference, it is treated as HTML and not
3279 sub format_diff_line
{
3280 my ($line, $diff_class, $from, $to) = @_;
3286 $line = untabify
($line);
3288 if ($from && $to && $line =~ m/^\@{2} /) {
3289 $line = format_unidiff_chunk_header
($line, $from, $to);
3290 } elsif ($from && $to && $line =~ m/^\@{3}/) {
3291 $line = format_cc_diff_chunk_header
($line, $from, $to);
3293 $line = esc_html
($line, -nbsp
=>1);
3297 my $diff_classes = "diff diff_body";
3298 $diff_classes .= " $diff_class" if ($diff_class);
3299 $line = "<div class=\"$diff_classes\">$line</div>\n";
3304 # Generates undef or something like "_snapshot_" or "snapshot (_tbz2_ _zip_)",
3305 # linked. Pass the hash of the tree/commit to snapshot.
3306 sub format_snapshot_links
{
3308 my $num_fmts = @snapshot_fmts;
3309 if ($num_fmts > 1) {
3310 # A parenthesized list of links bearing format names.
3311 # e.g. "snapshot (_tar.gz_ _zip_)"
3312 return "snapshot (" . join(' ', map
3319 }, $known_snapshot_formats{$_}{'display'})
3320 , @snapshot_fmts) . ")";
3321 } elsif ($num_fmts == 1) {
3322 # A single "snapshot" link whose tooltip bears the format name.
3324 my ($fmt) = @snapshot_fmts;
3330 snapshot_format
=>$fmt
3332 -title
=> "in format: $known_snapshot_formats{$fmt}{'display'}"
3334 } else { # $num_fmts == 0
3339 ## ......................................................................
3340 ## functions returning values to be passed, perhaps after some
3341 ## transformation, to other functions; e.g. returning arguments to href()
3343 # returns hash to be passed to href to generate gitweb URL
3344 # in -title key it returns description of link
3346 my $format = shift || 'Atom';
3347 my %res = (action
=> lc($format));
3348 my $matched_ref = 0;
3350 # feed links are possible only for project views
3351 return unless (defined $project);
3352 # some views should link to OPML, or to generic project feed,
3353 # or don't have specific feed yet (so they should use generic)
3354 return if (!$action || $action =~ /^(?:tags|heads|forks|tag|search)$/x);
3357 # branches refs uses 'refs/' + $get_branch_refs()[x] + '/' prefix
3358 # (fullname) to differentiate from tag links; this also makes
3359 # possible to detect branch links
3360 for my $ref (get_branch_refs
()) {
3361 if ((defined $hash_base && $hash_base =~ m!^refs/\Q$ref\E/(.*)$!) ||
3362 (defined $hash && $hash =~ m!^refs/\Q$ref\E/(.*)$!)) {
3364 $matched_ref = $ref;
3368 # find log type for feed description (title)
3370 if (defined $file_name) {
3371 $type = "history of $file_name";
3372 $type .= "/" if ($action eq 'tree');
3373 $type .= " on '$branch'" if (defined $branch);
3375 $type = "log of $branch" if (defined $branch);
3378 $res{-title
} = $type;
3379 $res{'hash'} = (defined $branch ?
"refs/$matched_ref/$branch" : undef);
3380 $res{'file_name'} = $file_name;
3385 ## ----------------------------------------------------------------------
3386 ## git utility subroutines, invoking git commands
3388 # returns path to the core git executable and the --git-dir parameter as list
3390 $number_of_git_cmds++;
3391 return $GIT, '--git-dir='.$git_dir;
3394 # opens a "-|" cmd pipe handle with 2>/dev/null and returns it
3397 # In order to be compatible with FCGI mode we must use POSIX
3398 # and access the STDERR_FILENO file descriptor directly
3400 use POSIX
qw(STDERR_FILENO dup dup2);
3402 open(my $null, '>', File
::Spec
->devnull) or die "couldn't open devnull: $!";
3403 (my $saveerr = dup
(STDERR_FILENO
)) or die "couldn't dup STDERR: $!";
3404 my $dup2ok = dup2
(fileno($null), STDERR_FILENO
);
3405 close($null) or !$dup2ok or die "couldn't close NULL: $!";
3406 $dup2ok or POSIX
::close($saveerr), die "couldn't dup NULL to STDERR: $!";
3407 my $result = open(my $fd, "-|", @_);
3408 $dup2ok = dup2
($saveerr, STDERR_FILENO
);
3409 POSIX
::close($saveerr) or !$dup2ok or die "couldn't close SAVEERR: $!";
3410 $dup2ok or die "couldn't dup SAVERR to STDERR: $!";
3412 return $result ?
$fd : undef;
3415 # opens a "-|" git_cmd pipe handle with 2>/dev/null and returns it
3417 return cmd_pipe git_cmd
(), @_;
3420 # quote the given arguments for passing them to the shell
3421 # quote_command("command", "arg 1", "arg with ' and ! characters")
3422 # => "'command' 'arg 1' 'arg with '\'' and '\!' characters'"
3423 # Try to avoid using this function wherever possible.
3426 map { my $a = $_; $a =~ s/(['!])/'\\$1'/g; "'$a'" } @_ );
3429 # get HEAD ref of given project as hash
3430 sub git_get_head_hash
{
3431 return git_get_full_hash
(shift, 'HEAD');
3434 sub git_get_full_hash
{
3435 return git_get_hash
(@_);
3438 sub git_get_short_hash
{
3439 return git_get_hash
(@_, '--short=7');
3443 my ($project, $hash, @options) = @_;
3444 my $o_git_dir = $git_dir;
3446 $git_dir = "$projectroot/$project";
3447 if (defined(my $fd = git_cmd_pipe
'rev-parse',
3448 '--verify', '-q', @options, $hash)) {
3450 chomp $retval if defined $retval;
3453 if (defined $o_git_dir) {
3454 $git_dir = $o_git_dir;
3459 # get type of given object
3463 defined(my $fd = git_cmd_pipe
"cat-file", '-t', $hash) or return;
3465 close $fd or return;
3470 # repository configuration
3471 our $config_file = '';
3474 # store multiple values for single key as anonymous array reference
3475 # single values stored directly in the hash, not as [ <value> ]
3476 sub hash_set_multi
{
3477 my ($hash, $key, $value) = @_;
3479 if (!exists $hash->{$key}) {
3480 $hash->{$key} = $value;
3481 } elsif (!ref $hash->{$key}) {
3482 $hash->{$key} = [ $hash->{$key}, $value ];
3484 push @
{$hash->{$key}}, $value;
3488 # return hash of git project configuration
3489 # optionally limited to some section, e.g. 'gitweb'
3490 sub git_parse_project_config
{
3491 my $section_regexp = shift;
3496 defined(my $fh = git_cmd_pipe
"config", '-z', '-l')
3499 while (my $keyval = to_utf8
(scalar <$fh>)) {
3501 my ($key, $value) = split(/\n/, $keyval, 2);
3503 hash_set_multi
(\
%config, $key, $value)
3504 if (!defined $section_regexp || $key =~ /^(?:$section_regexp)\./o);
3511 # convert config value to boolean: 'true' or 'false'
3512 # no value, number > 0, 'true' and 'yes' values are true
3513 # rest of values are treated as false (never as error)
3514 sub config_to_bool
{
3517 return 1 if !defined $val; # section.key
3519 # strip leading and trailing whitespace
3523 return (($val =~ /^\d+$/ && $val) || # section.key = 1
3524 ($val =~ /^(?:true|yes)$/i)); # section.key = true
3527 # convert config value to simple decimal number
3528 # an optional value suffix of 'k', 'm', or 'g' will cause the value
3529 # to be multiplied by 1024, 1048576, or 1073741824
3533 # strip leading and trailing whitespace
3537 if (my ($num, $unit) = ($val =~ /^([0-9]*)([kmg])$/i)) {
3539 # unknown unit is treated as 1
3540 return $num * ($unit eq 'g' ?
1073741824 :
3541 $unit eq 'm' ?
1048576 :
3542 $unit eq 'k' ?
1024 : 1);
3547 # convert config value to array reference, if needed
3548 sub config_to_multi
{
3551 return ref($val) ?
$val : (defined($val) ?
[ $val ] : []);
3554 sub git_get_project_config
{
3555 my ($key, $type) = @_;
3557 return unless defined $git_dir;
3560 return unless ($key);
3561 # only subsection, if exists, is case sensitive,
3562 # and not lowercased by 'git config -z -l'
3563 if (my ($hi, $mi, $lo) = ($key =~ /^([^.]*)\.(.*)\.([^.]*)$/)) {
3565 $key = join(".", lc($hi), $mi, lc($lo));
3566 return if ($lo =~ /\W/ || $hi =~ /\W/);
3570 return if ($key =~ /\W/);
3572 $key =~ s/^gitweb\.//;
3575 if (defined $type) {
3578 unless ($type eq 'bool' || $type eq 'int');
3582 if (!defined $config_file ||
3583 $config_file ne "$git_dir/config") {
3584 %config = git_parse_project_config
('gitweb');
3585 $config_file = "$git_dir/config";
3588 # check if config variable (key) exists
3589 return unless exists $config{"gitweb.$key"};
3592 if (!defined $type) {
3593 return $config{"gitweb.$key"};
3594 } elsif ($type eq 'bool') {
3595 # backward compatibility: 'git config --bool' returns true/false
3596 return config_to_bool
($config{"gitweb.$key"}) ?
'true' : 'false';
3597 } elsif ($type eq 'int') {
3598 return config_to_int
($config{"gitweb.$key"});
3600 return $config{"gitweb.$key"};
3603 # get hash of given path at given ref
3604 sub git_get_hash_by_path
{
3606 my $path = shift || return undef;
3611 defined(my $fd = git_cmd_pipe
"ls-tree", $base, "--", $path)
3612 or die_error
(500, "Open git-ls-tree failed");
3613 my $line = to_utf8
(scalar <$fd>);
3614 close $fd or return undef;
3616 if (!defined $line) {
3617 # there is no tree or hash given by $path at $base
3621 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'
3622 $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t/;
3623 if (defined $type && $type ne $2) {
3624 # type doesn't match
3630 # get path of entry with given hash at given tree-ish (ref)
3631 # used to get 'from' filename for combined diff (merge commit) for renames
3632 sub git_get_path_by_hash
{
3633 my $base = shift || return;
3634 my $hash = shift || return;
3638 defined(my $fd = git_cmd_pipe
"ls-tree", '-r', '-t', '-z', $base)
3640 while (my $line = to_utf8
(scalar <$fd>)) {
3643 #'040000 tree 595596a6a9117ddba9fe379b6b012b558bac8423 gitweb'
3644 #'100644 blob e02e90f0429be0d2a69b76571101f20b8f75530f gitweb/README'
3645 if ($line =~ m/(?:[0-9]+) (?:.+) $hash\t(.+)$/) {
3654 ## ......................................................................
3655 ## git utility functions, directly accessing git repository
3657 # get the value of config variable either from file named as the variable
3658 # itself in the repository ($GIT_DIR/$name file), or from gitweb.$name
3659 # configuration variable in the repository config file.
3660 sub git_get_file_or_project_config
{
3661 my ($path, $name) = @_;
3663 $git_dir = "$projectroot/$path";
3664 open my $fd, '<', "$git_dir/$name"
3665 or return git_get_project_config
($name);
3666 my $conf = to_utf8
(scalar <$fd>);
3668 if (defined $conf) {
3674 sub git_get_project_description
{
3676 return git_get_file_or_project_config
($path, 'description');
3679 sub git_get_project_category
{
3681 return git_get_file_or_project_config
($path, 'category');
3685 # supported formats:
3686 # * $GIT_DIR/ctags/<tagname> file (in 'ctags' subdirectory)
3687 # - if its contents is a number, use it as tag weight,
3688 # - otherwise add a tag with weight 1
3689 # * $GIT_DIR/ctags file, each line is a tag (with weight 1)
3690 # the same value multiple times increases tag weight
3691 # * `gitweb.ctag' multi-valued repo config variable
3692 sub git_get_project_ctags
{
3693 my $project = shift;
3696 $git_dir = "$projectroot/$project";
3697 if (opendir my $dh, "$git_dir/ctags") {
3698 my @files = grep { -f
$_ } map { "$git_dir/ctags/$_" } readdir($dh);
3699 foreach my $tagfile (@files) {
3700 open my $ct, '<', $tagfile
3706 (my $ctag = $tagfile) =~ s
#.*/##;
3707 $ctag = to_utf8
($ctag);
3708 if ($val =~ /^\d+$/) {
3709 $ctags->{$ctag} = $val;
3711 $ctags->{$ctag} = 1;
3716 } elsif (open my $fh, '<', "$git_dir/ctags") {
3717 while (my $line = to_utf8
(scalar <$fh>)) {
3719 $ctags->{$line}++ if $line;
3724 my $taglist = config_to_multi
(git_get_project_config
('ctag'));
3725 foreach my $tag (@
$taglist) {
3733 # return hash, where keys are content tags ('ctags'),
3734 # and values are sum of weights of given tag in every project
3735 sub git_gather_all_ctags
{
3736 my $projects = shift;
3739 foreach my $p (@
$projects) {
3740 foreach my $ct (keys %{$p->{'ctags'}}) {
3741 $ctags->{$ct} += $p->{'ctags'}->{$ct};
3748 sub git_populate_project_tagcloud
{
3749 my ($ctags, $action) = @_;
3751 # First, merge different-cased tags; tags vote on casing
3753 foreach (keys %$ctags) {
3754 $ctags_lc{lc $_}->{count
} += $ctags->{$_};
3755 if (not $ctags_lc{lc $_}->{topcount
}
3756 or $ctags_lc{lc $_}->{topcount
} < $ctags->{$_}) {
3757 $ctags_lc{lc $_}->{topcount
} = $ctags->{$_};
3758 $ctags_lc{lc $_}->{topname
} = $_;
3763 my $matched = $input_params{'ctag_filter'};
3764 if (eval { require HTML
::TagCloud
; 1; }) {
3765 $cloud = HTML
::TagCloud
->new;
3766 foreach my $ctag (sort keys %ctags_lc) {
3767 # Pad the title with spaces so that the cloud looks
3769 my $title = esc_html
($ctags_lc{$ctag}->{topname
});
3770 $title =~ s/ / /g;
3771 $title =~ s/^/ /g;
3772 $title =~ s/$/ /g;
3773 if (defined $matched && $matched eq $ctag) {
3774 $title = qq(<span
class="match">$title</span
>);
3776 $cloud->add($title, href
(-replay
=>1, action
=>$action, ctag_filter
=>$ctag),
3777 $ctags_lc{$ctag}->{count
});
3781 foreach my $ctag (keys %ctags_lc) {
3782 my $title = esc_html
($ctags_lc{$ctag}->{topname
}, -nbsp
=>1);
3783 if (defined $matched && $matched eq $ctag) {
3784 $title = qq(<span
class="match">$title</span
>);
3786 $cloud->{$ctag}{count
} = $ctags_lc{$ctag}->{count
};
3787 $cloud->{$ctag}{ctag
} =
3788 $cgi->a({-href
=>href
(-replay
=>1, action
=>$action, ctag_filter
=>$ctag)}, $title);
3794 sub git_show_project_tagcloud
{
3795 my ($cloud, $count) = @_;
3796 if (ref $cloud eq 'HTML::TagCloud') {
3797 return $cloud->html_and_css($count);
3799 my @tags = sort { $cloud->{$a}->{'count'} <=> $cloud->{$b}->{'count'} } keys %$cloud;
3801 '<div id="htmltagcloud"'.($project ?
'' : ' align="center"').'>' .
3803 $cloud->{$_}->{'ctag'}
3804 } splice(@tags, 0, $count)) .
3809 sub git_get_project_url_list
{
3812 $git_dir = "$projectroot/$path";
3813 open my $fd, '<', "$git_dir/cloneurl"
3814 or return wantarray ?
3815 @
{ config_to_multi
(git_get_project_config
('url')) } :
3816 config_to_multi
(git_get_project_config
('url'));
3817 my @git_project_url_list = map { chomp; to_utf8
($_) } <$fd>;
3820 return wantarray ?
@git_project_url_list : \
@git_project_url_list;
3823 sub git_get_projects_list
{
3825 my $paranoid = shift;
3827 defined($filter) or $filter = "";
3829 if (-d
$projects_list) {
3830 # search in directory
3831 my $dir = $projects_list;
3832 # remove the trailing "/"
3834 my $pfxlen = length("$dir");
3835 my $pfxdepth = ($dir =~ tr!/!!);
3836 # when filtering, search only given subdirectory
3837 if ($filter ne "" && !$paranoid) {
3843 follow_fast
=> 1, # follow symbolic links
3844 follow_skip
=> 2, # ignore duplicates
3845 dangling_symlinks
=> 0, # ignore dangling symlinks, silently
3848 our $project_maxdepth;
3850 # skip project-list toplevel, if we get it.
3851 return if (m!^[/.]$!);
3852 # only directories can be git repositories
3853 return unless (-d
$_);
3854 # don't traverse too deep (Find is super slow on os x)
3855 # $project_maxdepth excludes depth of $projectroot
3856 if (($File::Find
::name
=~ tr!/!!) - $pfxdepth > $project_maxdepth) {
3857 $File::Find
::prune
= 1;
3861 my $path = substr($File::Find
::name
, $pfxlen + 1);
3862 # paranoidly only filter here
3863 if ($paranoid && $filter && $path !~ m!^\Q$filter\E/!) {
3866 # we check related file in $projectroot
3867 if (check_export_ok
("$projectroot/$path")) {
3868 push @list, { path
=> $path };
3869 $File::Find
::prune
= 1;
3874 } elsif (-f
$projects_list) {
3875 # read from file(url-encoded):
3876 # 'git%2Fgit.git Linus+Torvalds'
3877 # 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin'
3878 # 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman'
3879 open my $fd, '<', $projects_list or return;
3881 while (my $line = <$fd>) {
3883 my ($path, $owner) = split ' ', $line;
3884 $path = unescape
($path);
3885 $owner = unescape
($owner);
3886 if (!defined $path) {
3889 # if $filter is rpovided, check if $path begins with $filter
3890 if ($filter ne "" && $path !~ m!^\Q$filter\E/!) {
3893 if (check_export_ok
("$projectroot/$path")) {
3898 $pr->{'owner'} = to_utf8
($owner);
3908 # written with help of Tree::Trie module (Perl Artistic License, GPL compatibile)
3909 # as side effects it sets 'forks' field to list of forks for forked projects
3910 sub filter_forks_from_projects_list
{
3911 my $projects = shift;
3913 my %trie; # prefix tree of directories (path components)
3914 # generate trie out of those directories that might contain forks
3915 foreach my $pr (@
$projects) {
3916 my $path = $pr->{'path'};
3917 $path =~ s/\.git$//; # forks of 'repo.git' are in 'repo/' directory
3918 next if ($path =~ m!/$!); # skip non-bare repositories, e.g. 'repo/.git'
3919 next if ($path eq ""); # skip '.git' repository: tests, git-instaweb
3920 next unless (-d
"$projectroot/$path"); # containing directory exists
3921 $pr->{'forks'} = []; # there can be 0 or more forks of project
3924 my @dirs = split('/', $path);
3925 # walk the trie, until either runs out of components or out of trie
3927 while (scalar @dirs &&
3928 exists($ref->{$dirs[0]})) {
3929 $ref = $ref->{shift @dirs};
3931 # create rest of trie structure from rest of components
3932 foreach my $dir (@dirs) {
3933 $ref = $ref->{$dir} = {};
3935 # create end marker, store $pr as a data
3936 $ref->{''} = $pr if (!exists $ref->{''});
3939 # filter out forks, by finding shortest prefix match for paths
3942 foreach my $pr (@
$projects) {
3946 foreach my $dir (split('/', $pr->{'path'})) {
3947 if (exists $ref->{''}) {
3948 # found [shortest] prefix, is a fork - skip it
3949 push @
{$ref->{''}{'forks'}}, $pr;
3952 if (!exists $ref->{$dir}) {
3953 # not in trie, cannot have prefix, not a fork
3954 push @filtered, $pr;
3957 # If the dir is there, we just walk one step down the trie.
3958 $ref = $ref->{$dir};
3960 # we ran out of trie
3961 # (shouldn't happen: it's either no match, or end marker)
3962 push @filtered, $pr;
3968 # note: fill_project_list_info must be run first,
3969 # for 'descr_long' and 'ctags' to be filled
3970 sub search_projects_list
{
3971 my ($projlist, %opts) = @_;
3972 my $tagfilter = $opts{'tagfilter'};
3973 my $search_re = $opts{'search_regexp'};
3976 unless ($tagfilter || $search_re);
3978 # searching projects require filling to be run before it;
3979 fill_project_list_info
($projlist,
3980 $tagfilter ?
'ctags' : (),
3981 $search_re ?
('path', 'descr') : ());
3984 foreach my $pr (@
$projlist) {
3987 next unless ref($pr->{'ctags'}) eq 'HASH';
3989 grep { lc($_) eq lc($tagfilter) } keys %{$pr->{'ctags'}};
3993 my $path = $pr->{'path'};
3994 $path =~ s/\.git$//; # should not be included in search
3996 $path =~ /$search_re/ ||
3997 $pr->{'descr_long'} =~ /$search_re/;
4000 push @projects, $pr;
4006 our $gitweb_project_owner = undef;
4007 sub git_get_project_list_from_file
{
4009 return if (defined $gitweb_project_owner);
4011 $gitweb_project_owner = {};
4012 # read from file (url-encoded):
4013 # 'git%2Fgit.git Linus+Torvalds'
4014 # 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin'
4015 # 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman'
4016 if (-f
$projects_list) {
4017 open(my $fd, '<', $projects_list);
4018 while (my $line = <$fd>) {
4020 my ($pr, $ow) = split ' ', $line;
4021 $pr = unescape
($pr);
4022 $ow = unescape
($ow);
4023 $gitweb_project_owner->{$pr} = to_utf8
($ow);
4029 sub git_get_project_owner
{
4033 return undef unless $proj;
4034 $git_dir = "$projectroot/$proj";
4036 if (defined $project && $proj eq $project) {
4037 $owner = git_get_project_config
('owner');
4039 if (!defined $owner && !defined $gitweb_project_owner) {
4040 git_get_project_list_from_file
();
4042 if (!defined $owner && exists $gitweb_project_owner->{$proj}) {
4043 $owner = $gitweb_project_owner->{$proj};
4045 if (!defined $owner && (!defined $project || $proj ne $project)) {
4046 $owner = git_get_project_config
('owner');
4048 if (!defined $owner) {
4049 $owner = get_file_owner
("$git_dir");
4055 sub parse_activity_date
{
4058 if ($dstr =~ /^\s*([-+]?\d+)(?:\s+([-+]\d{4}))?\s*$/) {
4062 if ($dstr =~ /^\s*(\d{4})-(\d{2})-(\d{2})[Tt _](\d{1,2}):(\d{2}):(\d{2})(?:[ _]?([Zz]|(?:[-+]\d{1,2}:?\d{2})))?\s*$/) {
4063 my ($Y,$m,$d,$H,$M,$S,$z) = ($1,$2,$3,$4,$5,$6,$7||'');
4064 my $seconds = timegm
(0+$S, 0+$M, 0+$H, 0+$d, $m-1, 0+$Y);
4065 defined($z) && $z ne '' or $z = 'Z';
4067 substr($z,1,0) = '0' if length($z) == 4;
4069 if (uc($z) ne 'Z') {
4070 $off = 60 * (60 * (0+substr($z,1,2)) + (0+substr($z,3,2)));
4071 $off = -$off if substr($z,0,1) eq '-';
4073 return $seconds - $off;
4078 # If $quick is true only look at $lastactivity_file
4079 sub git_get_last_activity
{
4080 my ($path, $quick) = @_;
4083 $git_dir = "$projectroot/$path";
4084 if ($lastactivity_file && open($fd, "<", "$git_dir/$lastactivity_file")) {
4085 my $activity = <$fd>;
4087 return (undef) unless defined $activity;
4089 return (undef) if $activity eq '';
4090 if (my $timestamp = parse_activity_date
($activity)) {
4091 return ($timestamp);
4094 return (undef) if $quick;
4095 defined($fd = git_cmd_pipe
'for-each-ref',
4096 '--format=%(committer)',
4097 '--sort=-committerdate',
4099 map { "refs/$_" } get_branch_refs
()) or return;
4100 my $most_recent = <$fd>;
4101 close $fd or return (undef);
4102 if (defined $most_recent &&
4103 $most_recent =~ / (\d+) [-+][01]\d\d\d$/) {
4105 return ($timestamp);
4110 # Implementation note: when a single remote is wanted, we cannot use 'git
4111 # remote show -n' because that command always work (assuming it's a remote URL
4112 # if it's not defined), and we cannot use 'git remote show' because that would
4113 # try to make a network roundtrip. So the only way to find if that particular
4114 # remote is defined is to walk the list provided by 'git remote -v' and stop if
4115 # and when we find what we want.
4116 sub git_get_remotes_list
{
4120 my $fd = git_cmd_pipe
'remote', '-v';
4122 while (my $remote = to_utf8
(scalar <$fd>)) {
4124 $remote =~ s!\t(.*?)\s+\((\w+)\)$!!;
4125 next if $wanted and not $remote eq $wanted;
4126 my ($url, $key) = ($1, $2);
4128 $remotes{$remote} ||= { 'heads' => [] };
4129 $remotes{$remote}{$key} = $url;
4131 close $fd or return;
4132 return wantarray ?
%remotes : \
%remotes;
4135 # Takes a hash of remotes as first parameter and fills it by adding the
4136 # available remote heads for each of the indicated remotes.
4137 sub fill_remote_heads
{
4138 my $remotes = shift;
4139 my @heads = map { "remotes/$_" } keys %$remotes;
4140 my @remoteheads = git_get_heads_list
(undef, @heads);
4141 foreach my $remote (keys %$remotes) {
4142 $remotes->{$remote}{'heads'} = [ grep {
4143 $_->{'name'} =~ s!^$remote/!!
4148 sub git_get_references
{
4149 my $type = shift || "";
4151 # 5dc01c595e6c6ec9ccda4f6f69c131c0dd945f8c refs/tags/v2.6.11
4152 # c39ae07f393806ccf406ef966e9a15afc43cc36a refs/tags/v2.6.11^{}
4153 defined(my $fd = git_cmd_pipe
"show-ref", "--dereference",
4154 ($type ?
("--", "refs/$type") : ())) # use -- <pattern> if $type
4157 while (my $line = to_utf8
(scalar <$fd>)) {
4159 if ($line =~ m!^([0-9a-fA-F]{40})\srefs/($type.*)$!) {
4160 if (defined $refs{$1}) {
4161 push @
{$refs{$1}}, $2;
4167 close $fd or return;
4171 sub git_get_rev_name_tags
{
4172 my $hash = shift || return undef;
4174 defined(my $fd = git_cmd_pipe
"name-rev", "--tags", $hash)
4176 my $name_rev = to_utf8
(scalar <$fd>);
4179 if ($name_rev =~ m
|^$hash tags
/(.*)$|) {
4182 # catches also '$hash undefined' output
4187 ## ----------------------------------------------------------------------
4188 ## parse to hash functions
4192 my $tz = shift || "-0000";
4195 my @months = ("Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec");
4196 my @days = ("Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat");
4197 my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($epoch);
4198 $date{'hour'} = $hour;
4199 $date{'minute'} = $min;
4200 $date{'mday'} = $mday;
4201 $date{'day'} = $days[$wday];
4202 $date{'month'} = $months[$mon];
4203 $date{'rfc2822'} = sprintf "%s, %d %s %4d %02d:%02d:%02d +0000",
4204 $days[$wday], $mday, $months[$mon], 1900+$year, $hour ,$min, $sec;
4205 $date{'mday-time'} = sprintf "%d %s %02d:%02d",
4206 $mday, $months[$mon], $hour ,$min;
4207 $date{'iso-8601'} = sprintf "%04d-%02d-%02dT%02d:%02d:%02dZ",
4208 1900+$year, 1+$mon, $mday, $hour ,$min, $sec;
4210 my ($tz_sign, $tz_hour, $tz_min) =
4211 ($tz =~ m/^([-+])(\d\d)(\d\d)$/);
4212 $tz_sign = ($tz_sign eq '-' ?
-1 : +1);
4213 my $local = $epoch + $tz_sign*((($tz_hour*60) + $tz_min)*60);
4214 ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($local);
4215 $date{'hour_local'} = $hour;
4216 $date{'minute_local'} = $min;
4217 $date{'mday_local'} = $mday;
4218 $date{'tz_local'} = $tz;
4219 $date{'iso-tz'} = sprintf("%04d-%02d-%02d %02d:%02d:%02d %s",
4220 1900+$year, $mon+1, $mday,
4221 $hour, $min, $sec, $tz);
4225 sub parse_file_date
{
4227 my $mtime = (stat("$projectroot/$project/$file"))[9];
4228 return () unless defined $mtime;
4229 my ($sec,$min,$hour,$mday,$mon,$year) = localtime($mtime);
4230 my $tzoffset = timegm
($sec,$min,$hour,$mday,$mon,$year+1900) - $mtime;
4232 if ($tzoffset <= 0) {
4236 $tzoffset = int($tzoffset/60);
4237 $tzstring .= sprintf("%02d%02d", int($tzoffset/60), $tzoffset%60);
4238 return parse_date
($mtime, $tzstring);
4246 defined(my $fd = git_cmd_pipe
"cat-file", "tag", $tag_id) or return;
4247 $tag{'id'} = $tag_id;
4248 while (my $line = to_utf8
(scalar <$fd>)) {
4250 if ($line =~ m/^object ([0-9a-fA-F]{40})$/) {
4251 $tag{'object'} = $1;
4252 } elsif ($line =~ m/^type (.+)$/) {
4254 } elsif ($line =~ m/^tag (.+)$/) {
4256 } elsif ($line =~ m/^tagger (.*) ([0-9]+) (.*)$/) {
4257 $tag{'author'} = $1;
4258 $tag{'author_epoch'} = $2;
4259 $tag{'author_tz'} = $3;
4260 if ($tag{'author'} =~ m/^([^<]+) <([^>]*)>/) {
4261 $tag{'author_name'} = $1;
4262 $tag{'author_email'} = $2;
4264 $tag{'author_name'} = $tag{'author'};
4266 } elsif ($line =~ m/--BEGIN/) {
4267 push @comment, $line;
4269 } elsif ($line eq "") {
4273 push @comment, map(to_utf8
($_), <$fd>);
4274 $tag{'comment'} = \
@comment;
4275 close $fd or return;
4276 if (!defined $tag{'name'}) {
4282 sub parse_commit_text
{
4283 my ($commit_text, $withparents) = @_;
4284 my @commit_lines = split '\n', $commit_text;
4287 pop @commit_lines; # Remove '\0'
4289 if (! @commit_lines) {
4293 my $header = shift @commit_lines;
4294 if ($header !~ m/^[0-9a-fA-F]{40}/) {
4297 ($co{'id'}, my @parents) = split ' ', $header;
4298 while (my $line = shift @commit_lines) {
4299 last if $line eq "\n";
4300 if ($line =~ m/^tree ([0-9a-fA-F]{40})$/) {
4302 } elsif ((!defined $withparents) && ($line =~ m/^parent ([0-9a-fA-F]{40})$/)) {
4304 } elsif ($line =~ m/^author (.*) ([0-9]+) (.*)$/) {
4305 $co{'author'} = to_utf8
($1);
4306 $co{'author_epoch'} = $2;
4307 $co{'author_tz'} = $3;
4308 if ($co{'author'} =~ m/^([^<]+) <([^>]*)>/) {
4309 $co{'author_name'} = $1;
4310 $co{'author_email'} = $2;
4312 $co{'author_name'} = $co{'author'};
4314 } elsif ($line =~ m/^committer (.*) ([0-9]+) (.*)$/) {
4315 $co{'committer'} = to_utf8
($1);
4316 $co{'committer_epoch'} = $2;
4317 $co{'committer_tz'} = $3;
4318 if ($co{'committer'} =~ m/^([^<]+) <([^>]*)>/) {
4319 $co{'committer_name'} = $1;
4320 $co{'committer_email'} = $2;
4322 $co{'committer_name'} = $co{'committer'};
4326 if (!defined $co{'tree'}) {
4329 $co{'parents'} = \
@parents;
4330 $co{'parent'} = $parents[0];
4332 @commit_lines = map to_utf8
($_), @commit_lines;
4333 foreach my $title (@commit_lines) {
4336 $co{'title'} = chop_str
($title, 80, 5);
4337 # remove leading stuff of merges to make the interesting part visible
4338 if (length($title) > 50) {
4339 $title =~ s/^Automatic //;
4340 $title =~ s/^merge (of|with) /Merge ... /i;
4341 if (length($title) > 50) {
4342 $title =~ s/(http|rsync):\/\///;
4344 if (length($title) > 50) {
4345 $title =~ s/(master|www|rsync)\.//;
4347 if (length($title) > 50) {
4348 $title =~ s/kernel.org:?//;
4350 if (length($title) > 50) {
4351 $title =~ s/\/pub\/scm//;
4354 $co{'title_short'} = chop_str
($title, 50, 5);
4358 if (! defined $co{'title'} || $co{'title'} eq "") {
4359 $co{'title'} = $co{'title_short'} = '(no commit message)';
4361 # remove added spaces
4362 foreach my $line (@commit_lines) {
4365 $co{'comment'} = \
@commit_lines;
4367 my $age_epoch = $co{'committer_epoch'};
4368 $co{'age_epoch'} = $age_epoch;
4369 my $time_now = time;
4370 $co{'age_string'} = age_string
($age_epoch, $time_now);
4371 $co{'age_string_date'} = age_string_date
($age_epoch, $time_now);
4372 $co{'age_string_age'} = age_string_age
($age_epoch, $time_now);
4377 my ($commit_id) = @_;
4382 defined(my $fd = git_cmd_pipe
"rev-list",
4388 or die_error
(500, "Open git-rev-list failed");
4389 %co = parse_commit_text
(<$fd>, 1);
4396 my ($commit_id, $maxcount, $skip, $filename, @args) = @_;
4404 defined(my $fd = git_cmd_pipe
"rev-list",
4407 ("--max-count=" . $maxcount),
4408 ("--skip=" . $skip),
4412 ($filename ?
($filename) : ()))
4413 or die_error
(500, "Open git-rev-list failed");
4414 while (my $line = <$fd>) {
4415 my %co = parse_commit_text
($line);
4420 return wantarray ?
@cos : \
@cos;
4423 # parse line of git-diff-tree "raw" output
4424 sub parse_difftree_raw_line
{
4428 # ':100644 100644 03b218260e99b78c6df0ed378e59ed9205ccc96d 3b93d5e7cc7f7dd4ebed13a5cc1a4ad976fc94d8 M ls-files.c'
4429 # ':100644 100644 7f9281985086971d3877aca27704f2aaf9c448ce bc190ebc71bbd923f2b728e505408f5e54bd073a M rev-tree.c'
4430 if ($line =~ m/^:([0-7]{6}) ([0-7]{6}) ([0-9a-fA-F]{40}) ([0-9a-fA-F]{40}) (.)([0-9]{0,3})\t(.*)$/) {
4431 $res{'from_mode'} = $1;
4432 $res{'to_mode'} = $2;
4433 $res{'from_id'} = $3;
4435 $res{'status'} = $5;
4436 $res{'similarity'} = $6;
4437 if ($res{'status'} eq 'R' || $res{'status'} eq 'C') { # renamed or copied
4438 ($res{'from_file'}, $res{'to_file'}) = map { unquote
($_) } split("\t", $7);
4440 $res{'from_file'} = $res{'to_file'} = $res{'file'} = unquote
($7);
4443 # '::100755 100755 100755 60e79ca1b01bc8b057abe17ddab484699a7f5fdb 94067cc5f73388f33722d52ae02f44692bc07490 94067cc5f73388f33722d52ae02f44692bc07490 MR git-gui/git-gui.sh'
4444 # combined diff (for merge commit)
4445 elsif ($line =~ s/^(::+)((?:[0-7]{6} )+)((?:[0-9a-fA-F]{40} )+)([a-zA-Z]+)\t(.*)$//) {
4446 $res{'nparents'} = length($1);
4447 $res{'from_mode'} = [ split(' ', $2) ];
4448 $res{'to_mode'} = pop @
{$res{'from_mode'}};
4449 $res{'from_id'} = [ split(' ', $3) ];
4450 $res{'to_id'} = pop @
{$res{'from_id'}};
4451 $res{'status'} = [ split('', $4) ];
4452 $res{'to_file'} = unquote
($5);
4454 # 'c512b523472485aef4fff9e57b229d9d243c967f'
4455 elsif ($line =~ m/^([0-9a-fA-F]{40})$/) {
4456 $res{'commit'} = $1;
4459 return wantarray ?
%res : \
%res;
4462 # wrapper: return parsed line of git-diff-tree "raw" output
4463 # (the argument might be raw line, or parsed info)
4464 sub parsed_difftree_line
{
4465 my $line_or_ref = shift;
4467 if (ref($line_or_ref) eq "HASH") {
4468 # pre-parsed (or generated by hand)
4469 return $line_or_ref;
4471 return parse_difftree_raw_line
($line_or_ref);
4475 # parse line of git-ls-tree output
4476 sub parse_ls_tree_line
{
4482 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa 16717 panic.c'
4483 $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40}) +(-|[0-9]+)\t(.+)$/s;
4492 $res{'name'} = unquote
($5);
4495 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'
4496 $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t(.+)$/s;
4504 $res{'name'} = unquote
($4);
4508 return wantarray ?
%res : \
%res;
4511 # generates _two_ hashes, references to which are passed as 2 and 3 argument
4512 sub parse_from_to_diffinfo
{
4513 my ($diffinfo, $from, $to, @parents) = @_;
4515 if ($diffinfo->{'nparents'}) {
4517 $from->{'file'} = [];
4518 $from->{'href'} = [];
4519 fill_from_file_info
($diffinfo, @parents)
4520 unless exists $diffinfo->{'from_file'};
4521 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
4522 $from->{'file'}[$i] =
4523 defined $diffinfo->{'from_file'}[$i] ?
4524 $diffinfo->{'from_file'}[$i] :
4525 $diffinfo->{'to_file'};
4526 if ($diffinfo->{'status'}[$i] ne "A") { # not new (added) file
4527 $from->{'href'}[$i] = href
(action
=>"blob",
4528 hash_base
=>$parents[$i],
4529 hash
=>$diffinfo->{'from_id'}[$i],
4530 file_name
=>$from->{'file'}[$i]);
4532 $from->{'href'}[$i] = undef;
4536 # ordinary (not combined) diff
4537 $from->{'file'} = $diffinfo->{'from_file'};
4538 if ($diffinfo->{'status'} ne "A") { # not new (added) file
4539 $from->{'href'} = href
(action
=>"blob", hash_base
=>$hash_parent,
4540 hash
=>$diffinfo->{'from_id'},
4541 file_name
=>$from->{'file'});
4543 delete $from->{'href'};
4547 $to->{'file'} = $diffinfo->{'to_file'};
4548 if (!is_deleted
($diffinfo)) { # file exists in result
4549 $to->{'href'} = href
(action
=>"blob", hash_base
=>$hash,
4550 hash
=>$diffinfo->{'to_id'},
4551 file_name
=>$to->{'file'});
4553 delete $to->{'href'};
4557 ## ......................................................................
4558 ## parse to array of hashes functions
4560 sub git_get_heads_list
{
4561 my ($limit, @classes) = @_;
4562 @classes = get_branch_refs
() unless @classes;
4563 my @patterns = map { "refs/$_" } @classes;
4566 defined(my $fd = git_cmd_pipe
'for-each-ref',
4567 ($limit ?
'--count='.($limit+1) : ()), '--sort=-committerdate',
4568 '--format=%(objectname) %(refname) %(subject)%00%(committer)',
4571 while (my $line = to_utf8
(scalar <$fd>)) {
4575 my ($refinfo, $committerinfo) = split(/\0/, $line);
4576 my ($hash, $name, $title) = split(' ', $refinfo, 3);
4577 my ($committer, $epoch, $tz) =
4578 ($committerinfo =~ /^(.*) ([0-9]+) (.*)$/);
4579 $ref_item{'fullname'} = $name;
4580 my $strip_refs = join '|', map { quotemeta } get_branch_refs
();
4581 $name =~ s!^refs/($strip_refs|remotes)/!!;
4582 $ref_item{'name'} = $name;
4583 # for refs neither in 'heads' nor 'remotes' we want to
4584 # show their ref dir
4585 my $ref_dir = (defined $1) ?
$1 : '';
4586 if ($ref_dir ne '' and $ref_dir ne 'heads' and $ref_dir ne 'remotes') {
4587 $ref_item{'name'} .= ' (' . $ref_dir . ')';
4590 $ref_item{'id'} = $hash;
4591 $ref_item{'title'} = $title || '(no commit message)';
4592 $ref_item{'epoch'} = $epoch;
4594 $ref_item{'age'} = age_string
($ref_item{'epoch'});
4596 $ref_item{'age'} = "unknown";
4599 push @headslist, \
%ref_item;
4603 return wantarray ?
@headslist : \
@headslist;
4606 sub git_get_tags_list
{
4609 my $all = shift || 0;
4610 my $order = shift || $default_refs_order;
4611 my $sortkey = $all && $order eq 'name' ?
'refname' : '-creatordate';
4613 defined(my $fd = git_cmd_pipe
'for-each-ref',
4614 ($limit ?
'--count='.($limit+1) : ()), "--sort=$sortkey",
4615 '--format=%(objectname) %(objecttype) %(refname) '.
4616 '%(*objectname) %(*objecttype) %(subject)%00%(creator)',
4617 ($all ?
'refs' : 'refs/tags'))
4619 while (my $line = to_utf8
(scalar <$fd>)) {
4623 my ($refinfo, $creatorinfo) = split(/\0/, $line);
4624 my ($id, $type, $name, $refid, $reftype, $title) = split(' ', $refinfo, 6);
4625 my ($creator, $epoch, $tz) =
4626 ($creatorinfo =~ /^(.*) ([0-9]+) (.*)$/);
4627 $ref_item{'fullname'} = $name;
4628 $name =~ s!^refs/!! if $all;
4629 $name =~ s!^refs/tags/!! unless $all;
4631 $ref_item{'type'} = $type;
4632 $ref_item{'id'} = $id;
4633 $ref_item{'name'} = $name;
4634 if ($type eq "tag") {
4635 $ref_item{'subject'} = $title;
4636 $ref_item{'reftype'} = $reftype;
4637 $ref_item{'refid'} = $refid;
4639 $ref_item{'reftype'} = $type;
4640 $ref_item{'refid'} = $id;
4643 if ($type eq "tag" || $type eq "commit") {
4644 $ref_item{'epoch'} = $epoch;
4646 $ref_item{'age'} = age_string
($ref_item{'epoch'});
4648 $ref_item{'age'} = "unknown";
4652 push @tagslist, \
%ref_item;
4656 return wantarray ?
@tagslist : \
@tagslist;
4659 ## ----------------------------------------------------------------------
4660 ## filesystem-related functions
4662 sub get_file_owner
{
4665 my ($dev, $ino, $mode, $nlink, $st_uid, $st_gid, $rdev, $size) = stat($path);
4666 my ($name, $passwd, $uid, $gid, $quota, $comment, $gcos, $dir, $shell) = getpwuid($st_uid);
4667 if (!defined $gcos) {
4671 $owner =~ s/[,;].*$//;
4672 return to_utf8
($owner);
4675 # assume that file exists
4677 my $filename = shift;
4679 open my $fd, '<', $filename;
4686 # return undef on failure
4687 sub collect_output
{
4688 defined(my $fd = cmd_pipe
@_) or return undef;
4693 my $result = join('', map({ to_utf8
($_) } <$fd>));
4694 close $fd or return undef;
4698 # return undef on failure
4699 # return '' if only comments
4700 sub collect_html_file
{
4701 my $filename = shift;
4703 open my $fd, '<', $filename or return undef;
4704 my $result = join('', map({ to_utf8
($_) } <$fd>));
4705 close $fd or return undef;
4706 return undef unless defined($result);
4708 $test =~ s/<!--(?:[^-]|(?:-(?!-)))*-->//gs;
4710 return $test eq '' ?
'' : $result;
4713 ## ......................................................................
4714 ## mimetype related functions
4716 sub mimetype_guess_file
{
4717 my $filename = shift;
4718 my $mimemap = shift;
4719 my $rawmode = shift;
4720 -r
$mimemap or return undef;
4723 open(my $mh, '<', $mimemap) or return undef;
4725 next if m/^#/; # skip comments
4726 my ($mimetype, @exts) = split(/\s+/);
4727 foreach my $ext (@exts) {
4728 $mimemap{$ext} = $mimetype;
4734 $ext = $1 if $filename =~ /\.([^.]*)$/;
4735 $ans = $mimemap{$ext} if $ext;
4738 $ans = 'text/html' if $l eq 'application/xhtml+xml';
4740 $ans = 'text/xml' if $l =~ m
!^application
/[^\s
:;,=]+\
+xml
$! ||
4741 $l eq 'image/svg+xml' ||
4742 $l eq 'application/xml-dtd' ||
4743 $l eq 'application/xml-external-parsed-entity';
4749 sub mimetype_guess
{
4750 my $filename = shift;
4751 my $rawmode = shift;
4753 $filename =~ /\./ or return undef;
4755 if ($mimetypes_file) {
4756 my $file = $mimetypes_file;
4757 if ($file !~ m!^/!) { # if it is relative path
4758 # it is relative to project
4759 $file = "$projectroot/$project/$file";
4761 $mime = mimetype_guess_file
($filename, $file, $rawmode);
4763 $mime ||= mimetype_guess_file
($filename, '/etc/mime.types', $rawmode);
4769 my $filename = shift;
4770 my $rawmode = shift;
4773 # The -T/-B file operators produce the wrong result unless a perlio
4774 # layer is present when the file handle is a pipe that delivers less
4775 # than 512 bytes of data before reaching EOF.
4777 # If we are running in a Perl that uses the stdio layer rather than the
4778 # unix+perlio layers we will end up adding a perlio layer on top of the
4779 # stdio layer and get a second level of buffering. This is harmless
4780 # and it makes the -T/-B file operators work properly in all cases.
4782 binmode $fd, ":perlio" or die_error
(500, "Adding perlio layer failed")
4783 unless grep /^perlio$/, PerlIO
::get_layers
($fd);
4785 $mime = mimetype_guess
($filename, $rawmode) if defined $filename;
4787 if (!$mime && $filename) {
4788 if ($filename =~ m/\.html?$/i) {
4789 $mime = 'text/html';
4790 } elsif ($filename =~ m/\.xht(?:ml)?$/i) {
4791 $mime = 'text/html';
4792 } elsif ($filename =~ m/\.te?xt?$/i) {
4793 $mime = 'text/plain';
4794 } elsif ($filename =~ m/\.(?:markdown|md)$/i) {
4795 $mime = 'text/plain';
4796 } elsif ($filename =~ m/\.png$/i) {
4797 $mime = 'image/png';
4798 } elsif ($filename =~ m/\.gif$/i) {
4799 $mime = 'image/gif';
4800 } elsif ($filename =~ m/\.jpe?g$/i) {
4801 $mime = 'image/jpeg';
4802 } elsif ($filename =~ m/\.svgz?$/i) {
4803 $mime = 'image/svg+xml';
4808 return $default_blob_plain_mimetype || 'application/octet-stream' unless $fd || $mime;
4810 $mime = -T
$fd ?
'text/plain' : 'application/octet-stream' unless $mime;
4818 return scalar($data =~ /^[\x00-\x7f]*$/);
4823 return utf8
::decode
($data);
4826 sub extract_html_charset
{
4827 return undef unless $_[0] && "$_[0]</head>" =~ m
#<head(?:\s+[^>]*)?(?<!/)>(.*?)</head\s*>#is;
4829 return $2 if $head =~ m
#<meta\s+charset\s*=\s*(['"])\s*([a-z0-9(:)_.+-]+)\s*\1\s*/?>#is;
4830 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) {
4831 my %kv = (lc($1) => $3, lc($4) => $6);
4832 my ($he, $c) = (lc($kv{'http-equiv'}), $kv{'content'});
4833 return $1 if $he && $c && $he eq 'content-type' &&
4834 $c =~ m!\s*text/html\s*;\s*charset\s*=\s*([a-z0-9(:)_.+-]+)\s*$!is;
4839 sub blob_contenttype
{
4840 my ($fd, $file_name, $type) = @_;
4842 $type ||= blob_mimetype
($fd, $file_name, 1);
4843 return $type unless $type =~ m!^text/.+!i;
4844 my ($leader, $charset, $htmlcharset);
4845 if ($fd && read($fd, $leader, 32768)) {{
4846 $charset='US-ASCII' if is_ascii
($leader);
4847 return ("$type; charset=UTF-8", $leader) if !$charset && is_valid_utf8
($leader);
4848 $charset='ISO-8859-1' unless $charset;
4849 $htmlcharset = extract_html_charset
($leader) if $type eq 'text/html';
4850 if ($htmlcharset && $charset ne 'US-ASCII') {
4851 $htmlcharset = undef if $htmlcharset =~ /^(?:utf-8|us-ascii)$/i
4854 return ("$type; charset=$htmlcharset", $leader) if $htmlcharset;
4855 my $defcharset = $default_text_plain_charset || '';
4856 $defcharset =~ s/^\s+//;
4857 $defcharset =~ s/\s+$//;
4858 $defcharset = '' if $charset && $charset ne 'US-ASCII' && $defcharset =~ /^(?:utf-8|us-ascii)$/i;
4859 return ("$type; charset=" . ($defcharset || 'ISO-8859-1'), $leader);
4862 # peek the first upto 128 bytes off a file handle
4870 return '' unless $fd && read($fd, $prefix128, 128);
4872 # In the general case, we're guaranteed only to be able to ungetc one
4873 # character (provided, of course, we actually got a character first).
4877 # 1) we are dealing with a :perlio layer since blob_mimetype will have
4878 # already been called at least once on the file handle before us
4880 # 2) we have an $fd positioned at the start of the input stream and
4881 # therefore know we were positioned at a buffer boundary before
4882 # reading the initial upto 128 bytes
4884 # 3) the buffer size is at least 512 bytes
4886 # 4) we are careful to only unget raw bytes
4888 # 5) we are attempting to unget exactly the same number of bytes we got
4890 # Given the above conditions we will ALWAYS be able to safely unget
4891 # the $prefix128 value we just got.
4893 # In fact, we could read up to 511 bytes and still be sure.
4894 # (Reading 512 might pop us into the next internal buffer, but probably
4895 # not since that could break the always able to unget at least the one
4896 # you just got guarantee.)
4898 map {$fd->ungetc(ord($_))} reverse(split //, $prefix128);
4903 # guess file syntax for syntax highlighting; return undef if no highlighting
4904 # the name of syntax can (in the future) depend on syntax highlighter used
4905 sub guess_file_syntax
{
4906 my ($fd, $mimetype, $file_name) = @_;
4907 return undef unless $fd && defined $file_name &&
4908 defined $mimetype && $mimetype =~ m!^text/.+!i;
4909 my $basename = basename
($file_name, '.in');
4910 return $highlight_basename{$basename}
4911 if exists $highlight_basename{$basename};
4913 # Peek to see if there's a shebang or xml line.
4914 # We always operate on bytes when testing this.
4917 my $shebang = peek128bytes
($fd);
4918 if (length($shebang) >= 4 && $shebang =~ /^#!/) { # 4 would be '#!/x'
4919 foreach my $key (keys %highlight_shebang) {
4920 my $ar = ref($highlight_shebang{$key}) ?
4921 $highlight_shebang{$key} :
4922 [$highlight_shebang{key
}];
4923 map {return $key if $shebang =~ /$_/} @
$ar;
4926 return 'xml' if $shebang =~ m!^\s*<\?xml\s!; # "xml" must be lowercase
4929 $basename =~ /\.([^.]*)$/;
4930 my $ext = $1 or return undef;
4931 return $highlight_ext{$ext}
4932 if exists $highlight_ext{$ext};
4937 # run highlighter and return FD of its output,
4938 # or return original FD if no highlighting
4939 sub run_highlighter
{
4940 my ($fd, $syntax) = @_;
4941 return $fd unless $fd && !eof($fd) && defined $highlight_bin && defined $syntax;
4943 defined(my $hifd = cmd_pipe
$posix_shell_bin, '-c',
4944 quote_command
(git_cmd
(), "cat-file", "blob", $hash)." | ".
4945 $to_utf8_pipe_command.
4946 quote_command
($highlight_bin).
4947 " --replace-tabs=8 --fragment --syntax $syntax")
4948 or die_error
(500, "Couldn't open file or run syntax highlighter");
4950 # just in case, should not happen as we tested !eof($fd) above
4951 return $fd if close($hifd);
4954 !$! or die_error
(500, "Couldn't close syntax highighter pipe");
4956 # leaving us with the only possibility a non-zero exit status (possibly a signal);
4957 # instead of dying horribly on this, just skip the highlighting
4958 # but do output a message about it to STDERR that will end up in the log
4959 print STDERR
"warning: skipping failed highlight for --syntax $syntax: ".
4960 sprintf("child exit status 0x%x\n", $?
);
4967 ## ======================================================================
4968 ## functions printing HTML: header, footer, error page
4970 sub get_page_title
{
4971 my $title = to_utf8
($site_name);
4973 unless (defined $project) {
4974 if (defined $project_filter) {
4975 $title .= " - projects in '" . esc_path
($project_filter) . "'";
4979 $title .= " - " . to_utf8
($project);
4981 return $title unless (defined $action);
4982 my $action_print = $action eq 'blame_incremental' ?
'blame' : $action;
4983 $title .= "/$action_print"; # $action is US-ASCII (7bit ASCII)
4985 return $title unless (defined $file_name);
4986 $title .= " - " . esc_path
($file_name);
4987 if ($action eq "tree" && $file_name !~ m
|/$|) {
4994 sub get_content_type_html
{
4995 # We do not ever emit application/xhtml+xml since that gives us
4996 # no benefits and it makes many browsers (e.g. Firefox) exceedingly
4997 # strict, which is troublesome for example when showing user-supplied
4998 # README.html files.
5002 sub print_feed_meta
{
5003 if (defined $project) {
5004 my %href_params = get_feed_info
();
5005 if (!exists $href_params{'-title'}) {
5006 $href_params{'-title'} = 'log';
5009 foreach my $format (qw(RSS Atom)) {
5010 my $type = lc($format);
5012 '-rel' => 'alternate',
5013 '-title' => esc_attr
("$project - $href_params{'-title'} - $format feed"),
5014 '-type' => "application/$type+xml"
5017 $href_params{'extra_options'} = undef;
5018 $href_params{'action'} = $type;
5019 $link_attr{'-href'} = href
(%href_params);
5021 "rel=\"$link_attr{'-rel'}\" ".
5022 "title=\"$link_attr{'-title'}\" ".
5023 "href=\"$link_attr{'-href'}\" ".
5024 "type=\"$link_attr{'-type'}\" ".
5027 $href_params{'extra_options'} = '--no-merges';
5028 $link_attr{'-href'} = href
(%href_params);
5029 $link_attr{'-title'} .= ' (no merges)';
5031 "rel=\"$link_attr{'-rel'}\" ".
5032 "title=\"$link_attr{'-title'}\" ".
5033 "href=\"$link_attr{'-href'}\" ".
5034 "type=\"$link_attr{'-type'}\" ".
5039 printf('<link rel="alternate" title="%s projects list" '.
5040 'href="%s" type="text/plain; charset=utf-8" />'."\n",
5041 esc_attr
($site_name), href
(project
=>undef, action
=>"project_index"));
5042 printf('<link rel="alternate" title="%s projects feeds" '.
5043 'href="%s" type="text/x-opml" />'."\n",
5044 esc_attr
($site_name), href
(project
=>undef, action
=>"opml"));
5048 sub print_header_links
{
5051 # print out each stylesheet that exist, providing backwards capability
5052 # for those people who defined $stylesheet in a config file
5053 if (defined $stylesheet) {
5054 print '<link rel="stylesheet" type="text/css" href="'.esc_url
($stylesheet).'"/>'."\n";
5056 foreach my $stylesheet (@stylesheets) {
5057 next unless $stylesheet;
5058 print '<link rel="stylesheet" type="text/css" href="'.esc_url
($stylesheet).'"/>'."\n";
5062 if ($status eq '200 OK');
5063 if (defined $favicon) {
5064 print qq(<link rel
="shortcut icon" href
=").esc_url($favicon).qq(" type
="image/png" />\n);
5068 sub print_nav_breadcrumbs_path
{
5069 my $dirprefix = undef;
5070 while (my $part = shift) {
5071 $dirprefix .= "/" if defined $dirprefix;
5072 $dirprefix .= $part;
5073 print $cgi->a({-href
=> href
(project
=> undef,
5074 project_filter
=> $dirprefix,
5075 action
=> "project_list")},
5076 esc_html
($part)) . " / ";
5080 sub print_nav_breadcrumbs
{
5083 for my $crumb (@extra_breadcrumbs, [ $home_link_str => $home_link ]) {
5084 print $cgi->a({-href
=> esc_url
($crumb->[1])}, $crumb->[0]) . " / ";
5086 if (defined $project) {
5087 my @dirname = split '/', $project;
5088 my $projectbasename = pop @dirname;
5089 print_nav_breadcrumbs_path
(@dirname);
5090 print $cgi->a({-href
=> href
(action
=>"summary")}, esc_html
($projectbasename));
5091 if (defined $action) {
5092 my $action_print = $action ;
5093 $action_print = 'blame' if $action_print eq 'blame_incremental';
5094 if (defined $opts{-action_extra
}) {
5095 $action_print = $cgi->a({-href
=> href
(action
=>$action)},
5098 print " / $action_print";
5100 if (defined $opts{-action_extra
}) {
5101 print " / $opts{-action_extra}";
5104 } elsif (defined $project_filter) {
5105 print_nav_breadcrumbs_path
(split '/', $project_filter);
5109 sub print_search_form
{
5110 if (!defined $searchtext) {
5114 if (defined $hash_base) {
5115 $search_hash = $hash_base;
5116 } elsif (defined $hash) {
5117 $search_hash = $hash;
5119 $search_hash = "HEAD";
5121 # We can't use href() here because we need to encode the
5122 # URL parameters into the form, not into the action link.
5123 my $action = $my_uri;
5124 my $use_pathinfo = gitweb_check_feature
('pathinfo');
5125 if ($use_pathinfo) {
5126 # See notes about doubled / in href()
5128 $action .= "/".esc_path_info
($project);
5130 $cgi->start_form(-method
=> "get", -action
=> $action);
5131 print sprintf(qq/<form method="%s" action="%s" enctype="%s">\n/,
5132 "get", CGI
::escapeHTML
($action), &CGI
::URL_ENCODED
) .
5133 "<div class=\"search\">\n" .
5135 $cgi->input({-name
=>"p", -value
=>$project, -type
=>"hidden"}) . "\n") .
5136 $cgi->input({-name
=>"a", -value
=>"search", -type
=>"hidden"}) . "\n" .
5137 $cgi->input({-name
=>"h", -value
=>$search_hash, -type
=>"hidden"}) . "\n" .
5138 $cgi->popup_menu(-name
=> 'st', -default => 'commit',
5139 -values => ['commit', 'grep', 'author', 'committer', 'pickaxe']) .
5140 " " . $cgi->a({-href
=> href
(action
=>"search_help"),
5141 -title
=> "search help" }, "?") . " search:\n",
5142 $cgi->textfield(-name
=> "s", -value
=> $searchtext, -override
=> 1) . "\n" .
5143 "<span title=\"Extended regular expression\">" .
5144 $cgi->checkbox(-name
=> 'sr', -value
=> 1, -label
=> 're',
5145 -checked
=> $search_use_regexp) .
5148 $cgi->end_form() . "\n";
5151 sub git_header_html
{
5152 my $status = shift || "200 OK";
5153 my $expires = shift;
5156 my $title = get_page_title
();
5157 my $content_type = get_content_type_html
();
5158 print $cgi->header(-type
=>$content_type, -charset
=> 'utf-8',
5159 -status
=> $status, -expires
=> $expires)
5160 unless ($opts{'-no_http_header'});
5161 my $mod_perl_version = $ENV{'MOD_PERL'} ?
" $ENV{'MOD_PERL'}" : '';
5163 <?xml version="1.0" encoding="utf-8"?>
5164 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
5165 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US" lang="en-US">
5166 <!-- git web interface version $version, (C) 2005-2006, Kay Sievers <kay.sievers\@vrfy.org>, Christian Gierke -->
5167 <!-- git core binaries version $git_version -->
5169 <meta http-equiv="content-type" content="$content_type; charset=utf-8"/>
5170 <meta name="generator" content="gitweb/$version git/$git_version$mod_perl_version"/>
5171 <meta name="robots" content="index, nofollow"/>
5172 <title>$title</title>
5173 <script type="text/javascript">/* <![CDATA[ */
5174 function fixBlameLinks() {
5175 var allLinks = document.getElementsByTagName("a");
5176 for (var i = 0; i < allLinks.length; i++) {
5177 var link = allLinks.item(i);
5178 if (link.className == 'blamelink')
5179 link.href = link.href.replace("/blame/", "/blame_incremental/");
5184 # the stylesheet, favicon etc urls won't work correctly with path_info
5185 # unless we set the appropriate base URL
5186 if ($ENV{'PATH_INFO'}) {
5187 print "<base href=\"".esc_url
($base_url)."\" />\n";
5189 print_header_links
($status);
5191 if (defined $site_html_head_string) {
5192 print to_utf8
($site_html_head_string);
5198 if (defined $site_header && -f
$site_header) {
5199 insert_file
($site_header);
5202 print "<div class=\"page_header\">\n";
5203 if (defined $logo) {
5204 print $cgi->a({-href
=> esc_url
($logo_url),
5205 -title
=> $logo_label},
5206 $cgi->img({-src
=> esc_url
($logo),
5207 -width
=> 72, -height
=> 27,
5209 -class => "logo"}));
5211 print_nav_breadcrumbs
(%opts);
5214 my $have_search = gitweb_check_feature
('search');
5215 if (defined $project && $have_search) {
5216 print_search_form
();
5220 sub compute_timed_interval
{
5221 return "<?gitweb compute_timed_interval?>" if $cache_mode_active;
5222 return tv_interval
($t0, [ gettimeofday
() ]);
5225 sub compute_commands_count
{
5226 return "<?gitweb compute_commands_count?>" if $cache_mode_active;
5227 my $s = $number_of_git_cmds == 1 ?
'' : 's';
5228 return '<span id="generating_cmd">'.
5229 $number_of_git_cmds.
5230 "</span> git command$s";
5233 sub git_footer_html
{
5234 my $feed_class = 'rss_logo';
5236 print "<div class=\"page_footer\">\n";
5237 if (defined $project) {
5238 my $descr = git_get_project_description
($project);
5239 if (defined $descr) {
5240 print "<div class=\"page_footer_text\">" . esc_html
($descr) . "</div>\n";
5243 my %href_params = get_feed_info
();
5244 if (!%href_params) {
5245 $feed_class .= ' generic';
5247 $href_params{'-title'} ||= 'log';
5249 foreach my $format (qw(RSS Atom)) {
5250 $href_params{'action'} = lc($format);
5251 print $cgi->a({-href
=> href
(%href_params),
5252 -title
=> "$href_params{'-title'} $format feed",
5253 -class => $feed_class}, $format)."\n";
5257 print $cgi->a({-href
=> href
(project
=>undef, action
=>"opml",
5258 project_filter
=> $project_filter),
5259 -class => $feed_class}, "OPML") . " ";
5260 print $cgi->a({-href
=> href
(project
=>undef, action
=>"project_index",
5261 project_filter
=> $project_filter),
5262 -class => $feed_class}, "TXT") . "\n";
5264 print "</div>\n"; # class="page_footer"
5266 if (defined $t0 && gitweb_check_feature
('timed')) {
5267 print "<div id=\"generating_info\">\n";
5268 print 'This page took '.
5269 '<span id="generating_time" class="time_span">'.
5270 compute_timed_interval
().
5273 compute_commands_count
().
5275 print "</div>\n"; # class="page_footer"
5278 if (defined $site_footer && -f
$site_footer) {
5279 insert_file
($site_footer);
5282 print qq!<script type
="text/javascript" src
="!.esc_url($javascript).qq!"></script
>\n!;
5283 if (defined $action &&
5284 $action eq 'blame_incremental') {
5285 print qq!<script type
="text/javascript">\n!.
5286 qq!startBlame
("!. href(action=>"blame_data
", -replay=>1) .qq!",\n!.
5287 qq! "!. href() .qq!");\n!.
5290 my ($jstimezone, $tz_cookie, $datetime_class) =
5291 gitweb_get_feature
('javascript-timezone');
5293 print qq!<script type
="text/javascript">\n!.
5294 qq!window
.onload
= function
() {\n!;
5295 if (gitweb_check_feature
('blame_incremental')) {
5296 print qq! fixBlameLinks
();\n!;
5298 if (gitweb_check_feature
('javascript-actions')) {
5299 print qq! fixLinks
();\n!;
5301 if ($jstimezone && $tz_cookie && $datetime_class) {
5302 print qq! var tz_cookie
= { name
: '$tz_cookie', expires
: 14, path
: '/' };\n!. # in days
5303 qq! onloadTZSetup
('$jstimezone', tz_cookie
, '$datetime_class');\n!;
5313 # die_error(<http_status_code>, <error_message>[, <detailed_html_description>])
5314 # Example: die_error(404, 'Hash not found')
5315 # By convention, use the following status codes (as defined in RFC 2616):
5316 # 400: Invalid or missing CGI parameters, or
5317 # requested object exists but has wrong type.
5318 # 403: Requested feature (like "pickaxe" or "snapshot") not enabled on
5319 # this server or project.
5320 # 404: Requested object/revision/project doesn't exist.
5321 # 500: The server isn't configured properly, or
5322 # an internal error occurred (e.g. failed assertions caused by bugs), or
5323 # an unknown error occurred (e.g. the git binary died unexpectedly).
5324 # 503: The server is currently unavailable (because it is overloaded,
5325 # or down for maintenance). Generally, this is a temporary state.
5327 my $status = shift || 500;
5328 my $error = esc_html
(shift) || "Internal Server Error";
5332 my %http_responses = (
5333 400 => '400 Bad Request',
5334 403 => '403 Forbidden',
5335 404 => '404 Not Found',
5336 500 => '500 Internal Server Error',
5337 503 => '503 Service Unavailable',
5339 git_header_html
($http_responses{$status}, undef, %opts);
5341 <div class="page_body">
5346 if (defined $extra) {
5354 unless ($opts{'-error_handler'});
5357 ## ----------------------------------------------------------------------
5358 ## functions printing or outputting HTML: navigation
5360 sub git_print_page_nav
{
5361 my ($current, $suppress, $head, $treehead, $treebase, $extra) = @_;
5362 $extra = '' if !defined $extra; # pager or formats
5364 my @navs = qw(summary log commit commitdiff tree refs);
5367 if (ref($suppress) eq 'ARRAY') {
5368 %omit = map { ($_ => 1) } @
$suppress;
5370 %omit = ($suppress => 1);
5372 @navs = grep { !$omit{$_} } @navs;
5375 my %arg = map { $_ => {action
=>$_} } @navs;
5376 if (defined $head) {
5377 for (qw(commit commitdiff)) {
5378 $arg{$_}{'hash'} = $head;
5380 if ($current =~ m/^(tree | log | shortlog | commit | commitdiff | search)$/x) {
5381 $arg{'log'}{'hash'} = $head;
5385 $arg{'log'}{'action'} = 'shortlog';
5386 if ($current eq 'log') {
5387 $current = 'shortlog';
5388 } elsif ($current eq 'shortlog') {
5391 $arg{'tree'}{'hash'} = $treehead if defined $treehead;
5392 $arg{'tree'}{'hash_base'} = $treebase if defined $treebase;
5394 my @actions = gitweb_get_feature
('actions');
5395 my $escname =~ esc_param
($project);
5396 my $minesc = $project;
5397 $minesc =~ s/([\x00-\x1F\x7F-\xFF <>"#%{}|\\^`?&=;])/sprintf("%%%02X",ord($1))/gse;
5400 'n' => $minesc, # project name with minimal required escapes
5401 'f' => $git_dir, # project path within filesystem
5402 'h' => $treehead || '', # current hash ('h' parameter)
5403 'b' => $treebase || '', # hash base ('hb' parameter)
5404 'e' => $escname, # project name with CGI-safe escapes
5407 my ($label, $link, $pos) = splice(@actions,0,3);
5409 @navs = map { $_ eq $pos ?
($_, $label) : $_ } @navs;
5411 $link =~ s/%([%nfhbe])/$repl{$1}/g;
5412 $arg{$label}{'_href'} = $link;
5415 print "<div class=\"page_nav\">\n" .
5417 map { $_ eq $current ?
5418 $_ : $cgi->a({-href
=> ($arg{$_}{_href
} ?
$arg{$_}{_href
} : href
(%{$arg{$_}}))}, "$_")
5420 print "<br/>\n$extra<br/>\n" .
5424 # returns a submenu for the nagivation of the refs views (tags, heads,
5425 # remotes) with the current view disabled and the remotes view only
5426 # available if the feature is enabled
5427 sub format_ref_views
{
5429 my @ref_views = qw{tags heads
};
5430 push @ref_views, 'remotes' if gitweb_check_feature
('remote_heads');
5431 return join " | ", map {
5432 $_ eq $current ?
$_ :
5433 $cgi->a({-href
=> href
(action
=>$_)}, $_)
5437 sub format_paging_nav
{
5438 my ($action, $page, $has_next_link) = @_;
5444 $cgi->a({-href
=> href
(-replay
=>1, page
=>undef)}, "first") .
5446 $cgi->a({-href
=> href
(-replay
=>1, page
=>$page-1),
5447 -accesskey
=> "p", -title
=> "Alt-p"}, "prev");
5449 $paging_nav .= "first · prev";
5452 if ($has_next_link) {
5453 $paging_nav .= " · " .
5454 $cgi->a({-href
=> href
(-replay
=>1, page
=>$page+1),
5455 -accesskey
=> "n", -title
=> "Alt-n"}, "next");
5457 $paging_nav .= " · next";
5463 sub format_log_nav
{
5464 my ($action, $page, $has_next_link) = @_;
5467 if ($action eq 'shortlog') {
5468 $paging_nav .= 'shortlog';
5470 $paging_nav .= $cgi->a({-href
=> href
(action
=>'shortlog', -replay
=>1)}, 'shortlog');
5472 $paging_nav .= ' | ';
5473 if ($action eq 'log') {
5474 $paging_nav .= 'fulllog';
5476 $paging_nav .= $cgi->a({-href
=> href
(action
=>'log', -replay
=>1)}, 'fulllog');
5479 $paging_nav .= " | " . format_paging_nav
($action, $page, $has_next_link);
5483 ## ......................................................................
5484 ## functions printing or outputting HTML: div
5486 sub git_print_header_div
{
5487 my ($action, $title, $hash, $hash_base, $extra) = @_;
5489 defined $extra or $extra = '';
5491 $args{'action'} = $action;
5492 $args{'hash'} = $hash if $hash;
5493 $args{'hash_base'} = $hash_base if $hash_base;
5495 my $link1 = $cgi->a({-href
=> href
(%args), -class => "title"},
5496 $title ?
$title : $action);
5497 my $link2 = $cgi->a({-href
=> href
(%args), -class => "cover"}, "");
5498 print "<div class=\"header\">\n" . '<span class="title">' .
5499 $link1 . $extra . $link2 . '</span>' . "\n</div>\n";
5502 sub format_repo_url
{
5503 my ($name, $url) = @_;
5504 return "<tr class=\"metadata_url\"><td>$name</td><td>$url</td></tr>\n";
5507 # Group output by placing it in a DIV element and adding a header.
5508 # Options for start_div() can be provided by passing a hash reference as the
5509 # first parameter to the function.
5510 # Options to git_print_header_div() can be provided by passing an array
5511 # reference. This must follow the options to start_div if they are present.
5512 # The content can be a scalar, which is output as-is, a scalar reference, which
5513 # is output after html escaping, an IO handle passed either as *handle or
5514 # *handle{IO}, or a function reference. In the latter case all following
5515 # parameters will be taken as argument to the content function call.
5516 sub git_print_section
{
5517 my ($div_args, $header_args, $content);
5519 if (ref($arg) eq 'HASH') {
5523 if (ref($arg) eq 'ARRAY') {
5524 $header_args = $arg;
5529 print $cgi->start_div($div_args);
5530 git_print_header_div
(@
$header_args);
5532 if (ref($content) eq 'CODE') {
5534 } elsif (ref($content) eq 'SCALAR') {
5535 print esc_html
($$content);
5536 } elsif (ref($content) eq 'GLOB' or ref($content) eq 'IO::Handle') {
5537 while (<$content>) {
5540 } elsif (!ref($content) && defined($content)) {
5544 print $cgi->end_div;
5547 sub format_timestamp_html
{
5549 my $useatnight = shift;
5550 defined($useatnight) or $useatnight = 1;
5551 my $strtime = $date->{'rfc2822'};
5553 my (undef, undef, $datetime_class) =
5554 gitweb_get_feature
('javascript-timezone');
5555 if ($datetime_class) {
5556 $strtime = qq!<span
class="$datetime_class">$strtime</span
>!;
5559 my $localtime_format = '(%d %02d:%02d %s)';
5560 if ($useatnight && $date->{'hour_local'} < 6) {
5561 $localtime_format = '(%d <span class="atnight">%02d:%02d</span> %s)';
5564 sprintf($localtime_format, $date->{'mday_local'},
5565 $date->{'hour_local'}, $date->{'minute_local'}, $date->{'tz_local'});
5570 sub format_lastrefresh_row
{
5571 return "<?gitweb format_lastrefresh_row?>" if $cache_mode_active;
5572 my %rd = parse_file_date
('.last_refresh');
5573 if (defined $rd{'rfc2822'}) {
5574 return "<tr id=\"metadata_lrefresh\"><td>last refresh</td>" .
5575 "<td>".format_timestamp_html
(\
%rd,0)."</td></tr>";
5580 # Outputs the author name and date in long form
5581 sub git_print_authorship
{
5584 my $tag = $opts{-tag
} || 'div';
5585 my $author = $co->{'author_name'};
5587 my %ad = parse_date
($co->{'author_epoch'}, $co->{'author_tz'});
5588 print "<$tag class=\"author_date\">" .
5589 format_search_author
($author, "author", esc_html
($author)) .
5590 " [".format_timestamp_html
(\
%ad)."]".
5591 git_get_avatar
($co->{'author_email'}, -pad_before
=> 1) .
5595 # Outputs table rows containing the full author or committer information,
5596 # in the format expected for 'commit' view (& similar).
5597 # Parameters are a commit hash reference, followed by the list of people
5598 # to output information for. If the list is empty it defaults to both
5599 # author and committer.
5600 sub git_print_authorship_rows
{
5602 # too bad we can't use @people = @_ || ('author', 'committer')
5604 @people = ('author', 'committer') unless @people;
5605 foreach my $who (@people) {
5606 my %wd = parse_date
($co->{"${who}_epoch"}, $co->{"${who}_tz"});
5607 print "<tr><td>$who</td><td>" .
5608 format_search_author
($co->{"${who}_name"}, $who,
5609 esc_html
($co->{"${who}_name"})) . " " .
5610 format_search_author
($co->{"${who}_email"}, $who,
5611 esc_html
("<" . $co->{"${who}_email"} . ">")) .
5612 "</td><td rowspan=\"2\">" .
5613 git_get_avatar
($co->{"${who}_email"}, -size
=> 'double') .
5617 format_timestamp_html
(\
%wd) .
5623 sub git_print_page_path
{
5629 print "<div class=\"page_path\">";
5630 print $cgi->a({-href
=> href
(action
=>"tree", hash_base
=>$hb),
5631 -title
=> 'tree root'}, to_utf8
("[$project]"));
5633 if (defined $name) {
5634 my @dirname = split '/', $name;
5635 my $basename = pop @dirname;
5638 foreach my $dir (@dirname) {
5639 $fullname .= ($fullname ?
'/' : '') . $dir;
5640 print $cgi->a({-href
=> href
(action
=>"tree", file_name
=>$fullname,
5642 -title
=> $fullname}, esc_path
($dir));
5645 if (defined $type && $type eq 'blob') {
5646 print $cgi->a({-href
=> href
(action
=>"blob_plain", file_name
=>$file_name,
5648 -title
=> $name}, esc_path
($basename));
5649 } elsif (defined $type && $type eq 'tree') {
5650 print $cgi->a({-href
=> href
(action
=>"tree", file_name
=>$file_name,
5652 -title
=> $name}, esc_path
($basename));
5655 print esc_path
($basename);
5658 print "<br/></div>\n";
5665 if ($opts{'-remove_title'}) {
5666 # remove title, i.e. first line of log
5669 # remove leading empty lines
5670 while (defined $log->[0] && $log->[0] eq "") {
5675 my $skip_blank_line = 0;
5676 foreach my $line (@
$log) {
5677 if ($line =~ m/^\s*([A-Z][-A-Za-z]*-[Bb]y|C[Cc]): /) {
5678 if (! $opts{'-remove_signoff'}) {
5679 print "<span class=\"signoff\">" . esc_html
($line) . "</span><br/>\n";
5680 $skip_blank_line = 1;
5685 if ($line =~ m
,\s
*([a
-z
]*link): (https?
://\S
+),i
) {
5686 if (! $opts{'-remove_signoff'}) {
5687 print "<span class=\"signoff\">" . esc_html
($1) . ": " .
5688 "<a href=\"" . esc_html
($2) . "\">" . esc_html
($2) . "</a>" .
5690 $skip_blank_line = 1;
5695 # print only one empty line
5696 # do not print empty line after signoff
5698 next if ($skip_blank_line);
5699 $skip_blank_line = 1;
5701 $skip_blank_line = 0;
5704 print format_log_line_html
($line) . "<br/>\n";
5707 if ($opts{'-final_empty_line'}) {
5708 # end with single empty line
5709 print "<br/>\n" unless $skip_blank_line;
5713 # return link target (what link points to)
5714 sub git_get_link_target
{
5719 defined(my $fd = git_cmd_pipe
"cat-file", "blob", $hash)
5723 $link_target = to_utf8
(scalar <$fd>);
5728 return $link_target;
5731 # given link target, and the directory (basedir) the link is in,
5732 # return target of link relative to top directory (top tree);
5733 # return undef if it is not possible (including absolute links).
5734 sub normalize_link_target
{
5735 my ($link_target, $basedir) = @_;
5737 # absolute symlinks (beginning with '/') cannot be normalized
5738 return if (substr($link_target, 0, 1) eq '/');
5740 # normalize link target to path from top (root) tree (dir)
5743 $path = $basedir . '/' . $link_target;
5745 # we are in top (root) tree (dir)
5746 $path = $link_target;
5749 # remove //, /./, and /../
5751 foreach my $part (split('/', $path)) {
5752 # discard '.' and ''
5753 next if (!$part || $part eq '.');
5755 if ($part eq '..') {
5759 # link leads outside repository (outside top dir)
5763 push @path_parts, $part;
5766 $path = join('/', @path_parts);
5771 # print tree entry (row of git_tree), but without encompassing <tr> element
5772 sub git_print_tree_entry
{
5773 my ($t, $basedir, $hash_base, $have_blame) = @_;
5776 $base_key{'hash_base'} = $hash_base if defined $hash_base;
5778 # The format of a table row is: mode list link. Where mode is
5779 # the mode of the entry, list is the name of the entry, an href,
5780 # and link is the action links of the entry.
5782 print "<td class=\"mode\">" . mode_str
($t->{'mode'}) . "</td>\n";
5783 if (exists $t->{'size'}) {
5784 print "<td class=\"size\">$t->{'size'}</td>\n";
5786 if ($t->{'type'} eq "blob") {
5787 print "<td class=\"list\">" .
5788 $cgi->a({-href
=> href
(action
=>"blob", hash
=>$t->{'hash'},
5789 file_name
=>"$basedir$t->{'name'}", %base_key),
5790 -class => "list"}, esc_path
($t->{'name'}));
5791 if (S_ISLNK
(oct $t->{'mode'})) {
5792 my $link_target = git_get_link_target
($t->{'hash'});
5794 my $norm_target = normalize_link_target
($link_target, $basedir);
5795 if (defined $norm_target) {
5797 $cgi->a({-href
=> href
(action
=>"object", hash_base
=>$hash_base,
5798 file_name
=>$norm_target),
5799 -title
=> $norm_target}, esc_path
($link_target));
5801 print " -> " . esc_path
($link_target);
5806 print "<td class=\"link\">";
5807 print $cgi->a({-href
=> href
(action
=>"blob", hash
=>$t->{'hash'},
5808 file_name
=>"$basedir$t->{'name'}", %base_key)},
5812 $cgi->a({-href
=> href
(action
=>"blame", hash
=>$t->{'hash'},
5813 file_name
=>"$basedir$t->{'name'}", %base_key),
5814 -class => "blamelink"},
5817 if (defined $hash_base) {
5819 $cgi->a({-href
=> href
(action
=>"history", hash_base
=>$hash_base,
5820 hash
=>$t->{'hash'}, file_name
=>"$basedir$t->{'name'}")},
5824 $cgi->a({-href
=> href
(action
=>"blob_plain", hash_base
=>$hash_base,
5825 file_name
=>"$basedir$t->{'name'}")},
5829 } elsif ($t->{'type'} eq "tree") {
5830 print "<td class=\"list\">";
5831 print $cgi->a({-href
=> href
(action
=>"tree", hash
=>$t->{'hash'},
5832 file_name
=>"$basedir$t->{'name'}",
5834 esc_path
($t->{'name'}));
5836 print "<td class=\"link\">";
5837 print $cgi->a({-href
=> href
(action
=>"tree", hash
=>$t->{'hash'},
5838 file_name
=>"$basedir$t->{'name'}",
5841 if (defined $hash_base) {
5843 $cgi->a({-href
=> href
(action
=>"history", hash_base
=>$hash_base,
5844 file_name
=>"$basedir$t->{'name'}")},
5849 # unknown object: we can only present history for it
5850 # (this includes 'commit' object, i.e. submodule support)
5851 print "<td class=\"list\">" .
5852 esc_path
($t->{'name'}) .
5854 print "<td class=\"link\">";
5855 if (defined $hash_base) {
5856 print $cgi->a({-href
=> href
(action
=>"history",
5857 hash_base
=>$hash_base,
5858 file_name
=>"$basedir$t->{'name'}")},
5865 ## ......................................................................
5866 ## functions printing large fragments of HTML
5868 # get pre-image filenames for merge (combined) diff
5869 sub fill_from_file_info
{
5870 my ($diff, @parents) = @_;
5872 $diff->{'from_file'} = [ ];
5873 $diff->{'from_file'}[$diff->{'nparents'} - 1] = undef;
5874 for (my $i = 0; $i < $diff->{'nparents'}; $i++) {
5875 if ($diff->{'status'}[$i] eq 'R' ||
5876 $diff->{'status'}[$i] eq 'C') {
5877 $diff->{'from_file'}[$i] =
5878 git_get_path_by_hash
($parents[$i], $diff->{'from_id'}[$i]);
5885 # is current raw difftree line of file deletion
5887 my $diffinfo = shift;
5889 return $diffinfo->{'to_id'} eq ('0' x
40);
5892 # does patch correspond to [previous] difftree raw line
5893 # $diffinfo - hashref of parsed raw diff format
5894 # $patchinfo - hashref of parsed patch diff format
5895 # (the same keys as in $diffinfo)
5896 sub is_patch_split
{
5897 my ($diffinfo, $patchinfo) = @_;
5899 return defined $diffinfo && defined $patchinfo
5900 && $diffinfo->{'to_file'} eq $patchinfo->{'to_file'};
5904 sub git_difftree_body
{
5905 my ($difftree, $hash, @parents) = @_;
5906 my ($parent) = $parents[0];
5907 my $have_blame = gitweb_check_feature
('blame');
5908 print "<div class=\"list_head\">\n";
5909 if ($#{$difftree} > 10) {
5910 print(($#{$difftree} + 1) . " files changed:\n");
5914 print "<table class=\"" .
5915 (@parents > 1 ?
"combined " : "") .
5918 # header only for combined diff in 'commitdiff' view
5919 my $has_header = @
$difftree && @parents > 1 && $action eq 'commitdiff';
5922 print "<thead><tr>\n" .
5923 "<th></th><th></th>\n"; # filename, patchN link
5924 for (my $i = 0; $i < @parents; $i++) {
5925 my $par = $parents[$i];
5927 $cgi->a({-href
=> href
(action
=>"commitdiff",
5928 hash
=>$hash, hash_parent
=>$par),
5929 -title
=> 'commitdiff to parent number ' .
5930 ($i+1) . ': ' . substr($par,0,7)},
5934 print "</tr></thead>\n<tbody>\n";
5939 foreach my $line (@
{$difftree}) {
5940 my $diff = parsed_difftree_line
($line);
5943 print "<tr class=\"dark\">\n";
5945 print "<tr class=\"light\">\n";
5949 if (exists $diff->{'nparents'}) { # combined diff
5951 fill_from_file_info
($diff, @parents)
5952 unless exists $diff->{'from_file'};
5954 if (!is_deleted
($diff)) {
5955 # file exists in the result (child) commit
5957 $cgi->a({-href
=> href
(action
=>"blob", hash
=>$diff->{'to_id'},
5958 file_name
=>$diff->{'to_file'},
5960 -class => "list"}, esc_path
($diff->{'to_file'})) .
5964 esc_path
($diff->{'to_file'}) .
5968 if ($action eq 'commitdiff') {
5971 print "<td class=\"link\">" .
5972 $cgi->a({-href
=> href
(-anchor
=>"patch$patchno")},
5978 my $has_history = 0;
5979 my $not_deleted = 0;
5980 for (my $i = 0; $i < $diff->{'nparents'}; $i++) {
5981 my $hash_parent = $parents[$i];
5982 my $from_hash = $diff->{'from_id'}[$i];
5983 my $from_path = $diff->{'from_file'}[$i];
5984 my $status = $diff->{'status'}[$i];
5986 $has_history ||= ($status ne 'A');
5987 $not_deleted ||= ($status ne 'D');
5989 if ($status eq 'A') {
5990 print "<td class=\"link\" align=\"right\"> | </td>\n";
5991 } elsif ($status eq 'D') {
5992 print "<td class=\"link\">" .
5993 $cgi->a({-href
=> href
(action
=>"blob",
5996 file_name
=>$from_path)},
6000 if ($diff->{'to_id'} eq $from_hash) {
6001 print "<td class=\"link nochange\">";
6003 print "<td class=\"link\">";
6005 print $cgi->a({-href
=> href
(action
=>"blobdiff",
6006 hash
=>$diff->{'to_id'},
6007 hash_parent
=>$from_hash,
6009 hash_parent_base
=>$hash_parent,
6010 file_name
=>$diff->{'to_file'},
6011 file_parent
=>$from_path)},
6017 print "<td class=\"link\">";
6019 print $cgi->a({-href
=> href
(action
=>"blob",
6020 hash
=>$diff->{'to_id'},
6021 file_name
=>$diff->{'to_file'},
6024 print " | " if ($has_history);
6027 print $cgi->a({-href
=> href
(action
=>"history",
6028 file_name
=>$diff->{'to_file'},
6035 next; # instead of 'else' clause, to avoid extra indent
6037 # else ordinary diff
6039 my ($to_mode_oct, $to_mode_str, $to_file_type);
6040 my ($from_mode_oct, $from_mode_str, $from_file_type);
6041 if ($diff->{'to_mode'} ne ('0' x
6)) {
6042 $to_mode_oct = oct $diff->{'to_mode'};
6043 if (S_ISREG
($to_mode_oct)) { # only for regular file
6044 $to_mode_str = sprintf("%04o", $to_mode_oct & 0777); # permission bits
6046 $to_file_type = file_type
($diff->{'to_mode'});
6048 if ($diff->{'from_mode'} ne ('0' x
6)) {
6049 $from_mode_oct = oct $diff->{'from_mode'};
6050 if (S_ISREG
($from_mode_oct)) { # only for regular file
6051 $from_mode_str = sprintf("%04o", $from_mode_oct & 0777); # permission bits
6053 $from_file_type = file_type
($diff->{'from_mode'});
6056 if ($diff->{'status'} eq "A") { # created
6057 my $mode_chng = "<span class=\"file_status new\">[new $to_file_type";
6058 $mode_chng .= " with mode: $to_mode_str" if $to_mode_str;
6059 $mode_chng .= "]</span>";
6061 print $cgi->a({-href
=> href
(action
=>"blob", hash
=>$diff->{'to_id'},
6062 hash_base
=>$hash, file_name
=>$diff->{'file'}),
6063 -class => "list"}, esc_path
($diff->{'file'}));
6065 print "<td>$mode_chng</td>\n";
6066 print "<td class=\"link\">";
6067 if ($action eq 'commitdiff') {
6070 print $cgi->a({-href
=> href
(-anchor
=>"patch$patchno")},
6074 print $cgi->a({-href
=> href
(action
=>"blob", hash
=>$diff->{'to_id'},
6075 hash_base
=>$hash, file_name
=>$diff->{'file'})},
6079 } elsif ($diff->{'status'} eq "D") { # deleted
6080 my $mode_chng = "<span class=\"file_status deleted\">[deleted $from_file_type]</span>";
6082 print $cgi->a({-href
=> href
(action
=>"blob", hash
=>$diff->{'from_id'},
6083 hash_base
=>$parent, file_name
=>$diff->{'file'}),
6084 -class => "list"}, esc_path
($diff->{'file'}));
6086 print "<td>$mode_chng</td>\n";
6087 print "<td class=\"link\">";
6088 if ($action eq 'commitdiff') {
6091 print $cgi->a({-href
=> href
(-anchor
=>"patch$patchno")},
6095 print $cgi->a({-href
=> href
(action
=>"blob", hash
=>$diff->{'from_id'},
6096 hash_base
=>$parent, file_name
=>$diff->{'file'})},
6099 print $cgi->a({-href
=> href
(action
=>"blame", hash_base
=>$parent,
6100 file_name
=>$diff->{'file'}),
6101 -class => "blamelink"},
6104 print $cgi->a({-href
=> href
(action
=>"history", hash_base
=>$parent,
6105 file_name
=>$diff->{'file'})},
6109 } elsif ($diff->{'status'} eq "M" || $diff->{'status'} eq "T") { # modified, or type changed
6110 my $mode_chnge = "";
6111 if ($diff->{'from_mode'} != $diff->{'to_mode'}) {
6112 $mode_chnge = "<span class=\"file_status mode_chnge\">[changed";
6113 if ($from_file_type ne $to_file_type) {
6114 $mode_chnge .= " from $from_file_type to $to_file_type";
6116 if (($from_mode_oct & 0777) != ($to_mode_oct & 0777)) {
6117 if ($from_mode_str && $to_mode_str) {
6118 $mode_chnge .= " mode: $from_mode_str->$to_mode_str";
6119 } elsif ($to_mode_str) {
6120 $mode_chnge .= " mode: $to_mode_str";
6123 $mode_chnge .= "]</span>\n";
6126 print $cgi->a({-href
=> href
(action
=>"blob", hash
=>$diff->{'to_id'},
6127 hash_base
=>$hash, file_name
=>$diff->{'file'}),
6128 -class => "list"}, esc_path
($diff->{'file'}));
6130 print "<td>$mode_chnge</td>\n";
6131 print "<td class=\"link\">";
6132 if ($action eq 'commitdiff') {
6135 print $cgi->a({-href
=> href
(-anchor
=>"patch$patchno")},
6138 } elsif ($diff->{'to_id'} ne $diff->{'from_id'}) {
6139 # "commit" view and modified file (not onlu mode changed)
6140 print $cgi->a({-href
=> href
(action
=>"blobdiff",
6141 hash
=>$diff->{'to_id'}, hash_parent
=>$diff->{'from_id'},
6142 hash_base
=>$hash, hash_parent_base
=>$parent,
6143 file_name
=>$diff->{'file'})},
6147 print $cgi->a({-href
=> href
(action
=>"blob", hash
=>$diff->{'to_id'},
6148 hash_base
=>$hash, file_name
=>$diff->{'file'})},
6151 print $cgi->a({-href
=> href
(action
=>"blame", hash_base
=>$hash,
6152 file_name
=>$diff->{'file'}),
6153 -class => "blamelink"},
6156 print $cgi->a({-href
=> href
(action
=>"history", hash_base
=>$hash,
6157 file_name
=>$diff->{'file'})},
6161 } elsif ($diff->{'status'} eq "R" || $diff->{'status'} eq "C") { # renamed or copied
6162 my %status_name = ('R' => 'moved', 'C' => 'copied');
6163 my $nstatus = $status_name{$diff->{'status'}};
6165 if ($diff->{'from_mode'} != $diff->{'to_mode'}) {
6166 # mode also for directories, so we cannot use $to_mode_str
6167 $mode_chng = sprintf(", mode: %04o", $to_mode_oct & 0777);
6170 $cgi->a({-href
=> href
(action
=>"blob", hash_base
=>$hash,
6171 hash
=>$diff->{'to_id'}, file_name
=>$diff->{'to_file'}),
6172 -class => "list"}, esc_path
($diff->{'to_file'})) . "</td>\n" .
6173 "<td><span class=\"file_status $nstatus\">[$nstatus from " .
6174 $cgi->a({-href
=> href
(action
=>"blob", hash_base
=>$parent,
6175 hash
=>$diff->{'from_id'}, file_name
=>$diff->{'from_file'}),
6176 -class => "list"}, esc_path
($diff->{'from_file'})) .
6177 " with " . (int $diff->{'similarity'}) . "% similarity$mode_chng]</span></td>\n" .
6178 "<td class=\"link\">";
6179 if ($action eq 'commitdiff') {
6182 print $cgi->a({-href
=> href
(-anchor
=>"patch$patchno")},
6185 } elsif ($diff->{'to_id'} ne $diff->{'from_id'}) {
6186 # "commit" view and modified file (not only pure rename or copy)
6187 print $cgi->a({-href
=> href
(action
=>"blobdiff",
6188 hash
=>$diff->{'to_id'}, hash_parent
=>$diff->{'from_id'},
6189 hash_base
=>$hash, hash_parent_base
=>$parent,
6190 file_name
=>$diff->{'to_file'}, file_parent
=>$diff->{'from_file'})},
6194 print $cgi->a({-href
=> href
(action
=>"blob", hash
=>$diff->{'to_id'},
6195 hash_base
=>$parent, file_name
=>$diff->{'to_file'})},
6198 print $cgi->a({-href
=> href
(action
=>"blame", hash_base
=>$hash,
6199 file_name
=>$diff->{'to_file'}),
6200 -class => "blamelink"},
6203 print $cgi->a({-href
=> href
(action
=>"history", hash_base
=>$hash,
6204 file_name
=>$diff->{'to_file'})},
6208 } # we should not encounter Unmerged (U) or Unknown (X) status
6211 print "</tbody>" if $has_header;
6215 # Print context lines and then rem/add lines in a side-by-side manner.
6216 sub print_sidebyside_diff_lines
{
6217 my ($ctx, $rem, $add) = @_;
6219 # print context block before add/rem block
6222 '<div class="chunk_block ctx">',
6223 '<div class="old">',
6226 '<div class="new">',
6235 '<div class="chunk_block rem">',
6236 '<div class="old">',
6243 '<div class="chunk_block add">',
6244 '<div class="new">',
6250 '<div class="chunk_block chg">',
6251 '<div class="old">',
6254 '<div class="new">',
6261 # Print context lines and then rem/add lines in inline manner.
6262 sub print_inline_diff_lines
{
6263 my ($ctx, $rem, $add) = @_;
6265 print @
$ctx, @
$rem, @
$add;
6268 # Format removed and added line, mark changed part and HTML-format them.
6269 # Implementation is based on contrib/diff-highlight
6270 sub format_rem_add_lines_pair
{
6271 my ($rem, $add, $num_parents) = @_;
6273 # We need to untabify lines before split()'ing them;
6274 # otherwise offsets would be invalid.
6277 $rem = untabify
($rem);
6278 $add = untabify
($add);
6280 my @rem = split(//, $rem);
6281 my @add = split(//, $add);
6282 my ($esc_rem, $esc_add);
6283 # Ignore leading +/- characters for each parent.
6284 my ($prefix_len, $suffix_len) = ($num_parents, 0);
6285 my ($prefix_has_nonspace, $suffix_has_nonspace);
6287 my $shorter = (@rem < @add) ?
@rem : @add;
6288 while ($prefix_len < $shorter) {
6289 last if ($rem[$prefix_len] ne $add[$prefix_len]);
6291 $prefix_has_nonspace = 1 if ($rem[$prefix_len] !~ /\s/);
6295 while ($prefix_len + $suffix_len < $shorter) {
6296 last if ($rem[-1 - $suffix_len] ne $add[-1 - $suffix_len]);
6298 $suffix_has_nonspace = 1 if ($rem[-1 - $suffix_len] !~ /\s/);
6302 # Mark lines that are different from each other, but have some common
6303 # part that isn't whitespace. If lines are completely different, don't
6304 # mark them because that would make output unreadable, especially if
6305 # diff consists of multiple lines.
6306 if ($prefix_has_nonspace || $suffix_has_nonspace) {
6307 $esc_rem = esc_html_hl_regions
($rem, 'marked',
6308 [$prefix_len, @rem - $suffix_len], -nbsp
=>1);
6309 $esc_add = esc_html_hl_regions
($add, 'marked',
6310 [$prefix_len, @add - $suffix_len], -nbsp
=>1);
6312 $esc_rem = esc_html
($rem, -nbsp
=>1);
6313 $esc_add = esc_html
($add, -nbsp
=>1);
6316 return format_diff_line
(\
$esc_rem, 'rem'),
6317 format_diff_line
(\
$esc_add, 'add');
6320 # HTML-format diff context, removed and added lines.
6321 sub format_ctx_rem_add_lines
{
6322 my ($ctx, $rem, $add, $num_parents) = @_;
6323 my (@new_ctx, @new_rem, @new_add);
6324 my $can_highlight = 0;
6325 my $is_combined = ($num_parents > 1);
6327 # Highlight if every removed line has a corresponding added line.
6328 if (@
$add > 0 && @
$add == @
$rem) {
6331 # Highlight lines in combined diff only if the chunk contains
6332 # diff between the same version, e.g.
6339 # Otherwise the highlightling would be confusing.
6341 for (my $i = 0; $i < @
$add; $i++) {
6342 my $prefix_rem = substr($rem->[$i], 0, $num_parents);
6343 my $prefix_add = substr($add->[$i], 0, $num_parents);
6345 $prefix_rem =~ s/-/+/g;
6347 if ($prefix_rem ne $prefix_add) {
6355 if ($can_highlight) {
6356 for (my $i = 0; $i < @
$add; $i++) {
6357 my ($line_rem, $line_add) = format_rem_add_lines_pair
(
6358 $rem->[$i], $add->[$i], $num_parents);
6359 push @new_rem, $line_rem;
6360 push @new_add, $line_add;
6363 @new_rem = map { format_diff_line
($_, 'rem') } @
$rem;
6364 @new_add = map { format_diff_line
($_, 'add') } @
$add;
6367 @new_ctx = map { format_diff_line
($_, 'ctx') } @
$ctx;
6369 return (\
@new_ctx, \
@new_rem, \
@new_add);
6372 # Print context lines and then rem/add lines.
6373 sub print_diff_lines
{
6374 my ($ctx, $rem, $add, $diff_style, $num_parents) = @_;
6375 my $is_combined = $num_parents > 1;
6377 ($ctx, $rem, $add) = format_ctx_rem_add_lines
($ctx, $rem, $add,
6380 if ($diff_style eq 'sidebyside' && !$is_combined) {
6381 print_sidebyside_diff_lines
($ctx, $rem, $add);
6383 # default 'inline' style and unknown styles
6384 print_inline_diff_lines
($ctx, $rem, $add);
6388 sub print_diff_chunk
{
6389 my ($diff_style, $num_parents, $from, $to, @chunk) = @_;
6390 my (@ctx, @rem, @add);
6392 # The class of the previous line.
6393 my $prev_class = '';
6395 return unless @chunk;
6397 # incomplete last line might be among removed or added lines,
6398 # or both, or among context lines: find which
6399 for (my $i = 1; $i < @chunk; $i++) {
6400 if ($chunk[$i][0] eq 'incomplete') {
6401 $chunk[$i][0] = $chunk[$i-1][0];
6406 push @chunk, ["", ""];
6408 foreach my $line_info (@chunk) {
6409 my ($class, $line) = @
$line_info;
6411 # print chunk headers
6412 if ($class && $class eq 'chunk_header') {
6413 print format_diff_line
($line, $class, $from, $to);
6417 ## print from accumulator when have some add/rem lines or end
6418 # of chunk (flush context lines), or when have add and rem
6419 # lines and new block is reached (otherwise add/rem lines could
6421 if (!$class || ((@rem || @add) && $class eq 'ctx') ||
6422 (@rem && @add && $class ne $prev_class)) {
6423 print_diff_lines
(\
@ctx, \
@rem, \
@add,
6424 $diff_style, $num_parents);
6425 @ctx = @rem = @add = ();
6428 ## adding lines to accumulator
6431 # rem, add or change
6432 if ($class eq 'rem') {
6434 } elsif ($class eq 'add') {
6438 if ($class eq 'ctx') {
6442 $prev_class = $class;
6446 sub git_patchset_body
{
6447 my ($fd, $diff_style, $difftree, $hash, @hash_parents) = @_;
6448 my ($hash_parent) = $hash_parents[0];
6450 my $is_combined = (@hash_parents > 1);
6452 my $patch_number = 0;
6457 my @chunk; # for side-by-side diff
6459 print "<div class=\"patchset\">\n";
6461 # skip to first patch
6462 while ($patch_line = to_utf8
(scalar <$fd>)) {
6465 last if ($patch_line =~ m/^diff /);
6469 while ($patch_line) {
6471 # parse "git diff" header line
6472 if ($patch_line =~ m/^diff --git (\"(?:[^\\\"]*(?:\\.[^\\\"]*)*)\"|[^ "]*) (.*)$/) {
6473 # $1 is from_name, which we do not use
6474 $to_name = unquote
($2);
6475 $to_name =~ s!^b/!!;
6476 } elsif ($patch_line =~ m/^diff --(cc|combined) ("?.*"?)$/) {
6477 # $1 is 'cc' or 'combined', which we do not use
6478 $to_name = unquote
($2);
6483 # check if current patch belong to current raw line
6484 # and parse raw git-diff line if needed
6485 if (is_patch_split
($diffinfo, { 'to_file' => $to_name })) {
6486 # this is continuation of a split patch
6487 print "<div class=\"patch cont\">\n";
6489 # advance raw git-diff output if needed
6490 $patch_idx++ if defined $diffinfo;
6492 # read and prepare patch information
6493 $diffinfo = parsed_difftree_line
($difftree->[$patch_idx]);
6495 # compact combined diff output can have some patches skipped
6496 # find which patch (using pathname of result) we are at now;
6498 while ($to_name ne $diffinfo->{'to_file'}) {
6499 print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n" .
6500 format_diff_cc_simplified
($diffinfo, @hash_parents) .
6501 "</div>\n"; # class="patch"
6506 last if $patch_idx > $#$difftree;
6507 $diffinfo = parsed_difftree_line
($difftree->[$patch_idx]);
6511 # modifies %from, %to hashes
6512 parse_from_to_diffinfo
($diffinfo, \
%from, \
%to, @hash_parents);
6514 # this is first patch for raw difftree line with $patch_idx index
6515 # we index @$difftree array from 0, but number patches from 1
6516 print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n";
6520 #assert($patch_line =~ m/^diff /) if DEBUG;
6521 #assert($patch_line !~ m!$/$!) if DEBUG; # is chomp-ed
6523 # print "git diff" header
6524 print format_git_diff_header_line
($patch_line, $diffinfo,
6527 # print extended diff header
6528 print "<div class=\"diff extended_header\">\n";
6530 while ($patch_line = to_utf8
(scalar<$fd>)) {
6533 last EXTENDED_HEADER
if ($patch_line =~ m/^--- |^diff /);
6535 print format_extended_diff_header_line
($patch_line, $diffinfo,
6538 print "</div>\n"; # class="diff extended_header"
6540 # from-file/to-file diff header
6541 if (! $patch_line) {
6542 print "</div>\n"; # class="patch"
6545 next PATCH
if ($patch_line =~ m/^diff /);
6546 #assert($patch_line =~ m/^---/) if DEBUG;
6548 my $last_patch_line = $patch_line;
6549 $patch_line = to_utf8
(scalar <$fd>);
6551 #assert($patch_line =~ m/^\+\+\+/) if DEBUG;
6553 print format_diff_from_to_header
($last_patch_line, $patch_line,
6554 $diffinfo, \
%from, \
%to,
6559 while ($patch_line = to_utf8
(scalar <$fd>)) {
6562 next PATCH
if ($patch_line =~ m/^diff /);
6564 my $class = diff_line_class
($patch_line, \
%from, \
%to);
6566 if ($class eq 'chunk_header') {
6567 print_diff_chunk
($diff_style, scalar @hash_parents, \
%from, \
%to, @chunk);
6571 push @chunk, [ $class, $patch_line ];
6576 print_diff_chunk
($diff_style, scalar @hash_parents, \
%from, \
%to, @chunk);
6579 print "</div>\n"; # class="patch"
6582 # for compact combined (--cc) format, with chunk and patch simplification
6583 # the patchset might be empty, but there might be unprocessed raw lines
6584 for (++$patch_idx if $patch_number > 0;
6585 $patch_idx < @
$difftree;
6587 # read and prepare patch information
6588 $diffinfo = parsed_difftree_line
($difftree->[$patch_idx]);
6590 # generate anchor for "patch" links in difftree / whatchanged part
6591 print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n" .
6592 format_diff_cc_simplified
($diffinfo, @hash_parents) .
6593 "</div>\n"; # class="patch"
6598 if ($patch_number == 0) {
6599 if (@hash_parents > 1) {
6600 print "<div class=\"diff nodifferences\">Trivial merge</div>\n";
6602 print "<div class=\"diff nodifferences\">No differences found</div>\n";
6606 print "</div>\n"; # class="patchset"
6609 # . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
6611 sub git_project_search_form
{
6612 my ($searchtext, $search_use_regexp) = @_;
6615 if ($project_filter) {
6616 $limit = " in '$project_filter'";
6619 print "<div class=\"projsearch\">\n";
6620 $cgi->start_form(-method
=> 'get', -action
=> $my_uri);
6621 print sprintf(qq/<form method="%s" action="%s" enctype="%s">\n/,
6622 'get', CGI
::escapeHTML
($my_uri), &CGI
::URL_ENCODED
) .
6623 $cgi->hidden(-name
=> 'a', -value
=> 'project_list') . "\n";
6624 print $cgi->hidden(-name
=> 'pf', -value
=> $project_filter). "\n"
6625 if (defined $project_filter);
6626 print $cgi->textfield(-name
=> 's', -value
=> $searchtext,
6627 -title
=> "Search project by name and description$limit",
6628 -size
=> 60) . "\n" .
6629 "<span title=\"Extended regular expression\">" .
6630 $cgi->checkbox(-name
=> 'sr', -value
=> 1, -label
=> 're',
6631 -checked
=> $search_use_regexp) .
6633 $cgi->submit(-name
=> 'btnS', -value
=> 'Search') .
6634 $cgi->end_form() . "\n" .
6635 "<span class=\"projectlist_link\">" .
6636 $cgi->a({-href
=> href
(project
=> undef, searchtext
=> undef,
6637 action
=> 'project_list',
6638 project_filter
=> $project_filter)},
6639 esc_html
("List all projects$limit")) . "</span><br />\n";
6640 print "<span class=\"projectlist_link\">" .
6641 $cgi->a({-href
=> href
(project
=> undef, searchtext
=> undef,
6642 action
=> 'project_list',
6643 project_filter
=> undef)},
6644 esc_html
("List all projects")) . "</span>\n" if $project_filter;
6648 # entry for given @keys needs filling if at least one of keys in list
6649 # is not present in %$project_info
6650 sub project_info_needs_filling
{
6651 my ($project_info, @keys) = @_;
6653 # return List::MoreUtils::any { !exists $project_info->{$_} } @keys;
6654 foreach my $key (@keys) {
6655 if (!exists $project_info->{$key}) {
6662 sub git_cache_file_format
{
6663 return GITWEB_CACHE_FORMAT
.
6664 (gitweb_check_feature
('forks') ?
" (forks)" : "");
6667 sub git_retrieve_cache_file
{
6668 my $cache_file = shift;
6670 use Storable
qw(retrieve);
6672 if ((my $dump = eval { retrieve
($cache_file) })) {
6674 ref($dump) eq 'ARRAY' &&
6676 ref($$dump[1]) eq 'ARRAY' &&
6677 @
{$$dump[1]} == 2 &&
6678 ref(${$$dump[1]}[0]) eq 'ARRAY' &&
6679 ref(${$$dump[1]}[1]) eq 'HASH' &&
6680 $$dump[0] eq git_cache_file_format
();
6686 sub git_store_cache_file
{
6687 my ($cache_file, $cachedata) = @_;
6689 use File
::Basename
qw(dirname);
6691 use POSIX
qw(:fcntl_h);
6692 use Storable
qw(store_fd);
6695 my $cache_d = dirname
($cache_file);
6697 umask($mask & ~0070) if $cache_grpshared;
6698 if ((-d
$cache_d || mkdir($cache_d, $cache_grpshared ?
0770 : 0700)) &&
6699 sysopen(my $fd, "$cache_file.lock", O_WRONLY
|O_CREAT
|O_EXCL
, $cache_grpshared ?
0660 : 0600)) {
6700 store_fd
([git_cache_file_format
(), $cachedata], $fd);
6702 rename "$cache_file.lock", $cache_file;
6703 $result = stat($cache_file)->mtime;
6705 umask($mask) if $cache_grpshared;
6709 sub verify_cached_project
{
6710 my ($hashref, $path) = @_;
6711 return undef unless $path;
6712 delete $$hashref{$path}, return undef unless is_valid_project
($path);
6713 return $$hashref{$path} if exists $$hashref{$path};
6715 # A valid project was requested but it's not yet in the cache
6716 # Manufacture a minimal project entry (path, name, description)
6717 # Also provide age, but only if it's available via $lastactivity_file
6719 my %proj = ('path' => $path);
6720 my $val = git_get_project_description
($path);
6721 defined $val or $val = '';
6722 $proj{'descr_long'} = $val;
6723 $proj{'descr'} = chop_str
($val, $projects_list_description_width, 5);
6724 unless ($omit_owner) {
6725 $val = git_get_project_owner
($path);
6726 defined $val or $val = '';
6727 $proj{'owner'} = $val;
6729 unless ($omit_age_column) {
6730 ($val) = git_get_last_activity
($path, 1);
6731 $proj{'age_epoch'} = $val if defined $val;
6733 $$hashref{$path} = \
%proj;
6737 sub git_filter_cached_projects
{
6738 my ($cache, $projlist, $verify) = @_;
6739 my $hashref = $$cache[1];
6741 sub {verify_cached_project
($hashref, $_[0])} :
6742 sub {$$hashref{$_[0]}};
6744 my $c = &$sub($_->{'path'});
6745 defined $c ?
($_ = $c) : ()
6749 # fills project list info (age, description, owner, category, forks, etc.)
6750 # for each project in the list, removing invalid projects from
6751 # returned list, or fill only specified info.
6753 # Invalid projects are removed from the returned list if and only if you
6754 # ask 'age_epoch' to be filled, because they are the only fields
6755 # that run unconditionally git command that requires repository, and
6756 # therefore do always check if project repository is invalid.
6759 # * fill_project_list_info(\@project_list, 'descr_long', 'ctags')
6760 # ensures that 'descr_long' and 'ctags' fields are filled
6761 # * @project_list = fill_project_list_info(\@project_list)
6762 # ensures that all fields are filled (and invalid projects removed)
6764 # NOTE: modifies $projlist, but does not remove entries from it
6765 sub fill_project_list_info
{
6766 my ($projlist, @wanted_keys) = @_;
6768 my $rebuild = @wanted_keys && $wanted_keys[0] eq 'rebuild-cache' && shift @wanted_keys;
6769 return fill_project_list_info_uncached
($projlist, @wanted_keys)
6770 unless $projlist_cache_lifetime && $projlist_cache_lifetime > 0;
6774 my $cache_lifetime = $rebuild ?
0 : $projlist_cache_lifetime;
6775 my $cache_file = "$cache_dir/$projlist_cache_name";
6781 if ($cache_lifetime && -f
$cache_file) {
6782 $cache_mtime = stat($cache_file)->mtime;
6783 $cache_dump = undef if $cache_mtime &&
6784 (!$cache_dump_mtime || $cache_dump_mtime != $cache_mtime);
6786 if (defined $cache_mtime && # caching is on and $cache_file exists
6787 $cache_mtime + $cache_lifetime*60 > $now &&
6788 ($cache_dump || ($cache_dump = git_retrieve_cache_file
($cache_file)))) {
6790 $cache_dump_mtime = $cache_mtime;
6791 $stale = $now - $cache_mtime;
6792 my $verify = ($action eq 'summary' || $action eq 'forks') &&
6793 gitweb_check_feature
('forks');
6794 @projects = git_filter_cached_projects
($cache_dump, $projlist, $verify);
6796 } else { # Cache miss.
6797 if (defined $cache_mtime) {
6798 # Postpone timeout by two minutes so that we get
6799 # enough time to do our job, or to be more exact
6800 # make cache expire after two minutes from now.
6801 my $time = $now - $cache_lifetime*60 + 120;
6802 utime $time, $time, $cache_file;
6804 my @all_projects = git_get_projects_list
();
6805 my %all_projects_filled = map { ( $_->{'path'} => $_ ) }
6806 fill_project_list_info_uncached
(\
@all_projects);
6807 map { $all_projects_filled{$_->{'path'}} = $_ }
6808 filter_forks_from_projects_list
([values(%all_projects_filled)])
6809 if gitweb_check_feature
('forks');
6810 $cache_dump = [[sort {$a->{'path'} cmp $b->{'path'}} values(%all_projects_filled)],
6811 \
%all_projects_filled];
6812 $cache_dump_mtime = git_store_cache_file
($cache_file, $cache_dump);
6813 @projects = git_filter_cached_projects
($cache_dump, $projlist);
6816 if ($cache_lifetime && $stale > 0) {
6817 print "<div class=\"stale_info\">Cached version (${stale}s old)</div>\n"
6818 unless $shown_stale_message;
6819 $shown_stale_message = 1;
6825 sub fill_project_list_info_uncached
{
6826 my ($projlist, @wanted_keys) = @_;
6828 my $filter_set = sub { return @_; };
6830 my %wanted_keys = map { $_ => 1 } @wanted_keys;
6831 $filter_set = sub { return grep { $wanted_keys{$_} } @_; };
6834 my $show_ctags = gitweb_check_feature
('ctags');
6836 foreach my $pr (@
$projlist) {
6837 if (project_info_needs_filling
($pr, $filter_set->('age_epoch'))) {
6838 my (@activity) = git_get_last_activity
($pr->{'path'});
6839 unless (@activity) {
6842 ($pr->{'age_epoch'}) = @activity;
6844 if (project_info_needs_filling
($pr, $filter_set->('descr', 'descr_long'))) {
6845 my $descr = git_get_project_description
($pr->{'path'}) || "";
6846 $descr = to_utf8
($descr);
6847 $pr->{'descr_long'} = $descr;
6848 $pr->{'descr'} = chop_str
($descr, $projects_list_description_width, 5);
6850 if (project_info_needs_filling
($pr, $filter_set->('owner'))) {
6851 $pr->{'owner'} = git_get_project_owner
("$pr->{'path'}") || "";
6854 project_info_needs_filling
($pr, $filter_set->('ctags'))) {
6855 $pr->{'ctags'} = git_get_project_ctags
($pr->{'path'});
6857 if ($projects_list_group_categories &&
6858 project_info_needs_filling
($pr, $filter_set->('category'))) {
6859 my $cat = git_get_project_category
($pr->{'path'}) ||
6860 $project_list_default_category;
6861 $pr->{'category'} = to_utf8
($cat);
6864 push @projects, $pr;
6870 sub sort_projects_list
{
6871 my ($projlist, $order) = @_;
6875 return sub { lc($a->{$key}) cmp lc($b->{$key}) };
6878 sub order_reverse_num_then_undef
{
6881 defined $a->{$key} ?
6882 (defined $b->{$key} ?
$b->{$key} <=> $a->{$key} : -1) :
6883 (defined $b->{$key} ?
1 : 0)
6888 project
=> order_str
('path'),
6889 descr
=> order_str
('descr_long'),
6890 owner
=> order_str
('owner'),
6891 age
=> order_reverse_num_then_undef
('age_epoch'),
6894 my $ordering = $orderings{$order};
6895 return defined $ordering ?
sort $ordering @
$projlist : @
$projlist;
6898 # returns a hash of categories, containing the list of project
6899 # belonging to each category
6900 sub build_projlist_by_category
{
6901 my ($projlist, $from, $to) = @_;
6904 $from = 0 unless defined $from;
6905 $to = $#$projlist if (!defined $to || $#$projlist < $to);
6907 for (my $i = $from; $i <= $to; $i++) {
6908 my $pr = $projlist->[$i];
6909 push @
{$categories{ $pr->{'category'} }}, $pr;
6912 return wantarray ?
%categories : \
%categories;
6915 # print 'sort by' <th> element, generating 'sort by $name' replay link
6916 # if that order is not selected
6918 print format_sort_th
(@_);
6921 sub format_sort_th
{
6922 my ($name, $order, $header) = @_;
6924 $header ||= ucfirst($name);
6926 if ($order eq $name) {
6927 $sort_th .= "<th>$header</th>\n";
6929 $sort_th .= "<th>" .
6930 $cgi->a({-href
=> href
(-replay
=>1, order
=>$name),
6931 -class => "header"}, $header) .
6938 sub git_project_list_rows
{
6939 my ($projlist, $from, $to, $check_forks) = @_;
6941 $from = 0 unless defined $from;
6942 $to = $#$projlist if (!defined $to || $#$projlist < $to);
6946 for (my $i = $from; $i <= $to; $i++) {
6947 my $pr = $projlist->[$i];
6950 print "<tr class=\"dark\">\n";
6952 print "<tr class=\"light\">\n";
6958 if ($pr->{'forks'}) {
6959 my $nforks = scalar @
{$pr->{'forks'}};
6960 my $s = $nforks == 1 ?
'' : 's';
6962 print $cgi->a({-href
=> href
(project
=>$pr->{'path'}, action
=>"forks"),
6963 -title
=> "$nforks fork$s"}, "+");
6965 print $cgi->span({-title
=> "$nforks fork$s"}, "+");
6970 my $path = $pr->{'path'};
6971 my $dotgit = $path =~ s/\.git$// ?
'.git' : '';
6972 print "<td>" . $cgi->a({-href
=> href
(project
=>$pr->{'path'}, action
=>"summary"),
6974 esc_html_match_hl
($path, $search_regexp).$dotgit) .
6976 "<td>" . $cgi->a({-href
=> href
(project
=>$pr->{'path'}, action
=>"summary"),
6978 -title
=> $pr->{'descr_long'}},
6980 ? esc_html_match_hl_chopped
($pr->{'descr_long'},
6981 $pr->{'descr'}, $search_regexp)
6982 : esc_html
($pr->{'descr'})) .
6984 unless ($omit_owner) {
6985 print "<td><i>" . ($owner_link_hook
6986 ?
$cgi->a({-href
=> $owner_link_hook->($pr->{'owner'}), -class => "list"},
6987 chop_and_escape_str
($pr->{'owner'}, 15))
6988 : chop_and_escape_str
($pr->{'owner'}, 15)) . "</i></td>\n";
6990 unless ($omit_age_column) {
6991 my ($age_epoch, $age_string) = ($pr->{'age_epoch'});
6992 $age_string = defined $age_epoch ? age_string
($age_epoch, $now) : "No commits";
6993 print "<td class=\"". age_class
($age_epoch, $now) . "\">" . $age_string . "</td>\n";
6995 print"<td class=\"link\">" .
6996 $cgi->a({-href
=> href
(project
=>$pr->{'path'}, action
=>"summary")}, "summary") . " | " .
6997 $cgi->a({-href
=> href
(project
=>$pr->{'path'}, action
=>"shortlog")}, "log") . " | " .
6998 $cgi->a({-href
=> href
(project
=>$pr->{'path'}, action
=>"tree")}, "tree") .
6999 ($pr->{'forks'} ?
" | " . $cgi->a({-href
=> href
(project
=>$pr->{'path'}, action
=>"forks")}, "forks") : '') .
7005 sub git_project_list_body
{
7006 # actually uses global variable $project
7007 my ($projlist, $order, $from, $to, $extra, $no_header, $ctags_action, $keep_top) = @_;
7008 my @projects = @
$projlist;
7010 my $check_forks = gitweb_check_feature
('forks');
7011 my $show_ctags = gitweb_check_feature
('ctags');
7012 my $tagfilter = $show_ctags ?
$input_params{'ctag_filter'} : undef;
7013 $check_forks = undef
7014 if ($tagfilter || $search_regexp);
7016 # filtering out forks before filling info allows us to do less work
7018 @projects = filter_forks_from_projects_list
(\
@projects);
7019 push @projects, { 'path' => "$project_filter.git" }
7020 if $project_filter && $keep_top && is_valid_project
("$project_filter.git");
7022 # search_projects_list pre-fills required info
7023 @projects = search_projects_list
(\
@projects,
7024 'search_regexp' => $search_regexp,
7025 'tagfilter' => $tagfilter)
7026 if ($tagfilter || $search_regexp);
7028 my @all_fields = ('descr', 'descr_long', 'ctags', 'category');
7029 push @all_fields, 'age_epoch' unless($omit_age_column);
7030 push @all_fields, 'owner' unless($omit_owner);
7031 @projects = fill_project_list_info
(\
@projects, @all_fields);
7033 $order ||= $default_projects_order;
7034 $from = 0 unless defined $from;
7035 $to = $#projects if (!defined $to || $#projects < $to);
7040 "<b>No such projects found</b><br />\n".
7041 "Click ".$cgi->a({-href
=>href
(project
=>undef,action
=>'project_list')},"here")." to view all projects<br />\n".
7042 "</center>\n<br />\n";
7046 @projects = sort_projects_list
(\
@projects, $order);
7049 my $ctags = git_gather_all_ctags
(\
@projects);
7050 my $cloud = git_populate_project_tagcloud
($ctags, $ctags_action||'project_list');
7051 print git_show_project_tagcloud
($cloud, 64);
7054 print "<table class=\"project_list\">\n";
7055 unless ($no_header) {
7058 print "<th></th>\n";
7060 print_sort_th
('project', $order, 'Project');
7061 print_sort_th
('descr', $order, 'Description');
7062 print_sort_th
('owner', $order, 'Owner') unless $omit_owner;
7063 print_sort_th
('age', $order, 'Last Change') unless $omit_age_column;
7064 print "<th></th>\n" . # for links
7068 if ($projects_list_group_categories) {
7069 # only display categories with projects in the $from-$to window
7070 @projects = sort {$a->{'category'} cmp $b->{'category'}} @projects[$from..$to];
7071 my %categories = build_projlist_by_category
(\
@projects, $from, $to);
7072 foreach my $cat (sort keys %categories) {
7073 unless ($cat eq "") {
7076 print "<td></td>\n";
7078 print "<td class=\"category\" colspan=\"5\">".esc_html
($cat)."</td>\n";
7082 git_project_list_rows
($categories{$cat}, undef, undef, $check_forks);
7085 git_project_list_rows
(\
@projects, $from, $to, $check_forks);
7088 if (defined $extra) {
7091 print "<td></td>\n";
7093 print "<td colspan=\"5\">$extra</td>\n" .
7100 # uses global variable $project
7101 my ($commitlist, $from, $to, $refs, $extra) = @_;
7103 $from = 0 unless defined $from;
7104 $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
7106 for (my $i = 0; $i <= $to; $i++) {
7107 my %co = %{$commitlist->[$i]};
7109 my $commit = $co{'id'};
7110 my $ref = format_ref_marker
($refs, $commit);
7111 git_print_header_div
('commit',
7112 "<span class=\"age\">$co{'age_string'}</span>" .
7113 esc_html
($co{'title'}),
7114 $commit, undef, $ref);
7115 print "<div class=\"title_text\">\n" .
7116 "<div class=\"log_link\">\n" .
7117 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$commit)}, "commit") .
7119 $cgi->a({-href
=> href
(action
=>"commitdiff", hash
=>$commit)}, "commitdiff") .
7121 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$commit, hash_base
=>$commit)}, "tree") .
7124 git_print_authorship
(\
%co, -tag
=> 'span');
7125 print "<br/>\n</div>\n";
7127 print "<div class=\"log_body\">\n";
7128 git_print_log
($co{'comment'}, -final_empty_line
=> 1);
7132 print "<div class=\"page_nav\">\n";
7138 sub git_shortlog_body
{
7139 # uses global variable $project
7140 my ($commitlist, $from, $to, $refs, $extra) = @_;
7142 $from = 0 unless defined $from;
7143 $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
7145 print "<table class=\"shortlog\">\n";
7147 for (my $i = $from; $i <= $to; $i++) {
7148 my %co = %{$commitlist->[$i]};
7149 my $commit = $co{'id'};
7150 my $ref = format_ref_marker
($refs, $commit);
7152 print "<tr class=\"dark\">\n";
7154 print "<tr class=\"light\">\n";
7157 # git_summary() used print "<td><i>$co{'age_string'}</i></td>\n" .
7158 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
7159 format_author_html
('td', \
%co, 10) . "<td>";
7160 print format_subject_html
($co{'title'}, $co{'title_short'},
7161 href
(action
=>"commit", hash
=>$commit), $ref);
7163 "<td class=\"link\">" .
7164 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$commit)}, "commit") . " | " .
7165 $cgi->a({-href
=> href
(action
=>"commitdiff", hash
=>$commit)}, "commitdiff") . " | " .
7166 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$commit, hash_base
=>$commit)}, "tree");
7167 my $snapshot_links = format_snapshot_links
($commit);
7168 if (defined $snapshot_links) {
7169 print " | " . $snapshot_links;
7174 if (defined $extra) {
7176 "<td colspan=\"4\">$extra</td>\n" .
7182 sub git_history_body
{
7183 # Warning: assumes constant type (blob or tree) during history
7184 my ($commitlist, $from, $to, $refs, $extra,
7185 $file_name, $file_hash, $ftype) = @_;
7187 $from = 0 unless defined $from;
7188 $to = $#{$commitlist} unless (defined $to && $to <= $#{$commitlist});
7190 print "<table class=\"history\">\n";
7192 for (my $i = $from; $i <= $to; $i++) {
7193 my %co = %{$commitlist->[$i]};
7197 my $commit = $co{'id'};
7199 my $ref = format_ref_marker
($refs, $commit);
7202 print "<tr class=\"dark\">\n";
7204 print "<tr class=\"light\">\n";
7207 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
7208 # shortlog: format_author_html('td', \%co, 10)
7209 format_author_html
('td', \
%co, 15, 3) . "<td>";
7210 # originally git_history used chop_str($co{'title'}, 50)
7211 print format_subject_html
($co{'title'}, $co{'title_short'},
7212 href
(action
=>"commit", hash
=>$commit), $ref);
7214 "<td class=\"link\">" .
7215 $cgi->a({-href
=> href
(action
=>$ftype, hash_base
=>$commit, file_name
=>$file_name)}, $ftype) . " | " .
7216 $cgi->a({-href
=> href
(action
=>"commitdiff", hash
=>$commit)}, "commitdiff");
7218 if ($ftype eq 'blob') {
7219 my $blob_current = $file_hash;
7220 my $blob_parent = git_get_hash_by_path
($commit, $file_name);
7221 if (defined $blob_current && defined $blob_parent &&
7222 $blob_current ne $blob_parent) {
7224 $cgi->a({-href
=> href
(action
=>"blobdiff",
7225 hash
=>$blob_current, hash_parent
=>$blob_parent,
7226 hash_base
=>$hash_base, hash_parent_base
=>$commit,
7227 file_name
=>$file_name)},
7234 if (defined $extra) {
7236 "<td colspan=\"4\">$extra</td>\n" .
7243 # uses global variable $project
7244 my ($taglist, $from, $to, $extra, $head_at, $full, $order) = @_;
7245 $from = 0 unless defined $from;
7246 $to = $#{$taglist} if (!defined $to || $#{$taglist} < $to);
7247 $order ||= $default_refs_order;
7249 print "<table class=\"tags\">\n";
7251 print "<tr class=\"tags_header\">\n";
7252 print_sort_th
('age', $order, 'Last Change');
7253 print_sort_th
('name', $order, 'Name');
7254 print "<th></th>\n" . # for comment
7255 "<th></th>\n" . # for tag
7256 "<th></th>\n" . # for links
7260 for (my $i = $from; $i <= $to; $i++) {
7261 my $entry = $taglist->[$i];
7263 my $comment = $tag{'subject'};
7265 if (defined $comment) {
7266 $comment_short = chop_str
($comment, 30, 5);
7268 my $curr = defined $head_at && $tag{'id'} eq $head_at;
7270 print "<tr class=\"dark\">\n";
7272 print "<tr class=\"light\">\n";
7275 if (defined $tag{'age'}) {
7276 print "<td><i>$tag{'age'}</i></td>\n";
7278 print "<td></td>\n";
7280 print(($curr ?
"<td class=\"current_head\">" : "<td>") .
7281 $cgi->a({-href
=> href
(action
=>$tag{'reftype'}, hash
=>$tag{'refid'}),
7282 -class => "list name"}, esc_html
($tag{'name'})) .
7285 if (defined $comment) {
7286 print format_subject_html
($comment, $comment_short,
7287 href
(action
=>"tag", hash
=>$tag{'id'}));
7290 "<td class=\"selflink\">";
7291 if ($tag{'type'} eq "tag") {
7292 print $cgi->a({-href
=> href
(action
=>"tag", hash
=>$tag{'id'})}, "tag");
7297 "<td class=\"link\">" . " | " .
7298 $cgi->a({-href
=> href
(action
=>$tag{'reftype'}, hash
=>$tag{'refid'})}, $tag{'reftype'});
7299 if ($tag{'reftype'} eq "commit") {
7300 print " | " . $cgi->a({-href
=> href
(action
=>"shortlog", hash
=>$tag{'fullname'})}, "log");
7301 print " | " . $cgi->a({-href
=> href
(action
=>"tree", hash
=>$tag{'fullname'})}, "tree") if $full;
7302 } elsif ($tag{'reftype'} eq "blob") {
7303 print " | " . $cgi->a({-href
=> href
(action
=>"blob_plain", hash
=>$tag{'refid'})}, "raw");
7308 if (defined $extra) {
7310 "<td colspan=\"5\">$extra</td>\n" .
7316 sub git_heads_body
{
7317 # uses global variable $project
7318 my ($headlist, $head_at, $from, $to, $extra) = @_;
7319 $from = 0 unless defined $from;
7320 $to = $#{$headlist} if (!defined $to || $#{$headlist} < $to);
7322 print "<table class=\"heads\">\n";
7324 for (my $i = $from; $i <= $to; $i++) {
7325 my $entry = $headlist->[$i];
7327 my $curr = defined $head_at && $ref{'id'} eq $head_at;
7329 print "<tr class=\"dark\">\n";
7331 print "<tr class=\"light\">\n";
7334 print "<td><i>$ref{'age'}</i></td>\n" .
7335 ($curr ?
"<td class=\"current_head\">" : "<td>") .
7336 $cgi->a({-href
=> href
(action
=>"shortlog", hash
=>$ref{'fullname'}),
7337 -class => "list name"},esc_html
($ref{'name'})) .
7339 "<td class=\"link\">" .
7340 $cgi->a({-href
=> href
(action
=>"shortlog", hash
=>$ref{'fullname'})}, "log") . " | " .
7341 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$ref{'fullname'}, hash_base
=>$ref{'fullname'})}, "tree") .
7345 if (defined $extra) {
7347 "<td colspan=\"3\">$extra</td>\n" .
7353 # Display a single remote block
7354 sub git_remote_block
{
7355 my ($remote, $rdata, $limit, $head) = @_;
7357 my $heads = $rdata->{'heads'};
7358 my $fetch = $rdata->{'fetch'};
7359 my $push = $rdata->{'push'};
7361 my $urls_table = "<table class=\"projects_list\">\n" ;
7363 if (defined $fetch) {
7364 if ($fetch eq $push) {
7365 $urls_table .= format_repo_url
("URL", $fetch);
7367 $urls_table .= format_repo_url
("Fetch URL", $fetch);
7368 $urls_table .= format_repo_url
("Push URL", $push) if defined $push;
7370 } elsif (defined $push) {
7371 $urls_table .= format_repo_url
("Push URL", $push);
7373 $urls_table .= format_repo_url
("", "No remote URL");
7376 $urls_table .= "</table>\n";
7379 if (defined $limit && $limit < @
$heads) {
7380 $dots = $cgi->a({-href
=> href
(action
=>"remotes", hash
=>$remote)}, "...");
7384 git_heads_body
($heads, $head, 0, $limit, $dots);
7387 # Display a list of remote names with the respective fetch and push URLs
7388 sub git_remotes_list
{
7389 my ($remotedata, $limit) = @_;
7390 print "<table class=\"heads\">\n";
7392 my @remotes = sort keys %$remotedata;
7394 my $limited = $limit && $limit < @remotes;
7396 $#remotes = $limit - 1 if $limited;
7398 while (my $remote = shift @remotes) {
7399 my $rdata = $remotedata->{$remote};
7400 my $fetch = $rdata->{'fetch'};
7401 my $push = $rdata->{'push'};
7403 print "<tr class=\"dark\">\n";
7405 print "<tr class=\"light\">\n";
7409 $cgi->a({-href
=> href
(action
=>'remotes', hash
=>$remote),
7410 -class=> "list name"},esc_html
($remote)) .
7412 print "<td class=\"link\">" .
7413 (defined $fetch ?
$cgi->a({-href
=> $fetch}, "fetch") : "fetch") .
7415 (defined $push ?
$cgi->a({-href
=> $push}, "push") : "push") .
7423 "<td colspan=\"3\">" .
7424 $cgi->a({-href
=> href
(action
=>"remotes")}, "...") .
7425 "</td>\n" . "</tr>\n";
7431 # Display remote heads grouped by remote, unless there are too many
7432 # remotes, in which case we only display the remote names
7433 sub git_remotes_body
{
7434 my ($remotedata, $limit, $head) = @_;
7435 if ($limit and $limit < keys %$remotedata) {
7436 git_remotes_list
($remotedata, $limit);
7438 fill_remote_heads
($remotedata);
7439 while (my ($remote, $rdata) = each %$remotedata) {
7440 git_print_section
({-class=>"remote", -id
=>$remote},
7441 ["remotes", $remote, $remote], sub {
7442 git_remote_block
($remote, $rdata, $limit, $head);
7448 sub git_search_message
{
7452 if ($searchtype eq 'commit') {
7453 $greptype = "--grep=";
7454 } elsif ($searchtype eq 'author') {
7455 $greptype = "--author=";
7456 } elsif ($searchtype eq 'committer') {
7457 $greptype = "--committer=";
7459 $greptype .= $searchtext;
7460 my @commitlist = parse_commits
($hash, 101, (100 * $page), undef,
7461 $greptype, '--regexp-ignore-case',
7462 $search_use_regexp ?
'--extended-regexp' : '--fixed-strings');
7464 my $paging_nav = '';
7467 $cgi->a({-href
=> href
(-replay
=>1, page
=>undef)},
7470 $cgi->a({-href
=> href
(-replay
=>1, page
=>$page-1),
7471 -accesskey
=> "p", -title
=> "Alt-p"}, "prev");
7473 $paging_nav .= "first · prev";
7476 if ($#commitlist >= 100) {
7478 $cgi->a({-href
=> href
(-replay
=>1, page
=>$page+1),
7479 -accesskey
=> "n", -title
=> "Alt-n"}, "next");
7480 $paging_nav .= " · $next_link";
7482 $paging_nav .= " · next";
7487 git_print_page_nav
('','', $hash,$co{'tree'},$hash, $paging_nav);
7488 git_print_header_div
('commit', esc_html
($co{'title'}), $hash);
7489 if ($page == 0 && !@commitlist) {
7490 print "<p>No match.</p>\n";
7492 git_search_grep_body
(\
@commitlist, 0, 99, $next_link);
7498 sub git_search_changes
{
7502 defined(my $fd = git_cmd_pipe
'--no-pager', 'log', @diff_opts,
7503 '--pretty=format:%H', '--no-abbrev', '--raw', "-S$searchtext",
7504 ($search_use_regexp ?
'--pickaxe-regex' : ()))
7505 or die_error
(500, "Open git-log failed");
7509 git_print_page_nav
('','', $hash,$co{'tree'},$hash);
7510 git_print_header_div
('commit', esc_html
($co{'title'}), $hash);
7512 print "<table class=\"pickaxe search\">\n";
7516 while (my $line = to_utf8
(scalar <$fd>)) {
7520 my %set = parse_difftree_raw_line
($line);
7521 if (defined $set{'commit'}) {
7522 # finish previous commit
7525 "<td class=\"link\">" .
7526 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$co{'id'})},
7529 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$co{'tree'},
7530 hash_base
=>$co{'id'})},
7537 print "<tr class=\"dark\">\n";
7539 print "<tr class=\"light\">\n";
7542 %co = parse_commit
($set{'commit'});
7543 my $author = chop_and_escape_str
($co{'author_name'}, 15, 5);
7544 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
7545 "<td><i>$author</i></td>\n" .
7547 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$co{'id'}),
7548 -class => "list subject"},
7549 chop_and_escape_str
($co{'title'}, 50) . "<br/>");
7550 } elsif (defined $set{'to_id'}) {
7551 next if ($set{'to_id'} =~ m/^0{40}$/);
7553 print $cgi->a({-href
=> href
(action
=>"blob", hash_base
=>$co{'id'},
7554 hash
=>$set{'to_id'}, file_name
=>$set{'to_file'}),
7556 "<span class=\"match\">" . esc_path
($set{'file'}) . "</span>") .
7562 # finish last commit (warning: repetition!)
7565 "<td class=\"link\">" .
7566 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$co{'id'})},
7569 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$co{'tree'},
7570 hash_base
=>$co{'id'})},
7581 sub git_search_files
{
7585 defined(my $fd = git_cmd_pipe
'grep', '-n', '-z',
7586 $search_use_regexp ?
('-E', '-i') : '-F',
7587 $searchtext, $co{'tree'})
7588 or die_error
(500, "Open git-grep failed");
7592 git_print_page_nav
('','', $hash,$co{'tree'},$hash);
7593 git_print_header_div
('commit', esc_html
($co{'title'}), $hash);
7595 print "<table class=\"grep_search\">\n";
7600 while (my $line = to_utf8
(scalar <$fd>)) {
7602 my ($file, $lno, $ltext, $binary);
7603 last if ($matches++ > 1000);
7604 if ($line =~ /^Binary file (.+) matches$/) {
7608 ($file, $lno, $ltext) = split(/\0/, $line, 3);
7609 $file =~ s/^$co{'tree'}://;
7611 if ($file ne $lastfile) {
7612 $lastfile and print "</td></tr>\n";
7614 print "<tr class=\"dark\">\n";
7616 print "<tr class=\"light\">\n";
7618 $file_href = href
(action
=>"blob", hash_base
=>$co{'id'},
7620 print "<td class=\"list\">".
7621 $cgi->a({-href
=> $file_href, -class => "list"}, esc_path
($file));
7622 print "</td><td>\n";
7626 print "<div class=\"binary\">Binary file</div>\n";
7628 $ltext = untabify
($ltext);
7629 if ($ltext =~ m/^(.*)($search_regexp)(.*)$/i) {
7630 $ltext = esc_html
($1, -nbsp
=>1);
7631 $ltext .= '<span class="match">';
7632 $ltext .= esc_html
($2, -nbsp
=>1);
7633 $ltext .= '</span>';
7634 $ltext .= esc_html
($3, -nbsp
=>1);
7636 $ltext = esc_html
($ltext, -nbsp
=>1);
7638 print "<div class=\"pre\">" .
7639 $cgi->a({-href
=> $file_href.'#l'.$lno,
7640 -class => "linenr"}, sprintf('%4i ', $lno)) .
7641 $ltext . "</div>\n";
7645 print "</td></tr>\n";
7646 if ($matches > 1000) {
7647 print "<div class=\"diff nodifferences\">Too many matches, listing trimmed</div>\n";
7650 print "<div class=\"diff nodifferences\">No matches found</div>\n";
7659 sub git_search_grep_body
{
7660 my ($commitlist, $from, $to, $extra) = @_;
7661 $from = 0 unless defined $from;
7662 $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
7664 print "<table class=\"commit_search\">\n";
7666 for (my $i = $from; $i <= $to; $i++) {
7667 my %co = %{$commitlist->[$i]};
7671 my $commit = $co{'id'};
7673 print "<tr class=\"dark\">\n";
7675 print "<tr class=\"light\">\n";
7678 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
7679 format_author_html
('td', \
%co, 15, 5) .
7681 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$co{'id'}),
7682 -class => "list subject"},
7683 chop_and_escape_str
($co{'title'}, 50) . "<br/>");
7684 my $comment = $co{'comment'};
7685 foreach my $line (@
$comment) {
7686 if ($line =~ m/^(.*?)($search_regexp)(.*)$/i) {
7687 my ($lead, $match, $trail) = ($1, $2, $3);
7688 $match = chop_str
($match, 70, 5, 'center');
7689 my $contextlen = int((80 - length($match))/2);
7690 $contextlen = 30 if ($contextlen > 30);
7691 $lead = chop_str
($lead, $contextlen, 10, 'left');
7692 $trail = chop_str
($trail, $contextlen, 10, 'right');
7694 $lead = esc_html
($lead);
7695 $match = esc_html
($match);
7696 $trail = esc_html
($trail);
7698 print "$lead<span class=\"match\">$match</span>$trail<br />";
7702 "<td class=\"link\">" .
7703 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$co{'id'})}, "commit") .
7705 $cgi->a({-href
=> href
(action
=>"commitdiff", hash
=>$co{'id'})}, "commitdiff") .
7707 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$co{'tree'}, hash_base
=>$co{'id'})}, "tree");
7711 if (defined $extra) {
7713 "<td colspan=\"3\">$extra</td>\n" .
7719 ## ======================================================================
7720 ## ======================================================================
7723 sub git_project_list_load
{
7724 my $empty_list_ok = shift;
7725 my $order = $input_params{'order'};
7726 if (defined $order && $order !~ m/none|project|descr|owner|age/) {
7727 die_error
(400, "Unknown order parameter");
7730 my @list = git_get_projects_list
($project_filter, $strict_export);
7731 if ($project_filter && (!@list || !gitweb_check_feature
('forks'))) {
7732 push @list, { 'path' => "$project_filter.git" }
7733 if is_valid_project
("$project_filter.git");
7736 die_error
(404, "No projects found") unless $empty_list_ok;
7739 return (\
@list, $order);
7743 my ($projlist, $order);
7745 if ($frontpage_no_project_list) {
7747 $project_filter = undef;
7749 ($projlist, $order) = git_project_list_load
(1);
7752 if (defined $home_text && -f
$home_text) {
7753 print "<div class=\"index_include\">\n";
7754 insert_file
($home_text);
7757 git_project_search_form
($searchtext, $search_use_regexp);
7758 if ($frontpage_no_project_list) {
7759 my $show_ctags = gitweb_check_feature
('ctags');
7760 if ($frontpage_no_project_list == 1 and $show_ctags) {
7761 my @projects = git_get_projects_list
($project_filter, $strict_export);
7762 @projects = filter_forks_from_projects_list
(\
@projects) if gitweb_check_feature
('forks');
7763 @projects = fill_project_list_info
(\
@projects, 'ctags');
7764 my $ctags = git_gather_all_ctags
(\
@projects);
7765 my $cloud = git_populate_project_tagcloud
($ctags, 'project_list');
7766 print git_show_project_tagcloud
($cloud, 64);
7769 git_project_list_body
($projlist, $order, undef, undef, undef, undef, undef, 1);
7774 sub git_project_list
{
7775 my ($projlist, $order) = git_project_list_load
();
7777 if (!$frontpage_no_project_list && defined $home_text && -f
$home_text) {
7778 print "<div class=\"index_include\">\n";
7779 insert_file
($home_text);
7782 git_project_search_form
();
7783 git_project_list_body
($projlist, $order, undef, undef, undef, undef, undef, 1);
7788 my $order = $input_params{'order'};
7789 if (defined $order && $order !~ m/none|project|descr|owner|age/) {
7790 die_error
(400, "Unknown order parameter");
7793 my $filter = $project;
7794 $filter =~ s/\.git$//;
7795 my @list = git_get_projects_list
($filter);
7797 die_error
(404, "No forks found");
7801 git_print_page_nav
('','');
7802 git_print_header_div
('summary', "$project forks");
7803 git_project_list_body
(\
@list, $order, undef, undef, undef, undef, 'forks');
7807 sub git_project_index
{
7808 my @projects = git_get_projects_list
($project_filter, $strict_export);
7810 die_error
(404, "No projects found");
7814 -type
=> 'text/plain',
7815 -charset
=> 'utf-8',
7816 -content_disposition
=> 'inline; filename="index.aux"');
7818 foreach my $pr (@projects) {
7819 if (!exists $pr->{'owner'}) {
7820 $pr->{'owner'} = git_get_project_owner
("$pr->{'path'}");
7823 my ($path, $owner) = ($pr->{'path'}, $pr->{'owner'});
7824 # quote as in CGI::Util::encode, but keep the slash, and use '+' for ' '
7825 $path =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf
("%%%02X", ord($1))/eg
;
7826 $owner =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf
("%%%02X", ord($1))/eg
;
7830 print "$path $owner\n";
7835 my $descr = git_get_project_description
($project) || "none";
7836 my %co = parse_commit
("HEAD");
7837 my %cd = %co ? parse_date
($co{'committer_epoch'}, $co{'committer_tz'}) : ();
7838 my $head = $co{'id'};
7839 my $remote_heads = gitweb_check_feature
('remote_heads');
7841 my $owner = git_get_project_owner
($project);
7842 my $homepage = git_get_project_config
('homepage');
7843 my $base_url = git_get_project_config
('baseurl');
7845 my $refs = git_get_references
();
7846 # These get_*_list functions return one more to allow us to see if
7847 # there are more ...
7848 my @taglist = git_get_tags_list
(16);
7849 my @headlist = git_get_heads_list
(16);
7850 my %remotedata = $remote_heads ? git_get_remotes_list
() : ();
7852 my $check_forks = gitweb_check_feature
('forks');
7855 # find forks of a project
7856 my $filter = $project;
7857 $filter =~ s/\.git$//;
7858 @forklist = git_get_projects_list
($filter);
7859 # filter out forks of forks
7860 @forklist = filter_forks_from_projects_list
(\
@forklist)
7865 git_print_page_nav
('summary','', $head);
7867 if ($check_forks and $project =~ m
#/#) {
7868 my $xproject = $project; $xproject =~ s
#/[^/]+$#.git#; #
7869 if (is_valid_project
($xproject) && -f
"$projectroot/$project/objects/info/alternates" && -s _
) {
7870 my $r = $cgi->a({-href
=> href
(project
=> $xproject, action
=> 'summary')}, $xproject);
7872 <div class="forkinfo">
7873 This project is a fork of the $r project. If you have that one
7874 already cloned locally, you can use
7875 <pre>git clone --reference /path/to/your/$xproject/incarnation mirror_URL</pre>
7876 to save bandwidth during cloning.
7882 print "<div class=\"title\"> </div>\n";
7883 print "<table class=\"projects_list\">\n" .
7884 "<tr id=\"metadata_desc\"><td>description</td><td>" . esc_html
($descr) . "</td></tr>\n";
7886 print "<tr id=\"metadata_homepage\"><td>homepage URL</td><td>" . $cgi->a({-href
=> $homepage}, $homepage) . "</td></tr>\n";
7889 print "<tr id=\"metadata_baseurl\"><td>repository URL</td><td>" . esc_html
($base_url) . "</td></tr>\n";
7891 if ($owner and not $omit_owner) {
7892 print "<tr id=\"metadata_owner\"><td>owner</td><td>" . ($owner_link_hook
7893 ?
$cgi->a({-href
=> $owner_link_hook->($owner)}, email_obfuscate
($owner))
7894 : email_obfuscate
($owner)) . "</td></tr>\n";
7896 if (defined $cd{'rfc2822'}) {
7897 print "<tr id=\"metadata_lchange\"><td>last change</td>" .
7898 "<td>".format_timestamp_html
(\
%cd)."</td></tr>\n";
7900 print format_lastrefresh_row
(), "\n";
7902 # use per project git URL list in $projectroot/$project/cloneurl
7903 # or make project git URL from git base URL and project name
7904 my $url_tag = $base_url ?
"mirror URL" : "URL";
7905 my $url_class = "metadata_url";
7906 my @url_list = git_get_project_url_list
($project);
7907 unless (@url_list) {
7908 @url_list = @git_base_url_list;
7909 if ((git_get_project_config
("showpush", '--bool')||'false') eq "true" ||
7910 -f
"$projectroot/$project/.nofetch") {
7911 my $pushidx = @url_list;
7912 foreach (@git_base_push_urls) {
7913 if ($https_hint_html && !ref($_) && $_ =~ /^https:/i) {
7914 push(@url_list, [$_, $https_hint_html]);
7916 push(@url_list, $_);
7919 if ($#url_list >= $pushidx) {
7920 my $pushtag = "push URL";
7921 my $classtag = "metadata_pushurl";
7922 if (ref($url_list[$pushidx])) {
7923 $url_list[$pushidx] = [
7924 ${$url_list[$pushidx]}[0],
7925 ${$url_list[$pushidx]}[1],
7929 $url_list[$pushidx] = [
7930 $url_list[$pushidx],
7937 push(@url_list, @git_base_mirror_urls);
7939 for (my $i=0; $i<=$#url_list; ++$i) {
7940 if (ref($url_list[$i])) {
7942 ${$url_list[$i]}[0] . "/$project",
7943 ${$url_list[$i]}[1],
7944 ${$url_list[$i]}[2],
7945 ${$url_list[$i]}[3]];
7947 $url_list[$i] .= "/$project";
7951 foreach (@url_list) {
7955 my $next_tag = undef;
7956 my $next_class = undef;
7959 $html_hint = " " . $$_[1] if defined($$_[1]);
7961 $next_class = $$_[3];
7965 next unless $git_url;
7966 $url_class = $next_class if $next_class;
7967 $url_tag = $next_tag if $next_tag;
7968 print "<tr class=\"$url_class\"><td>$url_tag</td><td>$git_url$html_hint</td></tr>\n";
7972 if (defined($git_base_bundles_url) && -d
"$projectroot/$project/bundles") {
7973 my $projname = $project;
7974 $projname =~ s
|^.*/||;
7975 my $url = "$git_base_bundles_url/$project/bundles";
7976 print format_repo_url
(
7978 "<a rel='nofollow' href='$url'>$projname downloadable bundles</a>");
7982 my $show_ctags = gitweb_check_feature
('ctags');
7984 my $ctags = git_get_project_ctags
($project);
7985 if (%$ctags || $show_ctags !~ /^\d+$/) {
7986 # without ability to add tags, don't show if there are none
7987 my $cloud = git_populate_project_tagcloud
($ctags, 'project_list');
7988 print "<tr id=\"metadata_ctags\">" .
7989 "<td style=\"vertical-align:middle\">content tags<br />";
7990 print "</td>\n<td>" unless %$ctags;
7991 print "<form action=\"$show_ctags\" method=\"post\" style=\"white-space:nowrap\">" .
7992 "<input type=\"hidden\" name=\"p\" value=\"$project\"/>" .
7993 "add: <input type=\"text\" name=\"t\" size=\"8\" /></form>"
7994 unless $show_ctags =~ /^\d+$/;
7995 print "</td>\n<td>" if %$ctags;
7996 print git_show_project_tagcloud
($cloud, 48)."</td>" .
8003 # If XSS prevention is on, we don't include README.html.
8004 # TODO: Allow a readme in some safe format.
8005 if (!$prevent_xss) {
8006 my $readme_name = "readme";
8008 if (-s
"$projectroot/$project/README.html") {
8009 $readme = collect_html_file
("$projectroot/$project/README.html");
8011 $readme = collect_output
($git_automatic_readme_html, "$projectroot/$project");
8012 if ($readme && $readme =~ /^<!-- README NAME: ((?:[^-]|(?:-(?!-)))+) -->/) {
8014 $readme =~ s/^<!--(?:[^-]|(?:-(?!-)))*-->\n?//;
8017 if (defined($readme)) {
8018 $readme =~ s/^\s+//s;
8019 $readme =~ s/\s+$//s;
8020 print "<div class=\"title\">$readme_name</div>\n",
8021 "<div class=\"readme\">\n",
8028 # we need to request one more than 16 (0..15) to check if
8030 my @commitlist = $head ? parse_commits
($head, 17) : ();
8032 git_print_header_div
('shortlog');
8033 git_shortlog_body
(\
@commitlist, 0, 15, $refs,
8034 $#commitlist <= 15 ?
undef :
8035 $cgi->a({-href
=> href
(action
=>"shortlog")}, "..."));
8039 git_print_header_div
('tags');
8040 git_tags_body
(\
@taglist, 0, 15,
8041 $#taglist <= 15 ?
undef :
8042 $cgi->a({-href
=> href
(action
=>"tags")}, "..."));
8046 git_print_header_div
('heads');
8047 git_heads_body
(\
@headlist, $head, 0, 15,
8048 $#headlist <= 15 ?
undef :
8049 $cgi->a({-href
=> href
(action
=>"heads")}, "..."));
8053 git_print_header_div
('remotes');
8054 git_remotes_body
(\
%remotedata, 15, $head);
8058 git_print_header_div
('forks');
8059 git_project_list_body
(\
@forklist, 'age', 0, 15,
8060 $#forklist <= 15 ?
undef :
8061 $cgi->a({-href
=> href
(action
=>"forks")}, "..."),
8062 'no_header', 'forks');
8069 my %tag = parse_tag
($hash);
8072 die_error
(404, "Unknown tag object");
8076 $fullhash = $hash if $hash =~ m/^[0-9a-fA-F]{40}$/;
8077 $fullhash = git_get_full_hash
($project, $hash) unless $fullhash;
8079 my $obj = $tag{'object'};
8081 if ($tag{'type'} eq 'commit') {
8082 git_print_page_nav
('','', $obj,undef,$obj);
8083 git_print_header_div
('commit', esc_html
($tag{'name'}), $hash);
8085 if ($tag{'type'} eq 'tree') {
8086 git_print_page_nav
('',['commit','commitdiff'], undef,undef,$obj);
8088 git_print_page_nav
('',['commit','commitdiff','tree'], undef,undef,undef);
8090 print "<div class=\"title\">".esc_html
($hash)."</div>\n";
8092 print "<div class=\"title_text\">\n" .
8093 "<table class=\"object_header\">\n" .
8094 "<tr><td>tag</td><td class=\"sha1\">$fullhash</td></tr>\n" .
8096 "<td>object</td>\n" .
8097 "<td>" . $cgi->a({-class => "list", -href
=> href
(action
=>$tag{'type'}, hash
=>$tag{'object'})},
8098 $tag{'object'}) . "</td>\n" .
8099 "<td class=\"link\">" . $cgi->a({-href
=> href
(action
=>$tag{'type'}, hash
=>$tag{'object'})},
8100 $tag{'type'}) . "</td>\n" .
8102 if (defined($tag{'author'})) {
8103 git_print_authorship_rows
(\
%tag, 'author');
8105 print "</table>\n\n" .
8107 print "<div class=\"page_body\">";
8108 my $comment = $tag{'comment'};
8109 foreach my $line (@
$comment) {
8111 print esc_html
($line, -nbsp
=>1) . "<br/>\n";
8117 sub git_blame_common
{
8118 my $format = shift || 'porcelain';
8119 if ($format eq 'porcelain' && $input_params{'javascript'}) {
8120 $format = 'incremental';
8121 $action = 'blame_incremental'; # for page title etc
8125 gitweb_check_feature
('blame')
8126 or die_error
(403, "Blame view not allowed");
8129 die_error
(400, "No file name given") unless $file_name;
8130 $hash_base ||= git_get_head_hash
($project);
8131 die_error
(404, "Couldn't find base commit") unless $hash_base;
8132 my %co = parse_commit
($hash_base)
8133 or die_error
(404, "Commit not found");
8135 if (!defined $hash) {
8136 $hash = git_get_hash_by_path
($hash_base, $file_name, "blob")
8137 or die_error
(404, "Error looking up file");
8139 $ftype = git_get_type
($hash);
8140 if ($ftype !~ "blob") {
8141 die_error
(400, "Object is not a blob");
8146 if ($format eq 'incremental') {
8147 # get file contents (as base)
8148 defined($fd = git_cmd_pipe
'cat-file', 'blob', $hash)
8149 or die_error
(500, "Open git-cat-file failed");
8150 } elsif ($format eq 'data') {
8151 # run git-blame --incremental
8152 defined($fd = git_cmd_pipe
"blame", "--incremental",
8153 $hash_base, "--", $file_name)
8154 or die_error
(500, "Open git-blame --incremental failed");
8156 # run git-blame --porcelain
8157 defined($fd = git_cmd_pipe
"blame", '-p',
8158 $hash_base, '--', $file_name)
8159 or die_error
(500, "Open git-blame --porcelain failed");
8162 # incremental blame data returns early
8163 if ($format eq 'data') {
8165 -type
=>"text/plain", -charset
=> "utf-8",
8166 -status
=> "200 OK");
8167 local $| = 1; # output autoflush
8172 or print "ERROR $!\n";
8175 if (defined $t0 && gitweb_check_feature
('timed')) {
8177 tv_interval
($t0, [ gettimeofday
() ]).
8178 ' '.$number_of_git_cmds;
8188 $cgi->a({-href
=> href
(action
=>"blob", -replay
=>1)},
8192 $cgi->a({-href
=> href
(action
=>"history", -replay
=>1)},
8195 $cgi->a({-href
=> href
(action
=>$action, file_name
=>$file_name)},
8197 git_print_page_nav
('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
8198 git_print_header_div
('commit', esc_html
($co{'title'}), $hash_base);
8199 git_print_page_path
($file_name, $ftype, $hash_base);
8202 if ($format eq 'incremental') {
8203 print "<noscript>\n<div class=\"error\"><center><b>\n".
8204 "This page requires JavaScript to run.\n Use ".
8205 $cgi->a({-href
=> href
(action
=>'blame',javascript
=>0,-replay
=>1)},
8208 "</b></center></div>\n</noscript>\n";
8210 print qq!<div id
="progress_bar" style
="width: 100%; background-color: yellow"></div
>\n!;
8213 print qq!<div
class="page_body">\n!;
8214 print qq!<div id
="progress_info">... / ...</div
>\n!
8215 if ($format eq 'incremental');
8216 print qq!<table id
="blame_table" class="blame" width
="100%">\n!.
8217 #qq!<col width="5.5em" /><col width="2.5em" /><col width="*" />\n!.
8219 qq!<tr
><th nowrap
="nowrap" style
="white-space:nowrap">!.
8220 qq!Commit
 <a href="javascript:extra_blame_columns()" id="columns_expander" !.
8221 qq!title
="toggles blame author information display">[+]</a></th
>!.
8222 qq!<th
class="extra_column">Author
</th><th class="extra_column">Date</th
>!.
8223 qq!<th
>Line
</th><th width="100%">Data</th
></tr
>\n!.
8227 my @rev_color = qw(light dark);
8228 my $num_colors = scalar(@rev_color);
8229 my $current_color = 0;
8231 if ($format eq 'incremental') {
8232 my $color_class = $rev_color[$current_color];
8237 while (my $line = to_utf8
(scalar <$fd>)) {
8241 print qq!<tr id
="l$linenr" class="$color_class">!.
8242 qq!<td
class="sha1"><a href
=""> </a></td
>!.
8243 qq!<td
class="extra_column" nowrap
="nowrap"></td
>!.
8244 qq!<td
class="extra_column" nowrap
="nowrap"></td
>!.
8245 qq!<td
class="linenr">!.
8246 qq!<a
class="linenr" href
="">$linenr</a></td
>!;
8247 print qq!<td
class="pre">! . esc_html
($line) . "</td>\n";
8251 } else { # porcelain, i.e. ordinary blame
8252 my %metainfo = (); # saves information about commits
8256 while (my $line = to_utf8
(scalar <$fd>)) {
8258 # the header: <SHA-1> <src lineno> <dst lineno> [<lines in group>]
8259 # no <lines in group> for subsequent lines in group of lines
8260 my ($full_rev, $orig_lineno, $lineno, $group_size) =
8261 ($line =~ /^([0-9a-f]{40}) (\d+) (\d+)(?: (\d+))?$/);
8262 if (!exists $metainfo{$full_rev}) {
8263 $metainfo{$full_rev} = { 'nprevious' => 0 };
8265 my $meta = $metainfo{$full_rev};
8267 while ($data = to_utf8
(scalar <$fd>)) {
8269 last if ($data =~ s/^\t//); # contents of line
8270 if ($data =~ /^(\S+)(?: (.*))?$/) {
8271 $meta->{$1} = $2 unless exists $meta->{$1};
8273 if ($data =~ /^previous /) {
8274 $meta->{'nprevious'}++;
8277 my $short_rev = substr($full_rev, 0, 8);
8278 my $author = $meta->{'author'};
8280 parse_date
($meta->{'author-time'}, $meta->{'author-tz'});
8281 my $date = $date{'iso-tz'};
8283 $current_color = ($current_color + 1) % $num_colors;
8285 my $tr_class = $rev_color[$current_color];
8286 $tr_class .= ' boundary' if (exists $meta->{'boundary'});
8287 $tr_class .= ' no-previous' if ($meta->{'nprevious'} == 0);
8288 $tr_class .= ' multiple-previous' if ($meta->{'nprevious'} > 1);
8289 print "<tr id=\"l$lineno\" class=\"$tr_class\">\n";
8291 my $rowspan = $group_size > 1 ?
" rowspan=\"$group_size\"" : "";
8292 print "<td class=\"sha1\"";
8293 print " title=\"". esc_html
($author) . ", $date\"";
8295 print $cgi->a({-href
=> href
(action
=>"commit",
8297 file_name
=>$file_name)},
8298 esc_html
($short_rev));
8299 if ($group_size >= 2) {
8300 my @author_initials = ($author =~ /\b([[:upper:]])\B/g);
8301 if (@author_initials) {
8303 esc_html
(join('', @author_initials));
8308 print "<td class=\"extra_column\" nowrap=\"nowrap\"$rowspan>". esc_html
($author) . "</td>";
8309 print "<td class=\"extra_column\" nowrap=\"nowrap\"$rowspan>". $date . "</td>";
8311 # 'previous' <sha1 of parent commit> <filename at commit>
8312 if (exists $meta->{'previous'} &&
8313 $meta->{'previous'} =~ /^([a-fA-F0-9]{40}) (.*)$/) {
8314 $meta->{'parent'} = $1;
8315 $meta->{'file_parent'} = unquote
($2);
8318 exists($meta->{'parent'}) ?
8319 $meta->{'parent'} : $full_rev;
8320 my $linenr_filename =
8321 exists($meta->{'file_parent'}) ?
8322 $meta->{'file_parent'} : unquote
($meta->{'filename'});
8323 my $blamed = href
(action
=> 'blame',
8324 file_name
=> $linenr_filename,
8325 hash_base
=> $linenr_commit);
8326 print "<td class=\"linenr\">";
8327 print $cgi->a({ -href
=> "$blamed#l$orig_lineno",
8328 -class => "linenr" },
8331 print "<td class=\"pre\">" . esc_html
($data) . "</td>\n";
8339 "</table>\n"; # class="blame"
8340 print "</div>\n"; # class="blame_body"
8342 or print "Reading blob failed\n";
8351 sub git_blame_incremental
{
8352 git_blame_common
('incremental');
8355 sub git_blame_data
{
8356 git_blame_common
('data');
8360 my $head = git_get_head_hash
($project);
8362 git_print_page_nav
('','', $head,undef,$head,format_ref_views
('tags'));
8363 git_print_header_div
('summary', $project);
8365 my @tagslist = git_get_tags_list
();
8367 git_tags_body
(\
@tagslist);
8373 my $order = $input_params{'order'};
8374 if (defined $order && $order !~ m/age|name/) {
8375 die_error
(400, "Unknown order parameter");
8378 my $head = git_get_head_hash
($project);
8380 git_print_page_nav
('','', $head,undef,$head,format_ref_views
('refs'));
8381 git_print_header_div
('summary', $project);
8383 my @refslist = git_get_tags_list
(undef, 1, $order);
8385 git_tags_body
(\
@refslist, undef, undef, undef, $head, 1, $order);
8391 my $head = git_get_head_hash
($project);
8393 git_print_page_nav
('','', $head,undef,$head,format_ref_views
('heads'));
8394 git_print_header_div
('summary', $project);
8396 my @headslist = git_get_heads_list
();
8398 git_heads_body
(\
@headslist, $head);
8403 # used both for single remote view and for list of all the remotes
8405 gitweb_check_feature
('remote_heads')
8406 or die_error
(403, "Remote heads view is disabled");
8408 my $head = git_get_head_hash
($project);
8409 my $remote = $input_params{'hash'};
8411 my $remotedata = git_get_remotes_list
($remote);
8412 die_error
(500, "Unable to get remote information") unless defined $remotedata;
8414 unless (%$remotedata) {
8415 die_error
(404, defined $remote ?
8416 "Remote $remote not found" :
8417 "No remotes found");
8420 git_header_html
(undef, undef, -action_extra
=> $remote);
8421 git_print_page_nav
('', '', $head, undef, $head,
8422 format_ref_views
($remote ?
'' : 'remotes'));
8424 fill_remote_heads
($remotedata);
8425 if (defined $remote) {
8426 git_print_header_div
('remotes', "$remote remote for $project");
8427 git_remote_block
($remote, $remotedata->{$remote}, undef, $head);
8429 git_print_header_div
('summary', "$project remotes");
8430 git_remotes_body
($remotedata, undef, $head);
8436 sub git_blob_plain
{
8440 if (!defined $hash) {
8441 if (defined $file_name) {
8442 my $base = $hash_base || git_get_head_hash
($project);
8443 $hash = git_get_hash_by_path
($base, $file_name, "blob")
8444 or die_error
(404, "Cannot find file");
8446 die_error
(400, "No file name defined");
8448 } elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) {
8449 # blobs defined by non-textual hash id's can be cached
8453 defined(my $fd = git_cmd_pipe
"cat-file", "blob", $hash)
8454 or die_error
(500, "Open git-cat-file blob '$hash' failed");
8457 # content-type (can include charset)
8459 ($type, $leader) = blob_contenttype
($fd, $file_name, $type);
8461 # "save as" filename, even when no $file_name is given
8462 my $save_as = "$hash";
8463 if (defined $file_name) {
8464 $save_as = $file_name;
8465 } elsif ($type =~ m/^text\//) {
8469 # With XSS prevention on, blobs of all types except a few known safe
8470 # ones are served with "Content-Disposition: attachment" to make sure
8471 # they don't run in our security domain. For certain image types,
8472 # blob view writes an <img> tag referring to blob_plain view, and we
8473 # want to be sure not to break that by serving the image as an
8474 # attachment (though Firefox 3 doesn't seem to care).
8475 my $sandbox = $prevent_xss &&
8476 $type !~ m!^(?:text/[a-z]+|image/(?:gif|png|jpeg))(?:[ ;]|$)!;
8478 # serve text/* as text/plain
8480 ($type =~ m!^text/[a-z]+\b(.*)$! ||
8481 ($type =~ m!^[a-z]+/[a-z]\+xml\b(.*)$! && -T
$fd))) {
8483 $rest = defined $rest ?
$rest : '';
8484 $type = "text/plain$rest";
8489 -expires
=> $expires,
8490 -content_disposition
=>
8491 ($sandbox ?
'attachment' : 'inline')
8492 . '; filename="' . $save_as . '"');
8493 binmode STDOUT
, ':raw';
8495 print $leader if defined $leader;
8497 while (read($fd, $buf, 32768)) {
8500 binmode STDOUT
, ':utf8'; # as set at the beginning of gitweb.cgi
8508 if (!defined $hash) {
8509 if (defined $file_name) {
8510 my $base = $hash_base || git_get_head_hash
($project);
8511 $hash = git_get_hash_by_path
($base, $file_name, "blob")
8512 or die_error
(404, "Cannot find file");
8514 die_error
(400, "No file name defined");
8516 } elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) {
8517 # blobs defined by non-textual hash id's can be cached
8520 my $fullhash = git_get_full_hash
($project, "$hash^{blob}");
8521 die_error
(404, "No such blob") unless defined($fullhash);
8523 my $have_blame = gitweb_check_feature
('blame');
8524 defined(my $fd = git_cmd_pipe
"cat-file", "blob", $fullhash)
8525 or die_error
(500, "Couldn't cat $file_name, $hash");
8527 my $mimetype = blob_mimetype
($fd, $file_name);
8528 # use 'blob_plain' (aka 'raw') view for files that cannot be displayed
8529 if ($mimetype !~ m!^(?:text/|image/(?:gif|png|jpeg)$)! && -B
$fd) {
8531 return git_blob_plain
($mimetype);
8533 # we can have blame only for text/* mimetype
8534 $have_blame &&= ($mimetype =~ m!^text/!);
8536 my $highlight = gitweb_check_feature
('highlight') && defined $highlight_bin;
8537 my $syntax = guess_file_syntax
($fd, $mimetype, $file_name) if $highlight;
8538 my $highlight_mode_active;
8539 ($fd, $highlight_mode_active) = run_highlighter
($fd, $syntax) if $syntax;
8541 git_header_html
(undef, $expires);
8542 my $formats_nav = '';
8543 if (defined $hash_base && (my %co = parse_commit
($hash_base))) {
8544 if (defined $file_name) {
8547 $cgi->a({-href
=> href
(action
=>"blame", -replay
=>1),
8548 -class => "blamelink"},
8553 $cgi->a({-href
=> href
(action
=>"history", -replay
=>1)},
8556 $cgi->a({-href
=> href
(action
=>"blob_plain", -replay
=>1)},
8559 $cgi->a({-href
=> href
(action
=>"blob",
8560 hash_base
=>"HEAD", file_name
=>$file_name)},
8564 $cgi->a({-href
=> href
(action
=>"blob_plain", -replay
=>1)},
8567 git_print_page_nav
('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
8568 git_print_header_div
('commit', esc_html
($co{'title'}), $hash_base);
8570 git_print_page_nav
('',['commit','commitdiff','tree'], undef,undef,undef);
8571 print "<div class=\"title\">".esc_html
($hash)."</div>\n";
8573 git_print_page_path
($file_name, "blob", $hash_base);
8574 print "<div class=\"title_text\">\n" .
8575 "<table class=\"object_header\">\n";
8576 print "<tr><td>blob</td><td class=\"sha1\">$fullhash</td></tr>\n";
8579 print "<div class=\"page_body\">\n";
8580 if ($mimetype =~ m!^image/!) {
8581 print qq!<img
class="blob" type
="!.esc_attr($mimetype).qq!"!;
8583 print qq! alt
="!.esc_attr($file_name).qq!" title
="!.esc_attr($file_name).qq!"!;
8586 href(action=>"blob_plain
", hash=>$hash,
8587 hash_base=>$hash_base, file_name=>$file_name) .
8589 close $fd; # ignore likely EPIPE error from child
8592 while (my $line = to_utf8
(scalar <$fd>)) {
8595 $line = untabify
($line);
8596 printf qq!<div
class="pre"><a id
="l%i" href
="%s#l%i" class="linenr">%4i </a>%s</div
>\n!,
8597 $nr, esc_attr
(href
(-replay
=> 1)), $nr, $nr,
8598 $highlight_mode_active ? sanitize
($line) : esc_html
($line, -nbsp
=>1);
8601 or print "Reading blob failed.\n";
8608 if (!defined $hash_base) {
8609 $hash_base = "HEAD";
8611 if (!defined $hash) {
8612 if (defined $file_name) {
8613 $hash = git_get_hash_by_path
($hash_base, $file_name, "tree");
8618 die_error
(404, "No such tree") unless defined($hash);
8619 my $fullhash = git_get_full_hash
($project, "$hash^{tree}");
8620 die_error
(404, "No such tree") unless defined($fullhash);
8622 my $show_sizes = gitweb_check_feature
('show-sizes');
8623 my $have_blame = gitweb_check_feature
('blame');
8628 defined(my $fd = git_cmd_pipe
"ls-tree", '-z',
8629 ($show_sizes ?
'-l' : ()), @extra_options, $fullhash)
8630 or die_error
(500, "Open git-ls-tree failed");
8631 @entries = map { chomp; to_utf8
($_) } <$fd>;
8633 or die_error
(404, "Reading tree failed");
8638 if (defined $hash_base && (my %co = parse_commit
($hash_base))) {
8639 my $refs = git_get_references
();
8640 my $ref = format_ref_marker
($refs, $co{'id'});
8642 if (defined $file_name) {
8644 $cgi->a({-href
=> href
(action
=>"history", -replay
=>1)},
8646 $cgi->a({-href
=> href
(action
=>"tree",
8647 hash_base
=>"HEAD", file_name
=>$file_name)},
8650 my $snapshot_links = format_snapshot_links
($hash);
8651 if (defined $snapshot_links) {
8652 # FIXME: Should be available when we have no hash base as well.
8653 push @views_nav, $snapshot_links;
8655 git_print_page_nav
('tree','', $hash_base, undef, undef,
8656 join(' | ', @views_nav));
8657 git_print_header_div
('commit', esc_html
($co{'title'}), $hash_base, undef, $ref);
8659 git_print_page_nav
('tree',['commit','commitdiff'], undef,undef,$hash_base);
8661 print "<div class=\"title\">".esc_html
($hash)."</div>\n";
8663 if (defined $file_name) {
8664 $basedir = $file_name;
8665 if ($basedir ne '' && substr($basedir, -1) ne '/') {
8668 git_print_page_path
($file_name, 'tree', $hash_base);
8670 print "<div class=\"title_text\">\n" .
8671 "<table class=\"object_header\">\n";
8672 print "<tr><td>tree</td><td class=\"sha1\">$fullhash</td></tr>\n";
8675 print "<div class=\"page_body\">\n";
8676 print "<table class=\"tree\">\n";
8678 # '..' (top directory) link if possible
8679 if (defined $hash_base &&
8680 defined $file_name && $file_name =~ m![^/]+$!) {
8682 print "<tr class=\"dark\">\n";
8684 print "<tr class=\"light\">\n";
8688 my $up = $file_name;
8689 $up =~ s!/?[^/]+$!!;
8690 undef $up unless $up;
8691 # based on git_print_tree_entry
8692 print '<td class="mode">' . mode_str
('040000') . "</td>\n";
8693 print '<td class="size"> </td>'."\n" if $show_sizes;
8694 print '<td class="list">';
8695 print $cgi->a({-href
=> href
(action
=>"tree",
8696 hash_base
=>$hash_base,
8700 print "<td class=\"link\"></td>\n";
8704 foreach my $line (@entries) {
8705 my %t = parse_ls_tree_line
($line, -z
=> 1, -l
=> $show_sizes);
8708 print "<tr class=\"dark\">\n";
8710 print "<tr class=\"light\">\n";
8714 git_print_tree_entry
(\
%t, $basedir, $hash_base, $have_blame);
8718 print "</table>\n" .
8723 sub sanitize_for_filename
{
8727 $name =~ s/[^[:alnum:]_.-]//g;
8733 my ($project, $hash) = @_;
8735 # path/to/project.git -> project
8736 # path/to/project/.git -> project
8737 my $name = to_utf8
($project);
8738 $name =~ s
,([^/])/*\
.git
$,$1,;
8739 $name = sanitize_for_filename
(basename
($name));
8742 if ($hash =~ /^[0-9a-fA-F]+$/) {
8743 # shorten SHA-1 hash
8744 my $full_hash = git_get_full_hash
($project, $hash);
8745 if ($full_hash =~ /^$hash/ && length($hash) > 7) {
8746 $ver = git_get_short_hash
($project, $hash);
8748 } elsif ($hash =~ m!^refs/tags/(.*)$!) {
8749 # tags don't need shortened SHA-1 hash
8752 # branches and other need shortened SHA-1 hash
8753 my $strip_refs = join '|', map { quotemeta } get_branch_refs
();
8754 if ($hash =~ m!^refs/($strip_refs|remotes)/(.*)$!) {
8755 my $ref_dir = (defined $1) ?
$1 : '';
8758 $ref_dir = sanitize_for_filename
($ref_dir);
8759 # for refs neither in heads nor remotes we want to
8760 # add a ref dir to archive name
8761 if ($ref_dir ne '' and $ref_dir ne 'heads' and $ref_dir ne 'remotes') {
8762 $ver = $ref_dir . '-' . $ver;
8765 $ver .= '-' . git_get_short_hash
($project, $hash);
8767 # special case of sanitization for filename - we change
8768 # slashes to dots instead of dashes
8769 # in case of hierarchical branch names
8771 $ver =~ s/[^[:alnum:]_.-]//g;
8773 # name = project-version_string
8774 $name = "$name-$ver";
8776 return wantarray ?
($name, $name) : $name;
8779 sub exit_if_unmodified_since
{
8780 my ($latest_epoch) = @_;
8783 my $if_modified = $cgi->http('IF_MODIFIED_SINCE');
8784 if (defined $if_modified) {
8786 if (eval { require HTTP
::Date
; 1; }) {
8787 $since = HTTP
::Date
::str2time
($if_modified);
8788 } elsif (eval { require Time
::ParseDate
; 1; }) {
8789 $since = Time
::ParseDate
::parsedate
($if_modified, GMT
=> 1);
8791 if (defined $since && $latest_epoch <= $since) {
8792 my %latest_date = parse_date
($latest_epoch);
8794 -last_modified
=> $latest_date{'rfc2822'},
8795 -status
=> '304 Not Modified');
8802 my $format = $input_params{'snapshot_format'};
8803 if (!@snapshot_fmts) {
8804 die_error
(403, "Snapshots not allowed");
8806 # default to first supported snapshot format
8807 $format ||= $snapshot_fmts[0];
8808 if ($format !~ m/^[a-z0-9]+$/) {
8809 die_error
(400, "Invalid snapshot format parameter");
8810 } elsif (!exists($known_snapshot_formats{$format})) {
8811 die_error
(400, "Unknown snapshot format");
8812 } elsif ($known_snapshot_formats{$format}{'disabled'}) {
8813 die_error
(403, "Snapshot format not allowed");
8814 } elsif (!grep($_ eq $format, @snapshot_fmts)) {
8815 die_error
(403, "Unsupported snapshot format");
8818 my $type = git_get_type
("$hash^{}");
8820 die_error
(404, 'Object does not exist');
8821 } elsif ($type eq 'blob') {
8822 die_error
(400, 'Object is not a tree-ish');
8825 my ($name, $prefix) = snapshot_name
($project, $hash);
8826 my $filename = "$name$known_snapshot_formats{$format}{'suffix'}";
8828 my %co = parse_commit
($hash);
8829 exit_if_unmodified_since
($co{'committer_epoch'}) if %co;
8832 git_cmd
(), 'archive',
8833 "--format=$known_snapshot_formats{$format}{'format'}",
8834 "--prefix=$prefix/", $hash);
8835 if (exists $known_snapshot_formats{$format}{'compressor'}) {
8836 @cmd = ($posix_shell_bin, '-c', quote_command
(@cmd) .
8837 ' | ' . quote_command
(@
{$known_snapshot_formats{$format}{'compressor'}}));
8840 $filename =~ s/(["\\])/\\$1/g;
8843 %latest_date = parse_date
($co{'committer_epoch'}, $co{'committer_tz'});
8847 -type
=> $known_snapshot_formats{$format}{'type'},
8848 -content_disposition
=> 'inline; filename="' . $filename . '"',
8849 %co ?
(-last_modified
=> $latest_date{'rfc2822'}) : (),
8850 -status
=> '200 OK');
8852 defined(my $fd = cmd_pipe
@cmd)
8853 or die_error
(500, "Execute git-archive failed");
8855 binmode STDOUT
, ':raw';
8858 while (read($fd, $buf, 32768)) {
8861 binmode STDOUT
, ':utf8'; # as set at the beginning of gitweb.cgi
8866 sub git_log_generic
{
8867 my ($fmt_name, $body_subr, $base, $parent, $file_name, $file_hash) = @_;
8869 my $head = git_get_head_hash
($project);
8870 if (!defined $base) {
8873 if (!defined $page) {
8876 my $refs = git_get_references
();
8878 my $commit_hash = $base;
8879 if (defined $parent) {
8880 $commit_hash = "$parent..$base";
8883 parse_commits
($commit_hash, 101, (100 * $page),
8884 defined $file_name ?
($file_name, "--full-history") : ());
8887 if (!defined $file_hash && defined $file_name) {
8888 # some commits could have deleted file in question,
8889 # and not have it in tree, but one of them has to have it
8890 for (my $i = 0; $i < @commitlist; $i++) {
8891 $file_hash = git_get_hash_by_path
($commitlist[$i]{'id'}, $file_name);
8892 last if defined $file_hash;
8895 if (defined $file_hash) {
8896 $ftype = git_get_type
($file_hash);
8898 if (defined $file_name && !defined $ftype) {
8899 die_error
(500, "Unknown type of object");
8902 if (defined $file_name) {
8903 %co = parse_commit
($base)
8904 or die_error
(404, "Unknown commit object");
8908 my $paging_nav = format_log_nav
($fmt_name, $page, $#commitlist >= 100);
8910 if ($#commitlist >= 100) {
8912 $cgi->a({-href
=> href
(-replay
=>1, page
=>$page+1),
8913 -accesskey
=> "n", -title
=> "Alt-n"}, "next");
8915 my ($patch_max) = gitweb_get_feature
('patches');
8916 if ($patch_max && !defined $file_name) {
8917 if ($patch_max < 0 || @commitlist <= $patch_max) {
8918 $paging_nav .= " · " .
8919 $cgi->a({-href
=> href
(action
=>"patches", -replay
=>1)},
8925 local $action = 'log';
8928 git_print_page_nav
($fmt_name,'', $hash,$hash,$hash, $paging_nav);
8929 if (defined $file_name) {
8930 git_print_header_div
('commit', esc_html
($co{'title'}), $base);
8932 git_print_header_div
('summary', $project)
8934 git_print_page_path
($file_name, $ftype, $hash_base)
8935 if (defined $file_name);
8937 $body_subr->(\
@commitlist, 0, 99, $refs, $next_link,
8938 $file_name, $file_hash, $ftype);
8944 git_log_generic
('log', \
&git_log_body
,
8945 $hash, $hash_parent);
8949 $hash ||= $hash_base || "HEAD";
8950 my %co = parse_commit
($hash)
8951 or die_error
(404, "Unknown commit object");
8953 my $parent = $co{'parent'};
8954 my $parents = $co{'parents'}; # listref
8956 # we need to prepare $formats_nav before any parameter munging
8958 if (!defined $parent) {
8960 $formats_nav .= '(initial)';
8961 } elsif (@
$parents == 1) {
8962 # single parent commit
8965 $cgi->a({-href
=> href
(action
=>"commit",
8967 esc_html
(substr($parent, 0, 7))) .
8974 $cgi->a({-href
=> href
(action
=>"commit",
8976 esc_html
(substr($_, 0, 7)));
8980 if (gitweb_check_feature
('patches') && @
$parents <= 1) {
8981 $formats_nav .= " | " .
8982 $cgi->a({-href
=> href
(action
=>"patch", -replay
=>1)},
8986 if (!defined $parent) {
8990 defined(my $fd = git_cmd_pipe
"diff-tree", '-r', "--no-commit-id",
8992 (@
$parents <= 1 ?
$parent : '-c'),
8994 or die_error
(500, "Open git-diff-tree failed");
8995 @difftree = map { chomp; to_utf8
($_) } <$fd>;
8996 close $fd or die_error
(404, "Reading git-diff-tree failed");
8998 # non-textual hash id's can be cached
9000 if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
9003 my $refs = git_get_references
();
9004 my $ref = format_ref_marker
($refs, $co{'id'});
9006 git_header_html
(undef, $expires);
9007 git_print_page_nav
('commit', '',
9008 $hash, $co{'tree'}, $hash,
9011 if (defined $co{'parent'}) {
9012 git_print_header_div
('commitdiff', esc_html
($co{'title'}), $hash, undef, $ref);
9014 git_print_header_div
('tree', esc_html
($co{'title'}), $co{'tree'}, $hash, $ref);
9016 print "<div class=\"title_text\">\n" .
9017 "<table class=\"object_header\">\n";
9018 print "<tr><td>commit</td><td class=\"sha1\">$co{'id'}</td></tr>\n";
9019 git_print_authorship_rows
(\
%co);
9022 "<td class=\"sha1\">" .
9023 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$co{'tree'}, hash_base
=>$hash),
9024 class => "list"}, $co{'tree'}) .
9026 "<td class=\"link\">" .
9027 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$co{'tree'}, hash_base
=>$hash)},
9029 my $snapshot_links = format_snapshot_links
($hash);
9030 if (defined $snapshot_links) {
9031 print " | " . $snapshot_links;
9036 foreach my $par (@
$parents) {
9039 "<td class=\"sha1\">" .
9040 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$par),
9041 class => "list"}, $par) .
9043 "<td class=\"link\">" .
9044 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$par)}, "commit") .
9046 $cgi->a({-href
=> href
(action
=>"commitdiff", hash
=>$hash, hash_parent
=>$par)}, "diff") .
9053 print "<div class=\"page_body\">\n";
9054 git_print_log
($co{'comment'});
9057 git_difftree_body
(\
@difftree, $hash, @
$parents);
9063 # object is defined by:
9064 # - hash or hash_base alone
9065 # - hash_base and file_name
9068 # - hash or hash_base alone
9069 if ($hash || ($hash_base && !defined $file_name)) {
9070 my $object_id = $hash || $hash_base;
9072 defined(my $fd = git_cmd_pipe
'cat-file', '-t', $object_id)
9073 or die_error
(404, "Object does not exist");
9075 defined $type && chomp $type;
9077 or die_error
(404, "Object does not exist");
9079 # - hash_base and file_name
9080 } elsif ($hash_base && defined $file_name) {
9081 $file_name =~ s
,/+$,,;
9083 system(git_cmd
(), "cat-file", '-e', $hash_base) == 0
9084 or die_error
(404, "Base object does not exist");
9086 # here errors should not happen
9087 defined(my $fd = git_cmd_pipe
"ls-tree", $hash_base, "--", $file_name)
9088 or die_error
(500, "Open git-ls-tree failed");
9089 my $line = to_utf8
(scalar <$fd>);
9092 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'
9093 unless ($line && $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t/) {
9094 die_error
(404, "File or directory for given base does not exist");
9099 die_error
(400, "Not enough information to find object");
9102 print $cgi->redirect(-uri
=> href
(action
=>$type, -full
=>1,
9103 hash
=>$hash, hash_base
=>$hash_base,
9104 file_name
=>$file_name),
9105 -status
=> '302 Found');
9109 my $format = shift || 'html';
9110 my $diff_style = $input_params{'diff_style'} || 'inline';
9117 # preparing $fd and %diffinfo for git_patchset_body
9119 if (defined $hash_base && defined $hash_parent_base) {
9120 if (defined $file_name) {
9122 defined($fd = git_cmd_pipe
"diff-tree", '-r', @diff_opts,
9123 $hash_parent_base, $hash_base,
9124 "--", (defined $file_parent ?
$file_parent : ()), $file_name)
9125 or die_error
(500, "Open git-diff-tree failed");
9126 @difftree = map { chomp; to_utf8
($_) } <$fd>;
9128 or die_error
(404, "Reading git-diff-tree failed");
9130 or die_error
(404, "Blob diff not found");
9132 } elsif (defined $hash &&
9133 $hash =~ /[0-9a-fA-F]{40}/) {
9134 # try to find filename from $hash
9136 # read filtered raw output
9137 defined($fd = git_cmd_pipe
"diff-tree", '-r', @diff_opts,
9138 $hash_parent_base, $hash_base, "--")
9139 or die_error
(500, "Open git-diff-tree failed");
9141 # ':100644 100644 03b21826... 3b93d5e7... M ls-files.c'
9143 grep { /^:[0-7]{6} [0-7]{6} [0-9a-fA-F]{40} $hash/ }
9144 map { chomp; to_utf8
($_) } <$fd>;
9146 or die_error
(404, "Reading git-diff-tree failed");
9148 or die_error
(404, "Blob diff not found");
9151 die_error
(400, "Missing one of the blob diff parameters");
9154 if (@difftree > 1) {
9155 die_error
(400, "Ambiguous blob diff specification");
9158 %diffinfo = parse_difftree_raw_line
($difftree[0]);
9159 $file_parent ||= $diffinfo{'from_file'} || $file_name;
9160 $file_name ||= $diffinfo{'to_file'};
9162 $hash_parent ||= $diffinfo{'from_id'};
9163 $hash ||= $diffinfo{'to_id'};
9165 # non-textual hash id's can be cached
9166 if ($hash_base =~ m/^[0-9a-fA-F]{40}$/ &&
9167 $hash_parent_base =~ m/^[0-9a-fA-F]{40}$/) {
9172 defined($fd = git_cmd_pipe
"diff-tree", '-r', @diff_opts,
9173 '-p', ($format eq 'html' ?
"--full-index" : ()),
9174 $hash_parent_base, $hash_base,
9175 "--", (defined $file_parent ?
$file_parent : ()), $file_name)
9176 or die_error
(500, "Open git-diff-tree failed");
9179 # old/legacy style URI -- not generated anymore since 1.4.3.
9181 die_error
('404 Not Found', "Missing one of the blob diff parameters")
9185 if ($format eq 'html') {
9187 $cgi->a({-href
=> href
(action
=>"blobdiff_plain", -replay
=>1)},
9189 $formats_nav .= diff_style_nav
($diff_style);
9190 git_header_html
(undef, $expires);
9191 if (defined $hash_base && (my %co = parse_commit
($hash_base))) {
9192 git_print_page_nav
('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
9193 git_print_header_div
('commit', esc_html
($co{'title'}), $hash_base);
9195 print "<div class=\"page_nav\"><br/>$formats_nav<br/></div>\n";
9196 print "<div class=\"title\">".esc_html
("$hash vs $hash_parent")."</div>\n";
9198 if (defined $file_name) {
9199 git_print_page_path
($file_name, "blob", $hash_base);
9201 print "<div class=\"page_path\"></div>\n";
9204 } elsif ($format eq 'plain') {
9206 -type
=> 'text/plain',
9207 -charset
=> 'utf-8',
9208 -expires
=> $expires,
9209 -content_disposition
=> 'inline; filename="' . "$file_name" . '.patch"');
9211 print "X-Git-Url: " . $cgi->self_url() . "\n\n";
9214 die_error
(400, "Unknown blobdiff format");
9218 if ($format eq 'html') {
9219 print "<div class=\"page_body\">\n";
9221 git_patchset_body
($fd, $diff_style,
9222 [ \
%diffinfo ], $hash_base, $hash_parent_base);
9225 print "</div>\n"; # class="page_body"
9229 while (my $line = to_utf8
(scalar <$fd>)) {
9230 $line =~ s!a/($hash|$hash_parent)!'a/'.esc_path($diffinfo{'from_file'})!eg;
9231 $line =~ s!b/($hash|$hash_parent)!'b/'.esc_path($diffinfo{'to_file'})!eg;
9235 last if $line =~ m!^\+\+\+!;
9244 sub git_blobdiff_plain
{
9245 git_blobdiff
('plain');
9248 # assumes that it is added as later part of already existing navigation,
9249 # so it returns "| foo | bar" rather than just "foo | bar"
9250 sub diff_style_nav
{
9251 my ($diff_style, $is_combined) = @_;
9252 $diff_style ||= 'inline';
9254 return "" if ($is_combined);
9256 my @styles = (inline
=> 'inline', 'sidebyside' => 'side by side');
9257 my %styles = @styles;
9259 @styles[ map { $_ * 2 } 0..$#styles/2 ];
9264 $_ eq $diff_style ?
$styles{$_} :
9265 $cgi->a({-href
=> href
(-replay
=>1, diff_style
=> $_)}, $styles{$_})
9269 sub git_commitdiff
{
9271 my $format = $params{-format
} || 'html';
9272 my $diff_style = $input_params{'diff_style'} || 'inline';
9274 my ($patch_max) = gitweb_get_feature
('patches');
9275 if ($format eq 'patch') {
9276 die_error
(403, "Patch view not allowed") unless $patch_max;
9279 $hash ||= $hash_base || "HEAD";
9280 my %co = parse_commit
($hash)
9281 or die_error
(404, "Unknown commit object");
9283 # choose format for commitdiff for merge
9284 if (! defined $hash_parent && @
{$co{'parents'}} > 1) {
9285 $hash_parent = '--cc';
9287 # we need to prepare $formats_nav before almost any parameter munging
9289 if ($format eq 'html') {
9291 $cgi->a({-href
=> href
(action
=>"commitdiff_plain", -replay
=>1)},
9293 if ($patch_max && @
{$co{'parents'}} <= 1) {
9294 $formats_nav .= " | " .
9295 $cgi->a({-href
=> href
(action
=>"patch", -replay
=>1)},
9298 $formats_nav .= diff_style_nav
($diff_style, @
{$co{'parents'}} > 1);
9300 if (defined $hash_parent &&
9301 $hash_parent ne '-c' && $hash_parent ne '--cc') {
9302 # commitdiff with two commits given
9303 my $hash_parent_short = $hash_parent;
9304 if ($hash_parent =~ m/^[0-9a-fA-F]{40}$/) {
9305 $hash_parent_short = substr($hash_parent, 0, 7);
9309 for (my $i = 0; $i < @
{$co{'parents'}}; $i++) {
9310 if ($co{'parents'}[$i] eq $hash_parent) {
9311 $formats_nav .= ' parent ' . ($i+1);
9315 $formats_nav .= ': ' .
9316 $cgi->a({-href
=> href
(-replay
=>1,
9317 hash
=>$hash_parent, hash_base
=>undef)},
9318 esc_html
($hash_parent_short)) .
9320 } elsif (!$co{'parent'}) {
9322 $formats_nav .= ' (initial)';
9323 } elsif (scalar @
{$co{'parents'}} == 1) {
9324 # single parent commit
9327 $cgi->a({-href
=> href
(-replay
=>1,
9328 hash
=>$co{'parent'}, hash_base
=>undef)},
9329 esc_html
(substr($co{'parent'}, 0, 7))) .
9333 if ($hash_parent eq '--cc') {
9334 $formats_nav .= ' | ' .
9335 $cgi->a({-href
=> href
(-replay
=>1,
9336 hash
=>$hash, hash_parent
=>'-c')},
9338 } else { # $hash_parent eq '-c'
9339 $formats_nav .= ' | ' .
9340 $cgi->a({-href
=> href
(-replay
=>1,
9341 hash
=>$hash, hash_parent
=>'--cc')},
9347 $cgi->a({-href
=> href
(-replay
=>1,
9348 hash
=>$_, hash_base
=>undef)},
9349 esc_html
(substr($_, 0, 7)));
9350 } @
{$co{'parents'}} ) .
9355 my $hash_parent_param = $hash_parent;
9356 if (!defined $hash_parent_param) {
9357 # --cc for multiple parents, --root for parentless
9358 $hash_parent_param =
9359 @
{$co{'parents'}} > 1 ?
'--cc' : $co{'parent'} || '--root';
9365 if ($format eq 'html') {
9366 defined($fd = git_cmd_pipe
"diff-tree", '-r', @diff_opts,
9367 "--no-commit-id", "--patch-with-raw", "--full-index",
9368 $hash_parent_param, $hash, "--")
9369 or die_error
(500, "Open git-diff-tree failed");
9371 while (my $line = to_utf8
(scalar <$fd>)) {
9373 # empty line ends raw part of diff-tree output
9375 push @difftree, scalar parse_difftree_raw_line
($line);
9378 } elsif ($format eq 'plain') {
9379 defined($fd = git_cmd_pipe
"diff-tree", '-r', @diff_opts,
9380 '-p', $hash_parent_param, $hash, "--")
9381 or die_error
(500, "Open git-diff-tree failed");
9382 } elsif ($format eq 'patch') {
9383 # For commit ranges, we limit the output to the number of
9384 # patches specified in the 'patches' feature.
9385 # For single commits, we limit the output to a single patch,
9386 # diverging from the git-format-patch default.
9387 my @commit_spec = ();
9389 if ($patch_max > 0) {
9390 push @commit_spec, "-$patch_max";
9392 push @commit_spec, '-n', "$hash_parent..$hash";
9394 if ($params{-single
}) {
9395 push @commit_spec, '-1';
9397 if ($patch_max > 0) {
9398 push @commit_spec, "-$patch_max";
9400 push @commit_spec, "-n";
9402 push @commit_spec, '--root', $hash;
9404 defined($fd = git_cmd_pipe
"format-patch", @diff_opts,
9405 '--encoding=utf8', '--stdout', @commit_spec)
9406 or die_error
(500, "Open git-format-patch failed");
9408 die_error
(400, "Unknown commitdiff format");
9411 # non-textual hash id's can be cached
9413 if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
9417 # write commit message
9418 if ($format eq 'html') {
9419 my $refs = git_get_references
();
9420 my $ref = format_ref_marker
($refs, $co{'id'});
9422 git_header_html
(undef, $expires);
9423 git_print_page_nav
('commitdiff','', $hash,$co{'tree'},$hash, $formats_nav);
9424 git_print_header_div
('commit', esc_html
($co{'title'}), $hash, undef, $ref);
9425 print "<div class=\"title_text\">\n" .
9426 "<table class=\"object_header\">\n";
9427 git_print_authorship_rows
(\
%co);
9430 print "<div class=\"page_body\">\n";
9431 if (@
{$co{'comment'}} > 1) {
9432 print "<div class=\"log\">\n";
9433 git_print_log
($co{'comment'}, -final_empty_line
=> 1, -remove_title
=> 1);
9434 print "</div>\n"; # class="log"
9437 } elsif ($format eq 'plain') {
9438 my $refs = git_get_references
("tags");
9439 my $tagname = git_get_rev_name_tags
($hash);
9440 my $filename = basename
($project) . "-$hash.patch";
9443 -type
=> 'text/plain',
9444 -charset
=> 'utf-8',
9445 -expires
=> $expires,
9446 -content_disposition
=> 'inline; filename="' . "$filename" . '"');
9447 my %ad = parse_date
($co{'author_epoch'}, $co{'author_tz'});
9448 print "From: " . to_utf8
($co{'author'}) . "\n";
9449 print "Date: $ad{'rfc2822'} ($ad{'tz_local'})\n";
9450 print "Subject: " . to_utf8
($co{'title'}) . "\n";
9452 print "X-Git-Tag: $tagname\n" if $tagname;
9453 print "X-Git-Url: " . $cgi->self_url() . "\n\n";
9455 foreach my $line (@
{$co{'comment'}}) {
9456 print to_utf8
($line) . "\n";
9459 } elsif ($format eq 'patch') {
9460 my $filename = basename
($project) . "-$hash.patch";
9463 -type
=> 'text/plain',
9464 -charset
=> 'utf-8',
9465 -expires
=> $expires,
9466 -content_disposition
=> 'inline; filename="' . "$filename" . '"');
9470 if ($format eq 'html') {
9471 my $use_parents = !defined $hash_parent ||
9472 $hash_parent eq '-c' || $hash_parent eq '--cc';
9473 git_difftree_body
(\
@difftree, $hash,
9474 $use_parents ? @
{$co{'parents'}} : $hash_parent);
9477 git_patchset_body
($fd, $diff_style,
9479 $use_parents ? @
{$co{'parents'}} : $hash_parent);
9481 print "</div>\n"; # class="page_body"
9484 } elsif ($format eq 'plain') {
9489 or print "Reading git-diff-tree failed\n";
9490 } elsif ($format eq 'patch') {
9495 or print "Reading git-format-patch failed\n";
9499 sub git_commitdiff_plain
{
9500 git_commitdiff
(-format
=> 'plain');
9503 # format-patch-style patches
9505 git_commitdiff
(-format
=> 'patch', -single
=> 1);
9509 git_commitdiff
(-format
=> 'patch');
9513 git_log_generic
('history', \
&git_history_body
,
9514 $hash_base, $hash_parent_base,
9519 $searchtype ||= 'commit';
9521 # check if appropriate features are enabled
9522 gitweb_check_feature
('search')
9523 or die_error
(403, "Search is disabled");
9524 if ($searchtype eq 'pickaxe') {
9525 # pickaxe may take all resources of your box and run for several minutes
9526 # with every query - so decide by yourself how public you make this feature
9527 gitweb_check_feature
('pickaxe')
9528 or die_error
(403, "Pickaxe search is disabled");
9530 if ($searchtype eq 'grep') {
9531 # grep search might be potentially CPU-intensive, too
9532 gitweb_check_feature
('grep')
9533 or die_error
(403, "Grep search is disabled");
9535 if ($search_use_regexp) {
9536 # regular expression search can be disabled to avoid potentially
9537 # malicious regular expressions
9538 gitweb_check_feature
('regexp')
9539 or die_error
(403, "Regular expression search is disabled");
9542 if (!defined $searchtext) {
9543 die_error
(400, "Text field is empty");
9545 if (!defined $hash) {
9546 $hash = git_get_head_hash
($project);
9548 my %co = parse_commit
($hash);
9550 die_error
(404, "Unknown commit object");
9552 if (!defined $page) {
9556 if ($searchtype eq 'commit' ||
9557 $searchtype eq 'author' ||
9558 $searchtype eq 'committer') {
9559 git_search_message
(%co);
9560 } elsif ($searchtype eq 'pickaxe') {
9561 git_search_changes
(%co);
9562 } elsif ($searchtype eq 'grep') {
9563 git_search_files
(%co);
9565 die_error
(400, "Unknown search type");
9569 sub git_search_help
{
9571 git_print_page_nav
('','', $hash,$hash,$hash);
9573 <p><strong>Pattern</strong> is by default a normal string that is matched precisely (but without
9574 regard to case, except in the case of pickaxe). However, when you check the <em>re</em> checkbox,
9575 the pattern entered is recognized as the POSIX extended
9576 <a href="http://en.wikipedia.org/wiki/Regular_expression">regular expression</a> (also case
9579 <dt><b>commit</b></dt>
9580 <dd>The commit messages and authorship information will be scanned for the given pattern.</dd>
9582 my $have_grep = gitweb_check_feature
('grep');
9585 <dt><b>grep</b></dt>
9586 <dd>All files in the currently selected tree (HEAD unless you are explicitly browsing
9587 a different one) are searched for the given pattern. On large trees, this search can take
9588 a while and put some strain on the server, so please use it with some consideration. Note that
9589 due to git-grep peculiarity, currently if regexp mode is turned off, the matches are
9590 case-sensitive.</dd>
9594 <dt><b>author</b></dt>
9595 <dd>Name and e-mail of the change author and date of birth of the patch will be scanned for the given pattern.</dd>
9596 <dt><b>committer</b></dt>
9597 <dd>Name and e-mail of the committer and date of commit will be scanned for the given pattern.</dd>
9599 my $have_pickaxe = gitweb_check_feature
('pickaxe');
9600 if ($have_pickaxe) {
9602 <dt><b>pickaxe</b></dt>
9603 <dd>All commits that caused the string to appear or disappear from any file (changes that
9604 added, removed or "modified" the string) will be listed. This search can take a while and
9605 takes a lot of strain on the server, so please use it wisely. Note that since you may be
9606 interested even in changes just changing the case as well, this search is case sensitive.</dd>
9614 git_log_generic
('shortlog', \
&git_shortlog_body
,
9615 $hash, $hash_parent);
9618 ## ......................................................................
9619 ## feeds (RSS, Atom; OPML)
9622 my $format = shift || 'atom';
9623 my $have_blame = gitweb_check_feature
('blame');
9625 # Atom: http://www.atomenabled.org/developers/syndication/
9626 # RSS: http://www.notestips.com/80256B3A007F2692/1/NAMO5P9UPQ
9627 if ($format ne 'rss' && $format ne 'atom') {
9628 die_error
(400, "Unknown web feed format");
9631 # log/feed of current (HEAD) branch, log of given branch, history of file/directory
9632 my $head = $hash || 'HEAD';
9633 my @commitlist = parse_commits
($head, 150, 0, $file_name);
9637 my $content_type = "application/$format+xml";
9638 if (defined $cgi->http('HTTP_ACCEPT') &&
9639 $cgi->Accept('text/xml') > $cgi->Accept($content_type)) {
9640 # browser (feed reader) prefers text/xml
9641 $content_type = 'text/xml';
9643 if (defined($commitlist[0])) {
9644 %latest_commit = %{$commitlist[0]};
9645 my $latest_epoch = $latest_commit{'committer_epoch'};
9646 exit_if_unmodified_since
($latest_epoch);
9647 %latest_date = parse_date
($latest_epoch, $latest_commit{'committer_tz'});
9650 -type
=> $content_type,
9651 -charset
=> 'utf-8',
9652 %latest_date ?
(-last_modified
=> $latest_date{'rfc2822'}) : (),
9653 -status
=> '200 OK');
9655 # Optimization: skip generating the body if client asks only
9656 # for Last-Modified date.
9657 return if ($cgi->request_method() eq 'HEAD');
9660 my $title = "$site_name - $project/$action";
9661 my $feed_type = 'log';
9662 if (defined $hash) {
9663 $title .= " - '$hash'";
9664 $feed_type = 'branch log';
9665 if (defined $file_name) {
9666 $title .= " :: $file_name";
9667 $feed_type = 'history';
9669 } elsif (defined $file_name) {
9670 $title .= " - $file_name";
9671 $feed_type = 'history';
9673 $title .= " $feed_type";
9674 $title = esc_html
($title);
9675 my $descr = git_get_project_description
($project);
9676 if (defined $descr) {
9677 $descr = esc_html
($descr);
9679 $descr = "$project " .
9680 ($format eq 'rss' ?
'RSS' : 'Atom') .
9683 my $owner = git_get_project_owner
($project);
9684 $owner = esc_html
($owner);
9688 if (defined $file_name) {
9689 $alt_url = href
(-full
=>1, action
=>"history", hash
=>$hash, file_name
=>$file_name);
9690 } elsif (defined $hash) {
9691 $alt_url = href
(-full
=>1, action
=>"log", hash
=>$hash);
9693 $alt_url = href
(-full
=>1, action
=>"summary");
9695 print qq!<?xml version
="1.0" encoding
="utf-8"?
>\n!;
9696 if ($format eq 'rss') {
9698 <rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
9701 print "<title>$title</title>\n" .
9702 "<link>$alt_url</link>\n" .
9703 "<description>$descr</description>\n" .
9704 "<language>en</language>\n" .
9705 # project owner is responsible for 'editorial' content
9706 "<managingEditor>$owner</managingEditor>\n";
9707 if (defined $logo || defined $favicon) {
9708 # prefer the logo to the favicon, since RSS
9709 # doesn't allow both
9710 my $img = esc_url
($logo || $favicon);
9712 "<url>$img</url>\n" .
9713 "<title>$title</title>\n" .
9714 "<link>$alt_url</link>\n" .
9718 print "<pubDate>$latest_date{'rfc2822'}</pubDate>\n";
9719 print "<lastBuildDate>$latest_date{'rfc2822'}</lastBuildDate>\n";
9721 print "<generator>gitweb v.$version/$git_version</generator>\n";
9722 } elsif ($format eq 'atom') {
9724 <feed xmlns="http://www.w3.org/2005/Atom">
9726 print "<title>$title</title>\n" .
9727 "<subtitle>$descr</subtitle>\n" .
9728 '<link rel="alternate" type="text/html" href="' .
9729 $alt_url . '" />' . "\n" .
9730 '<link rel="self" type="' . $content_type . '" href="' .
9731 $cgi->self_url() . '" />' . "\n" .
9732 "<id>" . href
(-full
=>1) . "</id>\n" .
9733 # use project owner for feed author
9734 '<author><name>'. email_obfuscate
($owner) . '</name></author>\n';
9735 if (defined $favicon) {
9736 print "<icon>" . esc_url
($favicon) . "</icon>\n";
9738 if (defined $logo) {
9739 # not twice as wide as tall: 72 x 27 pixels
9740 print "<logo>" . esc_url
($logo) . "</logo>\n";
9742 if (! %latest_date) {
9743 # dummy date to keep the feed valid until commits trickle in:
9744 print "<updated>1970-01-01T00:00:00Z</updated>\n";
9746 print "<updated>$latest_date{'iso-8601'}</updated>\n";
9748 print "<generator version='$version/$git_version'>gitweb</generator>\n";
9752 for (my $i = 0; $i <= $#commitlist; $i++) {
9753 my %co = %{$commitlist[$i]};
9754 my $commit = $co{'id'};
9755 # we read 150, we always show 30 and the ones more recent than 48 hours
9756 if (($i >= 20) && ((time - $co{'author_epoch'}) > 48*60*60)) {
9759 my %cd = parse_date
($co{'author_epoch'}, $co{'author_tz'});
9761 # get list of changed files
9762 defined(my $fd = git_cmd_pipe
"diff-tree", '-r', @diff_opts,
9763 $co{'parent'} || "--root",
9764 $co{'id'}, "--", (defined $file_name ?
$file_name : ()))
9766 my @difftree = map { chomp; to_utf8
($_) } <$fd>;
9770 # print element (entry, item)
9771 my $co_url = href
(-full
=>1, action
=>"commitdiff", hash
=>$commit);
9772 if ($format eq 'rss') {
9774 "<title>" . esc_html
($co{'title'}) . "</title>\n" .
9775 "<author>" . esc_html
($co{'author'}) . "</author>\n" .
9776 "<pubDate>$cd{'rfc2822'}</pubDate>\n" .
9777 "<guid isPermaLink=\"true\">$co_url</guid>\n" .
9778 "<link>$co_url</link>\n" .
9779 "<description>" . esc_html
($co{'title'}) . "</description>\n" .
9780 "<content:encoded>" .
9782 } elsif ($format eq 'atom') {
9784 "<title type=\"html\">" . esc_html
($co{'title'}) . "</title>\n" .
9785 "<updated>$cd{'iso-8601'}</updated>\n" .
9787 " <name>" . esc_html
($co{'author_name'}) . "</name>\n";
9788 if ($co{'author_email'}) {
9789 print " <email>" . esc_html
($co{'author_email'}) . "</email>\n";
9791 print "</author>\n" .
9792 # use committer for contributor
9794 " <name>" . esc_html
($co{'committer_name'}) . "</name>\n";
9795 if ($co{'committer_email'}) {
9796 print " <email>" . esc_html
($co{'committer_email'}) . "</email>\n";
9798 print "</contributor>\n" .
9799 "<published>$cd{'iso-8601'}</published>\n" .
9800 "<link rel=\"alternate\" type=\"text/html\" href=\"$co_url\" />\n" .
9801 "<id>$co_url</id>\n" .
9802 "<content type=\"xhtml\" xml:base=\"" . esc_url
($my_url) . "\">\n" .
9803 "<div xmlns=\"http://www.w3.org/1999/xhtml\">\n";
9805 my $comment = $co{'comment'};
9807 foreach my $line (@
$comment) {
9808 $line = esc_html
($line);
9811 print "</pre><ul>\n";
9812 foreach my $difftree_line (@difftree) {
9813 my %difftree = parse_difftree_raw_line
($difftree_line);
9814 next if !$difftree{'from_id'};
9816 my $file = $difftree{'file'} || $difftree{'to_file'};
9820 $cgi->a({-href
=> href
(-full
=>1, action
=>"blobdiff",
9821 hash
=>$difftree{'to_id'}, hash_parent
=>$difftree{'from_id'},
9822 hash_base
=>$co{'id'}, hash_parent_base
=>$co{'parent'},
9823 file_name
=>$file, file_parent
=>$difftree{'from_file'}),
9824 -title
=> "diff"}, 'D');
9826 print $cgi->a({-href
=> href
(-full
=>1, action
=>"blame",
9827 file_name
=>$file, hash_base
=>$commit),
9828 -class => "blamelink",
9829 -title
=> "blame"}, 'B');
9831 # if this is not a feed of a file history
9832 if (!defined $file_name || $file_name ne $file) {
9833 print $cgi->a({-href
=> href
(-full
=>1, action
=>"history",
9834 file_name
=>$file, hash
=>$commit),
9835 -title
=> "history"}, 'H');
9837 $file = esc_path
($file);
9841 if ($format eq 'rss') {
9842 print "</ul>]]>\n" .
9843 "</content:encoded>\n" .
9845 } elsif ($format eq 'atom') {
9846 print "</ul>\n</div>\n" .
9853 if ($format eq 'rss') {
9854 print "</channel>\n</rss>\n";
9855 } elsif ($format eq 'atom') {
9869 my @list = git_get_projects_list
($project_filter, $strict_export);
9871 die_error
(404, "No projects found");
9875 -type
=> 'text/xml',
9876 -charset
=> 'utf-8',
9877 -content_disposition
=> 'inline; filename="opml.xml"');
9879 my $title = esc_html
($site_name);
9880 my $filter = " within subdirectory ";
9881 if (defined $project_filter) {
9882 $filter .= esc_html
($project_filter);
9887 <?xml version="1.0" encoding="utf-8"?>
9888 <opml version="1.0">
9890 <title>$title OPML Export$filter</title>
9893 <outline text="git RSS feeds">
9896 foreach my $pr (@list) {
9898 my $head = git_get_head_hash
($proj{'path'});
9899 if (!defined $head) {
9902 $git_dir = "$projectroot/$proj{'path'}";
9903 my %co = parse_commit
($head);
9908 my $path = esc_html
(chop_str
($proj{'path'}, 25, 5));
9909 my $rss = href
('project' => $proj{'path'}, 'action' => 'rss', -full
=> 1);
9910 my $html = href
('project' => $proj{'path'}, 'action' => 'summary', -full
=> 1);
9911 print "<outline type=\"rss\" text=\"$path\" title=\"$path\" xmlUrl=\"$rss\" htmlUrl=\"$html\"/>\n";