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);
43 our $my_url = $cgi->url();
44 our $my_uri = $cgi->url(-absolute
=> 1);
46 # Base URL for relative URLs in gitweb ($logo, $favicon, ...),
47 # needed and used only for URLs with nonempty PATH_INFO
48 # This must be an absolute URL (i.e. no scheme, host or port), NOT a full one
49 our $base_url = $my_uri || '/';
51 # When the script is used as DirectoryIndex, the URL does not contain the name
52 # of the script file itself, and $cgi->url() fails to strip PATH_INFO, so we
53 # have to do it ourselves. We make $path_info global because it's also used
56 # Another issue with the script being the DirectoryIndex is that the resulting
57 # $my_url data is not the full script URL: this is good, because we want
58 # generated links to keep implying the script name if it wasn't explicitly
59 # indicated in the URL we're handling, but it means that $my_url cannot be used
61 # Therefore, if we needed to strip PATH_INFO, then we know that we have
62 # to build the base URL ourselves:
63 our $path_info = decode_utf8
($ENV{"PATH_INFO"});
65 # $path_info has already been URL-decoded by the web server, but
66 # $my_url and $my_uri have not. URL-decode them so we can properly
68 $my_url = unescape
($my_url);
69 $my_uri = unescape
($my_uri);
70 if ($my_url =~ s
,\Q
$path_info\E
$,, &&
71 $my_uri =~ s
,\Q
$path_info\E
$,, &&
72 defined $ENV{'SCRIPT_NAME'}) {
73 $base_url = $ENV{'SCRIPT_NAME'} || '/';
77 # target of the home link on top of all pages
78 our $home_link = $my_uri || "/";
81 # core git executable to use
82 # this can just be "git" if your webserver has a sensible PATH
83 our $GIT = "++GIT_BINDIR++/git";
85 # absolute fs-path which will be prepended to the project path
86 #our $projectroot = "/pub/scm";
87 our $projectroot = "++GITWEB_PROJECTROOT++";
89 # fs traversing limit for getting project list
90 # the number is relative to the projectroot
91 our $project_maxdepth = "++GITWEB_PROJECT_MAXDEPTH++";
93 # string of the home link on top of all pages
94 our $home_link_str = "++GITWEB_HOME_LINK_STR++";
96 # extra breadcrumbs preceding the home link
97 our @extra_breadcrumbs = ();
99 # name of your site or organization to appear in page titles
100 # replace this with something more descriptive for clearer bookmarks
101 our $site_name = "++GITWEB_SITENAME++"
102 || ($ENV{'SERVER_NAME'} || "Untitled") . " Git";
104 # html snippet to include in the <head> section of each page
105 our $site_html_head_string = "++GITWEB_SITE_HTML_HEAD_STRING++";
106 # filename of html text to include at top of each page
107 our $site_header = "++GITWEB_SITE_HEADER++";
108 # html text to include at home page
109 our $home_text = "++GITWEB_HOMETEXT++";
110 # filename of html text to include at bottom of each page
111 our $site_footer = "++GITWEB_SITE_FOOTER++";
114 our @stylesheets = ("++GITWEB_CSS++");
115 # URI of a single stylesheet, which can be overridden in GITWEB_CONFIG.
116 our $stylesheet = undef;
117 # URI of GIT logo (72x27 size)
118 our $logo = "++GITWEB_LOGO++";
119 # URI of GIT favicon, assumed to be image/png type
120 our $favicon = "++GITWEB_FAVICON++";
121 # URI of gitweb.js (JavaScript code for gitweb)
122 our $javascript = "++GITWEB_JS++";
124 # URI and label (title) of GIT logo link
125 #our $logo_url = "http://www.kernel.org/pub/software/scm/git/docs/";
126 #our $logo_label = "git documentation";
127 our $logo_url = "http://git-scm.com/";
128 our $logo_label = "git homepage";
130 # source of projects list
131 our $projects_list = "++GITWEB_LIST++";
133 # the width (in characters) of the projects list "Description" column
134 our $projects_list_description_width = 25;
136 # group projects by category on the projects list
137 # (enabled if this variable evaluates to true)
138 our $projects_list_group_categories = 0;
140 # default category if none specified
141 # (leave the empty string for no category)
142 our $project_list_default_category = "";
144 # default order of projects list
145 # valid values are none, project, descr, owner, and age
146 our $default_projects_order = "project";
148 # default order of refs list
149 # valid values are age and name
150 our $default_refs_order = "age";
152 # show repository only if this file exists
153 # (only effective if this variable evaluates to true)
154 our $export_ok = "++GITWEB_EXPORT_OK++";
156 # don't generate age column on the projects list page
157 our $omit_age_column = 0;
159 # use contents of this file (in iso, iso-strict or raw format) as
160 # the last activity data if it exists and is a valid date
161 our $lastactivity_file = undef;
163 # don't generate information about owners of repositories
166 # owner link hook given owner name (full and NOT obfuscated)
167 # should return full URL-escaped link to attach to owner, for example:
168 # sub { return "/showowner.cgi?owner=".CGI::Util::escape($_[0]); }
169 our $owner_link_hook = undef;
171 # show repository only if this subroutine returns true
172 # when given the path to the project, for example:
173 # sub { return -e "$_[0]/git-daemon-export-ok"; }
174 our $export_auth_hook = undef;
176 # only allow viewing of repositories also shown on the overview page
177 our $strict_export = "++GITWEB_STRICT_EXPORT++";
179 # base URL for bundle info link shown on summary page, but only if
180 # this config item is defined AND a 'bundles' subdirectory exists
181 # in the project's repository.
182 # i.e. full URL is "git_base_bundles_url/$project/bundles"
183 our $git_base_bundles_url = undef;
187 ## Any of the urls in @git_base_url_list, @git_base_mirror_urls or
188 ## @git_base_push_urls may be an array ref instead of a scalar in which
189 ## case ${}[0] is the url and ${}[1] is an html fragment "hint" to display
190 ## right after the URL.
192 # list of git base URLs used for URL to where fetch project from,
193 # i.e. full URL is "$git_base_url/$project"
194 our @git_base_url_list = grep { $_ ne '' } ("++GITWEB_BASE_URL++");
196 ## For push projects (a .nofetch file exists OR gitweb.showpush is true)
197 ## @git_base_url_list entries are shown as "URL" and @git_base_push_urls
198 ## are shown as "push URL" and @git_base_mirror_urls are ignored.
199 ## For non-push projects, @git_base_url_list and @git_base_mirror_urls are shown
200 ## as "URL" and @git_base_push_urls are ignored.
202 # URLs shown for mirrors but not for push projects in addition to base_url_list,
203 # extended by the project name (i.e. full URL is "$git_mirror_url/$project")
204 our @git_base_mirror_urls = ();
206 # URLs designated for pushing new changes, extended by the
207 # project name (i.e. "$git_base_push_url[0]/$project")
208 our @git_base_push_urls = ();
210 # https hint html inserted right after any https push URL (undef for none)
211 # ignored if the url already has its own hint
212 # this is supported for backwards compatibility but is now deprecated in favor
213 # of using an array ref in the @git_base_push_urls list instead
214 our $https_hint_html = undef;
216 # default blob_plain mimetype and default charset for text/plain blob
217 our $default_blob_plain_mimetype = 'application/octet-stream';
218 our $default_text_plain_charset = undef;
220 # file to use for guessing MIME types before trying /etc/mime.types
221 # (relative to the current git repository)
222 our $mimetypes_file = undef;
224 # assume this charset if line contains non-UTF-8 characters;
225 # it should be valid encoding (see Encoding::Supported(3pm) for list),
226 # for which encoding all byte sequences are valid, for example
227 # 'iso-8859-1' aka 'latin1' (it is decoded without checking, so it
228 # could be even 'utf-8' for the old behavior)
229 our $fallback_encoding = 'latin1';
231 # rename detection options for git-diff and git-diff-tree
232 # - default is '-M', with the cost proportional to
233 # (number of removed files) * (number of new files).
234 # - more costly is '-C' (which implies '-M'), with the cost proportional to
235 # (number of changed files + number of removed files) * (number of new files)
236 # - even more costly is '-C', '--find-copies-harder' with cost
237 # (number of files in the original tree) * (number of new files)
238 # - one might want to include '-B' option, e.g. '-B', '-M'
239 our @diff_opts = ('-M'); # taken from git_commit
241 # cache directory (relative to $GIT_DIR) for project-specific html page caches.
242 # the directory must exist and be writable by the process running gitweb.
243 # additionally some actions must be selected for caching in %html_cache_actions
244 # - default is 'htmlcache'
245 our $html_cache_dir = 'htmlcache';
247 # which actions to cache in $html_cache_dir
248 # if $html_cache_dir exists (relative to $GIT_DIR) and is writable by the
249 # process running gitweb, then any actions selected here will have their output
250 # cached and the cache file will be returned instead of regenerating the page
251 # if it exists. For this to be useful, an external process must create the
252 # 'changed' file (if it does not already exist) in the $html_cache_dir whenever
253 # the project information has been changed. Alternatively it may create a
254 # "$action.changed" file (if it does not exist) instead to limit the changes
255 # to just "$action" instead of any action. If 'changed' or "$action.changed"
256 # exist, then the cached version will never be used for "$action" and a new
257 # cache page will be regenerated (and the "changed" files removed as appropriate).
259 # Additionally if $projlist_cache_lifetime is > 0 AND the forks feature has been
260 # enabled ('$feature{'forks'}{'default'} = [1];') then additionally an external
261 # process must create the 'forkchange' file or update its timestamp if it already
262 # exists whenever a fork is added to or removed from the project (as well as
263 # create the 'changed' or "$action.changed" file). Otherwise the "forks"
264 # section on the summary page may remain out-of-date indefinately.
267 # currently only caching of the summary page is supported
268 # - to enable caching of the summary page use:
269 # $html_cache_actions{'summary'} = 1;
270 our %html_cache_actions = ();
272 # utility to automatically produce a default README.html if README.html is
273 # enabled and it does not exist or is 0 bytes in length. If this is set to an
274 # executable utility that takes an absolute path to a .git directory as its
275 # first argument and outputs an HTML fragment to use for README.html, then
276 # it will be called when README.html is enabled but empty or missing.
277 our $git_automatic_readme_html = undef;
279 # Disables features that would allow repository owners to inject script into
281 our $prevent_xss = 0;
283 # Path to a POSIX shell. Needed to run $highlight_bin and a snapshot compressor.
284 # Only used when highlight is enabled or snapshots with compressors are enabled.
285 our $posix_shell_bin = "++POSIX_SHELL_BIN++";
287 # Path to the highlight executable to use (must be the one from
288 # http://www.andre-simon.de due to assumptions about parameters and output).
289 # Useful if highlight is not installed on your webserver's PATH.
290 # [Default: highlight]
291 our $highlight_bin = "++HIGHLIGHT_BIN++";
293 # Whether to include project list on the gitweb front page; 0 means yes,
294 # 1 means no list but show tag cloud if enabled (all projects still need
295 # to be scanned, unless the info is cached), 2 means no list and no tag cloud
297 our $frontpage_no_project_list = 0;
299 # projects list cache for busy sites with many projects;
300 # if you set this to non-zero, it will be used as the cached
301 # index lifetime in minutes
303 # the cached list version is stored in $cache_dir/$cache_name and can
304 # be tweaked by other scripts running with the same uid as gitweb -
305 # use this ONLY at secure installations; only single gitweb project
306 # root per system is supported, unless you tweak configuration!
307 our $projlist_cache_lifetime = 0; # in minutes
308 # FHS compliant $cache_dir would be "/var/cache/gitweb"
310 (defined $ENV{'TMPDIR'} ?
$ENV{'TMPDIR'} : '/tmp').'/gitweb';
311 our $projlist_cache_name = 'gitweb.index.cache';
312 our $cache_grpshared = 0;
314 # information about snapshot formats that gitweb is capable of serving
315 our %known_snapshot_formats = (
317 # 'display' => display name,
318 # 'type' => mime type,
319 # 'suffix' => filename suffix,
320 # 'format' => --format for git-archive,
321 # 'compressor' => [compressor command and arguments]
322 # (array reference, optional)
323 # 'disabled' => boolean (optional)}
326 'display' => 'tar.gz',
327 'type' => 'application/x-gzip',
328 'suffix' => '.tar.gz',
330 'compressor' => ['gzip', '-n']},
333 'display' => 'tar.bz2',
334 'type' => 'application/x-bzip2',
335 'suffix' => '.tar.bz2',
337 'compressor' => ['bzip2']},
340 'display' => 'tar.xz',
341 'type' => 'application/x-xz',
342 'suffix' => '.tar.xz',
344 'compressor' => ['xz'],
349 'type' => 'application/x-zip',
354 # Aliases so we understand old gitweb.snapshot values in repository
356 our %known_snapshot_format_aliases = (
361 # backward compatibility: legacy gitweb config support
362 'x-gzip' => undef, 'gz' => undef,
363 'x-bzip2' => undef, 'bz2' => undef,
364 'x-zip' => undef, '' => undef,
367 # Pixel sizes for icons and avatars. If the default font sizes or lineheights
368 # are changed, it may be appropriate to change these values too via
375 # Used to set the maximum load that we will still respond to gitweb queries.
376 # If server load exceed this value then return "503 server busy" error.
377 # If gitweb cannot determined server load, it is taken to be 0.
378 # Leave it undefined (or set to 'undef') to turn off load checking.
381 # configuration for 'highlight' (http://www.andre-simon.de/)
383 our %highlight_basename = (
386 'SConstruct' => 'py', # SCons equivalent of Makefile
387 'Makefile' => 'make',
388 'makefile' => 'make',
389 'GNUmakefile' => 'make',
390 'BSDmakefile' => 'make',
392 # match by shebang regex
393 our %highlight_shebang = (
394 # Each entry has a key which is the syntax to use and
395 # a value which is either a qr regex or an array of qr regexs to match
396 # against the first 128 (less if the blob is shorter) BYTES of the blob.
397 # We match /usr/bin/env items separately to require "/usr/bin/env" and
398 # allow a limited subset of NAME=value items to appear.
399 'awk' => [ qr
,^#!\s*/(?:\w+/)*(?:[gnm]?awk)(?:\s|$),mo,
400 qr
,^#!\s*/usr/bin/env\s+(?:\w+=\w*\s+)*(?:[gnm]?awk)(?:\s|$),mo ],
401 'make' => [ qr
,^#!\s*/(?:\w+/)*(?:g?make)(?:\s|$),mo,
402 qr
,^#!\s*/usr/bin/env\s+(?:\w+=\w*\s+)*(?:g?make)(?:\s|$),mo ],
403 'php' => [ qr
,^#!\s*/(?:\w+/)*(?:php)(?:\s|$),mo,
404 qr
,^#!\s*/usr/bin/env\s+(?:\w+=\w*\s+)*(?:php)(?:\s|$),mo ],
405 'pl' => [ qr
,^#!\s*/(?:\w+/)*(?:perl)(?:\s|$),mo,
406 qr
,^#!\s*/usr/bin/env\s+(?:\w+=\w*\s+)*(?:perl)(?:\s|$),mo ],
407 'py' => [ qr
,^#!\s*/(?:\w+/)*(?:python)(?:\s|$),mo,
408 qr
,^#!\s*/usr/bin/env\s+(?:\w+=\w*\s+)*(?:python)(?:\s|$),mo ],
409 'sh' => [ qr
,^#!\s*/(?:\w+/)*(?:[bd]ash|t?csh|[akz]?sh)(?:\s|$),mo,
410 qr
,^#!\s*/usr/bin/env\s+(?:\w+=\w*\s+)*(?:[bd]ash|t?csh|[akz]?sh)(?:\s|$),mo ],
411 'rb' => [ qr
,^#!\s*/(?:\w+/)*(?:ruby)(?:\s|$),mo,
412 qr
,^#!\s*/usr/bin/env\s+(?:\w+=\w*\s+)*(?:ruby)(?:\s|$),mo ],
415 our %highlight_ext = (
416 # main extensions, defining name of syntax;
417 # see files in /usr/share/highlight/langDefs/ directory
418 (map { $_ => $_ } qw(
419 4gl a4c abnf abp ada agda ahk ampl amtrix applescript arc
420 arm as asm asp aspect ats au3 avenue awk bat bb bbcode bib
421 bms bnf boo c cb cfc chl clipper clojure clp cob cs css d
422 diff dot dylan e ebnf erl euphoria exp f90 flx for frink fs
423 go haskell hcl html httpd hx icl icn idl idlang ili
424 inc_luatex ini inp io iss j java js jsp lbn ldif lgt lhs
425 lisp lotos ls lsl lua ly make mel mercury mib miranda ml mo
426 mod2 mod3 mpl ms mssql n nas nbc nice nrx nsi nut nxc oberon
427 objc octave oorexx os oz pas php pike pl pl1 pov pro
428 progress ps ps1 psl pure py pyx q qmake qu r rb rebol rexx
429 rnc s sas sc scala scilab sh sma smalltalk sml sno spec spn
430 sql sybase tcl tcsh tex ttcn3 vala vb verilog vhd xml xpp y
432 # alternate extensions, see /etc/highlight/filetypes.conf
433 (map { $_ => '4gl' } qw(informix)),
434 (map { $_ => 'a4c' } qw(ascend)),
435 (map { $_ => 'abp' } qw(abp4)),
436 (map { $_ => 'ada' } qw(a adb ads gnad)),
437 (map { $_ => 'ahk' } qw(autohotkey)),
438 (map { $_ => 'ampl' } qw(dat run)),
439 (map { $_ => 'amtrix' } qw(hnd s4 s4h s4t t4)),
440 (map { $_ => 'as' } qw(actionscript)),
441 (map { $_ => 'asm' } qw(29k 68s 68x a51 assembler x68 x86)),
442 (map { $_ => 'asp' } qw(asa)),
443 (map { $_ => 'aspect' } qw(was wud)),
444 (map { $_ => 'ats' } qw(dats)),
445 (map { $_ => 'au3' } qw(autoit)),
446 (map { $_ => 'bat' } qw(cmd)),
447 (map { $_ => 'bb' } qw(blitzbasic)),
448 (map { $_ => 'bib' } qw(bibtex)),
449 (map { $_ => 'c' } qw(c++ cc cpp cu cxx h hh hpp hxx)),
450 (map { $_ => 'cb' } qw(clearbasic)),
451 (map { $_ => 'cfc' } qw(cfm coldfusion)),
452 (map { $_ => 'chl' } qw(chill)),
453 (map { $_ => 'cob' } qw(cbl cobol)),
454 (map { $_ => 'cs' } qw(csharp)),
455 (map { $_ => 'diff' } qw(patch)),
456 (map { $_ => 'dot' } qw(graphviz)),
457 (map { $_ => 'e' } qw(eiffel se)),
458 (map { $_ => 'erl' } qw(erlang hrl)),
459 (map { $_ => 'euphoria' } qw(eu ew ex exu exw wxu)),
460 (map { $_ => 'exp' } qw(express)),
461 (map { $_ => 'f90' } qw(f95)),
462 (map { $_ => 'flx' } qw(felix)),
463 (map { $_ => 'for' } qw(f f77 ftn)),
464 (map { $_ => 'fs' } qw(fsharp fsx)),
465 (map { $_ => 'haskell' } qw(hs)),
466 (map { $_ => 'html' } qw(htm xhtml)),
467 (map { $_ => 'hx' } qw(haxe)),
468 (map { $_ => 'icl' } qw(clean)),
469 (map { $_ => 'icn' } qw(icon)),
470 (map { $_ => 'ili' } qw(interlis)),
471 (map { $_ => 'inp' } qw(fame)),
472 (map { $_ => 'iss' } qw(innosetup)),
473 (map { $_ => 'j' } qw(jasmin)),
474 (map { $_ => 'java' } qw(groovy grv)),
475 (map { $_ => 'lbn' } qw(luban)),
476 (map { $_ => 'lgt' } qw(logtalk)),
477 (map { $_ => 'lisp' } qw(cl clisp el lsp sbcl scom)),
478 (map { $_ => 'ls' } qw(lotus)),
479 (map { $_ => 'lsl' } qw(lindenscript)),
480 (map { $_ => 'ly' } qw(lilypond)),
481 (map { $_ => 'make' } qw(mak mk kmk)),
482 (map { $_ => 'mel' } qw(maya)),
483 (map { $_ => 'mib' } qw(smi snmp)),
484 (map { $_ => 'ml' } qw(mli ocaml)),
485 (map { $_ => 'mo' } qw(modelica)),
486 (map { $_ => 'mod2' } qw(def mod)),
487 (map { $_ => 'mod3' } qw(i3 m3)),
488 (map { $_ => 'mpl' } qw(maple)),
489 (map { $_ => 'n' } qw(nemerle)),
490 (map { $_ => 'nas' } qw(nasal)),
491 (map { $_ => 'nrx' } qw(netrexx)),
492 (map { $_ => 'nsi' } qw(nsis)),
493 (map { $_ => 'nut' } qw(squirrel)),
494 (map { $_ => 'oberon' } qw(ooc)),
495 (map { $_ => 'objc' } qw(M m mm)),
496 (map { $_ => 'php' } qw(php3 php4 php5 php6)),
497 (map { $_ => 'pike' } qw(pmod)),
498 (map { $_ => 'pl' } qw(perl plex plx pm)),
499 (map { $_ => 'pl1' } qw(bdy ff fp fpp rpp sf sp spb spe spp sps wf wp wpb wpp wps)),
500 (map { $_ => 'progress' } qw(i p w)),
501 (map { $_ => 'py' } qw(python)),
502 (map { $_ => 'pyx' } qw(pyrex)),
503 (map { $_ => 'rb' } qw(pp rjs ruby)),
504 (map { $_ => 'rexx' } qw(rex rx the)),
505 (map { $_ => 'sc' } qw(paradox)),
506 (map { $_ => 'scilab' } qw(sce sci)),
507 (map { $_ => 'sh' } qw(bash ebuild eclass ksh zsh)),
508 (map { $_ => 'sma' } qw(small)),
509 (map { $_ => 'smalltalk' } qw(gst sq st)),
510 (map { $_ => 'sno' } qw(snobal)),
511 (map { $_ => 'sybase' } qw(sp)),
512 (map { $_ => 'tcl' } qw(itcl wish)),
513 (map { $_ => 'tex' } qw(cls sty)),
514 (map { $_ => 'vb' } qw(bas basic bi vbs)),
515 (map { $_ => 'verilog' } qw(v)),
516 (map { $_ => 'xml' } qw(dtd ecf ent hdr hub jnlp nrm plist resx sgm sgml svg tld vxml wml xsd xsl)),
517 (map { $_ => 'y' } qw(bison)),
520 # You define site-wide feature defaults here; override them with
521 # $GITWEB_CONFIG as necessary.
524 # 'sub' => feature-sub (subroutine),
525 # 'override' => allow-override (boolean),
526 # 'default' => [ default options...] (array reference)}
528 # if feature is overridable (it means that allow-override has true value),
529 # then feature-sub will be called with default options as parameters;
530 # return value of feature-sub indicates if to enable specified feature
532 # if there is no 'sub' key (no feature-sub), then feature cannot be
535 # use gitweb_get_feature(<feature>) to retrieve the <feature> value
536 # (an array) or gitweb_check_feature(<feature>) to check if <feature>
539 # Enable the 'blame' blob view, showing the last commit that modified
540 # each line in the file. This can be very CPU-intensive.
542 # To enable system wide have in $GITWEB_CONFIG
543 # $feature{'blame'}{'default'} = [1];
544 # To have project specific config enable override in $GITWEB_CONFIG
545 # $feature{'blame'}{'override'} = 1;
546 # and in project config gitweb.blame = 0|1;
548 'sub' => sub { feature_bool
('blame', @_) },
552 # Enable the 'incremental blame' blob view, which uses javascript to
553 # incrementally show the revisions of lines as they are discovered
554 # in the history. It is better for large histories, files and slow
555 # servers, but requires javascript in the client and can slow down the
556 # browser on large files.
558 # To enable system wide have in $GITWEB_CONFIG
559 # $feature{'blame_incremental'}{'default'} = [1];
560 # To have project specific config enable override in $GITWEB_CONFIG
561 # $feature{'blame_incremental'}{'override'} = 1;
562 # and in project config gitweb.blame_incremental = 0|1;
563 'blame_incremental' => {
564 'sub' => sub { feature_bool
('blame_incremental', @_) },
568 # Enable the 'snapshot' link, providing a compressed archive of any
569 # tree. This can potentially generate high traffic if you have large
572 # Value is a list of formats defined in %known_snapshot_formats that
574 # To disable system wide have in $GITWEB_CONFIG
575 # $feature{'snapshot'}{'default'} = [];
576 # To have project specific config enable override in $GITWEB_CONFIG
577 # $feature{'snapshot'}{'override'} = 1;
578 # and in project config, a comma-separated list of formats or "none"
579 # to disable. Example: gitweb.snapshot = tbz2,zip;
581 'sub' => \
&feature_snapshot
,
583 'default' => ['tgz']},
585 # Enable text search, which will list the commits which match author,
586 # committer or commit text to a given string. Enabled by default.
587 # Project specific override is not supported.
589 # Note that this controls all search features, which means that if
590 # it is disabled, then 'grep' and 'pickaxe' search would also be
596 # Enable regular expression search. Enabled by default.
597 # Note that you need to have 'search' feature enabled too.
599 # Note that this affects all git search features, which means that if
600 # it is disabled, none of the git search options will allow a regular
601 # expression (the "RE" checkbox) to be used. However, the project
602 # list search is unaffected by this setting (it uses Perl to do the
603 # matching not Git) and will always allow a regular expression to
604 # be used (by checking the box) regardless of this setting.
606 'sub' => sub { feature_bool
('regexp', @_) },
610 # Enable grep search, which will list the files in currently selected
611 # tree containing the given string. Enabled by default. This can be
612 # potentially CPU-intensive, of course.
613 # Note that you need to have 'search' feature enabled too.
615 # To enable system wide have in $GITWEB_CONFIG
616 # $feature{'grep'}{'default'} = [1];
617 # To have project specific config enable override in $GITWEB_CONFIG
618 # $feature{'grep'}{'override'} = 1;
619 # and in project config gitweb.grep = 0|1;
621 'sub' => sub { feature_bool
('grep', @_) },
625 # Enable the pickaxe search, which will list the commits that modified
626 # a given string in a file. This can be practical and quite faster
627 # alternative to 'blame', but still potentially CPU-intensive.
628 # Note that you need to have 'search' feature enabled too.
630 # To enable system wide have in $GITWEB_CONFIG
631 # $feature{'pickaxe'}{'default'} = [1];
632 # To have project specific config enable override in $GITWEB_CONFIG
633 # $feature{'pickaxe'}{'override'} = 1;
634 # and in project config gitweb.pickaxe = 0|1;
636 'sub' => sub { feature_bool
('pickaxe', @_) },
640 # Enable showing size of blobs in a 'tree' view, in a separate
641 # column, similar to what 'ls -l' does. This cost a bit of IO.
643 # To disable system wide have in $GITWEB_CONFIG
644 # $feature{'show-sizes'}{'default'} = [0];
645 # To have project specific config enable override in $GITWEB_CONFIG
646 # $feature{'show-sizes'}{'override'} = 1;
647 # and in project config gitweb.showsizes = 0|1;
649 'sub' => sub { feature_bool
('showsizes', @_) },
653 # Make gitweb use an alternative format of the URLs which can be
654 # more readable and natural-looking: project name is embedded
655 # directly in the path and the query string contains other
656 # auxiliary information. All gitweb installations recognize
657 # URL in either format; this configures in which formats gitweb
660 # To enable system wide have in $GITWEB_CONFIG
661 # $feature{'pathinfo'}{'default'} = [1];
662 # Project specific override is not supported.
664 # Note that you will need to change the default location of CSS,
665 # favicon, logo and possibly other files to an absolute URL. Also,
666 # if gitweb.cgi serves as your indexfile, you will need to force
667 # $my_uri to contain the script name in your $GITWEB_CONFIG (and you
668 # will also likely want to set $home_link if you're setting $my_uri).
673 # Make gitweb consider projects in project root subdirectories
674 # to be forks of existing projects. Given project $projname.git,
675 # projects matching $projname/*.git will not be shown in the main
676 # projects list, instead a '+' mark will be added to $projname
677 # there and a 'forks' view will be enabled for the project, listing
678 # all the forks. If project list is taken from a file, forks have
679 # to be listed after the main project.
681 # To enable system wide have in $GITWEB_CONFIG
682 # $feature{'forks'}{'default'} = [1];
683 # Project specific override is not supported.
688 # Insert custom links to the action bar of all project pages.
689 # This enables you mainly to link to third-party scripts integrating
690 # into gitweb; e.g. git-browser for graphical history representation
691 # or custom web-based repository administration interface.
693 # The 'default' value consists of a list of triplets in the form
694 # (label, link, position) where position is the label after which
695 # to insert the link and link is a format string where %n expands
696 # to the project name, %f to the project path within the filesystem,
697 # %h to the current hash (h gitweb parameter) and %b to the current
698 # hash base (hb gitweb parameter); %% expands to %. %e expands to the
699 # project name where all '+' characters have been replaced with '%2B'.
701 # To enable system wide have in $GITWEB_CONFIG e.g.
702 # $feature{'actions'}{'default'} = [('graphiclog',
703 # '/git-browser/by-commit.html?r=%n', 'summary')];
704 # Project specific override is not supported.
709 # Allow gitweb scan project content tags of project repository,
710 # and display the popular Web 2.0-ish "tag cloud" near the projects
711 # list. Note that this is something COMPLETELY different from the
714 # gitweb by itself can show existing tags, but it does not handle
715 # tagging itself; you need to do it externally, outside gitweb.
716 # The format is described in git_get_project_ctags() subroutine.
717 # You may want to install the HTML::TagCloud Perl module to get
718 # a pretty tag cloud instead of just a list of tags.
720 # To enable system wide have in $GITWEB_CONFIG
721 # $feature{'ctags'}{'default'} = [1];
722 # Project specific override is not supported.
724 # A value of 0 means no ctags display or editing. A value of
725 # 1 enables ctags display but never editing. A non-empty value
726 # that is not a string of digits enables ctags display AND the
727 # ability to add tags using a form that uses method POST and
728 # an action value set to the configured 'ctags' value.
733 # The maximum number of patches in a patchset generated in patch
734 # view. Set this to 0 or undef to disable patch view, or to a
735 # negative number to remove any limit.
737 # To disable system wide have in $GITWEB_CONFIG
738 # $feature{'patches'}{'default'} = [0];
739 # To have project specific config enable override in $GITWEB_CONFIG
740 # $feature{'patches'}{'override'} = 1;
741 # and in project config gitweb.patches = 0|n;
742 # where n is the maximum number of patches allowed in a patchset.
744 'sub' => \
&feature_patches
,
748 # Avatar support. When this feature is enabled, views such as
749 # shortlog or commit will display an avatar associated with
750 # the email of the committer(s) and/or author(s).
752 # Currently available providers are gravatar and picon.
753 # If an unknown provider is specified, the feature is disabled.
755 # Gravatar depends on Digest::MD5.
756 # Picon currently relies on the indiana.edu database.
758 # To enable system wide have in $GITWEB_CONFIG
759 # $feature{'avatar'}{'default'} = ['<provider>'];
760 # where <provider> is either gravatar or picon.
761 # To have project specific config enable override in $GITWEB_CONFIG
762 # $feature{'avatar'}{'override'} = 1;
763 # and in project config gitweb.avatar = <provider>;
765 'sub' => \
&feature_avatar
,
769 # Enable displaying how much time and how many git commands
770 # it took to generate and display page. Disabled by default.
771 # Project specific override is not supported.
776 # Enable turning some links into links to actions which require
777 # JavaScript to run (like 'blame_incremental'). Not enabled by
778 # default. Project specific override is currently not supported.
779 'javascript-actions' => {
783 # Enable and configure ability to change common timezone for dates
784 # in gitweb output via JavaScript. Enabled by default.
785 # Project specific override is not supported.
786 'javascript-timezone' => {
789 'local', # default timezone: 'utc', 'local', or '(-|+)HHMM' format,
790 # or undef to turn off this feature
791 'gitweb_tz', # name of cookie where to store selected timezone
792 'datetime', # CSS class used to mark up dates for manipulation
795 # Syntax highlighting support. This is based on Daniel Svensson's
796 # and Sham Chukoury's work in gitweb-xmms2.git.
797 # It requires the 'highlight' program present in $PATH,
798 # and therefore is disabled by default.
800 # To enable system wide have in $GITWEB_CONFIG
801 # $feature{'highlight'}{'default'} = [1];
804 'sub' => sub { feature_bool
('highlight', @_) },
808 # Enable displaying of remote heads in the heads list
810 # To enable system wide have in $GITWEB_CONFIG
811 # $feature{'remote_heads'}{'default'} = [1];
812 # To have project specific config enable override in $GITWEB_CONFIG
813 # $feature{'remote_heads'}{'override'} = 1;
814 # and in project config gitweb.remoteheads = 0|1;
816 'sub' => sub { feature_bool
('remote_heads', @_) },
820 # Enable showing branches under other refs in addition to heads
822 # To set system wide extra branch refs have in $GITWEB_CONFIG
823 # $feature{'extra-branch-refs'}{'default'} = ['dirs', 'of', 'choice'];
824 # To have project specific config enable override in $GITWEB_CONFIG
825 # $feature{'extra-branch-refs'}{'override'} = 1;
826 # and in project config gitweb.extrabranchrefs = dirs of choice
827 # Every directory is separated with whitespace.
829 'extra-branch-refs' => {
830 'sub' => \
&feature_extra_branch_refs
,
835 sub gitweb_get_feature
{
837 return unless exists $feature{$name};
838 my ($sub, $override, @defaults) = (
839 $feature{$name}{'sub'},
840 $feature{$name}{'override'},
841 @
{$feature{$name}{'default'}});
842 # project specific override is possible only if we have project
843 our $git_dir; # global variable, declared later
844 if (!$override || !defined $git_dir) {
848 warn "feature $name is not overridable";
851 return $sub->(@defaults);
854 # A wrapper to check if a given feature is enabled.
855 # With this, you can say
857 # my $bool_feat = gitweb_check_feature('bool_feat');
858 # gitweb_check_feature('bool_feat') or somecode;
862 # my ($bool_feat) = gitweb_get_feature('bool_feat');
863 # (gitweb_get_feature('bool_feat'))[0] or somecode;
865 sub gitweb_check_feature
{
866 return (gitweb_get_feature
(@_))[0];
872 my ($val) = git_get_project_config
($key, '--bool');
876 } elsif ($val eq 'true') {
878 } elsif ($val eq 'false') {
883 sub feature_snapshot
{
886 my ($val) = git_get_project_config
('snapshot');
889 @fmts = ($val eq 'none' ?
() : split /\s*[,\s]\s*/, $val);
895 sub feature_patches
{
896 my @val = (git_get_project_config
('patches', '--int'));
906 my @val = (git_get_project_config
('avatar'));
908 return @val ?
@val : @_;
911 sub feature_extra_branch_refs
{
912 my (@branch_refs) = @_;
913 my $values = git_get_project_config
('extrabranchrefs');
916 $values = config_to_multi
($values);
918 foreach my $value (@
{$values}) {
919 push @branch_refs, split /\s+/, $value;
926 # checking HEAD file with -e is fragile if the repository was
927 # initialized long time ago (i.e. symlink HEAD) and was pack-ref'ed
929 sub check_head_link
{
931 return 0 unless -d
"$dir/objects" && -x _
;
932 return 0 unless -d
"$dir/refs" && -x _
;
933 my $headfile = "$dir/HEAD";
934 return -l
$headfile ?
935 readlink($headfile) =~ /^refs\/heads\
// : -f
$headfile;
938 sub check_export_ok
{
940 return (check_head_link
($dir) &&
941 (!$export_ok || -e
"$dir/$export_ok") &&
942 (!$export_auth_hook || $export_auth_hook->($dir)));
945 # process alternate names for backward compatibility
946 # filter out unsupported (unknown) snapshot formats
947 sub filter_snapshot_fmts
{
951 exists $known_snapshot_format_aliases{$_} ?
952 $known_snapshot_format_aliases{$_} : $_} @fmts;
954 exists $known_snapshot_formats{$_} &&
955 !$known_snapshot_formats{$_}{'disabled'}} @fmts;
958 sub filter_and_validate_refs
{
960 my %unique_refs = ();
962 foreach my $ref (@refs) {
963 die_error
(500, "Invalid ref '$ref' in 'extra-branch-refs' feature") unless (is_valid_ref_format
($ref));
964 # 'heads' are added implicitly in get_branch_refs().
965 $unique_refs{$ref} = 1 if ($ref ne 'heads');
967 return sort keys %unique_refs;
970 # If it is set to code reference, it is code that it is to be run once per
971 # request, allowing updating configurations that change with each request,
972 # while running other code in config file only once.
974 # Otherwise, if it is false then gitweb would process config file only once;
975 # if it is true then gitweb config would be run for each request.
976 our $per_request_config = 1;
978 # If true and fileno STDIN is 0 and getsockname succeeds and getpeername fails
979 # with ENOTCONN, then FCGI mode will be activated automatically in just the
980 # same way as though the --fcgi option had been given instead.
983 # read and parse gitweb config file given by its parameter.
984 # returns true on success, false on recoverable error, allowing
985 # to chain this subroutine, using first file that exists.
986 # dies on errors during parsing config file, as it is unrecoverable.
987 sub read_config_file
{
988 my $filename = shift;
989 return unless defined $filename;
990 # die if there are errors parsing config file
999 our ($GITWEB_CONFIG, $GITWEB_CONFIG_SYSTEM, $GITWEB_CONFIG_COMMON);
1000 sub evaluate_gitweb_config
{
1001 our $GITWEB_CONFIG = $ENV{'GITWEB_CONFIG'} || "++GITWEB_CONFIG++";
1002 our $GITWEB_CONFIG_SYSTEM = $ENV{'GITWEB_CONFIG_SYSTEM'} || "++GITWEB_CONFIG_SYSTEM++";
1003 our $GITWEB_CONFIG_COMMON = $ENV{'GITWEB_CONFIG_COMMON'} || "++GITWEB_CONFIG_COMMON++";
1005 # Protect against duplications of file names, to not read config twice.
1006 # Only one of $GITWEB_CONFIG and $GITWEB_CONFIG_SYSTEM is used, so
1007 # there possibility of duplication of filename there doesn't matter.
1008 $GITWEB_CONFIG = "" if ($GITWEB_CONFIG eq $GITWEB_CONFIG_COMMON);
1009 $GITWEB_CONFIG_SYSTEM = "" if ($GITWEB_CONFIG_SYSTEM eq $GITWEB_CONFIG_COMMON);
1011 # Common system-wide settings for convenience.
1012 # Those settings can be ovverriden by GITWEB_CONFIG or GITWEB_CONFIG_SYSTEM.
1013 read_config_file
($GITWEB_CONFIG_COMMON);
1015 # Use first config file that exists. This means use the per-instance
1016 # GITWEB_CONFIG if exists, otherwise use GITWEB_SYSTEM_CONFIG.
1017 read_config_file
($GITWEB_CONFIG) and return;
1018 read_config_file
($GITWEB_CONFIG_SYSTEM);
1022 our $to_utf8_pipe_command = '';
1024 sub evaluate_encoding
{
1025 my $requested = $fallback_encoding || 'ISO-8859-1';
1026 my $obj = Encode
::find_encoding
($requested) or
1027 die_error
(400, "Requested fallback encoding not found");
1028 if ($obj->name eq 'iso-8859-1') {
1029 # Use Windows-1252 instead as required by the HTML 5 standard
1030 my $altobj = Encode
::find_encoding
('Windows-1252');
1031 $obj = $altobj if $altobj;
1033 $encode_object = $obj;
1034 my $nm = lc($encode_object->name);
1035 unless ($nm eq 'cp1252' || $nm eq 'ascii' || $nm eq 'utf8' ||
1036 $nm =~ /^utf-8/ || $nm =~ /^iso-8859-/) {
1037 $to_utf8_pipe_command =
1038 quote_command
($^X
, '-CO', '-MEncode=decode,FB_DEFAULT', '-pse',
1039 '$_ = decode($fe, $_, FB_DEFAULT) if !utf8::decode($_);',
1040 '--', "-fe=$fallback_encoding")." | ";
1044 sub evaluate_email_obfuscate
{
1047 if (!$email && eval { require HTML
::Email
::Obfuscate
; 1 }) {
1048 $email = HTML
::Email
::Obfuscate
->new(lite
=> 1);
1052 # Get loadavg of system, to compare against $maxload.
1053 # Currently it requires '/proc/loadavg' present to get loadavg;
1054 # if it is not present it returns 0, which means no load checking.
1056 if( -e
'/proc/loadavg' ){
1057 open my $fd, '<', '/proc/loadavg'
1059 my @load = split(/\s+/, scalar <$fd>);
1062 # The first three columns measure CPU and IO utilization of the last one,
1063 # five, and 10 minute periods. The fourth column shows the number of
1064 # currently running processes and the total number of processes in the m/n
1065 # format. The last column displays the last process ID used.
1066 return $load[0] || 0;
1068 # additional checks for load average should go here for things that don't export
1074 # version of the core git binary
1076 our $git_vernum = "0"; # guaranteed to always match /^\d+(\.\d+)*$/
1077 sub evaluate_git_version
{
1078 $git_version = $version; # don't leak system information to attackers
1079 $git_vernum eq "0" or return; # don't run it again
1082 if (defined(my $fd = cmd_pipe
$GIT, '--version')) {
1085 $number_of_git_cmds++;
1087 $git_vernum = $1 if defined($vers) && $vers =~ /git\s+version\s+(\d+(?:\.\d+)*)$/io;
1091 if (defined $maxload && get_loadavg
() > $maxload) {
1092 die_error
(503, "The load average on the server is too high");
1096 # ======================================================================
1097 # input validation and dispatch
1099 # input parameters can be collected from a variety of sources (presently, CGI
1100 # and PATH_INFO), so we define an %input_params hash that collects them all
1101 # together during validation: this allows subsequent uses (e.g. href()) to be
1102 # agnostic of the parameter origin
1104 our %input_params = ();
1106 # input parameters are stored with the long parameter name as key. This will
1107 # also be used in the href subroutine to convert parameters to their CGI
1108 # equivalent, and since the href() usage is the most frequent one, we store
1109 # the name -> CGI key mapping here, instead of the reverse.
1111 # XXX: Warning: If you touch this, check the search form for updating,
1114 our @cgi_param_mapping = (
1118 file_parent
=> "fp",
1120 hash_parent
=> "hp",
1122 hash_parent_base
=> "hpb",
1127 snapshot_format
=> "sf",
1129 extra_options
=> "opt",
1130 search_use_regexp
=> "sr",
1133 project_filter
=> "pf",
1134 # this must be last entry (for manipulation from JavaScript)
1137 our %cgi_param_mapping = @cgi_param_mapping;
1139 # we will also need to know the possible actions, for validation
1141 "blame" => \
&git_blame
,
1142 "blame_incremental" => \
&git_blame_incremental
,
1143 "blame_data" => \
&git_blame_data
,
1144 "blobdiff" => \
&git_blobdiff
,
1145 "blobdiff_plain" => \
&git_blobdiff_plain
,
1146 "blob" => \
&git_blob
,
1147 "blob_plain" => \
&git_blob_plain
,
1148 "commitdiff" => \
&git_commitdiff
,
1149 "commitdiff_plain" => \
&git_commitdiff_plain
,
1150 "commit" => \
&git_commit
,
1151 "forks" => \
&git_forks
,
1152 "heads" => \
&git_heads
,
1153 "history" => \
&git_history
,
1155 "patch" => \
&git_patch
,
1156 "patches" => \
&git_patches
,
1157 "refs" => \
&git_refs
,
1158 "remotes" => \
&git_remotes
,
1160 "atom" => \
&git_atom
,
1161 "search" => \
&git_search
,
1162 "search_help" => \
&git_search_help
,
1163 "shortlog" => \
&git_shortlog
,
1164 "summary" => \
&git_summary
,
1166 "tags" => \
&git_tags
,
1167 "tree" => \
&git_tree
,
1168 "snapshot" => \
&git_snapshot
,
1169 "object" => \
&git_object
,
1170 # those below don't need $project
1171 "opml" => \
&git_opml
,
1172 "frontpage" => \
&git_frontpage
,
1173 "project_list" => \
&git_project_list
,
1174 "project_index" => \
&git_project_index
,
1177 # the only actions we will allow to be cached
1178 my %supported_cache_actions;
1179 BEGIN {%supported_cache_actions = map {( $_ => 1 )} qw(summary)}
1181 # finally, we have the hash of allowed extra_options for the commands that
1183 our %allowed_options = (
1184 "--no-merges" => [ qw(rss atom log shortlog history) ],
1187 # fill %input_params with the CGI parameters. All values except for 'opt'
1188 # should be single values, but opt can be an array. We should probably
1189 # build an array of parameters that can be multi-valued, but since for the time
1190 # being it's only this one, we just single it out
1191 sub evaluate_query_params
{
1194 while (my ($name, $symbol) = each %cgi_param_mapping) {
1195 if ($symbol eq 'opt') {
1196 $input_params{$name} = [ map { decode_utf8
($_) } $cgi->multi_param($symbol) ];
1198 $input_params{$name} = decode_utf8
($cgi->param($symbol));
1202 # Backwards compatibility - by_tag= <=> t=
1203 if ($input_params{'ctag'}) {
1204 $input_params{'ctag_filter'} = $input_params{'ctag'};
1208 # now read PATH_INFO and update the parameter list for missing parameters
1209 sub evaluate_path_info
{
1210 return if defined $input_params{'project'};
1211 return if !$path_info;
1212 $path_info =~ s
,^/+,,;
1213 return if !$path_info;
1215 # find which part of PATH_INFO is project
1216 my $project = $path_info;
1217 $project =~ s
,/+$,,;
1218 while ($project && !check_head_link
("$projectroot/$project")) {
1219 $project =~ s
,/*[^/]*$,,;
1221 return unless $project;
1222 $input_params{'project'} = $project;
1224 # do not change any parameters if an action is given using the query string
1225 return if $input_params{'action'};
1226 $path_info =~ s
,^\Q
$project\E
/*,,;
1228 # next, check if we have an action
1229 my $action = $path_info;
1230 $action =~ s
,/.*$,,;
1231 if (exists $actions{$action}) {
1232 $path_info =~ s
,^$action/*,,;
1233 $input_params{'action'} = $action;
1236 # list of actions that want hash_base instead of hash, but can have no
1237 # pathname (f) parameter
1243 # we want to catch, among others
1244 # [$hash_parent_base[:$file_parent]..]$hash_parent[:$file_name]
1245 my ($parentrefname, $parentpathname, $refname, $pathname) =
1246 ($path_info =~ /^(?:(.+?)(?::(.+))?\.\.)?([^:]+?)?(?::(.+))?$/);
1248 # first, analyze the 'current' part
1249 if (defined $pathname) {
1250 # we got "branch:filename" or "branch:dir/"
1251 # we could use git_get_type(branch:pathname), but:
1252 # - it needs $git_dir
1253 # - it does a git() call
1254 # - the convention of terminating directories with a slash
1255 # makes it superfluous
1256 # - embedding the action in the PATH_INFO would make it even
1258 $pathname =~ s
,^/+,,;
1259 if (!$pathname || substr($pathname, -1) eq "/") {
1260 $input_params{'action'} ||= "tree";
1261 $pathname =~ s
,/$,,;
1263 # the default action depends on whether we had parent info
1265 if ($parentrefname) {
1266 $input_params{'action'} ||= "blobdiff_plain";
1268 $input_params{'action'} ||= "blob_plain";
1271 $input_params{'hash_base'} ||= $refname;
1272 $input_params{'file_name'} ||= $pathname;
1273 } elsif (defined $refname) {
1274 # we got "branch". In this case we have to choose if we have to
1275 # set hash or hash_base.
1277 # Most of the actions without a pathname only want hash to be
1278 # set, except for the ones specified in @wants_base that want
1279 # hash_base instead. It should also be noted that hand-crafted
1280 # links having 'history' as an action and no pathname or hash
1281 # set will fail, but that happens regardless of PATH_INFO.
1282 if (defined $parentrefname) {
1283 # if there is parent let the default be 'shortlog' action
1284 # (for http://git.example.com/repo.git/A..B links); if there
1285 # is no parent, dispatch will detect type of object and set
1286 # action appropriately if required (if action is not set)
1287 $input_params{'action'} ||= "shortlog";
1289 if ($input_params{'action'} &&
1290 grep { $_ eq $input_params{'action'} } @wants_base) {
1291 $input_params{'hash_base'} ||= $refname;
1293 $input_params{'hash'} ||= $refname;
1297 # next, handle the 'parent' part, if present
1298 if (defined $parentrefname) {
1299 # a missing pathspec defaults to the 'current' filename, allowing e.g.
1300 # someproject/blobdiff/oldrev..newrev:/filename
1301 if ($parentpathname) {
1302 $parentpathname =~ s
,^/+,,;
1303 $parentpathname =~ s
,/$,,;
1304 $input_params{'file_parent'} ||= $parentpathname;
1306 $input_params{'file_parent'} ||= $input_params{'file_name'};
1308 # we assume that hash_parent_base is wanted if a path was specified,
1309 # or if the action wants hash_base instead of hash
1310 if (defined $input_params{'file_parent'} ||
1311 grep { $_ eq $input_params{'action'} } @wants_base) {
1312 $input_params{'hash_parent_base'} ||= $parentrefname;
1314 $input_params{'hash_parent'} ||= $parentrefname;
1318 # for the snapshot action, we allow URLs in the form
1319 # $project/snapshot/$hash.ext
1320 # where .ext determines the snapshot and gets removed from the
1321 # passed $refname to provide the $hash.
1323 # To be able to tell that $refname includes the format extension, we
1324 # require the following two conditions to be satisfied:
1325 # - the hash input parameter MUST have been set from the $refname part
1326 # of the URL (i.e. they must be equal)
1327 # - the snapshot format MUST NOT have been defined already (e.g. from
1329 # It's also useless to try any matching unless $refname has a dot,
1330 # so we check for that too
1331 if (defined $input_params{'action'} &&
1332 $input_params{'action'} eq 'snapshot' &&
1333 defined $refname && index($refname, '.') != -1 &&
1334 $refname eq $input_params{'hash'} &&
1335 !defined $input_params{'snapshot_format'}) {
1336 # We loop over the known snapshot formats, checking for
1337 # extensions. Allowed extensions are both the defined suffix
1338 # (which includes the initial dot already) and the snapshot
1339 # format key itself, with a prepended dot
1340 while (my ($fmt, $opt) = each %known_snapshot_formats) {
1341 my $hash = $refname;
1342 unless ($hash =~ s/(\Q$opt->{'suffix'}\E|\Q.$fmt\E)$//) {
1346 # a valid suffix was found, so set the snapshot format
1347 # and reset the hash parameter
1348 $input_params{'snapshot_format'} = $fmt;
1349 $input_params{'hash'} = $hash;
1350 # we also set the format suffix to the one requested
1351 # in the URL: this way a request for e.g. .tgz returns
1352 # a .tgz instead of a .tar.gz
1353 $known_snapshot_formats{$fmt}{'suffix'} = $sfx;
1359 our ($action, $project, $file_name, $file_parent, $hash, $hash_parent, $hash_base,
1360 $hash_parent_base, @extra_options, $page, $searchtype, $search_use_regexp,
1361 $searchtext, $search_regexp, $project_filter);
1362 sub evaluate_and_validate_params
{
1363 our $action = $input_params{'action'};
1364 if (defined $action) {
1365 if (!is_valid_action
($action)) {
1366 die_error
(400, "Invalid action parameter");
1370 # parameters which are pathnames
1371 our $project = $input_params{'project'};
1372 if (defined $project) {
1373 if (!is_valid_project
($project)) {
1375 die_error
(404, "No such project");
1379 our $project_filter = $input_params{'project_filter'};
1380 if (defined $project_filter) {
1381 if (!is_valid_pathname
($project_filter)) {
1382 die_error
(404, "Invalid project_filter parameter");
1386 our $file_name = $input_params{'file_name'};
1387 if (defined $file_name) {
1388 if (!is_valid_pathname
($file_name)) {
1389 die_error
(400, "Invalid file parameter");
1393 our $file_parent = $input_params{'file_parent'};
1394 if (defined $file_parent) {
1395 if (!is_valid_pathname
($file_parent)) {
1396 die_error
(400, "Invalid file parent parameter");
1400 # parameters which are refnames
1401 our $hash = $input_params{'hash'};
1402 if (defined $hash) {
1403 if (!is_valid_refname
($hash)) {
1404 die_error
(400, "Invalid hash parameter");
1408 our $hash_parent = $input_params{'hash_parent'};
1409 if (defined $hash_parent) {
1410 if (!is_valid_refname
($hash_parent)) {
1411 die_error
(400, "Invalid hash parent parameter");
1415 our $hash_base = $input_params{'hash_base'};
1416 if (defined $hash_base) {
1417 if (!is_valid_refname
($hash_base)) {
1418 die_error
(400, "Invalid hash base parameter");
1422 our @extra_options = @
{$input_params{'extra_options'}};
1423 # @extra_options is always defined, since it can only be (currently) set from
1424 # CGI, and $cgi->param() returns the empty array in array context if the param
1426 foreach my $opt (@extra_options) {
1427 if (not exists $allowed_options{$opt}) {
1428 die_error
(400, "Invalid option parameter");
1430 if (not grep(/^$action$/, @
{$allowed_options{$opt}})) {
1431 die_error
(400, "Invalid option parameter for this action");
1435 our $hash_parent_base = $input_params{'hash_parent_base'};
1436 if (defined $hash_parent_base) {
1437 if (!is_valid_refname
($hash_parent_base)) {
1438 die_error
(400, "Invalid hash parent base parameter");
1443 our $page = $input_params{'page'};
1444 if (defined $page) {
1445 if ($page =~ m/[^0-9]/) {
1446 die_error
(400, "Invalid page parameter");
1450 our $searchtype = $input_params{'searchtype'};
1451 if (defined $searchtype) {
1452 if ($searchtype =~ m/[^a-z]/) {
1453 die_error
(400, "Invalid searchtype parameter");
1457 our $search_use_regexp = $input_params{'search_use_regexp'};
1459 our $searchtext = $input_params{'searchtext'};
1460 our $search_regexp = undef;
1461 if (defined $searchtext) {
1462 if (length($searchtext) < 2) {
1463 die_error
(403, "At least two characters are required for search parameter");
1465 if ($search_use_regexp) {
1466 $search_regexp = $searchtext;
1467 if (!eval { qr/$search_regexp/; 1; }) {
1468 (my $error = $@
) =~ s/ at \S+ line \d+.*\n?//;
1469 die_error
(400, "Invalid search regexp '$search_regexp'",
1473 $search_regexp = quotemeta $searchtext;
1478 # path to the current git repository
1480 sub evaluate_git_dir
{
1481 our $git_dir = $project ?
"$projectroot/$project" : undef;
1484 our (@snapshot_fmts, $git_avatar, @extra_branch_refs);
1485 sub configure_gitweb_features
{
1486 # list of supported snapshot formats
1487 our @snapshot_fmts = gitweb_get_feature
('snapshot');
1488 @snapshot_fmts = filter_snapshot_fmts
(@snapshot_fmts);
1490 # check that the avatar feature is set to a known provider name,
1491 # and for each provider check if the dependencies are satisfied.
1492 # if the provider name is invalid or the dependencies are not met,
1493 # reset $git_avatar to the empty string.
1494 our ($git_avatar) = gitweb_get_feature
('avatar');
1495 if ($git_avatar eq 'gravatar') {
1496 $git_avatar = '' unless (eval { require Digest
::MD5
; 1; });
1497 } elsif ($git_avatar eq 'picon') {
1503 our @extra_branch_refs = gitweb_get_feature
('extra-branch-refs');
1504 @extra_branch_refs = filter_and_validate_refs
(@extra_branch_refs);
1507 sub get_branch_refs
{
1508 return ('heads', @extra_branch_refs);
1511 # custom error handler: 'die <message>' is Internal Server Error
1512 sub handle_errors_html
{
1513 my $msg = shift; # it is already HTML escaped
1515 # to avoid infinite loop where error occurs in die_error,
1516 # change handler to default handler, disabling handle_errors_html
1517 set_message
("Error occurred when inside die_error:\n$msg");
1519 # you cannot jump out of die_error when called as error handler;
1520 # the subroutine set via CGI::Carp::set_message is called _after_
1521 # HTTP headers are already written, so it cannot write them itself
1522 die_error
(undef, undef, $msg, -error_handler
=> 1, -no_http_header
=> 1);
1524 set_message
(\
&handle_errors_html
);
1526 our $shown_stale_message = 0;
1527 our $cache_dump = undef;
1528 our $cache_dump_mtime = undef;
1531 my $cache_mode_active;
1533 if (!defined $action) {
1534 if (defined $hash) {
1535 $action = git_get_type
($hash);
1536 $action or die_error
(404, "Object does not exist");
1537 } elsif (defined $hash_base && defined $file_name) {
1538 $action = git_get_type
("$hash_base:$file_name");
1539 $action or die_error
(404, "File or directory does not exist");
1540 } elsif (defined $project) {
1541 $action = 'summary';
1543 $action = 'frontpage';
1546 if (!defined($actions{$action})) {
1547 die_error
(400, "Unknown action");
1549 if ($action !~ m/^(?:opml|frontpage|project_list|project_index)$/ &&
1551 die_error
(400, "Project needed");
1554 my $cached_page = $supported_cache_actions{$action}
1555 ? cached_action_page
($action)
1557 goto DUMPCACHE
if $cached_page;
1558 local *SAVEOUT
= *STDOUT
;
1559 $cache_mode_active = $supported_cache_actions{$action}
1560 ? cached_action_start
($action)
1563 configure_gitweb_features
();
1564 $actions{$action}->();
1566 return unless $cache_mode_active;
1568 $cached_page = cached_action_finish
($action);
1573 $cache_mode_active = 0;
1574 # Avoid any extra unwanted encoding steps as $cached_page is raw bytes
1575 binmode STDOUT
, ':raw';
1576 our $fcgi_raw_mode = 1;
1577 print expand_gitweb_pi
($cached_page, time);
1578 binmode STDOUT
, ':utf8'; # as set at the beginning of gitweb.cgi
1583 our $t0 = [ gettimeofday
() ]
1585 our $number_of_git_cmds = 0;
1588 our $first_request = 1;
1589 our $evaluate_uri_force = undef;
1593 # Only allow GET and HEAD methods
1594 if (!$ENV{'REQUEST_METHOD'} || ($ENV{'REQUEST_METHOD'} ne 'GET' && $ENV{'REQUEST_METHOD'} ne 'HEAD')) {
1596 Status: 405 Method Not Allowed
1597 Content-Type: text/plain
1600 405 Method Not Allowed
1606 &$evaluate_uri_force() if $evaluate_uri_force;
1607 if ($per_request_config) {
1608 if (ref($per_request_config) eq 'CODE') {
1609 $per_request_config->();
1610 } elsif (!$first_request) {
1611 evaluate_gitweb_config
();
1612 evaluate_email_obfuscate
();
1617 # $projectroot and $projects_list might be set in gitweb config file
1618 $projects_list ||= $projectroot;
1620 evaluate_query_params
();
1621 evaluate_path_info
();
1622 evaluate_and_validate_params
();
1628 our $is_last_request = sub { 1 };
1629 our ($pre_dispatch_hook, $post_dispatch_hook, $pre_listen_hook);
1633 our $fcgi_nproc_active = 0;
1634 our $fcgi_raw_mode = 0;
1637 my $stdinfno = fileno STDIN
;
1638 return 0 unless defined $stdinfno && $stdinfno == 0;
1639 return 0 unless getsockname STDIN
;
1640 return 0 if getpeername STDIN
;
1641 return $!{ENOTCONN
}?
1:0;
1643 sub configure_as_fcgi
{
1644 return if $fcgi_mode;
1649 # We have gone to great effort to make sure that all incoming data has
1650 # been converted from whatever format it was in into UTF-8. We have
1651 # even taken care to make sure the output handle is in ':utf8' mode.
1652 # Now along comes FCGI and blows it with:
1654 # Use of wide characters in FCGI::Stream::PRINT is deprecated
1655 # and will stop wprking[sic] in a future version of FCGI
1657 # To fix this we replace FCGI::Stream::PRINT with our own routine that
1658 # first encodes everything and then calls the original routine, but
1659 # not if $fcgi_raw_mode is true (then we just call the original routine).
1661 # Note that we could do this by using utf8::is_utf8 to check instead
1662 # of having a $fcgi_raw_mode global, but that would be slower to run
1663 # the test on each element and much slower than skipping the conversion
1664 # entirely when we know we're outputting raw bytes.
1665 my $orig = \
&FCGI
::Stream
::PRINT
;
1666 undef *FCGI
::Stream
::PRINT
;
1667 *FCGI
::Stream
::PRINT
= sub {
1668 @_ = (shift, map {my $x=$_; utf8
::encode
($x); $x} @_)
1669 unless $fcgi_raw_mode;
1673 our $CGI = 'CGI::Fast';
1677 my $request_number = 0;
1678 # let each child service 100 requests
1679 our $is_last_request = sub { ++$request_number >= 100 };
1682 my $script_name = $ENV{'SCRIPT_NAME'} || $ENV{'SCRIPT_FILENAME'} || __FILE__
;
1684 if $script_name =~ /\.fcgi$/ || ($auto_fcgi && is_fcgi
());
1686 my $nproc_sub = sub {
1687 my ($arg, $val) = @_;
1688 return unless eval { require FCGI
::ProcManager
; 1; };
1689 $fcgi_nproc_active = 1;
1690 my $proc_manager = FCGI
::ProcManager
->new({
1691 n_processes
=> $val,
1693 our $pre_listen_hook = sub { $proc_manager->pm_manage() };
1694 our $pre_dispatch_hook = sub { $proc_manager->pm_pre_dispatch() };
1695 our $post_dispatch_hook = sub { $proc_manager->pm_post_dispatch() };
1698 require Getopt
::Long
;
1699 Getopt
::Long
::GetOptions
(
1700 'fastcgi|fcgi|f' => \
&configure_as_fcgi
,
1701 'nproc|n=i' => $nproc_sub,
1704 if (!$fcgi_nproc_active && defined $ENV{'GITWEB_FCGI_NPROC'} && $ENV{'GITWEB_FCGI_NPROC'} =~ /^\d+$/) {
1705 &$nproc_sub('nproc', $ENV{'GITWEB_FCGI_NPROC'});
1709 # Any "our" variable that could possibly influence correct handling of
1710 # a CGI request MUST be reset in this subroutine
1711 sub _reset_globals
{
1712 # Note that $t0 and $number_of_git_commands are handled by reset_timer
1713 our %input_params = ();
1714 our ($action, $project, $file_name, $file_parent, $hash, $hash_parent, $hash_base,
1715 $hash_parent_base, @extra_options, $page, $searchtype, $search_use_regexp,
1716 $searchtext, $search_regexp, $project_filter) = ();
1717 our $git_dir = undef;
1718 our (@snapshot_fmts, $git_avatar, @extra_branch_refs) = ();
1719 our %avatar_cache = ();
1720 our $config_file = '';
1722 our $gitweb_project_owner = undef;
1723 our $shown_stale_message = 0;
1724 our $fcgi_raw_mode = 0;
1725 keys %known_snapshot_formats; # reset 'each' iterator
1729 evaluate_gitweb_config
();
1730 evaluate_encoding
();
1731 evaluate_email_obfuscate
();
1732 evaluate_git_version
();
1733 my ($ml, $mi, $bu, $hl, $subroutine) = ($my_url, $my_uri, $base_url, $home_link, '');
1734 $subroutine .= '$my_url = $ml;' if defined $my_url && $my_url ne '';
1735 $subroutine .= '$my_uri = $mi;' if defined $my_uri; # this one can be ""
1736 $subroutine .= '$base_url = $bu;' if defined $base_url && $base_url ne '';
1737 $subroutine .= '$home_link = $hl;' if defined $home_link && $home_link ne '';
1738 $evaluate_uri_force = eval "sub {$subroutine}" if $subroutine;
1742 $pre_listen_hook->()
1743 if $pre_listen_hook;
1746 while ($cgi = $CGI->new()) {
1747 $pre_dispatch_hook->()
1748 if $pre_dispatch_hook;
1750 # most globals can simply be reset
1753 # evaluate_path_info corrupts %known_snapshot_formats
1754 # so we need a deepish copy of it -- note that
1755 # _reset_globals already took care of resetting its
1756 # hash iterator that evaluate_path_info also leaves
1757 # in an indeterminate state
1759 while (my ($k,$v) = each(%known_snapshot_formats)) {
1760 $formats{$k} = {%{$known_snapshot_formats{$k}}};
1762 local *known_snapshot_formats
= \
%formats;
1764 eval {run_request
()};
1766 $post_dispatch_hook->()
1767 if $post_dispatch_hook;
1770 last REQUEST
if ($is_last_request->());
1778 if (defined caller) {
1779 # wrapped in a subroutine processing requests,
1780 # e.g. mod_perl with ModPerl::Registry, or PSGI with Plack::App::WrapCGI
1783 # pure CGI script, serving single request
1787 ## ======================================================================
1790 # possible values of extra options
1791 # -full => 0|1 - use absolute/full URL ($my_uri/$my_url as base)
1792 # -replay => 1 - start from a current view (replay with modifications)
1793 # -path_info => 0|1 - don't use/use path_info URL (if possible)
1794 # -anchor => ANCHOR - add #ANCHOR to end of URL, implies -replay if used alone
1797 # default is to use -absolute url() i.e. $my_uri
1798 my $href = $params{-full
} ?
$my_url : $my_uri;
1800 # implicit -replay, must be first of implicit params
1801 $params{-replay
} = 1 if (keys %params == 1 && $params{-anchor
});
1803 $params{'project'} = $project unless exists $params{'project'};
1805 if ($params{-replay
}) {
1806 while (my ($name, $symbol) = each %cgi_param_mapping) {
1807 if (!exists $params{$name}) {
1808 $params{$name} = $input_params{$name};
1813 my $use_pathinfo = gitweb_check_feature
('pathinfo');
1814 if (defined $params{'project'} &&
1815 (exists $params{-path_info
} ?
$params{-path_info
} : $use_pathinfo)) {
1816 # try to put as many parameters as possible in PATH_INFO:
1819 # - hash_parent or hash_parent_base:/file_parent
1820 # - hash or hash_base:/filename
1821 # - the snapshot_format as an appropriate suffix
1823 # When the script is the root DirectoryIndex for the domain,
1824 # $href here would be something like http://gitweb.example.com/
1825 # Thus, we strip any trailing / from $href, to spare us double
1826 # slashes in the final URL
1829 # Then add the project name, if present
1830 $href .= "/".esc_path_info
($params{'project'});
1831 delete $params{'project'};
1833 # since we destructively absorb parameters, we keep this
1834 # boolean that remembers if we're handling a snapshot
1835 my $is_snapshot = $params{'action'} eq 'snapshot';
1837 # Summary just uses the project path URL, any other action is
1839 if (defined $params{'action'}) {
1840 $href .= "/".esc_path_info
($params{'action'})
1841 unless $params{'action'} eq 'summary';
1842 delete $params{'action'};
1845 # Next, we put hash_parent_base:/file_parent..hash_base:/file_name,
1846 # stripping nonexistent or useless pieces
1847 $href .= "/" if ($params{'hash_base'} || $params{'hash_parent_base'}
1848 || $params{'hash_parent'} || $params{'hash'});
1849 if (defined $params{'hash_base'}) {
1850 if (defined $params{'hash_parent_base'}) {
1851 $href .= esc_path_info
($params{'hash_parent_base'});
1852 # skip the file_parent if it's the same as the file_name
1853 if (defined $params{'file_parent'}) {
1854 if (defined $params{'file_name'} && $params{'file_parent'} eq $params{'file_name'}) {
1855 delete $params{'file_parent'};
1856 } elsif ($params{'file_parent'} !~ /\.\./) {
1857 $href .= ":/".esc_path_info
($params{'file_parent'});
1858 delete $params{'file_parent'};
1862 delete $params{'hash_parent'};
1863 delete $params{'hash_parent_base'};
1864 } elsif (defined $params{'hash_parent'}) {
1865 $href .= esc_path_info
($params{'hash_parent'}). "..";
1866 delete $params{'hash_parent'};
1869 $href .= esc_path_info
($params{'hash_base'});
1870 if (defined $params{'file_name'} && $params{'file_name'} !~ /\.\./) {
1871 $href .= ":/".esc_path_info
($params{'file_name'});
1872 delete $params{'file_name'};
1874 delete $params{'hash'};
1875 delete $params{'hash_base'};
1876 } elsif (defined $params{'hash'}) {
1877 $href .= esc_path_info
($params{'hash'});
1878 delete $params{'hash'};
1881 # If the action was a snapshot, we can absorb the
1882 # snapshot_format parameter too
1884 my $fmt = $params{'snapshot_format'};
1885 # snapshot_format should always be defined when href()
1886 # is called, but just in case some code forgets, we
1887 # fall back to the default
1888 $fmt ||= $snapshot_fmts[0];
1889 $href .= $known_snapshot_formats{$fmt}{'suffix'};
1890 delete $params{'snapshot_format'};
1894 # now encode the parameters explicitly
1896 for (my $i = 0; $i < @cgi_param_mapping; $i += 2) {
1897 my ($name, $symbol) = ($cgi_param_mapping[$i], $cgi_param_mapping[$i+1]);
1898 if (defined $params{$name}) {
1899 if (ref($params{$name}) eq "ARRAY") {
1900 foreach my $par (@
{$params{$name}}) {
1901 push @result, $symbol . "=" . esc_param
($par);
1904 push @result, $symbol . "=" . esc_param
($params{$name});
1908 $href .= "?" . join(';', @result) if scalar @result;
1910 # final transformation: trailing spaces must be escaped (URI-encoded)
1911 $href =~ s/(\s+)$/CGI::escape($1)/e;
1913 if ($params{-anchor
}) {
1914 $href .= "#".esc_param
($params{-anchor
});
1921 ## ======================================================================
1922 ## validation, quoting/unquoting and escaping
1924 sub is_valid_action
{
1926 return undef unless exists $actions{$input};
1930 sub is_valid_project
{
1933 return unless defined $input;
1934 if (!is_valid_pathname
($input) ||
1935 !(-d
"$projectroot/$input") ||
1936 !check_export_ok
("$projectroot/$input") ||
1937 ($strict_export && !project_in_list
($input))) {
1944 sub is_valid_pathname
{
1947 return undef unless defined $input;
1948 # no '.' or '..' as elements of path, i.e. no '.' or '..'
1949 # at the beginning, at the end, and between slashes.
1950 # also this catches doubled slashes
1951 if ($input =~ m!(^|/)(|\.|\.\.)(/|$)!) {
1954 # no null characters
1955 if ($input =~ m!\0!) {
1961 sub is_valid_ref_format
{
1964 return undef unless defined $input;
1965 # restrictions on ref name according to git-check-ref-format
1966 if ($input =~ m!(/\.|\.\.|[\000-\040\177 ~^:?*\[]|/$)!) {
1972 sub is_valid_refname
{
1975 return undef unless defined $input;
1976 # textual hashes are O.K.
1977 if ($input =~ m/^[0-9a-fA-F]{40}$/) {
1980 # allow repeated trailing '[~^]n*' suffix(es)
1981 $input =~ s/^([^~^]+)(?:[~^]\d*)+$/$1/;
1982 # it must be correct pathname
1983 is_valid_pathname
($input) or return undef;
1984 # check git-check-ref-format restrictions
1985 is_valid_ref_format
($input) or return undef;
1989 # decode sequences of octets in utf8 into Perl's internal form,
1990 # which is utf-8 with utf8 flag set if needed. gitweb writes out
1991 # in utf-8 thanks to "binmode STDOUT, ':utf8'" at beginning
1994 return undef unless defined $str;
1996 if (utf8
::is_utf8
($str) || utf8
::decode
($str)) {
1999 return $encode_object->decode($str, Encode
::FB_DEFAULT
);
2003 # quote unsafe chars, but keep the slash, even when it's not
2004 # correct, but quoted slashes look too horrible in bookmarks
2007 return undef unless defined $str;
2008 $str =~ s/([^A-Za-z0-9\-_.~()\/:@ ]+)/CGI
::escape
($1)/eg
;
2013 # the quoting rules for path_info fragment are slightly different
2016 return undef unless defined $str;
2018 # path_info doesn't treat '+' as space (specially), but '?' must be escaped
2019 $str =~ s/([^A-Za-z0-9\-_.~();\/;:@&= +]+)/CGI
::escape
($1)/eg
;
2024 # quote unsafe chars in whole URL, so some characters cannot be quoted
2027 return undef unless defined $str;
2028 $str =~ s/([^A-Za-z0-9\-_.~();\/;?:@&= ]+)/CGI
::escape
($1)/eg
;
2033 # quote unsafe characters in HTML attributes
2036 # for XHTML conformance escaping '"' to '"' is not enough
2037 return esc_html
(@_);
2040 # replace invalid utf8 character with SUBSTITUTION sequence
2045 return undef unless defined $str;
2047 $str = to_utf8
($str);
2048 $str = $cgi->escapeHTML($str);
2049 if ($opts{'-nbsp'}) {
2050 $str =~ s/ / /g;
2053 $str =~ s
|([[:cntrl
:]])|(($1 ne "\t") ? quot_cec
($1) : $1)|eg
;
2057 # quote control characters and escape filename to HTML
2062 return undef unless defined $str;
2064 $str = to_utf8
($str);
2065 $str = $cgi->escapeHTML($str);
2066 if ($opts{'-nbsp'}) {
2067 $str =~ s/ / /g;
2070 $str =~ s
|([[:cntrl
:]])|quot_cec
($1)|eg
;
2074 # Sanitize for use in XHTML + application/xml+xhtml (valid XML 1.0)
2078 return undef unless defined $str;
2080 $str = to_utf8
($str);
2082 $str =~ s
|([[:cntrl
:]])|(index("\t\n\r", $1) != -1 ?
$1 : quot_cec
($1))|eg
;
2086 # Make control characters "printable", using character escape codes (CEC)
2090 my %es = ( # character escape codes, aka escape sequences
2091 "\t" => '\t', # tab (HT)
2092 "\n" => '\n', # line feed (LF)
2093 "\r" => '\r', # carrige return (CR)
2094 "\f" => '\f', # form feed (FF)
2095 "\b" => '\b', # backspace (BS)
2096 "\a" => '\a', # alarm (bell) (BEL)
2097 "\e" => '\e', # escape (ESC)
2098 "\013" => '\v', # vertical tab (VT)
2099 "\000" => '\0', # nul character (NUL)
2101 my $chr = ( (exists $es{$cntrl})
2103 : sprintf('\x%02x', ord($cntrl)) );
2104 if ($opts{-nohtml
}) {
2107 return "<span class=\"cntrl\">$chr</span>";
2111 # Alternatively use unicode control pictures codepoints,
2112 # Unicode "printable representation" (PR)
2117 my $chr = sprintf('&#%04d;', 0x2400+ord($cntrl));
2118 if ($opts{-nohtml
}) {
2121 return "<span class=\"cntrl\">$chr</span>";
2125 # git may return quoted and escaped filenames
2131 my %es = ( # character escape codes, aka escape sequences
2132 't' => "\t", # tab (HT, TAB)
2133 'n' => "\n", # newline (NL)
2134 'r' => "\r", # return (CR)
2135 'f' => "\f", # form feed (FF)
2136 'b' => "\b", # backspace (BS)
2137 'a' => "\a", # alarm (bell) (BEL)
2138 'e' => "\e", # escape (ESC)
2139 'v' => "\013", # vertical tab (VT)
2142 if ($seq =~ m/^[0-7]{1,3}$/) {
2143 # octal char sequence
2144 return chr(oct($seq));
2145 } elsif (exists $es{$seq}) {
2146 # C escape sequence, aka character escape code
2149 # quoted ordinary character
2153 if ($str =~ m/^"(.*)"$/) {
2156 $str =~ s/\\([^0-7]|[0-7]{1,3})/unq($1)/eg;
2161 # escape tabs (convert tabs to spaces)
2165 while ((my $pos = index($line, "\t")) != -1) {
2166 if (my $count = (8 - ($pos % 8))) {
2167 my $spaces = ' ' x
$count;
2168 $line =~ s/\t/$spaces/;
2175 sub project_in_list
{
2176 my $project = shift;
2177 my @list = git_get_projects_list
();
2178 return @list && scalar(grep { $_->{'path'} eq $project } @list);
2181 sub cached_page_precondition_check
{
2184 $action eq 'summary' &&
2185 $projlist_cache_lifetime > 0 &&
2186 gitweb_check_feature
('forks');
2188 # Note that ALL the 'forkchange' logic is in this function.
2189 # It does NOT belong in cached_action_page NOR in cached_action_start
2190 # NOR in cached_action_finish. None of those functions should know anything
2191 # about nor do anything to any of the 'forkchange'/"$action.forkchange" files.
2193 # besides the basic 'changed' "$action.changed" check, we may only use
2194 # a summary cache if:
2196 # 1) we are not using a project list cache file
2198 # 2) we are not using the 'forks' feature
2200 # 3) there is no 'forkchange' nor "$action.forkchange" file in $html_cache_dir
2202 # 4) there is no cache file ("$cache_dir/$projlist_cache_name")
2204 # 5) the OLDER of 'forkchange'/"$action.forkchange" is NEWER than the cache file
2206 # Otherwise we must re-generate the cache because we've had a fork change
2207 # (either a fork was added or a fork was removed) AND the change has been
2208 # picked up in the cache file AND we've not got that in our cached copy
2210 # For (5) regenerating the cached page wouldn't get us anything if the project
2211 # cache file is older than the 'forkchange'/"$action.forkchange" because the
2212 # forks information comes from the project cache file and it's clearly not
2213 # picked up the changes yet so we may continue to use a cached page until it does.
2215 my $htmlcd = "$projectroot/$project/$html_cache_dir";
2216 my $fc_mt = (stat("$htmlcd/forkchange"))[9];
2217 my $afc_mt = (stat("$htmlcd/$action.forkchange"))[9];
2218 return 1 unless defined($fc_mt) || defined($afc_mt);
2219 my $prj_mt = (stat("$cache_dir/$projlist_cache_name"))[9];
2220 return 1 unless $prj_mt;
2221 my $old_mt = $fc_mt;
2222 $old_mt = $afc_mt if !defined($old_mt) || (defined($afc_mt) && $afc_mt < $old_mt);
2223 return 1 if $old_mt > $prj_mt;
2225 # We're going to regenerate the cached page because we know the project cache
2226 # has new fork information that we cannot possibly have in our cached copy.
2228 # However, if both 'forkchange' and "$action.forkchange" exist and one of
2229 # them is older than the project cache and one of them is newer, we still
2230 # need to regenerate the page cache, but we will also need to do it again
2231 # in the future because there's yet another fork update not yet in the cache.
2233 # So we make sure to touch "$action.changed" to force a cache regeneration
2234 # and then we remove either or both of 'forkchange'/"$action.forkchange" if
2235 # they're older than the project cache (they've served their purpose, we're
2236 # forcing a page regeneration by touching "$action.changed" but the project
2237 # cache was rebuilt since then so there are no more pending fork updates to
2238 # pick up in the future and they need to go).
2240 # For best results, the external code that touches 'forkchange' should always
2241 # touch 'forkchange' and additionally touch 'summary.forkchange' but only
2242 # if it does not already exist. That way the cached page will be regenerated
2243 # each time it's requested and ANY fork updates are available in the proj
2244 # cache rather than waiting until they all are before updating.
2246 # Note that we take a shortcut here and will zap 'forkchange' since we know
2247 # that it only affects the 'summary' cache. If, in the future, it affects
2248 # other cache types, it will first need to be propogated down to
2249 # "$action.forkchange" for those types before we zap it.
2252 open $fd, '>', "$htmlcd/$action.changed" and close $fd;
2253 $fc_mt=undef, unlink "$htmlcd/forkchange" if defined $fc_mt && $fc_mt < $prj_mt;
2254 $afc_mt=undef, unlink "$htmlcd/$action.forkchange" if defined $afc_mt && $afc_mt < $prj_mt;
2256 # Now we propagate 'forkchange' to "$action.forkchange" if we have the
2257 # one and not the other.
2259 if (defined $fc_mt && ! defined $afc_mt) {
2260 open $fd, '>', "$htmlcd/$action.forkchange" and close $fd;
2261 -e
"$htmlcd/$action.forkchange" and
2262 utime($fc_mt, $fc_mt, "$htmlcd/$action.forkchange") and
2263 unlink "$htmlcd/forkchange";
2269 sub cached_action_page
{
2272 return undef unless $action && $html_cache_actions{$action} && $html_cache_dir;
2273 my $htmlcd = "$projectroot/$project/$html_cache_dir";
2274 return undef if -e
"$htmlcd/changed" || -e
"$htmlcd/$action.changed";
2275 return undef unless cached_page_precondition_check
($action);
2276 open my $fd, '<', "$htmlcd/$action" or return undef;
2279 my $cached_page = <$fd>;
2280 close $fd or return undef;
2281 return $cached_page;
2284 package Git
::Gitweb
::CacheFile
;
2287 use POSIX
qw(:fcntl_h);
2289 my $cachefile = shift;
2291 sysopen(my $self, $cachefile, O_WRONLY
|O_CREAT
|O_EXCL
, 0664)
2293 $$self->{'cachefile'} = $cachefile;
2294 $$self->{'opened'} = 1;
2295 $$self->{'contents'} = '';
2296 return bless $self, $class;
2301 if ($$self->{'opened'}) {
2302 $$self->{'opened'} = 0;
2303 my $result = close $self;
2304 unlink $$self->{'cachefile'} unless $result;
2312 if ($$self->{'opened'}) {
2313 $self->CLOSE() and unlink $$self->{'cachefile'};
2319 @_ = (map {my $x=$_; utf8
::encode
($x); $x} @_) unless $fcgi_raw_mode;
2320 print $self @_ if $$self->{'opened'};
2321 $$self->{'contents'} .= join('', @_);
2327 my $template = shift;
2328 return $self->PRINT(sprintf $template, @_);
2333 return $$self->{'contents'};
2338 # Caller is responsible for preserving STDOUT beforehand if needed
2339 sub cached_action_start
{
2342 return undef unless $html_cache_actions{$action} && $html_cache_dir;
2343 my $htmlcd = "$projectroot/$project/$html_cache_dir";
2344 return undef unless -d
$htmlcd;
2345 if (-e
"$htmlcd/changed") {
2346 foreach my $cacheable (keys(%html_cache_actions)) {
2347 next unless $supported_cache_actions{$cacheable} &&
2348 $html_cache_actions{$cacheable};
2350 open $fd, '>', "$htmlcd/$cacheable.changed"
2353 unlink "$htmlcd/changed";
2356 tie
*CACHEFILE
, 'Git::Gitweb::CacheFile', "$htmlcd/$action.lock" or return undef;
2357 *STDOUT
= *CACHEFILE
;
2358 unlink "$htmlcd/$action", "$htmlcd/$action.changed";
2362 # Caller is responsible for restoring STDOUT afterward if needed
2363 sub cached_action_finish
{
2368 my $obj = tied *STDOUT
;
2369 return undef unless ref($obj) eq 'Git::Gitweb::CacheFile';
2370 my $cached_page = $obj->contents;
2371 (my $result = close(STDOUT
)) or warn "couldn't close cache file on STDOUT: $!";
2372 # Do not leave STDOUT file descriptor invalid!
2374 open(NULL
, '>', File
::Spec
->devnull) or die "couldn't open NULL to devnull: $!";
2376 return $cached_page unless $result;
2377 my $htmlcd = "$projectroot/$project/$html_cache_dir";
2378 return $cached_page unless -d
$htmlcd;
2379 unlink "$htmlcd/$action.lock" unless rename "$htmlcd/$action.lock", "$htmlcd/$action";
2380 return $cached_page;
2384 BEGIN {%expand_pi_subs = (
2385 'age_string' => \
&age_string
,
2386 'age_string_date' => \
&age_string_date
,
2387 'age_string_age' => \
&age_string_age
,
2388 'compute_timed_interval' => \
&compute_timed_interval
,
2389 'compute_commands_count' => \
&compute_commands_count
,
2390 'format_lastrefresh_row' => \
&format_lastrefresh_row
,
2393 # Expands any <?gitweb...> processing instructions and returns the result
2394 sub expand_gitweb_pi
{
2397 my @time_now = gettimeofday
();
2398 $page =~ s
{<\?gitweb
(?
:\s
+([^\s
>]+)([^>]*))?\s
*\?>}
2400 (ref($expand_pi_subs{$1}) eq 'CODE' ?
2401 $expand_pi_subs{$1}->(split(' ',$2), @time_now) :
2407 ## ----------------------------------------------------------------------
2408 ## HTML aware string manipulation
2410 # Try to chop given string on a word boundary between position
2411 # $len and $len+$add_len. If there is no word boundary there,
2412 # chop at $len+$add_len. Do not chop if chopped part plus ellipsis
2413 # (marking chopped part) would be longer than given string.
2417 my $add_len = shift || 10;
2418 my $where = shift || 'right'; # 'left' | 'center' | 'right'
2420 # Make sure perl knows it is utf8 encoded so we don't
2421 # cut in the middle of a utf8 multibyte char.
2422 $str = to_utf8
($str);
2424 # allow only $len chars, but don't cut a word if it would fit in $add_len
2425 # if it doesn't fit, cut it if it's still longer than the dots we would add
2426 # remove chopped character entities entirely
2428 # when chopping in the middle, distribute $len into left and right part
2429 # return early if chopping wouldn't make string shorter
2430 if ($where eq 'center') {
2431 return $str if ($len + 5 >= length($str)); # filler is length 5
2434 return $str if ($len + 4 >= length($str)); # filler is length 4
2437 # regexps: ending and beginning with word part up to $add_len
2438 my $endre = qr/.{$len}\w{0,$add_len}/;
2439 my $begre = qr/\w{0,$add_len}.{$len}/;
2441 if ($where eq 'left') {
2442 $str =~ m/^(.*?)($begre)$/;
2443 my ($lead, $body) = ($1, $2);
2444 if (length($lead) > 4) {
2447 return "$lead$body";
2449 } elsif ($where eq 'center') {
2450 $str =~ m/^($endre)(.*)$/;
2451 my ($left, $str) = ($1, $2);
2452 $str =~ m/^(.*?)($begre)$/;
2453 my ($mid, $right) = ($1, $2);
2454 if (length($mid) > 5) {
2457 return "$left$mid$right";
2460 $str =~ m/^($endre)(.*)$/;
2463 if (length($tail) > 4) {
2466 return "$body$tail";
2470 # pass-through email filter, obfuscating it when possible
2471 sub email_obfuscate
{
2475 $str = $email->escape_html($str);
2476 # Stock HTML::Email::Obfuscate version likes to produce
2478 $str =~ s
#<(/?)B>#<$1b>#g;
2481 $str = esc_html
($str);
2482 $str =~ s/@/@/;
2487 # takes the same arguments as chop_str, but also wraps a <span> around the
2488 # result with a title attribute if it does get chopped. Additionally, the
2489 # string is HTML-escaped.
2490 sub chop_and_escape_str
{
2493 my $chopped = chop_str
(@_);
2494 $str = to_utf8
($str);
2495 if ($chopped eq $str) {
2496 return email_obfuscate
($chopped);
2499 $str =~ s/[[:cntrl:]]/?/g;
2500 return $cgi->span({-title
=>$str}, email_obfuscate
($chopped));
2504 # Highlight selected fragments of string, using given CSS class,
2505 # and escape HTML. It is assumed that fragments do not overlap.
2506 # Regions are passed as list of pairs (array references).
2508 # Example: esc_html_hl_regions("foobar", "mark", [ 0, 3 ]) returns
2509 # '<span class="mark">foo</span>bar'
2510 sub esc_html_hl_regions
{
2511 my ($str, $css_class, @sel) = @_;
2512 my %opts = grep { ref($_) ne 'ARRAY' } @sel;
2513 @sel = grep { ref($_) eq 'ARRAY' } @sel;
2514 return esc_html
($str, %opts) unless @sel;
2520 my ($begin, $end) = @
$s;
2522 # Don't create empty <span> elements.
2523 next if $end <= $begin;
2525 my $escaped = esc_html
(substr($str, $begin, $end - $begin),
2528 $out .= esc_html
(substr($str, $pos, $begin - $pos), %opts)
2529 if ($begin - $pos > 0);
2530 $out .= $cgi->span({-class => $css_class}, $escaped);
2534 $out .= esc_html
(substr($str, $pos), %opts)
2535 if ($pos < length($str));
2540 # return positions of beginning and end of each match
2542 my ($str, $regexp) = @_;
2543 return unless (defined $str && defined $regexp);
2546 while ($str =~ /$regexp/g) {
2547 push @matches, [$-[0], $+[0]];
2552 # highlight match (if any), and escape HTML
2553 sub esc_html_match_hl
{
2554 my ($str, $regexp) = @_;
2555 return esc_html
($str) unless defined $regexp;
2557 my @matches = matchpos_list
($str, $regexp);
2558 return esc_html
($str) unless @matches;
2560 return esc_html_hl_regions
($str, 'match', @matches);
2564 # highlight match (if any) of shortened string, and escape HTML
2565 sub esc_html_match_hl_chopped
{
2566 my ($str, $chopped, $regexp) = @_;
2567 return esc_html_match_hl
($str, $regexp) unless defined $chopped;
2569 my @matches = matchpos_list
($str, $regexp);
2570 return esc_html
($chopped) unless @matches;
2572 # filter matches so that we mark chopped string
2573 my $tail = "... "; # see chop_str
2574 unless ($chopped =~ s/\Q$tail\E$//) {
2577 my $chop_len = length($chopped);
2578 my $tail_len = length($tail);
2581 for my $m (@matches) {
2582 if ($m->[0] > $chop_len) {
2583 push @filtered, [ $chop_len, $chop_len + $tail_len ] if ($tail_len > 0);
2585 } elsif ($m->[1] > $chop_len) {
2586 push @filtered, [ $m->[0], $chop_len + $tail_len ];
2592 return esc_html_hl_regions
($chopped . $tail, 'match', @filtered);
2595 ## ----------------------------------------------------------------------
2596 ## functions returning short strings
2598 # CSS class for given age epoch value (in seconds)
2599 # and reference time (optional, defaults to now) as second value
2601 my ($age_epoch, $time_now) = @_;
2602 return "noage" unless defined $age_epoch;
2603 defined $time_now or $time_now = time;
2604 my $age = $time_now - $age_epoch;
2606 if ($age < 60*60*2) {
2608 } elsif ($age < 60*60*24*2) {
2615 # convert age epoch in seconds to "nn units ago" string
2616 # reference time used is now unless second argument passed in
2617 # to get the old behavior, pass 0 as the first argument and
2618 # the time in seconds as the second
2620 my ($age_epoch, $time_now) = @_;
2621 return "unknown" unless defined $age_epoch;
2622 return "<?gitweb age_string $age_epoch?>" if $cache_mode_active;
2623 defined $time_now or $time_now = time;
2624 my $age = $time_now - $age_epoch;
2627 if ($age > 60*60*24*365*2) {
2628 $age_str = (int $age/60/60/24/365);
2629 $age_str .= " years ago";
2630 } elsif ($age > 60*60*24*(365/12)*2) {
2631 $age_str = int $age/60/60/24/(365/12);
2632 $age_str .= " months ago";
2633 } elsif ($age > 60*60*24*7*2) {
2634 $age_str = int $age/60/60/24/7;
2635 $age_str .= " weeks ago";
2636 } elsif ($age > 60*60*24*2) {
2637 $age_str = int $age/60/60/24;
2638 $age_str .= " days ago";
2639 } elsif ($age > 60*60*2) {
2640 $age_str = int $age/60/60;
2641 $age_str .= " hours ago";
2642 } elsif ($age > 60*2) {
2643 $age_str = int $age/60;
2644 $age_str .= " min ago";
2645 } elsif ($age > 2) {
2646 $age_str = int $age;
2647 $age_str .= " sec ago";
2649 $age_str .= " right now";
2654 # returns age_string if the age is <= 2 weeks otherwise an absolute date
2655 # this is typically shown to the user directly with the age_string_age as a title
2656 sub age_string_date
{
2657 my ($age_epoch, $time_now) = @_;
2658 return "unknown" unless defined $age_epoch;
2659 return "<?gitweb age_string_date $age_epoch?>" if $cache_mode_active;
2660 defined $time_now or $time_now = time;
2661 my $age = $time_now - $age_epoch;
2663 if ($age > 60*60*24*7*2) {
2664 my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($age_epoch);
2665 return sprintf "%4i-%02u-%02i", 1900 + $year, $mon+1, $mday;
2667 return age_string
($age_epoch, $time_now);
2671 # returns an absolute date if the age is <= 2 weeks otherwise age_string
2672 # this is typically used for the 'title' attribute so it will show as a tooltip
2673 sub age_string_age
{
2674 my ($age_epoch, $time_now) = @_;
2675 return "unknown" unless defined $age_epoch;
2676 return "<?gitweb age_string_age $age_epoch?>" if $cache_mode_active;
2677 defined $time_now or $time_now = time;
2678 my $age = $time_now - $age_epoch;
2680 if ($age > 60*60*24*7*2) {
2681 return age_string
($age_epoch, $time_now);
2683 my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($age_epoch);
2684 return sprintf "%4i-%02u-%02i", 1900 + $year, $mon+1, $mday;
2689 S_IFINVALID
=> 0030000,
2690 S_IFGITLINK
=> 0160000,
2693 # submodule/subproject, a commit object reference
2697 return (($mode & S_IFMT
) == S_IFGITLINK
)
2700 # convert file mode in octal to symbolic file mode string
2702 my $mode = oct shift;
2704 if (S_ISGITLINK
($mode)) {
2705 return 'm---------';
2706 } elsif (S_ISDIR
($mode & S_IFMT
)) {
2707 return 'drwxr-xr-x';
2708 } elsif (S_ISLNK
($mode)) {
2709 return 'lrwxrwxrwx';
2710 } elsif (S_ISREG
($mode)) {
2711 # git cares only about the executable bit
2712 if ($mode & S_IXUSR
) {
2713 return '-rwxr-xr-x';
2715 return '-rw-r--r--';
2718 return '----------';
2722 # convert file mode in octal to file type string
2726 if ($mode !~ m/^[0-7]+$/) {
2732 if (S_ISGITLINK
($mode)) {
2734 } elsif (S_ISDIR
($mode & S_IFMT
)) {
2736 } elsif (S_ISLNK
($mode)) {
2738 } elsif (S_ISREG
($mode)) {
2745 # convert file mode in octal to file type description string
2746 sub file_type_long
{
2749 if ($mode !~ m/^[0-7]+$/) {
2755 if (S_ISGITLINK
($mode)) {
2757 } elsif (S_ISDIR
($mode & S_IFMT
)) {
2759 } elsif (S_ISLNK
($mode)) {
2761 } elsif (S_ISREG
($mode)) {
2762 if ($mode & S_IXUSR
) {
2763 return "executable";
2773 ## ----------------------------------------------------------------------
2774 ## functions returning short HTML fragments, or transforming HTML fragments
2775 ## which don't belong to other sections
2777 # format line of commit message.
2778 sub format_log_line_html
{
2781 $line = esc_html
($line, -nbsp
=>1);
2785 # The output of "git describe", e.g. v2.10.0-297-gf6727b0
2786 # or hadoop-20160921-113441-20-g094fb7d
2787 (?
<!-) # see strbuf_check_tag_ref(). Tags can't start with -
2789 (?
!\
.) # refs can't end with ".", see check_refname_format()
2792 # Just a normal looking Git SHA1
2797 $cgi->a({-href
=> href
(action
=>"object", hash
=>$1),
2798 -class => "text"}, $1);
2799 }egx
unless $line =~ /^\s*git-svn-id:/;
2804 # format marker of refs pointing to given object
2806 # the destination action is chosen based on object type and current context:
2807 # - for annotated tags, we choose the tag view unless it's the current view
2808 # already, in which case we go to shortlog view
2809 # - for other refs, we keep the current view if we're in history, shortlog or
2810 # log view, and select shortlog otherwise
2811 sub format_ref_marker
{
2812 my ($refs, $id) = @_;
2815 if (defined $refs->{$id}) {
2816 foreach my $ref (@
{$refs->{$id}}) {
2817 # this code exploits the fact that non-lightweight tags are the
2818 # only indirect objects, and that they are the only objects for which
2819 # we want to use tag instead of shortlog as action
2820 my ($type, $name) = qw();
2821 my $indirect = ($ref =~ s/\^\{\}$//);
2822 # e.g. tags/v2.6.11 or heads/next
2823 if ($ref =~ m!^(.*?)s?/(.*)$!) {
2832 $class .= " indirect" if $indirect;
2834 my $dest_action = "shortlog";
2837 $dest_action = "tag" unless $action eq "tag";
2838 } elsif ($action =~ /^(history|(short)?log)$/) {
2839 $dest_action = $action;
2843 $dest .= "refs/" unless $ref =~ m
!^refs
/!;
2846 my $link = $cgi->a({
2848 action
=>$dest_action,
2850 )}, esc_html
($name));
2852 $markers .= "<span class=\"".esc_attr
($class)."\" title=\"".esc_attr
($ref)."\">" .
2858 return '<span class="refs">'. $markers . '</span>';
2864 # format, perhaps shortened and with markers, title line
2865 sub format_subject_html
{
2866 my ($long, $short, $href, $extra) = @_;
2867 $extra = '' unless defined($extra);
2869 if (length($short) < length($long)) {
2871 $long =~ s/[[:cntrl:]]/?/g;
2872 return $cgi->a({-href
=> $href, -class => "list subject",
2873 -title
=> to_utf8
($long)},
2874 esc_html
($short)) . $extra;
2876 return $cgi->a({-href
=> $href, -class => "list subject"},
2877 esc_html
($long)) . $extra;
2881 # Rather than recomputing the url for an email multiple times, we cache it
2882 # after the first hit. This gives a visible benefit in views where the avatar
2883 # for the same email is used repeatedly (e.g. shortlog).
2884 # The cache is shared by all avatar engines (currently gravatar only), which
2885 # are free to use it as preferred. Since only one avatar engine is used for any
2886 # given page, there's no risk for cache conflicts.
2887 our %avatar_cache = ();
2889 # Compute the picon url for a given email, by using the picon search service over at
2890 # http://www.cs.indiana.edu/picons/search.html
2892 my $email = lc shift;
2893 if (!$avatar_cache{$email}) {
2894 my ($user, $domain) = split('@', $email);
2895 $avatar_cache{$email} =
2896 "//www.cs.indiana.edu/cgi-pub/kinzler/piconsearch.cgi/" .
2898 "users+domains+unknown/up/single";
2900 return $avatar_cache{$email};
2903 # Compute the gravatar url for a given email, if it's not in the cache already.
2904 # Gravatar stores only the part of the URL before the size, since that's the
2905 # one computationally more expensive. This also allows reuse of the cache for
2906 # different sizes (for this particular engine).
2908 my $email = lc shift;
2910 $avatar_cache{$email} ||=
2911 "//www.gravatar.com/avatar/" .
2912 Digest
::MD5
::md5_hex
($email) . "?s=";
2913 return $avatar_cache{$email} . $size;
2916 # Insert an avatar for the given $email at the given $size if the feature
2918 sub git_get_avatar
{
2919 my ($email, %opts) = @_;
2920 my $pre_white = ($opts{-pad_before
} ?
" " : "");
2921 my $post_white = ($opts{-pad_after
} ?
" " : "");
2922 $opts{-size
} ||= 'default';
2923 my $size = $avatar_size{$opts{-size
}} || $avatar_size{'default'};
2925 if ($git_avatar eq 'gravatar') {
2926 $url = gravatar_url
($email, $size);
2927 } elsif ($git_avatar eq 'picon') {
2928 $url = picon_url
($email);
2930 # Other providers can be added by extending the if chain, defining $url
2931 # as needed. If no variant puts something in $url, we assume avatars
2932 # are completely disabled/unavailable.
2935 "<img width=\"$size\" " .
2936 "class=\"avatar\" " .
2937 "src=\"".esc_url
($url)."\" " .
2945 sub format_search_author
{
2946 my ($author, $searchtype, $displaytext) = @_;
2947 my $have_search = gitweb_check_feature
('search');
2951 if ($searchtype eq 'author') {
2952 $performed = "authored";
2953 } elsif ($searchtype eq 'committer') {
2954 $performed = "committed";
2957 return $cgi->a({-href
=> href
(action
=>"search", hash
=>$hash,
2958 searchtext
=>$author,
2959 searchtype
=>$searchtype), class=>"list",
2960 title
=>"Search for commits $performed by $author"},
2964 return $displaytext;
2968 # format the author name of the given commit with the given tag
2969 # the author name is chopped and escaped according to the other
2970 # optional parameters (see chop_str).
2971 sub format_author_html
{
2974 my $author = chop_and_escape_str
($co->{'author_name'}, @_);
2975 return "<$tag class=\"author\">" .
2976 format_search_author
($co->{'author_name'}, "author",
2977 git_get_avatar
($co->{'author_email'}, -pad_after
=> 1) .
2982 # format git diff header line, i.e. "diff --(git|combined|cc) ..."
2983 sub format_git_diff_header_line
{
2985 my $diffinfo = shift;
2986 my ($from, $to) = @_;
2988 if ($diffinfo->{'nparents'}) {
2990 $line =~ s!^(diff (.*?) )"?.*$!$1!;
2991 if ($to->{'href'}) {
2992 $line .= $cgi->a({-href
=> $to->{'href'}, -class => "path"},
2993 esc_path
($to->{'file'}));
2994 } else { # file was deleted (no href)
2995 $line .= esc_path
($to->{'file'});
2999 $line =~ s!^(diff (.*?) )"?a/.*$!$1!;
3000 if ($from->{'href'}) {
3001 $line .= $cgi->a({-href
=> $from->{'href'}, -class => "path"},
3002 'a/' . esc_path
($from->{'file'}));
3003 } else { # file was added (no href)
3004 $line .= 'a/' . esc_path
($from->{'file'});
3007 if ($to->{'href'}) {
3008 $line .= $cgi->a({-href
=> $to->{'href'}, -class => "path"},
3009 'b/' . esc_path
($to->{'file'}));
3010 } else { # file was deleted
3011 $line .= 'b/' . esc_path
($to->{'file'});
3015 return "<div class=\"diff header\">$line</div>\n";
3018 # format extended diff header line, before patch itself
3019 sub format_extended_diff_header_line
{
3021 my $diffinfo = shift;
3022 my ($from, $to) = @_;
3025 if ($line =~ s!^((copy|rename) from ).*$!$1! && $from->{'href'}) {
3026 $line .= $cgi->a({-href
=>$from->{'href'}, -class=>"path"},
3027 esc_path
($from->{'file'}));
3029 if ($line =~ s!^((copy|rename) to ).*$!$1! && $to->{'href'}) {
3030 $line .= $cgi->a({-href
=>$to->{'href'}, -class=>"path"},
3031 esc_path
($to->{'file'}));
3033 # match single <mode>
3034 if ($line =~ m/\s(\d{6})$/) {
3035 $line .= '<span class="info"> (' .
3036 file_type_long
($1) .
3040 if ($line =~ m/^index [0-9a-fA-F]{40},[0-9a-fA-F]{40}/) {
3041 # can match only for combined diff
3043 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
3044 if ($from->{'href'}[$i]) {
3045 $line .= $cgi->a({-href
=>$from->{'href'}[$i],
3047 substr($diffinfo->{'from_id'}[$i],0,7));
3052 $line .= ',' if ($i < $diffinfo->{'nparents'} - 1);
3055 if ($to->{'href'}) {
3056 $line .= $cgi->a({-href
=>$to->{'href'}, -class=>"hash"},
3057 substr($diffinfo->{'to_id'},0,7));
3062 } elsif ($line =~ m/^index [0-9a-fA-F]{40}..[0-9a-fA-F]{40}/) {
3063 # can match only for ordinary diff
3064 my ($from_link, $to_link);
3065 if ($from->{'href'}) {
3066 $from_link = $cgi->a({-href
=>$from->{'href'}, -class=>"hash"},
3067 substr($diffinfo->{'from_id'},0,7));
3069 $from_link = '0' x
7;
3071 if ($to->{'href'}) {
3072 $to_link = $cgi->a({-href
=>$to->{'href'}, -class=>"hash"},
3073 substr($diffinfo->{'to_id'},0,7));
3077 my ($from_id, $to_id) = ($diffinfo->{'from_id'}, $diffinfo->{'to_id'});
3078 $line =~ s!$from_id\.\.$to_id!$from_link..$to_link!;
3081 return $line . "<br/>\n";
3084 # format from-file/to-file diff header
3085 sub format_diff_from_to_header
{
3086 my ($from_line, $to_line, $diffinfo, $from, $to, @parents) = @_;
3091 #assert($line =~ m/^---/) if DEBUG;
3092 # no extra formatting for "^--- /dev/null"
3093 if (! $diffinfo->{'nparents'}) {
3094 # ordinary (single parent) diff
3095 if ($line =~ m!^--- "?a/!) {
3096 if ($from->{'href'}) {
3098 $cgi->a({-href
=>$from->{'href'}, -class=>"path"},
3099 esc_path
($from->{'file'}));
3102 esc_path
($from->{'file'});
3105 $result .= qq!<div
class="diff from_file">$line</div
>\n!;
3108 # combined diff (merge commit)
3109 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
3110 if ($from->{'href'}[$i]) {
3112 $cgi->a({-href
=>href
(action
=>"blobdiff",
3113 hash_parent
=>$diffinfo->{'from_id'}[$i],
3114 hash_parent_base
=>$parents[$i],
3115 file_parent
=>$from->{'file'}[$i],
3116 hash
=>$diffinfo->{'to_id'},
3118 file_name
=>$to->{'file'}),
3120 -title
=>"diff" . ($i+1)},
3123 $cgi->a({-href
=>$from->{'href'}[$i], -class=>"path"},
3124 esc_path
($from->{'file'}[$i]));
3126 $line = '--- /dev/null';
3128 $result .= qq!<div
class="diff from_file">$line</div
>\n!;
3133 #assert($line =~ m/^\+\+\+/) if DEBUG;
3134 # no extra formatting for "^+++ /dev/null"
3135 if ($line =~ m!^\+\+\+ "?b/!) {
3136 if ($to->{'href'}) {
3138 $cgi->a({-href
=>$to->{'href'}, -class=>"path"},
3139 esc_path
($to->{'file'}));
3142 esc_path
($to->{'file'});
3145 $result .= qq!<div
class="diff to_file">$line</div
>\n!;
3150 # create note for patch simplified by combined diff
3151 sub format_diff_cc_simplified
{
3152 my ($diffinfo, @parents) = @_;
3155 $result .= "<div class=\"diff header\">" .
3157 if (!is_deleted
($diffinfo)) {
3158 $result .= $cgi->a({-href
=> href
(action
=>"blob",
3160 hash
=>$diffinfo->{'to_id'},
3161 file_name
=>$diffinfo->{'to_file'}),
3163 esc_path
($diffinfo->{'to_file'}));
3165 $result .= esc_path
($diffinfo->{'to_file'});
3167 $result .= "</div>\n" . # class="diff header"
3168 "<div class=\"diff nodifferences\">" .
3170 "</div>\n"; # class="diff nodifferences"
3175 sub diff_line_class
{
3176 my ($line, $from, $to) = @_;
3181 if ($from && $to && ref($from->{'href'}) eq "ARRAY") {
3182 $num_sign = scalar @
{$from->{'href'}};
3185 my @diff_line_classifier = (
3186 { regexp
=> qr/^\@\@{$num_sign} /, class => "chunk_header"},
3187 { regexp
=> qr/^\\/, class => "incomplete" },
3188 { regexp
=> qr/^ {$num_sign}/, class => "ctx" },
3189 # classifier for context must come before classifier add/rem,
3190 # or we would have to use more complicated regexp, for example
3191 # qr/(?= {0,$m}\+)[+ ]{$num_sign}/, where $m = $num_sign - 1;
3192 { regexp
=> qr/^[+ ]{$num_sign}/, class => "add" },
3193 { regexp
=> qr/^[- ]{$num_sign}/, class => "rem" },
3195 for my $clsfy (@diff_line_classifier) {
3196 return $clsfy->{'class'}
3197 if ($line =~ $clsfy->{'regexp'});
3204 # assumes that $from and $to are defined and correctly filled,
3205 # and that $line holds a line of chunk header for unified diff
3206 sub format_unidiff_chunk_header
{
3207 my ($line, $from, $to) = @_;
3209 my ($from_text, $from_start, $from_lines, $to_text, $to_start, $to_lines, $section) =
3210 $line =~ m/^\@{2} (-(\d+)(?:,(\d+))?) (\+(\d+)(?:,(\d+))?) \@{2}(.*)$/;
3212 $from_lines = 0 unless defined $from_lines;
3213 $to_lines = 0 unless defined $to_lines;
3215 if ($from->{'href'}) {
3216 $from_text = $cgi->a({-href
=>"$from->{'href'}#l$from_start",
3217 -class=>"list"}, $from_text);
3219 if ($to->{'href'}) {
3220 $to_text = $cgi->a({-href
=>"$to->{'href'}#l$to_start",
3221 -class=>"list"}, $to_text);
3223 $line = "<span class=\"chunk_info\">@@ $from_text $to_text @@</span>" .
3224 "<span class=\"section\">" . esc_html
($section, -nbsp
=>1) . "</span>";
3228 # assumes that $from and $to are defined and correctly filled,
3229 # and that $line holds a line of chunk header for combined diff
3230 sub format_cc_diff_chunk_header
{
3231 my ($line, $from, $to) = @_;
3233 my ($prefix, $ranges, $section) = $line =~ m/^(\@+) (.*?) \@+(.*)$/;
3234 my (@from_text, @from_start, @from_nlines, $to_text, $to_start, $to_nlines);
3236 @from_text = split(' ', $ranges);
3237 for (my $i = 0; $i < @from_text; ++$i) {
3238 ($from_start[$i], $from_nlines[$i]) =
3239 (split(',', substr($from_text[$i], 1)), 0);
3242 $to_text = pop @from_text;
3243 $to_start = pop @from_start;
3244 $to_nlines = pop @from_nlines;
3246 $line = "<span class=\"chunk_info\">$prefix ";
3247 for (my $i = 0; $i < @from_text; ++$i) {
3248 if ($from->{'href'}[$i]) {
3249 $line .= $cgi->a({-href
=>"$from->{'href'}[$i]#l$from_start[$i]",
3250 -class=>"list"}, $from_text[$i]);
3252 $line .= $from_text[$i];
3256 if ($to->{'href'}) {
3257 $line .= $cgi->a({-href
=>"$to->{'href'}#l$to_start",
3258 -class=>"list"}, $to_text);
3262 $line .= " $prefix</span>" .
3263 "<span class=\"section\">" . esc_html
($section, -nbsp
=>1) . "</span>";
3267 # process patch (diff) line (not to be used for diff headers),
3268 # returning HTML-formatted (but not wrapped) line.
3269 # If the line is passed as a reference, it is treated as HTML and not
3271 sub format_diff_line
{
3272 my ($line, $diff_class, $from, $to) = @_;
3278 $line = untabify
($line);
3280 if ($from && $to && $line =~ m/^\@{2} /) {
3281 $line = format_unidiff_chunk_header
($line, $from, $to);
3282 } elsif ($from && $to && $line =~ m/^\@{3}/) {
3283 $line = format_cc_diff_chunk_header
($line, $from, $to);
3285 $line = esc_html
($line, -nbsp
=>1);
3289 my $diff_classes = "diff diff_body";
3290 $diff_classes .= " $diff_class" if ($diff_class);
3291 $line = "<div class=\"$diff_classes\">$line</div>\n";
3296 # Generates undef or something like "_snapshot_" or "snapshot (_tbz2_ _zip_)",
3297 # linked. Pass the hash of the tree/commit to snapshot.
3298 sub format_snapshot_links
{
3300 my $num_fmts = @snapshot_fmts;
3301 if ($num_fmts > 1) {
3302 # A parenthesized list of links bearing format names.
3303 # e.g. "snapshot (_tar.gz_ _zip_)"
3304 return "snapshot (" . join(' ', map
3311 }, $known_snapshot_formats{$_}{'display'})
3312 , @snapshot_fmts) . ")";
3313 } elsif ($num_fmts == 1) {
3314 # A single "snapshot" link whose tooltip bears the format name.
3316 my ($fmt) = @snapshot_fmts;
3322 snapshot_format
=>$fmt
3324 -title
=> "in format: $known_snapshot_formats{$fmt}{'display'}"
3326 } else { # $num_fmts == 0
3331 ## ......................................................................
3332 ## functions returning values to be passed, perhaps after some
3333 ## transformation, to other functions; e.g. returning arguments to href()
3335 # returns hash to be passed to href to generate gitweb URL
3336 # in -title key it returns description of link
3338 my $format = shift || 'Atom';
3339 my %res = (action
=> lc($format));
3340 my $matched_ref = 0;
3342 # feed links are possible only for project views
3343 return unless (defined $project);
3344 # some views should link to OPML, or to generic project feed,
3345 # or don't have specific feed yet (so they should use generic)
3346 return if (!$action || $action =~ /^(?:tags|heads|forks|tag|search)$/x);
3349 # branches refs uses 'refs/' + $get_branch_refs()[x] + '/' prefix
3350 # (fullname) to differentiate from tag links; this also makes
3351 # possible to detect branch links
3352 for my $ref (get_branch_refs
()) {
3353 if ((defined $hash_base && $hash_base =~ m!^refs/\Q$ref\E/(.*)$!) ||
3354 (defined $hash && $hash =~ m!^refs/\Q$ref\E/(.*)$!)) {
3356 $matched_ref = $ref;
3360 # find log type for feed description (title)
3362 if (defined $file_name) {
3363 $type = "history of $file_name";
3364 $type .= "/" if ($action eq 'tree');
3365 $type .= " on '$branch'" if (defined $branch);
3367 $type = "log of $branch" if (defined $branch);
3370 $res{-title
} = $type;
3371 $res{'hash'} = (defined $branch ?
"refs/$matched_ref/$branch" : undef);
3372 $res{'file_name'} = $file_name;
3377 ## ----------------------------------------------------------------------
3378 ## git utility subroutines, invoking git commands
3380 # returns path to the core git executable and the --git-dir parameter as list
3382 $number_of_git_cmds++;
3383 return $GIT, '--git-dir='.$git_dir;
3386 # opens a "-|" cmd pipe handle with 2>/dev/null and returns it
3389 # In order to be compatible with FCGI mode we must use POSIX
3390 # and access the STDERR_FILENO file descriptor directly
3392 use POSIX
qw(STDERR_FILENO dup dup2);
3394 open(my $null, '>', File
::Spec
->devnull) or die "couldn't open devnull: $!";
3395 (my $saveerr = dup
(STDERR_FILENO
)) or die "couldn't dup STDERR: $!";
3396 my $dup2ok = dup2
(fileno($null), STDERR_FILENO
);
3397 close($null) or !$dup2ok or die "couldn't close NULL: $!";
3398 $dup2ok or POSIX
::close($saveerr), die "couldn't dup NULL to STDERR: $!";
3399 my $result = open(my $fd, "-|", @_);
3400 $dup2ok = dup2
($saveerr, STDERR_FILENO
);
3401 POSIX
::close($saveerr) or !$dup2ok or die "couldn't close SAVEERR: $!";
3402 $dup2ok or die "couldn't dup SAVERR to STDERR: $!";
3404 return $result ?
$fd : undef;
3407 # opens a "-|" git_cmd pipe handle with 2>/dev/null and returns it
3409 return cmd_pipe git_cmd
(), @_;
3412 # quote the given arguments for passing them to the shell
3413 # quote_command("command", "arg 1", "arg with ' and ! characters")
3414 # => "'command' 'arg 1' 'arg with '\'' and '\!' characters'"
3415 # Try to avoid using this function wherever possible.
3418 map { my $a = $_; $a =~ s/(['!])/'\\$1'/g; "'$a'" } @_ );
3421 # get HEAD ref of given project as hash
3422 sub git_get_head_hash
{
3423 return git_get_full_hash
(shift, 'HEAD');
3426 sub git_get_full_hash
{
3427 return git_get_hash
(@_);
3430 sub git_get_short_hash
{
3431 return git_get_hash
(@_, '--short=7');
3435 my ($project, $hash, @options) = @_;
3436 my $o_git_dir = $git_dir;
3438 $git_dir = "$projectroot/$project";
3439 if (defined(my $fd = git_cmd_pipe
'rev-parse',
3440 '--verify', '-q', @options, $hash)) {
3442 chomp $retval if defined $retval;
3445 if (defined $o_git_dir) {
3446 $git_dir = $o_git_dir;
3451 # get type of given object
3455 defined(my $fd = git_cmd_pipe
"cat-file", '-t', $hash) or return;
3457 close $fd or return;
3462 # repository configuration
3463 our $config_file = '';
3466 # store multiple values for single key as anonymous array reference
3467 # single values stored directly in the hash, not as [ <value> ]
3468 sub hash_set_multi
{
3469 my ($hash, $key, $value) = @_;
3471 if (!exists $hash->{$key}) {
3472 $hash->{$key} = $value;
3473 } elsif (!ref $hash->{$key}) {
3474 $hash->{$key} = [ $hash->{$key}, $value ];
3476 push @
{$hash->{$key}}, $value;
3480 # return hash of git project configuration
3481 # optionally limited to some section, e.g. 'gitweb'
3482 sub git_parse_project_config
{
3483 my $section_regexp = shift;
3488 defined(my $fh = git_cmd_pipe
"config", '-z', '-l')
3491 while (my $keyval = to_utf8
(scalar <$fh>)) {
3493 my ($key, $value) = split(/\n/, $keyval, 2);
3495 hash_set_multi
(\
%config, $key, $value)
3496 if (!defined $section_regexp || $key =~ /^(?:$section_regexp)\./o);
3503 # convert config value to boolean: 'true' or 'false'
3504 # no value, number > 0, 'true' and 'yes' values are true
3505 # rest of values are treated as false (never as error)
3506 sub config_to_bool
{
3509 return 1 if !defined $val; # section.key
3511 # strip leading and trailing whitespace
3515 return (($val =~ /^\d+$/ && $val) || # section.key = 1
3516 ($val =~ /^(?:true|yes)$/i)); # section.key = true
3519 # convert config value to simple decimal number
3520 # an optional value suffix of 'k', 'm', or 'g' will cause the value
3521 # to be multiplied by 1024, 1048576, or 1073741824
3525 # strip leading and trailing whitespace
3529 if (my ($num, $unit) = ($val =~ /^([0-9]*)([kmg])$/i)) {
3531 # unknown unit is treated as 1
3532 return $num * ($unit eq 'g' ?
1073741824 :
3533 $unit eq 'm' ?
1048576 :
3534 $unit eq 'k' ?
1024 : 1);
3539 # convert config value to array reference, if needed
3540 sub config_to_multi
{
3543 return ref($val) ?
$val : (defined($val) ?
[ $val ] : []);
3546 sub git_get_project_config
{
3547 my ($key, $type) = @_;
3549 return unless defined $git_dir;
3552 return unless ($key);
3553 # only subsection, if exists, is case sensitive,
3554 # and not lowercased by 'git config -z -l'
3555 if (my ($hi, $mi, $lo) = ($key =~ /^([^.]*)\.(.*)\.([^.]*)$/)) {
3557 $key = join(".", lc($hi), $mi, lc($lo));
3558 return if ($lo =~ /\W/ || $hi =~ /\W/);
3562 return if ($key =~ /\W/);
3564 $key =~ s/^gitweb\.//;
3567 if (defined $type) {
3570 unless ($type eq 'bool' || $type eq 'int');
3574 if (!defined $config_file ||
3575 $config_file ne "$git_dir/config") {
3576 %config = git_parse_project_config
('gitweb');
3577 $config_file = "$git_dir/config";
3580 # check if config variable (key) exists
3581 return unless exists $config{"gitweb.$key"};
3584 if (!defined $type) {
3585 return $config{"gitweb.$key"};
3586 } elsif ($type eq 'bool') {
3587 # backward compatibility: 'git config --bool' returns true/false
3588 return config_to_bool
($config{"gitweb.$key"}) ?
'true' : 'false';
3589 } elsif ($type eq 'int') {
3590 return config_to_int
($config{"gitweb.$key"});
3592 return $config{"gitweb.$key"};
3595 # get hash of given path at given ref
3596 sub git_get_hash_by_path
{
3598 my $path = shift || return undef;
3603 defined(my $fd = git_cmd_pipe
"ls-tree", $base, "--", $path)
3604 or die_error
(500, "Open git-ls-tree failed");
3605 my $line = to_utf8
(scalar <$fd>);
3606 close $fd or return undef;
3608 if (!defined $line) {
3609 # there is no tree or hash given by $path at $base
3613 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'
3614 $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t/;
3615 if (defined $type && $type ne $2) {
3616 # type doesn't match
3622 # get path of entry with given hash at given tree-ish (ref)
3623 # used to get 'from' filename for combined diff (merge commit) for renames
3624 sub git_get_path_by_hash
{
3625 my $base = shift || return;
3626 my $hash = shift || return;
3630 defined(my $fd = git_cmd_pipe
"ls-tree", '-r', '-t', '-z', $base)
3632 while (my $line = to_utf8
(scalar <$fd>)) {
3635 #'040000 tree 595596a6a9117ddba9fe379b6b012b558bac8423 gitweb'
3636 #'100644 blob e02e90f0429be0d2a69b76571101f20b8f75530f gitweb/README'
3637 if ($line =~ m/(?:[0-9]+) (?:.+) $hash\t(.+)$/) {
3646 ## ......................................................................
3647 ## git utility functions, directly accessing git repository
3649 # get the value of config variable either from file named as the variable
3650 # itself in the repository ($GIT_DIR/$name file), or from gitweb.$name
3651 # configuration variable in the repository config file.
3652 sub git_get_file_or_project_config
{
3653 my ($path, $name) = @_;
3655 $git_dir = "$projectroot/$path";
3656 open my $fd, '<', "$git_dir/$name"
3657 or return git_get_project_config
($name);
3658 my $conf = to_utf8
(scalar <$fd>);
3660 if (defined $conf) {
3666 sub git_get_project_description
{
3668 return git_get_file_or_project_config
($path, 'description');
3671 sub git_get_project_category
{
3673 return git_get_file_or_project_config
($path, 'category');
3677 # supported formats:
3678 # * $GIT_DIR/ctags/<tagname> file (in 'ctags' subdirectory)
3679 # - if its contents is a number, use it as tag weight,
3680 # - otherwise add a tag with weight 1
3681 # * $GIT_DIR/ctags file, each line is a tag (with weight 1)
3682 # the same value multiple times increases tag weight
3683 # * `gitweb.ctag' multi-valued repo config variable
3684 sub git_get_project_ctags
{
3685 my $project = shift;
3688 $git_dir = "$projectroot/$project";
3689 if (opendir my $dh, "$git_dir/ctags") {
3690 my @files = grep { -f
$_ } map { "$git_dir/ctags/$_" } readdir($dh);
3691 foreach my $tagfile (@files) {
3692 open my $ct, '<', $tagfile
3698 (my $ctag = $tagfile) =~ s
#.*/##;
3699 $ctag = to_utf8
($ctag);
3700 if ($val =~ /^\d+$/) {
3701 $ctags->{$ctag} = $val;
3703 $ctags->{$ctag} = 1;
3708 } elsif (open my $fh, '<', "$git_dir/ctags") {
3709 while (my $line = to_utf8
(scalar <$fh>)) {
3711 $ctags->{$line}++ if $line;
3716 my $taglist = config_to_multi
(git_get_project_config
('ctag'));
3717 foreach my $tag (@
$taglist) {
3725 # return hash, where keys are content tags ('ctags'),
3726 # and values are sum of weights of given tag in every project
3727 sub git_gather_all_ctags
{
3728 my $projects = shift;
3731 foreach my $p (@
$projects) {
3732 foreach my $ct (keys %{$p->{'ctags'}}) {
3733 $ctags->{$ct} += $p->{'ctags'}->{$ct};
3740 sub git_populate_project_tagcloud
{
3741 my ($ctags, $action) = @_;
3743 # First, merge different-cased tags; tags vote on casing
3745 foreach (keys %$ctags) {
3746 $ctags_lc{lc $_}->{count
} += $ctags->{$_};
3747 if (not $ctags_lc{lc $_}->{topcount
}
3748 or $ctags_lc{lc $_}->{topcount
} < $ctags->{$_}) {
3749 $ctags_lc{lc $_}->{topcount
} = $ctags->{$_};
3750 $ctags_lc{lc $_}->{topname
} = $_;
3755 my $matched = $input_params{'ctag_filter'};
3756 if (eval { require HTML
::TagCloud
; 1; }) {
3757 $cloud = HTML
::TagCloud
->new;
3758 foreach my $ctag (sort keys %ctags_lc) {
3759 # Pad the title with spaces so that the cloud looks
3761 my $title = esc_html
($ctags_lc{$ctag}->{topname
});
3762 $title =~ s/ / /g;
3763 $title =~ s/^/ /g;
3764 $title =~ s/$/ /g;
3765 if (defined $matched && $matched eq $ctag) {
3766 $title = qq(<span
class="match">$title</span
>);
3768 $cloud->add($title, href
(-replay
=>1, action
=>$action, ctag_filter
=>$ctag),
3769 $ctags_lc{$ctag}->{count
});
3773 foreach my $ctag (keys %ctags_lc) {
3774 my $title = esc_html
($ctags_lc{$ctag}->{topname
}, -nbsp
=>1);
3775 if (defined $matched && $matched eq $ctag) {
3776 $title = qq(<span
class="match">$title</span
>);
3778 $cloud->{$ctag}{count
} = $ctags_lc{$ctag}->{count
};
3779 $cloud->{$ctag}{ctag
} =
3780 $cgi->a({-href
=>href
(-replay
=>1, action
=>$action, ctag_filter
=>$ctag)}, $title);
3786 sub git_show_project_tagcloud
{
3787 my ($cloud, $count) = @_;
3788 if (ref $cloud eq 'HTML::TagCloud') {
3789 return $cloud->html_and_css($count);
3791 my @tags = sort { $cloud->{$a}->{'count'} <=> $cloud->{$b}->{'count'} } keys %$cloud;
3793 '<div id="htmltagcloud"'.($project ?
'' : ' align="center"').'>' .
3795 $cloud->{$_}->{'ctag'}
3796 } splice(@tags, 0, $count)) .
3801 sub git_get_project_url_list
{
3804 $git_dir = "$projectroot/$path";
3805 open my $fd, '<', "$git_dir/cloneurl"
3806 or return wantarray ?
3807 @
{ config_to_multi
(git_get_project_config
('url')) } :
3808 config_to_multi
(git_get_project_config
('url'));
3809 my @git_project_url_list = map { chomp; to_utf8
($_) } <$fd>;
3812 return wantarray ?
@git_project_url_list : \
@git_project_url_list;
3815 sub git_get_projects_list
{
3816 my $filter = shift || '';
3817 my $paranoid = shift;
3820 if (-d
$projects_list) {
3821 # search in directory
3822 my $dir = $projects_list;
3823 # remove the trailing "/"
3825 my $pfxlen = length("$dir");
3826 my $pfxdepth = ($dir =~ tr!/!!);
3827 # when filtering, search only given subdirectory
3828 if ($filter && !$paranoid) {
3834 follow_fast
=> 1, # follow symbolic links
3835 follow_skip
=> 2, # ignore duplicates
3836 dangling_symlinks
=> 0, # ignore dangling symlinks, silently
3839 our $project_maxdepth;
3841 # skip project-list toplevel, if we get it.
3842 return if (m!^[/.]$!);
3843 # only directories can be git repositories
3844 return unless (-d
$_);
3845 # don't traverse too deep (Find is super slow on os x)
3846 # $project_maxdepth excludes depth of $projectroot
3847 if (($File::Find
::name
=~ tr!/!!) - $pfxdepth > $project_maxdepth) {
3848 $File::Find
::prune
= 1;
3852 my $path = substr($File::Find
::name
, $pfxlen + 1);
3853 # paranoidly only filter here
3854 if ($paranoid && $filter && $path !~ m!^\Q$filter\E/!) {
3857 # we check related file in $projectroot
3858 if (check_export_ok
("$projectroot/$path")) {
3859 push @list, { path
=> $path };
3860 $File::Find
::prune
= 1;
3865 } elsif (-f
$projects_list) {
3866 # read from file(url-encoded):
3867 # 'git%2Fgit.git Linus+Torvalds'
3868 # 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin'
3869 # 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman'
3870 open my $fd, '<', $projects_list or return;
3872 while (my $line = <$fd>) {
3874 my ($path, $owner) = split ' ', $line;
3875 $path = unescape
($path);
3876 $owner = unescape
($owner);
3877 if (!defined $path) {
3880 # if $filter is rpovided, check if $path begins with $filter
3881 if ($filter && $path !~ m!^\Q$filter\E/!) {
3884 if (check_export_ok
("$projectroot/$path")) {
3889 $pr->{'owner'} = to_utf8
($owner);
3899 # written with help of Tree::Trie module (Perl Artistic License, GPL compatibile)
3900 # as side effects it sets 'forks' field to list of forks for forked projects
3901 sub filter_forks_from_projects_list
{
3902 my $projects = shift;
3904 my %trie; # prefix tree of directories (path components)
3905 # generate trie out of those directories that might contain forks
3906 foreach my $pr (@
$projects) {
3907 my $path = $pr->{'path'};
3908 $path =~ s/\.git$//; # forks of 'repo.git' are in 'repo/' directory
3909 next if ($path =~ m!/$!); # skip non-bare repositories, e.g. 'repo/.git'
3910 next unless ($path); # skip '.git' repository: tests, git-instaweb
3911 next unless (-d
"$projectroot/$path"); # containing directory exists
3912 $pr->{'forks'} = []; # there can be 0 or more forks of project
3915 my @dirs = split('/', $path);
3916 # walk the trie, until either runs out of components or out of trie
3918 while (scalar @dirs &&
3919 exists($ref->{$dirs[0]})) {
3920 $ref = $ref->{shift @dirs};
3922 # create rest of trie structure from rest of components
3923 foreach my $dir (@dirs) {
3924 $ref = $ref->{$dir} = {};
3926 # create end marker, store $pr as a data
3927 $ref->{''} = $pr if (!exists $ref->{''});
3930 # filter out forks, by finding shortest prefix match for paths
3933 foreach my $pr (@
$projects) {
3937 foreach my $dir (split('/', $pr->{'path'})) {
3938 if (exists $ref->{''}) {
3939 # found [shortest] prefix, is a fork - skip it
3940 push @
{$ref->{''}{'forks'}}, $pr;
3943 if (!exists $ref->{$dir}) {
3944 # not in trie, cannot have prefix, not a fork
3945 push @filtered, $pr;
3948 # If the dir is there, we just walk one step down the trie.
3949 $ref = $ref->{$dir};
3951 # we ran out of trie
3952 # (shouldn't happen: it's either no match, or end marker)
3953 push @filtered, $pr;
3959 # note: fill_project_list_info must be run first,
3960 # for 'descr_long' and 'ctags' to be filled
3961 sub search_projects_list
{
3962 my ($projlist, %opts) = @_;
3963 my $tagfilter = $opts{'tagfilter'};
3964 my $search_re = $opts{'search_regexp'};
3967 unless ($tagfilter || $search_re);
3969 # searching projects require filling to be run before it;
3970 fill_project_list_info
($projlist,
3971 $tagfilter ?
'ctags' : (),
3972 $search_re ?
('path', 'descr') : ());
3975 foreach my $pr (@
$projlist) {
3978 next unless ref($pr->{'ctags'}) eq 'HASH';
3980 grep { lc($_) eq lc($tagfilter) } keys %{$pr->{'ctags'}};
3984 my $path = $pr->{'path'};
3985 $path =~ s/\.git$//; # should not be included in search
3987 $path =~ /$search_re/ ||
3988 $pr->{'descr_long'} =~ /$search_re/;
3991 push @projects, $pr;
3997 our $gitweb_project_owner = undef;
3998 sub git_get_project_list_from_file
{
4000 return if (defined $gitweb_project_owner);
4002 $gitweb_project_owner = {};
4003 # read from file (url-encoded):
4004 # 'git%2Fgit.git Linus+Torvalds'
4005 # 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin'
4006 # 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman'
4007 if (-f
$projects_list) {
4008 open(my $fd, '<', $projects_list);
4009 while (my $line = <$fd>) {
4011 my ($pr, $ow) = split ' ', $line;
4012 $pr = unescape
($pr);
4013 $ow = unescape
($ow);
4014 $gitweb_project_owner->{$pr} = to_utf8
($ow);
4020 sub git_get_project_owner
{
4024 return undef unless $proj;
4025 $git_dir = "$projectroot/$proj";
4027 if (defined $project && $proj eq $project) {
4028 $owner = git_get_project_config
('owner');
4030 if (!defined $owner && !defined $gitweb_project_owner) {
4031 git_get_project_list_from_file
();
4033 if (!defined $owner && exists $gitweb_project_owner->{$proj}) {
4034 $owner = $gitweb_project_owner->{$proj};
4036 if (!defined $owner && (!defined $project || $proj ne $project)) {
4037 $owner = git_get_project_config
('owner');
4039 if (!defined $owner) {
4040 $owner = get_file_owner
("$git_dir");
4046 sub parse_activity_date
{
4049 if ($dstr =~ /^\s*([-+]?\d+)(?:\s+([-+]\d{4}))?\s*$/) {
4053 if ($dstr =~ /^\s*(\d{4})-(\d{2})-(\d{2})[Tt _](\d{1,2}):(\d{2}):(\d{2})(?:[ _]?([Zz]|(?:[-+]\d{1,2}:?\d{2})))?\s*$/) {
4054 my ($Y,$m,$d,$H,$M,$S,$z) = ($1,$2,$3,$4,$5,$6,$7||'');
4055 my $seconds = timegm
(0+$S, 0+$M, 0+$H, 0+$d, $m-1, $Y-1900);
4056 defined($z) && $z ne '' or $z = 'Z';
4058 substr($z,1,0) = '0' if length($z) == 4;
4060 if (uc($z) ne 'Z') {
4061 $off = 60 * (60 * (0+substr($z,1,2)) + (0+substr($z,3,2)));
4062 $off = -$off if substr($z,0,1) eq '-';
4064 return $seconds - $off;
4069 # If $quick is true only look at $lastactivity_file
4070 sub git_get_last_activity
{
4071 my ($path, $quick) = @_;
4074 $git_dir = "$projectroot/$path";
4075 if ($lastactivity_file && open($fd, "<", "$git_dir/$lastactivity_file")) {
4076 my $activity = <$fd>;
4078 return (undef) unless defined $activity;
4080 return (undef) if $activity eq '';
4081 if (my $timestamp = parse_activity_date
($activity)) {
4082 return ($timestamp);
4085 return (undef) if $quick;
4086 defined($fd = git_cmd_pipe
'for-each-ref',
4087 '--format=%(committer)',
4088 '--sort=-committerdate',
4090 map { "refs/$_" } get_branch_refs
()) or return;
4091 my $most_recent = <$fd>;
4092 close $fd or return (undef);
4093 if (defined $most_recent &&
4094 $most_recent =~ / (\d+) [-+][01]\d\d\d$/) {
4096 return ($timestamp);
4101 # Implementation note: when a single remote is wanted, we cannot use 'git
4102 # remote show -n' because that command always work (assuming it's a remote URL
4103 # if it's not defined), and we cannot use 'git remote show' because that would
4104 # try to make a network roundtrip. So the only way to find if that particular
4105 # remote is defined is to walk the list provided by 'git remote -v' and stop if
4106 # and when we find what we want.
4107 sub git_get_remotes_list
{
4111 my $fd = git_cmd_pipe
'remote', '-v';
4113 while (my $remote = to_utf8
(scalar <$fd>)) {
4115 $remote =~ s!\t(.*?)\s+\((\w+)\)$!!;
4116 next if $wanted and not $remote eq $wanted;
4117 my ($url, $key) = ($1, $2);
4119 $remotes{$remote} ||= { 'heads' => [] };
4120 $remotes{$remote}{$key} = $url;
4122 close $fd or return;
4123 return wantarray ?
%remotes : \
%remotes;
4126 # Takes a hash of remotes as first parameter and fills it by adding the
4127 # available remote heads for each of the indicated remotes.
4128 sub fill_remote_heads
{
4129 my $remotes = shift;
4130 my @heads = map { "remotes/$_" } keys %$remotes;
4131 my @remoteheads = git_get_heads_list
(undef, @heads);
4132 foreach my $remote (keys %$remotes) {
4133 $remotes->{$remote}{'heads'} = [ grep {
4134 $_->{'name'} =~ s!^$remote/!!
4139 sub git_get_references
{
4140 my $type = shift || "";
4142 # 5dc01c595e6c6ec9ccda4f6f69c131c0dd945f8c refs/tags/v2.6.11
4143 # c39ae07f393806ccf406ef966e9a15afc43cc36a refs/tags/v2.6.11^{}
4144 defined(my $fd = git_cmd_pipe
"show-ref", "--dereference",
4145 ($type ?
("--", "refs/$type") : ())) # use -- <pattern> if $type
4148 while (my $line = to_utf8
(scalar <$fd>)) {
4150 if ($line =~ m!^([0-9a-fA-F]{40})\srefs/($type.*)$!) {
4151 if (defined $refs{$1}) {
4152 push @
{$refs{$1}}, $2;
4158 close $fd or return;
4162 sub git_get_rev_name_tags
{
4163 my $hash = shift || return undef;
4165 defined(my $fd = git_cmd_pipe
"name-rev", "--tags", $hash)
4167 my $name_rev = to_utf8
(scalar <$fd>);
4170 if ($name_rev =~ m
|^$hash tags
/(.*)$|) {
4173 # catches also '$hash undefined' output
4178 ## ----------------------------------------------------------------------
4179 ## parse to hash functions
4183 my $tz = shift || "-0000";
4186 my @months = ("Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec");
4187 my @days = ("Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat");
4188 my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($epoch);
4189 $date{'hour'} = $hour;
4190 $date{'minute'} = $min;
4191 $date{'mday'} = $mday;
4192 $date{'day'} = $days[$wday];
4193 $date{'month'} = $months[$mon];
4194 $date{'rfc2822'} = sprintf "%s, %d %s %4d %02d:%02d:%02d +0000",
4195 $days[$wday], $mday, $months[$mon], 1900+$year, $hour ,$min, $sec;
4196 $date{'mday-time'} = sprintf "%d %s %02d:%02d",
4197 $mday, $months[$mon], $hour ,$min;
4198 $date{'iso-8601'} = sprintf "%04d-%02d-%02dT%02d:%02d:%02dZ",
4199 1900+$year, 1+$mon, $mday, $hour ,$min, $sec;
4201 my ($tz_sign, $tz_hour, $tz_min) =
4202 ($tz =~ m/^([-+])(\d\d)(\d\d)$/);
4203 $tz_sign = ($tz_sign eq '-' ?
-1 : +1);
4204 my $local = $epoch + $tz_sign*((($tz_hour*60) + $tz_min)*60);
4205 ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($local);
4206 $date{'hour_local'} = $hour;
4207 $date{'minute_local'} = $min;
4208 $date{'mday_local'} = $mday;
4209 $date{'tz_local'} = $tz;
4210 $date{'iso-tz'} = sprintf("%04d-%02d-%02d %02d:%02d:%02d %s",
4211 1900+$year, $mon+1, $mday,
4212 $hour, $min, $sec, $tz);
4216 sub parse_file_date
{
4218 my $mtime = (stat("$projectroot/$project/$file"))[9];
4219 return () unless defined $mtime;
4220 my $tzoffset = timegm
((localtime($mtime))[0..5]) - $mtime;
4222 if ($tzoffset <= 0) {
4226 $tzoffset = int($tzoffset/60);
4227 $tzstring .= sprintf("%02d%02d", int($tzoffset/60), $tzoffset%60);
4228 return parse_date
($mtime, $tzstring);
4236 defined(my $fd = git_cmd_pipe
"cat-file", "tag", $tag_id) or return;
4237 $tag{'id'} = $tag_id;
4238 while (my $line = to_utf8
(scalar <$fd>)) {
4240 if ($line =~ m/^object ([0-9a-fA-F]{40})$/) {
4241 $tag{'object'} = $1;
4242 } elsif ($line =~ m/^type (.+)$/) {
4244 } elsif ($line =~ m/^tag (.+)$/) {
4246 } elsif ($line =~ m/^tagger (.*) ([0-9]+) (.*)$/) {
4247 $tag{'author'} = $1;
4248 $tag{'author_epoch'} = $2;
4249 $tag{'author_tz'} = $3;
4250 if ($tag{'author'} =~ m/^([^<]+) <([^>]*)>/) {
4251 $tag{'author_name'} = $1;
4252 $tag{'author_email'} = $2;
4254 $tag{'author_name'} = $tag{'author'};
4256 } elsif ($line =~ m/--BEGIN/) {
4257 push @comment, $line;
4259 } elsif ($line eq "") {
4263 push @comment, map(to_utf8
($_), <$fd>);
4264 $tag{'comment'} = \
@comment;
4265 close $fd or return;
4266 if (!defined $tag{'name'}) {
4272 sub parse_commit_text
{
4273 my ($commit_text, $withparents) = @_;
4274 my @commit_lines = split '\n', $commit_text;
4277 pop @commit_lines; # Remove '\0'
4279 if (! @commit_lines) {
4283 my $header = shift @commit_lines;
4284 if ($header !~ m/^[0-9a-fA-F]{40}/) {
4287 ($co{'id'}, my @parents) = split ' ', $header;
4288 while (my $line = shift @commit_lines) {
4289 last if $line eq "\n";
4290 if ($line =~ m/^tree ([0-9a-fA-F]{40})$/) {
4292 } elsif ((!defined $withparents) && ($line =~ m/^parent ([0-9a-fA-F]{40})$/)) {
4294 } elsif ($line =~ m/^author (.*) ([0-9]+) (.*)$/) {
4295 $co{'author'} = to_utf8
($1);
4296 $co{'author_epoch'} = $2;
4297 $co{'author_tz'} = $3;
4298 if ($co{'author'} =~ m/^([^<]+) <([^>]*)>/) {
4299 $co{'author_name'} = $1;
4300 $co{'author_email'} = $2;
4302 $co{'author_name'} = $co{'author'};
4304 } elsif ($line =~ m/^committer (.*) ([0-9]+) (.*)$/) {
4305 $co{'committer'} = to_utf8
($1);
4306 $co{'committer_epoch'} = $2;
4307 $co{'committer_tz'} = $3;
4308 if ($co{'committer'} =~ m/^([^<]+) <([^>]*)>/) {
4309 $co{'committer_name'} = $1;
4310 $co{'committer_email'} = $2;
4312 $co{'committer_name'} = $co{'committer'};
4316 if (!defined $co{'tree'}) {
4319 $co{'parents'} = \
@parents;
4320 $co{'parent'} = $parents[0];
4322 @commit_lines = map to_utf8
($_), @commit_lines;
4323 foreach my $title (@commit_lines) {
4326 $co{'title'} = chop_str
($title, 80, 5);
4327 # remove leading stuff of merges to make the interesting part visible
4328 if (length($title) > 50) {
4329 $title =~ s/^Automatic //;
4330 $title =~ s/^merge (of|with) /Merge ... /i;
4331 if (length($title) > 50) {
4332 $title =~ s/(http|rsync):\/\///;
4334 if (length($title) > 50) {
4335 $title =~ s/(master|www|rsync)\.//;
4337 if (length($title) > 50) {
4338 $title =~ s/kernel.org:?//;
4340 if (length($title) > 50) {
4341 $title =~ s/\/pub\/scm//;
4344 $co{'title_short'} = chop_str
($title, 50, 5);
4348 if (! defined $co{'title'} || $co{'title'} eq "") {
4349 $co{'title'} = $co{'title_short'} = '(no commit message)';
4351 # remove added spaces
4352 foreach my $line (@commit_lines) {
4355 $co{'comment'} = \
@commit_lines;
4357 my $age_epoch = $co{'committer_epoch'};
4358 $co{'age_epoch'} = $age_epoch;
4359 my $time_now = time;
4360 $co{'age_string'} = age_string
($age_epoch, $time_now);
4361 $co{'age_string_date'} = age_string_date
($age_epoch, $time_now);
4362 $co{'age_string_age'} = age_string_age
($age_epoch, $time_now);
4367 my ($commit_id) = @_;
4372 defined(my $fd = git_cmd_pipe
"rev-list",
4378 or die_error
(500, "Open git-rev-list failed");
4379 %co = parse_commit_text
(<$fd>, 1);
4386 my ($commit_id, $maxcount, $skip, $filename, @args) = @_;
4394 defined(my $fd = git_cmd_pipe
"rev-list",
4397 ("--max-count=" . $maxcount),
4398 ("--skip=" . $skip),
4402 ($filename ?
($filename) : ()))
4403 or die_error
(500, "Open git-rev-list failed");
4404 while (my $line = <$fd>) {
4405 my %co = parse_commit_text
($line);
4410 return wantarray ?
@cos : \
@cos;
4413 # parse line of git-diff-tree "raw" output
4414 sub parse_difftree_raw_line
{
4418 # ':100644 100644 03b218260e99b78c6df0ed378e59ed9205ccc96d 3b93d5e7cc7f7dd4ebed13a5cc1a4ad976fc94d8 M ls-files.c'
4419 # ':100644 100644 7f9281985086971d3877aca27704f2aaf9c448ce bc190ebc71bbd923f2b728e505408f5e54bd073a M rev-tree.c'
4420 if ($line =~ m/^:([0-7]{6}) ([0-7]{6}) ([0-9a-fA-F]{40}) ([0-9a-fA-F]{40}) (.)([0-9]{0,3})\t(.*)$/) {
4421 $res{'from_mode'} = $1;
4422 $res{'to_mode'} = $2;
4423 $res{'from_id'} = $3;
4425 $res{'status'} = $5;
4426 $res{'similarity'} = $6;
4427 if ($res{'status'} eq 'R' || $res{'status'} eq 'C') { # renamed or copied
4428 ($res{'from_file'}, $res{'to_file'}) = map { unquote
($_) } split("\t", $7);
4430 $res{'from_file'} = $res{'to_file'} = $res{'file'} = unquote
($7);
4433 # '::100755 100755 100755 60e79ca1b01bc8b057abe17ddab484699a7f5fdb 94067cc5f73388f33722d52ae02f44692bc07490 94067cc5f73388f33722d52ae02f44692bc07490 MR git-gui/git-gui.sh'
4434 # combined diff (for merge commit)
4435 elsif ($line =~ s/^(::+)((?:[0-7]{6} )+)((?:[0-9a-fA-F]{40} )+)([a-zA-Z]+)\t(.*)$//) {
4436 $res{'nparents'} = length($1);
4437 $res{'from_mode'} = [ split(' ', $2) ];
4438 $res{'to_mode'} = pop @
{$res{'from_mode'}};
4439 $res{'from_id'} = [ split(' ', $3) ];
4440 $res{'to_id'} = pop @
{$res{'from_id'}};
4441 $res{'status'} = [ split('', $4) ];
4442 $res{'to_file'} = unquote
($5);
4444 # 'c512b523472485aef4fff9e57b229d9d243c967f'
4445 elsif ($line =~ m/^([0-9a-fA-F]{40})$/) {
4446 $res{'commit'} = $1;
4449 return wantarray ?
%res : \
%res;
4452 # wrapper: return parsed line of git-diff-tree "raw" output
4453 # (the argument might be raw line, or parsed info)
4454 sub parsed_difftree_line
{
4455 my $line_or_ref = shift;
4457 if (ref($line_or_ref) eq "HASH") {
4458 # pre-parsed (or generated by hand)
4459 return $line_or_ref;
4461 return parse_difftree_raw_line
($line_or_ref);
4465 # parse line of git-ls-tree output
4466 sub parse_ls_tree_line
{
4472 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa 16717 panic.c'
4473 $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40}) +(-|[0-9]+)\t(.+)$/s;
4482 $res{'name'} = unquote
($5);
4485 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'
4486 $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t(.+)$/s;
4494 $res{'name'} = unquote
($4);
4498 return wantarray ?
%res : \
%res;
4501 # generates _two_ hashes, references to which are passed as 2 and 3 argument
4502 sub parse_from_to_diffinfo
{
4503 my ($diffinfo, $from, $to, @parents) = @_;
4505 if ($diffinfo->{'nparents'}) {
4507 $from->{'file'} = [];
4508 $from->{'href'} = [];
4509 fill_from_file_info
($diffinfo, @parents)
4510 unless exists $diffinfo->{'from_file'};
4511 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
4512 $from->{'file'}[$i] =
4513 defined $diffinfo->{'from_file'}[$i] ?
4514 $diffinfo->{'from_file'}[$i] :
4515 $diffinfo->{'to_file'};
4516 if ($diffinfo->{'status'}[$i] ne "A") { # not new (added) file
4517 $from->{'href'}[$i] = href
(action
=>"blob",
4518 hash_base
=>$parents[$i],
4519 hash
=>$diffinfo->{'from_id'}[$i],
4520 file_name
=>$from->{'file'}[$i]);
4522 $from->{'href'}[$i] = undef;
4526 # ordinary (not combined) diff
4527 $from->{'file'} = $diffinfo->{'from_file'};
4528 if ($diffinfo->{'status'} ne "A") { # not new (added) file
4529 $from->{'href'} = href
(action
=>"blob", hash_base
=>$hash_parent,
4530 hash
=>$diffinfo->{'from_id'},
4531 file_name
=>$from->{'file'});
4533 delete $from->{'href'};
4537 $to->{'file'} = $diffinfo->{'to_file'};
4538 if (!is_deleted
($diffinfo)) { # file exists in result
4539 $to->{'href'} = href
(action
=>"blob", hash_base
=>$hash,
4540 hash
=>$diffinfo->{'to_id'},
4541 file_name
=>$to->{'file'});
4543 delete $to->{'href'};
4547 ## ......................................................................
4548 ## parse to array of hashes functions
4550 sub git_get_heads_list
{
4551 my ($limit, @classes) = @_;
4552 @classes = get_branch_refs
() unless @classes;
4553 my @patterns = map { "refs/$_" } @classes;
4556 defined(my $fd = git_cmd_pipe
'for-each-ref',
4557 ($limit ?
'--count='.($limit+1) : ()), '--sort=-committerdate',
4558 '--format=%(objectname) %(refname) %(subject)%00%(committer)',
4561 while (my $line = to_utf8
(scalar <$fd>)) {
4565 my ($refinfo, $committerinfo) = split(/\0/, $line);
4566 my ($hash, $name, $title) = split(' ', $refinfo, 3);
4567 my ($committer, $epoch, $tz) =
4568 ($committerinfo =~ /^(.*) ([0-9]+) (.*)$/);
4569 $ref_item{'fullname'} = $name;
4570 my $strip_refs = join '|', map { quotemeta } get_branch_refs
();
4571 $name =~ s!^refs/($strip_refs|remotes)/!!;
4572 $ref_item{'name'} = $name;
4573 # for refs neither in 'heads' nor 'remotes' we want to
4574 # show their ref dir
4575 my $ref_dir = (defined $1) ?
$1 : '';
4576 if ($ref_dir ne '' and $ref_dir ne 'heads' and $ref_dir ne 'remotes') {
4577 $ref_item{'name'} .= ' (' . $ref_dir . ')';
4580 $ref_item{'id'} = $hash;
4581 $ref_item{'title'} = $title || '(no commit message)';
4582 $ref_item{'epoch'} = $epoch;
4584 $ref_item{'age'} = age_string
($ref_item{'epoch'});
4586 $ref_item{'age'} = "unknown";
4589 push @headslist, \
%ref_item;
4593 return wantarray ?
@headslist : \
@headslist;
4596 sub git_get_tags_list
{
4599 my $all = shift || 0;
4600 my $order = shift || $default_refs_order;
4601 my $sortkey = $all && $order eq 'name' ?
'refname' : '-creatordate';
4603 defined(my $fd = git_cmd_pipe
'for-each-ref',
4604 ($limit ?
'--count='.($limit+1) : ()), "--sort=$sortkey",
4605 '--format=%(objectname) %(objecttype) %(refname) '.
4606 '%(*objectname) %(*objecttype) %(subject)%00%(creator)',
4607 ($all ?
'refs' : 'refs/tags'))
4609 while (my $line = to_utf8
(scalar <$fd>)) {
4613 my ($refinfo, $creatorinfo) = split(/\0/, $line);
4614 my ($id, $type, $name, $refid, $reftype, $title) = split(' ', $refinfo, 6);
4615 my ($creator, $epoch, $tz) =
4616 ($creatorinfo =~ /^(.*) ([0-9]+) (.*)$/);
4617 $ref_item{'fullname'} = $name;
4618 $name =~ s!^refs/!! if $all;
4619 $name =~ s!^refs/tags/!! unless $all;
4621 $ref_item{'type'} = $type;
4622 $ref_item{'id'} = $id;
4623 $ref_item{'name'} = $name;
4624 if ($type eq "tag") {
4625 $ref_item{'subject'} = $title;
4626 $ref_item{'reftype'} = $reftype;
4627 $ref_item{'refid'} = $refid;
4629 $ref_item{'reftype'} = $type;
4630 $ref_item{'refid'} = $id;
4633 if ($type eq "tag" || $type eq "commit") {
4634 $ref_item{'epoch'} = $epoch;
4636 $ref_item{'age'} = age_string
($ref_item{'epoch'});
4638 $ref_item{'age'} = "unknown";
4642 push @tagslist, \
%ref_item;
4646 return wantarray ?
@tagslist : \
@tagslist;
4649 ## ----------------------------------------------------------------------
4650 ## filesystem-related functions
4652 sub get_file_owner
{
4655 my ($dev, $ino, $mode, $nlink, $st_uid, $st_gid, $rdev, $size) = stat($path);
4656 my ($name, $passwd, $uid, $gid, $quota, $comment, $gcos, $dir, $shell) = getpwuid($st_uid);
4657 if (!defined $gcos) {
4661 $owner =~ s/[,;].*$//;
4662 return to_utf8
($owner);
4665 # assume that file exists
4667 my $filename = shift;
4669 open my $fd, '<', $filename;
4676 # return undef on failure
4677 sub collect_output
{
4678 defined(my $fd = cmd_pipe
@_) or return undef;
4683 my $result = join('', map({ to_utf8
($_) } <$fd>));
4684 close $fd or return undef;
4688 # return undef on failure
4689 # return '' if only comments
4690 sub collect_html_file
{
4691 my $filename = shift;
4693 open my $fd, '<', $filename or return undef;
4694 my $result = join('', map({ to_utf8
($_) } <$fd>));
4695 close $fd or return undef;
4696 return undef unless defined($result);
4698 $test =~ s/<!--(?:[^-]|(?:-(?!-)))*-->//gs;
4700 return $test eq '' ?
'' : $result;
4703 ## ......................................................................
4704 ## mimetype related functions
4706 sub mimetype_guess_file
{
4707 my $filename = shift;
4708 my $mimemap = shift;
4709 my $rawmode = shift;
4710 -r
$mimemap or return undef;
4713 open(my $mh, '<', $mimemap) or return undef;
4715 next if m/^#/; # skip comments
4716 my ($mimetype, @exts) = split(/\s+/);
4717 foreach my $ext (@exts) {
4718 $mimemap{$ext} = $mimetype;
4724 $ext = $1 if $filename =~ /\.([^.]*)$/;
4725 $ans = $mimemap{$ext} if $ext;
4728 $ans = 'text/html' if $l eq 'application/xhtml+xml';
4730 $ans = 'text/xml' if $l =~ m
!^application
/[^\s
:;,=]+\
+xml
$! ||
4731 $l eq 'image/svg+xml' ||
4732 $l eq 'application/xml-dtd' ||
4733 $l eq 'application/xml-external-parsed-entity';
4739 sub mimetype_guess
{
4740 my $filename = shift;
4741 my $rawmode = shift;
4743 $filename =~ /\./ or return undef;
4745 if ($mimetypes_file) {
4746 my $file = $mimetypes_file;
4747 if ($file !~ m!^/!) { # if it is relative path
4748 # it is relative to project
4749 $file = "$projectroot/$project/$file";
4751 $mime = mimetype_guess_file
($filename, $file, $rawmode);
4753 $mime ||= mimetype_guess_file
($filename, '/etc/mime.types', $rawmode);
4759 my $filename = shift;
4760 my $rawmode = shift;
4763 # The -T/-B file operators produce the wrong result unless a perlio
4764 # layer is present when the file handle is a pipe that delivers less
4765 # than 512 bytes of data before reaching EOF.
4767 # If we are running in a Perl that uses the stdio layer rather than the
4768 # unix+perlio layers we will end up adding a perlio layer on top of the
4769 # stdio layer and get a second level of buffering. This is harmless
4770 # and it makes the -T/-B file operators work properly in all cases.
4772 binmode $fd, ":perlio" or die_error
(500, "Adding perlio layer failed")
4773 unless grep /^perlio$/, PerlIO
::get_layers
($fd);
4775 $mime = mimetype_guess
($filename, $rawmode) if defined $filename;
4777 if (!$mime && $filename) {
4778 if ($filename =~ m/\.html?$/i) {
4779 $mime = 'text/html';
4780 } elsif ($filename =~ m/\.xht(?:ml)?$/i) {
4781 $mime = 'text/html';
4782 } elsif ($filename =~ m/\.te?xt?$/i) {
4783 $mime = 'text/plain';
4784 } elsif ($filename =~ m/\.(?:markdown|md)$/i) {
4785 $mime = 'text/plain';
4786 } elsif ($filename =~ m/\.png$/i) {
4787 $mime = 'image/png';
4788 } elsif ($filename =~ m/\.gif$/i) {
4789 $mime = 'image/gif';
4790 } elsif ($filename =~ m/\.jpe?g$/i) {
4791 $mime = 'image/jpeg';
4792 } elsif ($filename =~ m/\.svgz?$/i) {
4793 $mime = 'image/svg+xml';
4798 return $default_blob_plain_mimetype || 'application/octet-stream' unless $fd || $mime;
4800 $mime = -T
$fd ?
'text/plain' : 'application/octet-stream' unless $mime;
4808 return scalar($data =~ /^[\x00-\x7f]*$/);
4813 return utf8
::decode
($data);
4816 sub extract_html_charset
{
4817 return undef unless $_[0] && "$_[0]</head>" =~ m
#<head(?:\s+[^>]*)?(?<!/)>(.*?)</head\s*>#is;
4819 return $2 if $head =~ m
#<meta\s+charset\s*=\s*(['"])\s*([a-z0-9(:)_.+-]+)\s*\1\s*/?>#is;
4820 while ($head =~ m
#<meta\s+(http-equiv|content)\s*=\s*(['"])\s*([^\2]+?)\s*\2\s*(http-equiv|content)\s*=\s*(['"])\s*([^\5]+?)\s*\5\s*/?>#sig) {
4821 my %kv = (lc($1) => $3, lc($4) => $6);
4822 my ($he, $c) = (lc($kv{'http-equiv'}), $kv{'content'});
4823 return $1 if $he && $c && $he eq 'content-type' &&
4824 $c =~ m!\s*text/html\s*;\s*charset\s*=\s*([a-z0-9(:)_.+-]+)\s*$!is;
4829 sub blob_contenttype
{
4830 my ($fd, $file_name, $type) = @_;
4832 $type ||= blob_mimetype
($fd, $file_name, 1);
4833 return $type unless $type =~ m!^text/.+!i;
4834 my ($leader, $charset, $htmlcharset);
4835 if ($fd && read($fd, $leader, 32768)) {{
4836 $charset='US-ASCII' if is_ascii
($leader);
4837 return ("$type; charset=UTF-8", $leader) if !$charset && is_valid_utf8
($leader);
4838 $charset='ISO-8859-1' unless $charset;
4839 $htmlcharset = extract_html_charset
($leader) if $type eq 'text/html';
4840 if ($htmlcharset && $charset ne 'US-ASCII') {
4841 $htmlcharset = undef if $htmlcharset =~ /^(?:utf-8|us-ascii)$/i
4844 return ("$type; charset=$htmlcharset", $leader) if $htmlcharset;
4845 my $defcharset = $default_text_plain_charset || '';
4846 $defcharset =~ s/^\s+//;
4847 $defcharset =~ s/\s+$//;
4848 $defcharset = '' if $charset && $charset ne 'US-ASCII' && $defcharset =~ /^(?:utf-8|us-ascii)$/i;
4849 return ("$type; charset=" . ($defcharset || 'ISO-8859-1'), $leader);
4852 # peek the first upto 128 bytes off a file handle
4860 return '' unless $fd && read($fd, $prefix128, 128);
4862 # In the general case, we're guaranteed only to be able to ungetc one
4863 # character (provided, of course, we actually got a character first).
4867 # 1) we are dealing with a :perlio layer since blob_mimetype will have
4868 # already been called at least once on the file handle before us
4870 # 2) we have an $fd positioned at the start of the input stream and
4871 # therefore know we were positioned at a buffer boundary before
4872 # reading the initial upto 128 bytes
4874 # 3) the buffer size is at least 512 bytes
4876 # 4) we are careful to only unget raw bytes
4878 # 5) we are attempting to unget exactly the same number of bytes we got
4880 # Given the above conditions we will ALWAYS be able to safely unget
4881 # the $prefix128 value we just got.
4883 # In fact, we could read up to 511 bytes and still be sure.
4884 # (Reading 512 might pop us into the next internal buffer, but probably
4885 # not since that could break the always able to unget at least the one
4886 # you just got guarantee.)
4888 map {$fd->ungetc(ord($_))} reverse(split //, $prefix128);
4893 # guess file syntax for syntax highlighting; return undef if no highlighting
4894 # the name of syntax can (in the future) depend on syntax highlighter used
4895 sub guess_file_syntax
{
4896 my ($fd, $mimetype, $file_name) = @_;
4897 return undef unless $fd && defined $file_name &&
4898 defined $mimetype && $mimetype =~ m!^text/.+!i;
4899 my $basename = basename
($file_name, '.in');
4900 return $highlight_basename{$basename}
4901 if exists $highlight_basename{$basename};
4903 # Peek to see if there's a shebang or xml line.
4904 # We always operate on bytes when testing this.
4907 my $shebang = peek128bytes
($fd);
4908 if (length($shebang) >= 4 && $shebang =~ /^#!/) { # 4 would be '#!/x'
4909 foreach my $key (keys %highlight_shebang) {
4910 my $ar = ref($highlight_shebang{$key}) ?
4911 $highlight_shebang{$key} :
4912 [$highlight_shebang{key
}];
4913 map {return $key if $shebang =~ /$_/} @
$ar;
4916 return 'xml' if $shebang =~ m!^\s*<\?xml\s!; # "xml" must be lowercase
4919 $basename =~ /\.([^.]*)$/;
4920 my $ext = $1 or return undef;
4921 return $highlight_ext{$ext}
4922 if exists $highlight_ext{$ext};
4927 # run highlighter and return FD of its output,
4928 # or return original FD if no highlighting
4929 sub run_highlighter
{
4930 my ($fd, $syntax) = @_;
4931 return $fd unless $fd && !eof($fd) && defined $highlight_bin && defined $syntax;
4933 defined(my $hifd = cmd_pipe
$posix_shell_bin, '-c',
4934 quote_command
(git_cmd
(), "cat-file", "blob", $hash)." | ".
4935 $to_utf8_pipe_command.
4936 quote_command
($highlight_bin).
4937 " --replace-tabs=8 --fragment --syntax $syntax")
4938 or die_error
(500, "Couldn't open file or run syntax highlighter");
4940 # just in case, should not happen as we tested !eof($fd) above
4941 return $fd if close($hifd);
4944 !$! or die_error
(500, "Couldn't close syntax highighter pipe");
4946 # leaving us with the only possibility a non-zero exit status (possibly a signal);
4947 # instead of dying horribly on this, just skip the highlighting
4948 # but do output a message about it to STDERR that will end up in the log
4949 print STDERR
"warning: skipping failed highlight for --syntax $syntax: ".
4950 sprintf("child exit status 0x%x\n", $?
);
4957 ## ======================================================================
4958 ## functions printing HTML: header, footer, error page
4960 sub get_page_title
{
4961 my $title = to_utf8
($site_name);
4963 unless (defined $project) {
4964 if (defined $project_filter) {
4965 $title .= " - projects in '" . esc_path
($project_filter) . "'";
4969 $title .= " - " . to_utf8
($project);
4971 return $title unless (defined $action);
4972 my $action_print = $action eq 'blame_incremental' ?
'blame' : $action;
4973 $title .= "/$action_print"; # $action is US-ASCII (7bit ASCII)
4975 return $title unless (defined $file_name);
4976 $title .= " - " . esc_path
($file_name);
4977 if ($action eq "tree" && $file_name !~ m
|/$|) {
4984 sub get_content_type_html
{
4985 # We do not ever emit application/xhtml+xml since that gives us
4986 # no benefits and it makes many browsers (e.g. Firefox) exceedingly
4987 # strict, which is troublesome for example when showing user-supplied
4988 # README.html files.
4992 sub print_feed_meta
{
4993 if (defined $project) {
4994 my %href_params = get_feed_info
();
4995 if (!exists $href_params{'-title'}) {
4996 $href_params{'-title'} = 'log';
4999 foreach my $format (qw(RSS Atom)) {
5000 my $type = lc($format);
5002 '-rel' => 'alternate',
5003 '-title' => esc_attr
("$project - $href_params{'-title'} - $format feed"),
5004 '-type' => "application/$type+xml"
5007 $href_params{'extra_options'} = undef;
5008 $href_params{'action'} = $type;
5009 $link_attr{'-href'} = href
(%href_params);
5011 "rel=\"$link_attr{'-rel'}\" ".
5012 "title=\"$link_attr{'-title'}\" ".
5013 "href=\"$link_attr{'-href'}\" ".
5014 "type=\"$link_attr{'-type'}\" ".
5017 $href_params{'extra_options'} = '--no-merges';
5018 $link_attr{'-href'} = href
(%href_params);
5019 $link_attr{'-title'} .= ' (no merges)';
5021 "rel=\"$link_attr{'-rel'}\" ".
5022 "title=\"$link_attr{'-title'}\" ".
5023 "href=\"$link_attr{'-href'}\" ".
5024 "type=\"$link_attr{'-type'}\" ".
5029 printf('<link rel="alternate" title="%s projects list" '.
5030 'href="%s" type="text/plain; charset=utf-8" />'."\n",
5031 esc_attr
($site_name), href
(project
=>undef, action
=>"project_index"));
5032 printf('<link rel="alternate" title="%s projects feeds" '.
5033 'href="%s" type="text/x-opml" />'."\n",
5034 esc_attr
($site_name), href
(project
=>undef, action
=>"opml"));
5038 sub print_header_links
{
5041 # print out each stylesheet that exist, providing backwards capability
5042 # for those people who defined $stylesheet in a config file
5043 if (defined $stylesheet) {
5044 print '<link rel="stylesheet" type="text/css" href="'.esc_url
($stylesheet).'"/>'."\n";
5046 foreach my $stylesheet (@stylesheets) {
5047 next unless $stylesheet;
5048 print '<link rel="stylesheet" type="text/css" href="'.esc_url
($stylesheet).'"/>'."\n";
5052 if ($status eq '200 OK');
5053 if (defined $favicon) {
5054 print qq(<link rel
="shortcut icon" href
=").esc_url($favicon).qq(" type
="image/png" />\n);
5058 sub print_nav_breadcrumbs_path
{
5059 my $dirprefix = undef;
5060 while (my $part = shift) {
5061 $dirprefix .= "/" if defined $dirprefix;
5062 $dirprefix .= $part;
5063 print $cgi->a({-href
=> href
(project
=> undef,
5064 project_filter
=> $dirprefix,
5065 action
=> "project_list")},
5066 esc_html
($part)) . " / ";
5070 sub print_nav_breadcrumbs
{
5073 for my $crumb (@extra_breadcrumbs, [ $home_link_str => $home_link ]) {
5074 print $cgi->a({-href
=> esc_url
($crumb->[1])}, $crumb->[0]) . " / ";
5076 if (defined $project) {
5077 my @dirname = split '/', $project;
5078 my $projectbasename = pop @dirname;
5079 print_nav_breadcrumbs_path
(@dirname);
5080 print $cgi->a({-href
=> href
(action
=>"summary")}, esc_html
($projectbasename));
5081 if (defined $action) {
5082 my $action_print = $action ;
5083 $action_print = 'blame' if $action_print eq 'blame_incremental';
5084 if (defined $opts{-action_extra
}) {
5085 $action_print = $cgi->a({-href
=> href
(action
=>$action)},
5088 print " / $action_print";
5090 if (defined $opts{-action_extra
}) {
5091 print " / $opts{-action_extra}";
5094 } elsif (defined $project_filter) {
5095 print_nav_breadcrumbs_path
(split '/', $project_filter);
5099 sub print_search_form
{
5100 if (!defined $searchtext) {
5104 if (defined $hash_base) {
5105 $search_hash = $hash_base;
5106 } elsif (defined $hash) {
5107 $search_hash = $hash;
5109 $search_hash = "HEAD";
5111 # We can't use href() here because we need to encode the
5112 # URL parameters into the form, not into the action link.
5113 my $action = $my_uri;
5114 my $use_pathinfo = gitweb_check_feature
('pathinfo');
5115 if ($use_pathinfo) {
5116 # See notes about doubled / in href()
5118 $action .= "/".esc_path_info
($project);
5120 print $cgi->start_form(-method
=> "get", -action
=> $action) .
5121 "<div class=\"search\">\n" .
5123 $cgi->input({-name
=>"p", -value
=>$project, -type
=>"hidden"}) . "\n") .
5124 $cgi->input({-name
=>"a", -value
=>"search", -type
=>"hidden"}) . "\n" .
5125 $cgi->input({-name
=>"h", -value
=>$search_hash, -type
=>"hidden"}) . "\n" .
5126 $cgi->popup_menu(-name
=> 'st', -default => 'commit',
5127 -values => ['commit', 'grep', 'author', 'committer', 'pickaxe']) .
5128 " " . $cgi->a({-href
=> href
(action
=>"search_help"),
5129 -title
=> "search help" }, "?") . " search:\n",
5130 $cgi->textfield(-name
=> "s", -value
=> $searchtext, -override
=> 1) . "\n" .
5131 "<span title=\"Extended regular expression\">" .
5132 $cgi->checkbox(-name
=> 'sr', -value
=> 1, -label
=> 're',
5133 -checked
=> $search_use_regexp) .
5136 $cgi->end_form() . "\n";
5139 sub git_header_html
{
5140 my $status = shift || "200 OK";
5141 my $expires = shift;
5144 my $title = get_page_title
();
5145 my $content_type = get_content_type_html
();
5146 print $cgi->header(-type
=>$content_type, -charset
=> 'utf-8',
5147 -status
=> $status, -expires
=> $expires)
5148 unless ($opts{'-no_http_header'});
5149 my $mod_perl_version = $ENV{'MOD_PERL'} ?
" $ENV{'MOD_PERL'}" : '';
5151 <?xml version="1.0" encoding="utf-8"?>
5152 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
5153 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US" lang="en-US">
5154 <!-- git web interface version $version, (C) 2005-2006, Kay Sievers <kay.sievers\@vrfy.org>, Christian Gierke -->
5155 <!-- git core binaries version $git_version -->
5157 <meta http-equiv="content-type" content="$content_type; charset=utf-8"/>
5158 <meta name="generator" content="gitweb/$version git/$git_version$mod_perl_version"/>
5159 <meta name="robots" content="index, nofollow"/>
5160 <title>$title</title>
5161 <script type="text/javascript">/* <![CDATA[ */
5162 function fixBlameLinks() {
5163 var allLinks = document.getElementsByTagName("a");
5164 for (var i = 0; i < allLinks.length; i++) {
5165 var link = allLinks.item(i);
5166 if (link.className == 'blamelink')
5167 link.href = link.href.replace("/blame/", "/blame_incremental/");
5172 # the stylesheet, favicon etc urls won't work correctly with path_info
5173 # unless we set the appropriate base URL
5174 if ($ENV{'PATH_INFO'}) {
5175 print "<base href=\"".esc_url
($base_url)."\" />\n";
5177 print_header_links
($status);
5179 if (defined $site_html_head_string) {
5180 print to_utf8
($site_html_head_string);
5186 if (defined $site_header && -f
$site_header) {
5187 insert_file
($site_header);
5190 print "<div class=\"page_header\">\n";
5191 if (defined $logo) {
5192 print $cgi->a({-href
=> esc_url
($logo_url),
5193 -title
=> $logo_label},
5194 $cgi->img({-src
=> esc_url
($logo),
5195 -width
=> 72, -height
=> 27,
5197 -class => "logo"}));
5199 print_nav_breadcrumbs
(%opts);
5202 my $have_search = gitweb_check_feature
('search');
5203 if (defined $project && $have_search) {
5204 print_search_form
();
5208 sub compute_timed_interval
{
5209 return "<?gitweb compute_timed_interval?>" if $cache_mode_active;
5210 return tv_interval
($t0, [ gettimeofday
() ]);
5213 sub compute_commands_count
{
5214 return "<?gitweb compute_commands_count?>" if $cache_mode_active;
5215 my $s = $number_of_git_cmds == 1 ?
'' : 's';
5216 return '<span id="generating_cmd">'.
5217 $number_of_git_cmds.
5218 "</span> git command$s";
5221 sub git_footer_html
{
5222 my $feed_class = 'rss_logo';
5224 print "<div class=\"page_footer\">\n";
5225 if (defined $project) {
5226 my $descr = git_get_project_description
($project);
5227 if (defined $descr) {
5228 print "<div class=\"page_footer_text\">" . esc_html
($descr) . "</div>\n";
5231 my %href_params = get_feed_info
();
5232 if (!%href_params) {
5233 $feed_class .= ' generic';
5235 $href_params{'-title'} ||= 'log';
5237 foreach my $format (qw(RSS Atom)) {
5238 $href_params{'action'} = lc($format);
5239 print $cgi->a({-href
=> href
(%href_params),
5240 -title
=> "$href_params{'-title'} $format feed",
5241 -class => $feed_class}, $format)."\n";
5245 print $cgi->a({-href
=> href
(project
=>undef, action
=>"opml",
5246 project_filter
=> $project_filter),
5247 -class => $feed_class}, "OPML") . " ";
5248 print $cgi->a({-href
=> href
(project
=>undef, action
=>"project_index",
5249 project_filter
=> $project_filter),
5250 -class => $feed_class}, "TXT") . "\n";
5252 print "</div>\n"; # class="page_footer"
5254 if (defined $t0 && gitweb_check_feature
('timed')) {
5255 print "<div id=\"generating_info\">\n";
5256 print 'This page took '.
5257 '<span id="generating_time" class="time_span">'.
5258 compute_timed_interval
().
5261 compute_commands_count
().
5263 print "</div>\n"; # class="page_footer"
5266 if (defined $site_footer && -f
$site_footer) {
5267 insert_file
($site_footer);
5270 print qq!<script type
="text/javascript" src
="!.esc_url($javascript).qq!"></script
>\n!;
5271 if (defined $action &&
5272 $action eq 'blame_incremental') {
5273 print qq!<script type
="text/javascript">\n!.
5274 qq!startBlame
("!. href(action=>"blame_data
", -replay=>1) .qq!",\n!.
5275 qq! "!. href() .qq!");\n!.
5278 my ($jstimezone, $tz_cookie, $datetime_class) =
5279 gitweb_get_feature
('javascript-timezone');
5281 print qq!<script type
="text/javascript">\n!.
5282 qq!window
.onload
= function
() {\n!;
5283 if (gitweb_check_feature
('blame_incremental')) {
5284 print qq! fixBlameLinks
();\n!;
5286 if (gitweb_check_feature
('javascript-actions')) {
5287 print qq! fixLinks
();\n!;
5289 if ($jstimezone && $tz_cookie && $datetime_class) {
5290 print qq! var tz_cookie
= { name
: '$tz_cookie', expires
: 14, path
: '/' };\n!. # in days
5291 qq! onloadTZSetup
('$jstimezone', tz_cookie
, '$datetime_class');\n!;
5301 # die_error(<http_status_code>, <error_message>[, <detailed_html_description>])
5302 # Example: die_error(404, 'Hash not found')
5303 # By convention, use the following status codes (as defined in RFC 2616):
5304 # 400: Invalid or missing CGI parameters, or
5305 # requested object exists but has wrong type.
5306 # 403: Requested feature (like "pickaxe" or "snapshot") not enabled on
5307 # this server or project.
5308 # 404: Requested object/revision/project doesn't exist.
5309 # 500: The server isn't configured properly, or
5310 # an internal error occurred (e.g. failed assertions caused by bugs), or
5311 # an unknown error occurred (e.g. the git binary died unexpectedly).
5312 # 503: The server is currently unavailable (because it is overloaded,
5313 # or down for maintenance). Generally, this is a temporary state.
5315 my $status = shift || 500;
5316 my $error = esc_html
(shift) || "Internal Server Error";
5320 my %http_responses = (
5321 400 => '400 Bad Request',
5322 403 => '403 Forbidden',
5323 404 => '404 Not Found',
5324 500 => '500 Internal Server Error',
5325 503 => '503 Service Unavailable',
5327 git_header_html
($http_responses{$status}, undef, %opts);
5329 <div class="page_body">
5334 if (defined $extra) {
5342 unless ($opts{'-error_handler'});
5345 ## ----------------------------------------------------------------------
5346 ## functions printing or outputting HTML: navigation
5348 sub git_print_page_nav
{
5349 my ($current, $suppress, $head, $treehead, $treebase, $extra) = @_;
5350 $extra = '' if !defined $extra; # pager or formats
5352 my @navs = qw(summary log commit commitdiff tree refs);
5355 if (ref($suppress) eq 'ARRAY') {
5356 %omit = map { ($_ => 1) } @
$suppress;
5358 %omit = ($suppress => 1);
5360 @navs = grep { !$omit{$_} } @navs;
5363 my %arg = map { $_ => {action
=>$_} } @navs;
5364 if (defined $head) {
5365 for (qw(commit commitdiff)) {
5366 $arg{$_}{'hash'} = $head;
5368 if ($current =~ m/^(tree | log | shortlog | commit | commitdiff | search)$/x) {
5369 $arg{'log'}{'hash'} = $head;
5373 $arg{'log'}{'action'} = 'shortlog';
5374 if ($current eq 'log') {
5375 $current = 'shortlog';
5376 } elsif ($current eq 'shortlog') {
5379 $arg{'tree'}{'hash'} = $treehead if defined $treehead;
5380 $arg{'tree'}{'hash_base'} = $treebase if defined $treebase;
5382 my @actions = gitweb_get_feature
('actions');
5383 my $escname = $project;
5384 $escname =~ s/[+]/%2B/g;
5387 'n' => $project, # project name
5388 'f' => $git_dir, # project path within filesystem
5389 'h' => $treehead || '', # current hash ('h' parameter)
5390 'b' => $treebase || '', # hash base ('hb' parameter)
5391 'e' => $escname, # project name with '+' escaped
5394 my ($label, $link, $pos) = splice(@actions,0,3);
5396 @navs = map { $_ eq $pos ?
($_, $label) : $_ } @navs;
5398 $link =~ s/%([%nfhbe])/$repl{$1}/g;
5399 $arg{$label}{'_href'} = $link;
5402 print "<div class=\"page_nav\">\n" .
5404 map { $_ eq $current ?
5405 $_ : $cgi->a({-href
=> ($arg{$_}{_href
} ?
$arg{$_}{_href
} : href
(%{$arg{$_}}))}, "$_")
5407 print "<br/>\n$extra<br/>\n" .
5411 # returns a submenu for the nagivation of the refs views (tags, heads,
5412 # remotes) with the current view disabled and the remotes view only
5413 # available if the feature is enabled
5414 sub format_ref_views
{
5416 my @ref_views = qw{tags heads
};
5417 push @ref_views, 'remotes' if gitweb_check_feature
('remote_heads');
5418 return join " | ", map {
5419 $_ eq $current ?
$_ :
5420 $cgi->a({-href
=> href
(action
=>$_)}, $_)
5424 sub format_paging_nav
{
5425 my ($action, $page, $has_next_link) = @_;
5431 $cgi->a({-href
=> href
(-replay
=>1, page
=>undef)}, "first") .
5433 $cgi->a({-href
=> href
(-replay
=>1, page
=>$page-1),
5434 -accesskey
=> "p", -title
=> "Alt-p"}, "prev");
5436 $paging_nav .= "first · prev";
5439 if ($has_next_link) {
5440 $paging_nav .= " · " .
5441 $cgi->a({-href
=> href
(-replay
=>1, page
=>$page+1),
5442 -accesskey
=> "n", -title
=> "Alt-n"}, "next");
5444 $paging_nav .= " · next";
5450 sub format_log_nav
{
5451 my ($action, $page, $has_next_link) = @_;
5454 if ($action eq 'shortlog') {
5455 $paging_nav .= 'shortlog';
5457 $paging_nav .= $cgi->a({-href
=> href
(action
=>'shortlog', -replay
=>1)}, 'shortlog');
5459 $paging_nav .= ' | ';
5460 if ($action eq 'log') {
5461 $paging_nav .= 'fulllog';
5463 $paging_nav .= $cgi->a({-href
=> href
(action
=>'log', -replay
=>1)}, 'fulllog');
5466 $paging_nav .= " | " . format_paging_nav
($action, $page, $has_next_link);
5470 ## ......................................................................
5471 ## functions printing or outputting HTML: div
5473 sub git_print_header_div
{
5474 my ($action, $title, $hash, $hash_base, $extra) = @_;
5476 defined $extra or $extra = '';
5478 $args{'action'} = $action;
5479 $args{'hash'} = $hash if $hash;
5480 $args{'hash_base'} = $hash_base if $hash_base;
5482 my $link1 = $cgi->a({-href
=> href
(%args), -class => "title"},
5483 $title ?
$title : $action);
5484 my $link2 = $cgi->a({-href
=> href
(%args), -class => "cover"}, "");
5485 print "<div class=\"header\">\n" . '<span class="title">' .
5486 $link1 . $extra . $link2 . '</span>' . "\n</div>\n";
5489 sub format_repo_url
{
5490 my ($name, $url) = @_;
5491 return "<tr class=\"metadata_url\"><td>$name</td><td>$url</td></tr>\n";
5494 # Group output by placing it in a DIV element and adding a header.
5495 # Options for start_div() can be provided by passing a hash reference as the
5496 # first parameter to the function.
5497 # Options to git_print_header_div() can be provided by passing an array
5498 # reference. This must follow the options to start_div if they are present.
5499 # The content can be a scalar, which is output as-is, a scalar reference, which
5500 # is output after html escaping, an IO handle passed either as *handle or
5501 # *handle{IO}, or a function reference. In the latter case all following
5502 # parameters will be taken as argument to the content function call.
5503 sub git_print_section
{
5504 my ($div_args, $header_args, $content);
5506 if (ref($arg) eq 'HASH') {
5510 if (ref($arg) eq 'ARRAY') {
5511 $header_args = $arg;
5516 print $cgi->start_div($div_args);
5517 git_print_header_div
(@
$header_args);
5519 if (ref($content) eq 'CODE') {
5521 } elsif (ref($content) eq 'SCALAR') {
5522 print esc_html
($$content);
5523 } elsif (ref($content) eq 'GLOB' or ref($content) eq 'IO::Handle') {
5524 while (<$content>) {
5527 } elsif (!ref($content) && defined($content)) {
5531 print $cgi->end_div;
5534 sub format_timestamp_html
{
5536 my $useatnight = shift;
5537 defined($useatnight) or $useatnight = 1;
5538 my $strtime = $date->{'rfc2822'};
5540 my (undef, undef, $datetime_class) =
5541 gitweb_get_feature
('javascript-timezone');
5542 if ($datetime_class) {
5543 $strtime = qq!<span
class="$datetime_class">$strtime</span
>!;
5546 my $localtime_format = '(%d %02d:%02d %s)';
5547 if ($useatnight && $date->{'hour_local'} < 6) {
5548 $localtime_format = '(%d <span class="atnight">%02d:%02d</span> %s)';
5551 sprintf($localtime_format, $date->{'mday_local'},
5552 $date->{'hour_local'}, $date->{'minute_local'}, $date->{'tz_local'});
5557 sub format_lastrefresh_row
{
5558 return "<?gitweb format_lastrefresh_row?>" if $cache_mode_active;
5559 my %rd = parse_file_date
('.last_refresh');
5560 if (defined $rd{'rfc2822'}) {
5561 return "<tr id=\"metadata_lrefresh\"><td>last refresh</td>" .
5562 "<td>".format_timestamp_html
(\
%rd,0)."</td></tr>";
5567 # Outputs the author name and date in long form
5568 sub git_print_authorship
{
5571 my $tag = $opts{-tag
} || 'div';
5572 my $author = $co->{'author_name'};
5574 my %ad = parse_date
($co->{'author_epoch'}, $co->{'author_tz'});
5575 print "<$tag class=\"author_date\">" .
5576 format_search_author
($author, "author", esc_html
($author)) .
5577 " [".format_timestamp_html
(\
%ad)."]".
5578 git_get_avatar
($co->{'author_email'}, -pad_before
=> 1) .
5582 # Outputs table rows containing the full author or committer information,
5583 # in the format expected for 'commit' view (& similar).
5584 # Parameters are a commit hash reference, followed by the list of people
5585 # to output information for. If the list is empty it defaults to both
5586 # author and committer.
5587 sub git_print_authorship_rows
{
5589 # too bad we can't use @people = @_ || ('author', 'committer')
5591 @people = ('author', 'committer') unless @people;
5592 foreach my $who (@people) {
5593 my %wd = parse_date
($co->{"${who}_epoch"}, $co->{"${who}_tz"});
5594 print "<tr><td>$who</td><td>" .
5595 format_search_author
($co->{"${who}_name"}, $who,
5596 esc_html
($co->{"${who}_name"})) . " " .
5597 format_search_author
($co->{"${who}_email"}, $who,
5598 esc_html
("<" . $co->{"${who}_email"} . ">")) .
5599 "</td><td rowspan=\"2\">" .
5600 git_get_avatar
($co->{"${who}_email"}, -size
=> 'double') .
5604 format_timestamp_html
(\
%wd) .
5610 sub git_print_page_path
{
5616 print "<div class=\"page_path\">";
5617 print $cgi->a({-href
=> href
(action
=>"tree", hash_base
=>$hb),
5618 -title
=> 'tree root'}, to_utf8
("[$project]"));
5620 if (defined $name) {
5621 my @dirname = split '/', $name;
5622 my $basename = pop @dirname;
5625 foreach my $dir (@dirname) {
5626 $fullname .= ($fullname ?
'/' : '') . $dir;
5627 print $cgi->a({-href
=> href
(action
=>"tree", file_name
=>$fullname,
5629 -title
=> $fullname}, esc_path
($dir));
5632 if (defined $type && $type eq 'blob') {
5633 print $cgi->a({-href
=> href
(action
=>"blob_plain", file_name
=>$file_name,
5635 -title
=> $name}, esc_path
($basename));
5636 } elsif (defined $type && $type eq 'tree') {
5637 print $cgi->a({-href
=> href
(action
=>"tree", file_name
=>$file_name,
5639 -title
=> $name}, esc_path
($basename));
5642 print esc_path
($basename);
5645 print "<br/></div>\n";
5652 if ($opts{'-remove_title'}) {
5653 # remove title, i.e. first line of log
5656 # remove leading empty lines
5657 while (defined $log->[0] && $log->[0] eq "") {
5662 my $skip_blank_line = 0;
5663 foreach my $line (@
$log) {
5664 if ($line =~ m/^\s*([A-Z][-A-Za-z]*-[Bb]y|C[Cc]): /) {
5665 if (! $opts{'-remove_signoff'}) {
5666 print "<span class=\"signoff\">" . esc_html
($line) . "</span><br/>\n";
5667 $skip_blank_line = 1;
5672 if ($line =~ m
,\s
*([a
-z
]*link): (https?
://\S
+),i
) {
5673 if (! $opts{'-remove_signoff'}) {
5674 print "<span class=\"signoff\">" . esc_html
($1) . ": " .
5675 "<a href=\"" . esc_html
($2) . "\">" . esc_html
($2) . "</a>" .
5677 $skip_blank_line = 1;
5682 # print only one empty line
5683 # do not print empty line after signoff
5685 next if ($skip_blank_line);
5686 $skip_blank_line = 1;
5688 $skip_blank_line = 0;
5691 print format_log_line_html
($line) . "<br/>\n";
5694 if ($opts{'-final_empty_line'}) {
5695 # end with single empty line
5696 print "<br/>\n" unless $skip_blank_line;
5700 # return link target (what link points to)
5701 sub git_get_link_target
{
5706 defined(my $fd = git_cmd_pipe
"cat-file", "blob", $hash)
5710 $link_target = to_utf8
(scalar <$fd>);
5715 return $link_target;
5718 # given link target, and the directory (basedir) the link is in,
5719 # return target of link relative to top directory (top tree);
5720 # return undef if it is not possible (including absolute links).
5721 sub normalize_link_target
{
5722 my ($link_target, $basedir) = @_;
5724 # absolute symlinks (beginning with '/') cannot be normalized
5725 return if (substr($link_target, 0, 1) eq '/');
5727 # normalize link target to path from top (root) tree (dir)
5730 $path = $basedir . '/' . $link_target;
5732 # we are in top (root) tree (dir)
5733 $path = $link_target;
5736 # remove //, /./, and /../
5738 foreach my $part (split('/', $path)) {
5739 # discard '.' and ''
5740 next if (!$part || $part eq '.');
5742 if ($part eq '..') {
5746 # link leads outside repository (outside top dir)
5750 push @path_parts, $part;
5753 $path = join('/', @path_parts);
5758 # print tree entry (row of git_tree), but without encompassing <tr> element
5759 sub git_print_tree_entry
{
5760 my ($t, $basedir, $hash_base, $have_blame) = @_;
5763 $base_key{'hash_base'} = $hash_base if defined $hash_base;
5765 # The format of a table row is: mode list link. Where mode is
5766 # the mode of the entry, list is the name of the entry, an href,
5767 # and link is the action links of the entry.
5769 print "<td class=\"mode\">" . mode_str
($t->{'mode'}) . "</td>\n";
5770 if (exists $t->{'size'}) {
5771 print "<td class=\"size\">$t->{'size'}</td>\n";
5773 if ($t->{'type'} eq "blob") {
5774 print "<td class=\"list\">" .
5775 $cgi->a({-href
=> href
(action
=>"blob", hash
=>$t->{'hash'},
5776 file_name
=>"$basedir$t->{'name'}", %base_key),
5777 -class => "list"}, esc_path
($t->{'name'}));
5778 if (S_ISLNK
(oct $t->{'mode'})) {
5779 my $link_target = git_get_link_target
($t->{'hash'});
5781 my $norm_target = normalize_link_target
($link_target, $basedir);
5782 if (defined $norm_target) {
5784 $cgi->a({-href
=> href
(action
=>"object", hash_base
=>$hash_base,
5785 file_name
=>$norm_target),
5786 -title
=> $norm_target}, esc_path
($link_target));
5788 print " -> " . esc_path
($link_target);
5793 print "<td class=\"link\">";
5794 print $cgi->a({-href
=> href
(action
=>"blob", hash
=>$t->{'hash'},
5795 file_name
=>"$basedir$t->{'name'}", %base_key)},
5799 $cgi->a({-href
=> href
(action
=>"blame", hash
=>$t->{'hash'},
5800 file_name
=>"$basedir$t->{'name'}", %base_key),
5801 -class => "blamelink"},
5804 if (defined $hash_base) {
5806 $cgi->a({-href
=> href
(action
=>"history", hash_base
=>$hash_base,
5807 hash
=>$t->{'hash'}, file_name
=>"$basedir$t->{'name'}")},
5811 $cgi->a({-href
=> href
(action
=>"blob_plain", hash_base
=>$hash_base,
5812 file_name
=>"$basedir$t->{'name'}")},
5816 } elsif ($t->{'type'} eq "tree") {
5817 print "<td class=\"list\">";
5818 print $cgi->a({-href
=> href
(action
=>"tree", hash
=>$t->{'hash'},
5819 file_name
=>"$basedir$t->{'name'}",
5821 esc_path
($t->{'name'}));
5823 print "<td class=\"link\">";
5824 print $cgi->a({-href
=> href
(action
=>"tree", hash
=>$t->{'hash'},
5825 file_name
=>"$basedir$t->{'name'}",
5828 if (defined $hash_base) {
5830 $cgi->a({-href
=> href
(action
=>"history", hash_base
=>$hash_base,
5831 file_name
=>"$basedir$t->{'name'}")},
5836 # unknown object: we can only present history for it
5837 # (this includes 'commit' object, i.e. submodule support)
5838 print "<td class=\"list\">" .
5839 esc_path
($t->{'name'}) .
5841 print "<td class=\"link\">";
5842 if (defined $hash_base) {
5843 print $cgi->a({-href
=> href
(action
=>"history",
5844 hash_base
=>$hash_base,
5845 file_name
=>"$basedir$t->{'name'}")},
5852 ## ......................................................................
5853 ## functions printing large fragments of HTML
5855 # get pre-image filenames for merge (combined) diff
5856 sub fill_from_file_info
{
5857 my ($diff, @parents) = @_;
5859 $diff->{'from_file'} = [ ];
5860 $diff->{'from_file'}[$diff->{'nparents'} - 1] = undef;
5861 for (my $i = 0; $i < $diff->{'nparents'}; $i++) {
5862 if ($diff->{'status'}[$i] eq 'R' ||
5863 $diff->{'status'}[$i] eq 'C') {
5864 $diff->{'from_file'}[$i] =
5865 git_get_path_by_hash
($parents[$i], $diff->{'from_id'}[$i]);
5872 # is current raw difftree line of file deletion
5874 my $diffinfo = shift;
5876 return $diffinfo->{'to_id'} eq ('0' x
40);
5879 # does patch correspond to [previous] difftree raw line
5880 # $diffinfo - hashref of parsed raw diff format
5881 # $patchinfo - hashref of parsed patch diff format
5882 # (the same keys as in $diffinfo)
5883 sub is_patch_split
{
5884 my ($diffinfo, $patchinfo) = @_;
5886 return defined $diffinfo && defined $patchinfo
5887 && $diffinfo->{'to_file'} eq $patchinfo->{'to_file'};
5891 sub git_difftree_body
{
5892 my ($difftree, $hash, @parents) = @_;
5893 my ($parent) = $parents[0];
5894 my $have_blame = gitweb_check_feature
('blame');
5895 print "<div class=\"list_head\">\n";
5896 if ($#{$difftree} > 10) {
5897 print(($#{$difftree} + 1) . " files changed:\n");
5901 print "<table class=\"" .
5902 (@parents > 1 ?
"combined " : "") .
5905 # header only for combined diff in 'commitdiff' view
5906 my $has_header = @
$difftree && @parents > 1 && $action eq 'commitdiff';
5909 print "<thead><tr>\n" .
5910 "<th></th><th></th>\n"; # filename, patchN link
5911 for (my $i = 0; $i < @parents; $i++) {
5912 my $par = $parents[$i];
5914 $cgi->a({-href
=> href
(action
=>"commitdiff",
5915 hash
=>$hash, hash_parent
=>$par),
5916 -title
=> 'commitdiff to parent number ' .
5917 ($i+1) . ': ' . substr($par,0,7)},
5921 print "</tr></thead>\n<tbody>\n";
5926 foreach my $line (@
{$difftree}) {
5927 my $diff = parsed_difftree_line
($line);
5930 print "<tr class=\"dark\">\n";
5932 print "<tr class=\"light\">\n";
5936 if (exists $diff->{'nparents'}) { # combined diff
5938 fill_from_file_info
($diff, @parents)
5939 unless exists $diff->{'from_file'};
5941 if (!is_deleted
($diff)) {
5942 # file exists in the result (child) commit
5944 $cgi->a({-href
=> href
(action
=>"blob", hash
=>$diff->{'to_id'},
5945 file_name
=>$diff->{'to_file'},
5947 -class => "list"}, esc_path
($diff->{'to_file'})) .
5951 esc_path
($diff->{'to_file'}) .
5955 if ($action eq 'commitdiff') {
5958 print "<td class=\"link\">" .
5959 $cgi->a({-href
=> href
(-anchor
=>"patch$patchno")},
5965 my $has_history = 0;
5966 my $not_deleted = 0;
5967 for (my $i = 0; $i < $diff->{'nparents'}; $i++) {
5968 my $hash_parent = $parents[$i];
5969 my $from_hash = $diff->{'from_id'}[$i];
5970 my $from_path = $diff->{'from_file'}[$i];
5971 my $status = $diff->{'status'}[$i];
5973 $has_history ||= ($status ne 'A');
5974 $not_deleted ||= ($status ne 'D');
5976 if ($status eq 'A') {
5977 print "<td class=\"link\" align=\"right\"> | </td>\n";
5978 } elsif ($status eq 'D') {
5979 print "<td class=\"link\">" .
5980 $cgi->a({-href
=> href
(action
=>"blob",
5983 file_name
=>$from_path)},
5987 if ($diff->{'to_id'} eq $from_hash) {
5988 print "<td class=\"link nochange\">";
5990 print "<td class=\"link\">";
5992 print $cgi->a({-href
=> href
(action
=>"blobdiff",
5993 hash
=>$diff->{'to_id'},
5994 hash_parent
=>$from_hash,
5996 hash_parent_base
=>$hash_parent,
5997 file_name
=>$diff->{'to_file'},
5998 file_parent
=>$from_path)},
6004 print "<td class=\"link\">";
6006 print $cgi->a({-href
=> href
(action
=>"blob",
6007 hash
=>$diff->{'to_id'},
6008 file_name
=>$diff->{'to_file'},
6011 print " | " if ($has_history);
6014 print $cgi->a({-href
=> href
(action
=>"history",
6015 file_name
=>$diff->{'to_file'},
6022 next; # instead of 'else' clause, to avoid extra indent
6024 # else ordinary diff
6026 my ($to_mode_oct, $to_mode_str, $to_file_type);
6027 my ($from_mode_oct, $from_mode_str, $from_file_type);
6028 if ($diff->{'to_mode'} ne ('0' x
6)) {
6029 $to_mode_oct = oct $diff->{'to_mode'};
6030 if (S_ISREG
($to_mode_oct)) { # only for regular file
6031 $to_mode_str = sprintf("%04o", $to_mode_oct & 0777); # permission bits
6033 $to_file_type = file_type
($diff->{'to_mode'});
6035 if ($diff->{'from_mode'} ne ('0' x
6)) {
6036 $from_mode_oct = oct $diff->{'from_mode'};
6037 if (S_ISREG
($from_mode_oct)) { # only for regular file
6038 $from_mode_str = sprintf("%04o", $from_mode_oct & 0777); # permission bits
6040 $from_file_type = file_type
($diff->{'from_mode'});
6043 if ($diff->{'status'} eq "A") { # created
6044 my $mode_chng = "<span class=\"file_status new\">[new $to_file_type";
6045 $mode_chng .= " with mode: $to_mode_str" if $to_mode_str;
6046 $mode_chng .= "]</span>";
6048 print $cgi->a({-href
=> href
(action
=>"blob", hash
=>$diff->{'to_id'},
6049 hash_base
=>$hash, file_name
=>$diff->{'file'}),
6050 -class => "list"}, esc_path
($diff->{'file'}));
6052 print "<td>$mode_chng</td>\n";
6053 print "<td class=\"link\">";
6054 if ($action eq 'commitdiff') {
6057 print $cgi->a({-href
=> href
(-anchor
=>"patch$patchno")},
6061 print $cgi->a({-href
=> href
(action
=>"blob", hash
=>$diff->{'to_id'},
6062 hash_base
=>$hash, file_name
=>$diff->{'file'})},
6066 } elsif ($diff->{'status'} eq "D") { # deleted
6067 my $mode_chng = "<span class=\"file_status deleted\">[deleted $from_file_type]</span>";
6069 print $cgi->a({-href
=> href
(action
=>"blob", hash
=>$diff->{'from_id'},
6070 hash_base
=>$parent, file_name
=>$diff->{'file'}),
6071 -class => "list"}, esc_path
($diff->{'file'}));
6073 print "<td>$mode_chng</td>\n";
6074 print "<td class=\"link\">";
6075 if ($action eq 'commitdiff') {
6078 print $cgi->a({-href
=> href
(-anchor
=>"patch$patchno")},
6082 print $cgi->a({-href
=> href
(action
=>"blob", hash
=>$diff->{'from_id'},
6083 hash_base
=>$parent, file_name
=>$diff->{'file'})},
6086 print $cgi->a({-href
=> href
(action
=>"blame", hash_base
=>$parent,
6087 file_name
=>$diff->{'file'}),
6088 -class => "blamelink"},
6091 print $cgi->a({-href
=> href
(action
=>"history", hash_base
=>$parent,
6092 file_name
=>$diff->{'file'})},
6096 } elsif ($diff->{'status'} eq "M" || $diff->{'status'} eq "T") { # modified, or type changed
6097 my $mode_chnge = "";
6098 if ($diff->{'from_mode'} != $diff->{'to_mode'}) {
6099 $mode_chnge = "<span class=\"file_status mode_chnge\">[changed";
6100 if ($from_file_type ne $to_file_type) {
6101 $mode_chnge .= " from $from_file_type to $to_file_type";
6103 if (($from_mode_oct & 0777) != ($to_mode_oct & 0777)) {
6104 if ($from_mode_str && $to_mode_str) {
6105 $mode_chnge .= " mode: $from_mode_str->$to_mode_str";
6106 } elsif ($to_mode_str) {
6107 $mode_chnge .= " mode: $to_mode_str";
6110 $mode_chnge .= "]</span>\n";
6113 print $cgi->a({-href
=> href
(action
=>"blob", hash
=>$diff->{'to_id'},
6114 hash_base
=>$hash, file_name
=>$diff->{'file'}),
6115 -class => "list"}, esc_path
($diff->{'file'}));
6117 print "<td>$mode_chnge</td>\n";
6118 print "<td class=\"link\">";
6119 if ($action eq 'commitdiff') {
6122 print $cgi->a({-href
=> href
(-anchor
=>"patch$patchno")},
6125 } elsif ($diff->{'to_id'} ne $diff->{'from_id'}) {
6126 # "commit" view and modified file (not onlu mode changed)
6127 print $cgi->a({-href
=> href
(action
=>"blobdiff",
6128 hash
=>$diff->{'to_id'}, hash_parent
=>$diff->{'from_id'},
6129 hash_base
=>$hash, hash_parent_base
=>$parent,
6130 file_name
=>$diff->{'file'})},
6134 print $cgi->a({-href
=> href
(action
=>"blob", hash
=>$diff->{'to_id'},
6135 hash_base
=>$hash, file_name
=>$diff->{'file'})},
6138 print $cgi->a({-href
=> href
(action
=>"blame", hash_base
=>$hash,
6139 file_name
=>$diff->{'file'}),
6140 -class => "blamelink"},
6143 print $cgi->a({-href
=> href
(action
=>"history", hash_base
=>$hash,
6144 file_name
=>$diff->{'file'})},
6148 } elsif ($diff->{'status'} eq "R" || $diff->{'status'} eq "C") { # renamed or copied
6149 my %status_name = ('R' => 'moved', 'C' => 'copied');
6150 my $nstatus = $status_name{$diff->{'status'}};
6152 if ($diff->{'from_mode'} != $diff->{'to_mode'}) {
6153 # mode also for directories, so we cannot use $to_mode_str
6154 $mode_chng = sprintf(", mode: %04o", $to_mode_oct & 0777);
6157 $cgi->a({-href
=> href
(action
=>"blob", hash_base
=>$hash,
6158 hash
=>$diff->{'to_id'}, file_name
=>$diff->{'to_file'}),
6159 -class => "list"}, esc_path
($diff->{'to_file'})) . "</td>\n" .
6160 "<td><span class=\"file_status $nstatus\">[$nstatus from " .
6161 $cgi->a({-href
=> href
(action
=>"blob", hash_base
=>$parent,
6162 hash
=>$diff->{'from_id'}, file_name
=>$diff->{'from_file'}),
6163 -class => "list"}, esc_path
($diff->{'from_file'})) .
6164 " with " . (int $diff->{'similarity'}) . "% similarity$mode_chng]</span></td>\n" .
6165 "<td class=\"link\">";
6166 if ($action eq 'commitdiff') {
6169 print $cgi->a({-href
=> href
(-anchor
=>"patch$patchno")},
6172 } elsif ($diff->{'to_id'} ne $diff->{'from_id'}) {
6173 # "commit" view and modified file (not only pure rename or copy)
6174 print $cgi->a({-href
=> href
(action
=>"blobdiff",
6175 hash
=>$diff->{'to_id'}, hash_parent
=>$diff->{'from_id'},
6176 hash_base
=>$hash, hash_parent_base
=>$parent,
6177 file_name
=>$diff->{'to_file'}, file_parent
=>$diff->{'from_file'})},
6181 print $cgi->a({-href
=> href
(action
=>"blob", hash
=>$diff->{'to_id'},
6182 hash_base
=>$parent, file_name
=>$diff->{'to_file'})},
6185 print $cgi->a({-href
=> href
(action
=>"blame", hash_base
=>$hash,
6186 file_name
=>$diff->{'to_file'}),
6187 -class => "blamelink"},
6190 print $cgi->a({-href
=> href
(action
=>"history", hash_base
=>$hash,
6191 file_name
=>$diff->{'to_file'})},
6195 } # we should not encounter Unmerged (U) or Unknown (X) status
6198 print "</tbody>" if $has_header;
6202 # Print context lines and then rem/add lines in a side-by-side manner.
6203 sub print_sidebyside_diff_lines
{
6204 my ($ctx, $rem, $add) = @_;
6206 # print context block before add/rem block
6209 '<div class="chunk_block ctx">',
6210 '<div class="old">',
6213 '<div class="new">',
6222 '<div class="chunk_block rem">',
6223 '<div class="old">',
6230 '<div class="chunk_block add">',
6231 '<div class="new">',
6237 '<div class="chunk_block chg">',
6238 '<div class="old">',
6241 '<div class="new">',
6248 # Print context lines and then rem/add lines in inline manner.
6249 sub print_inline_diff_lines
{
6250 my ($ctx, $rem, $add) = @_;
6252 print @
$ctx, @
$rem, @
$add;
6255 # Format removed and added line, mark changed part and HTML-format them.
6256 # Implementation is based on contrib/diff-highlight
6257 sub format_rem_add_lines_pair
{
6258 my ($rem, $add, $num_parents) = @_;
6260 # We need to untabify lines before split()'ing them;
6261 # otherwise offsets would be invalid.
6264 $rem = untabify
($rem);
6265 $add = untabify
($add);
6267 my @rem = split(//, $rem);
6268 my @add = split(//, $add);
6269 my ($esc_rem, $esc_add);
6270 # Ignore leading +/- characters for each parent.
6271 my ($prefix_len, $suffix_len) = ($num_parents, 0);
6272 my ($prefix_has_nonspace, $suffix_has_nonspace);
6274 my $shorter = (@rem < @add) ?
@rem : @add;
6275 while ($prefix_len < $shorter) {
6276 last if ($rem[$prefix_len] ne $add[$prefix_len]);
6278 $prefix_has_nonspace = 1 if ($rem[$prefix_len] !~ /\s/);
6282 while ($prefix_len + $suffix_len < $shorter) {
6283 last if ($rem[-1 - $suffix_len] ne $add[-1 - $suffix_len]);
6285 $suffix_has_nonspace = 1 if ($rem[-1 - $suffix_len] !~ /\s/);
6289 # Mark lines that are different from each other, but have some common
6290 # part that isn't whitespace. If lines are completely different, don't
6291 # mark them because that would make output unreadable, especially if
6292 # diff consists of multiple lines.
6293 if ($prefix_has_nonspace || $suffix_has_nonspace) {
6294 $esc_rem = esc_html_hl_regions
($rem, 'marked',
6295 [$prefix_len, @rem - $suffix_len], -nbsp
=>1);
6296 $esc_add = esc_html_hl_regions
($add, 'marked',
6297 [$prefix_len, @add - $suffix_len], -nbsp
=>1);
6299 $esc_rem = esc_html
($rem, -nbsp
=>1);
6300 $esc_add = esc_html
($add, -nbsp
=>1);
6303 return format_diff_line
(\
$esc_rem, 'rem'),
6304 format_diff_line
(\
$esc_add, 'add');
6307 # HTML-format diff context, removed and added lines.
6308 sub format_ctx_rem_add_lines
{
6309 my ($ctx, $rem, $add, $num_parents) = @_;
6310 my (@new_ctx, @new_rem, @new_add);
6311 my $can_highlight = 0;
6312 my $is_combined = ($num_parents > 1);
6314 # Highlight if every removed line has a corresponding added line.
6315 if (@
$add > 0 && @
$add == @
$rem) {
6318 # Highlight lines in combined diff only if the chunk contains
6319 # diff between the same version, e.g.
6326 # Otherwise the highlightling would be confusing.
6328 for (my $i = 0; $i < @
$add; $i++) {
6329 my $prefix_rem = substr($rem->[$i], 0, $num_parents);
6330 my $prefix_add = substr($add->[$i], 0, $num_parents);
6332 $prefix_rem =~ s/-/+/g;
6334 if ($prefix_rem ne $prefix_add) {
6342 if ($can_highlight) {
6343 for (my $i = 0; $i < @
$add; $i++) {
6344 my ($line_rem, $line_add) = format_rem_add_lines_pair
(
6345 $rem->[$i], $add->[$i], $num_parents);
6346 push @new_rem, $line_rem;
6347 push @new_add, $line_add;
6350 @new_rem = map { format_diff_line
($_, 'rem') } @
$rem;
6351 @new_add = map { format_diff_line
($_, 'add') } @
$add;
6354 @new_ctx = map { format_diff_line
($_, 'ctx') } @
$ctx;
6356 return (\
@new_ctx, \
@new_rem, \
@new_add);
6359 # Print context lines and then rem/add lines.
6360 sub print_diff_lines
{
6361 my ($ctx, $rem, $add, $diff_style, $num_parents) = @_;
6362 my $is_combined = $num_parents > 1;
6364 ($ctx, $rem, $add) = format_ctx_rem_add_lines
($ctx, $rem, $add,
6367 if ($diff_style eq 'sidebyside' && !$is_combined) {
6368 print_sidebyside_diff_lines
($ctx, $rem, $add);
6370 # default 'inline' style and unknown styles
6371 print_inline_diff_lines
($ctx, $rem, $add);
6375 sub print_diff_chunk
{
6376 my ($diff_style, $num_parents, $from, $to, @chunk) = @_;
6377 my (@ctx, @rem, @add);
6379 # The class of the previous line.
6380 my $prev_class = '';
6382 return unless @chunk;
6384 # incomplete last line might be among removed or added lines,
6385 # or both, or among context lines: find which
6386 for (my $i = 1; $i < @chunk; $i++) {
6387 if ($chunk[$i][0] eq 'incomplete') {
6388 $chunk[$i][0] = $chunk[$i-1][0];
6393 push @chunk, ["", ""];
6395 foreach my $line_info (@chunk) {
6396 my ($class, $line) = @
$line_info;
6398 # print chunk headers
6399 if ($class && $class eq 'chunk_header') {
6400 print format_diff_line
($line, $class, $from, $to);
6404 ## print from accumulator when have some add/rem lines or end
6405 # of chunk (flush context lines), or when have add and rem
6406 # lines and new block is reached (otherwise add/rem lines could
6408 if (!$class || ((@rem || @add) && $class eq 'ctx') ||
6409 (@rem && @add && $class ne $prev_class)) {
6410 print_diff_lines
(\
@ctx, \
@rem, \
@add,
6411 $diff_style, $num_parents);
6412 @ctx = @rem = @add = ();
6415 ## adding lines to accumulator
6418 # rem, add or change
6419 if ($class eq 'rem') {
6421 } elsif ($class eq 'add') {
6425 if ($class eq 'ctx') {
6429 $prev_class = $class;
6433 sub git_patchset_body
{
6434 my ($fd, $diff_style, $difftree, $hash, @hash_parents) = @_;
6435 my ($hash_parent) = $hash_parents[0];
6437 my $is_combined = (@hash_parents > 1);
6439 my $patch_number = 0;
6444 my @chunk; # for side-by-side diff
6446 print "<div class=\"patchset\">\n";
6448 # skip to first patch
6449 while ($patch_line = to_utf8
(scalar <$fd>)) {
6452 last if ($patch_line =~ m/^diff /);
6456 while ($patch_line) {
6458 # parse "git diff" header line
6459 if ($patch_line =~ m/^diff --git (\"(?:[^\\\"]*(?:\\.[^\\\"]*)*)\"|[^ "]*) (.*)$/) {
6460 # $1 is from_name, which we do not use
6461 $to_name = unquote
($2);
6462 $to_name =~ s!^b/!!;
6463 } elsif ($patch_line =~ m/^diff --(cc|combined) ("?.*"?)$/) {
6464 # $1 is 'cc' or 'combined', which we do not use
6465 $to_name = unquote
($2);
6470 # check if current patch belong to current raw line
6471 # and parse raw git-diff line if needed
6472 if (is_patch_split
($diffinfo, { 'to_file' => $to_name })) {
6473 # this is continuation of a split patch
6474 print "<div class=\"patch cont\">\n";
6476 # advance raw git-diff output if needed
6477 $patch_idx++ if defined $diffinfo;
6479 # read and prepare patch information
6480 $diffinfo = parsed_difftree_line
($difftree->[$patch_idx]);
6482 # compact combined diff output can have some patches skipped
6483 # find which patch (using pathname of result) we are at now;
6485 while ($to_name ne $diffinfo->{'to_file'}) {
6486 print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n" .
6487 format_diff_cc_simplified
($diffinfo, @hash_parents) .
6488 "</div>\n"; # class="patch"
6493 last if $patch_idx > $#$difftree;
6494 $diffinfo = parsed_difftree_line
($difftree->[$patch_idx]);
6498 # modifies %from, %to hashes
6499 parse_from_to_diffinfo
($diffinfo, \
%from, \
%to, @hash_parents);
6501 # this is first patch for raw difftree line with $patch_idx index
6502 # we index @$difftree array from 0, but number patches from 1
6503 print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n";
6507 #assert($patch_line =~ m/^diff /) if DEBUG;
6508 #assert($patch_line !~ m!$/$!) if DEBUG; # is chomp-ed
6510 # print "git diff" header
6511 print format_git_diff_header_line
($patch_line, $diffinfo,
6514 # print extended diff header
6515 print "<div class=\"diff extended_header\">\n";
6517 while ($patch_line = to_utf8
(scalar<$fd>)) {
6520 last EXTENDED_HEADER
if ($patch_line =~ m/^--- |^diff /);
6522 print format_extended_diff_header_line
($patch_line, $diffinfo,
6525 print "</div>\n"; # class="diff extended_header"
6527 # from-file/to-file diff header
6528 if (! $patch_line) {
6529 print "</div>\n"; # class="patch"
6532 next PATCH
if ($patch_line =~ m/^diff /);
6533 #assert($patch_line =~ m/^---/) if DEBUG;
6535 my $last_patch_line = $patch_line;
6536 $patch_line = to_utf8
(scalar <$fd>);
6538 #assert($patch_line =~ m/^\+\+\+/) if DEBUG;
6540 print format_diff_from_to_header
($last_patch_line, $patch_line,
6541 $diffinfo, \
%from, \
%to,
6546 while ($patch_line = to_utf8
(scalar <$fd>)) {
6549 next PATCH
if ($patch_line =~ m/^diff /);
6551 my $class = diff_line_class
($patch_line, \
%from, \
%to);
6553 if ($class eq 'chunk_header') {
6554 print_diff_chunk
($diff_style, scalar @hash_parents, \
%from, \
%to, @chunk);
6558 push @chunk, [ $class, $patch_line ];
6563 print_diff_chunk
($diff_style, scalar @hash_parents, \
%from, \
%to, @chunk);
6566 print "</div>\n"; # class="patch"
6569 # for compact combined (--cc) format, with chunk and patch simplification
6570 # the patchset might be empty, but there might be unprocessed raw lines
6571 for (++$patch_idx if $patch_number > 0;
6572 $patch_idx < @
$difftree;
6574 # read and prepare patch information
6575 $diffinfo = parsed_difftree_line
($difftree->[$patch_idx]);
6577 # generate anchor for "patch" links in difftree / whatchanged part
6578 print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n" .
6579 format_diff_cc_simplified
($diffinfo, @hash_parents) .
6580 "</div>\n"; # class="patch"
6585 if ($patch_number == 0) {
6586 if (@hash_parents > 1) {
6587 print "<div class=\"diff nodifferences\">Trivial merge</div>\n";
6589 print "<div class=\"diff nodifferences\">No differences found</div>\n";
6593 print "</div>\n"; # class="patchset"
6596 # . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
6598 sub git_project_search_form
{
6599 my ($searchtext, $search_use_regexp) = @_;
6602 if ($project_filter) {
6603 $limit = " in '$project_filter'";
6606 print "<div class=\"projsearch\">\n";
6607 print $cgi->start_form(-method
=> 'get', -action
=> $my_uri) .
6608 $cgi->hidden(-name
=> 'a', -value
=> 'project_list') . "\n";
6609 print $cgi->hidden(-name
=> 'pf', -value
=> $project_filter). "\n"
6610 if (defined $project_filter);
6611 print $cgi->textfield(-name
=> 's', -value
=> $searchtext,
6612 -title
=> "Search project by name and description$limit",
6613 -size
=> 60) . "\n" .
6614 "<span title=\"Extended regular expression\">" .
6615 $cgi->checkbox(-name
=> 'sr', -value
=> 1, -label
=> 're',
6616 -checked
=> $search_use_regexp) .
6618 $cgi->submit(-name
=> 'btnS', -value
=> 'Search') .
6619 $cgi->end_form() . "\n" .
6620 "<span class=\"projectlist_link\">" .
6621 $cgi->a({-href
=> href
(project
=> undef, searchtext
=> undef,
6622 action
=> 'project_list',
6623 project_filter
=> $project_filter)},
6624 esc_html
("List all projects$limit")) . "</span><br />\n";
6625 print "<span class=\"projectlist_link\">" .
6626 $cgi->a({-href
=> href
(project
=> undef, searchtext
=> undef,
6627 action
=> 'project_list',
6628 project_filter
=> undef)},
6629 esc_html
("List all projects")) . "</span>\n" if $project_filter;
6633 # entry for given @keys needs filling if at least one of keys in list
6634 # is not present in %$project_info
6635 sub project_info_needs_filling
{
6636 my ($project_info, @keys) = @_;
6638 # return List::MoreUtils::any { !exists $project_info->{$_} } @keys;
6639 foreach my $key (@keys) {
6640 if (!exists $project_info->{$key}) {
6647 sub git_cache_file_format
{
6648 return GITWEB_CACHE_FORMAT
.
6649 (gitweb_check_feature
('forks') ?
" (forks)" : "");
6652 sub git_retrieve_cache_file
{
6653 my $cache_file = shift;
6655 use Storable
qw(retrieve);
6657 if ((my $dump = eval { retrieve
($cache_file) })) {
6659 ref($dump) eq 'ARRAY' &&
6661 ref($$dump[1]) eq 'ARRAY' &&
6662 @
{$$dump[1]} == 2 &&
6663 ref(${$$dump[1]}[0]) eq 'ARRAY' &&
6664 ref(${$$dump[1]}[1]) eq 'HASH' &&
6665 $$dump[0] eq git_cache_file_format
();
6671 sub git_store_cache_file
{
6672 my ($cache_file, $cachedata) = @_;
6674 use File
::Basename
qw(dirname);
6676 use POSIX
qw(:fcntl_h);
6677 use Storable
qw(store_fd);
6680 my $cache_d = dirname
($cache_file);
6682 umask($mask & ~0070) if $cache_grpshared;
6683 if ((-d
$cache_d || mkdir($cache_d, $cache_grpshared ?
0770 : 0700)) &&
6684 sysopen(my $fd, "$cache_file.lock", O_WRONLY
|O_CREAT
|O_EXCL
, $cache_grpshared ?
0660 : 0600)) {
6685 store_fd
([git_cache_file_format
(), $cachedata], $fd);
6687 rename "$cache_file.lock", $cache_file;
6688 $result = stat($cache_file)->mtime;
6690 umask($mask) if $cache_grpshared;
6694 sub verify_cached_project
{
6695 my ($hashref, $path) = @_;
6696 return undef unless $path;
6697 delete $$hashref{$path}, return undef unless is_valid_project
($path);
6698 return $$hashref{$path} if exists $$hashref{$path};
6700 # A valid project was requested but it's not yet in the cache
6701 # Manufacture a minimal project entry (path, name, description)
6702 # Also provide age, but only if it's available via $lastactivity_file
6704 my %proj = ('path' => $path);
6705 my $val = git_get_project_description
($path);
6706 defined $val or $val = '';
6707 $proj{'descr_long'} = $val;
6708 $proj{'descr'} = chop_str
($val, $projects_list_description_width, 5);
6709 unless ($omit_owner) {
6710 $val = git_get_project_owner
($path);
6711 defined $val or $val = '';
6712 $proj{'owner'} = $val;
6714 unless ($omit_age_column) {
6715 ($val) = git_get_last_activity
($path, 1);
6716 $proj{'age_epoch'} = $val if defined $val;
6718 $$hashref{$path} = \
%proj;
6722 sub git_filter_cached_projects
{
6723 my ($cache, $projlist, $verify) = @_;
6724 my $hashref = $$cache[1];
6726 sub {verify_cached_project
($hashref, $_[0])} :
6727 sub {$$hashref{$_[0]}};
6729 my $c = &$sub($_->{'path'});
6730 defined $c ?
($_ = $c) : ()
6734 # fills project list info (age, description, owner, category, forks, etc.)
6735 # for each project in the list, removing invalid projects from
6736 # returned list, or fill only specified info.
6738 # Invalid projects are removed from the returned list if and only if you
6739 # ask 'age_epoch' to be filled, because they are the only fields
6740 # that run unconditionally git command that requires repository, and
6741 # therefore do always check if project repository is invalid.
6744 # * fill_project_list_info(\@project_list, 'descr_long', 'ctags')
6745 # ensures that 'descr_long' and 'ctags' fields are filled
6746 # * @project_list = fill_project_list_info(\@project_list)
6747 # ensures that all fields are filled (and invalid projects removed)
6749 # NOTE: modifies $projlist, but does not remove entries from it
6750 sub fill_project_list_info
{
6751 my ($projlist, @wanted_keys) = @_;
6753 my $rebuild = @wanted_keys && $wanted_keys[0] eq 'rebuild-cache' && shift @wanted_keys;
6754 return fill_project_list_info_uncached
($projlist, @wanted_keys)
6755 unless $projlist_cache_lifetime && $projlist_cache_lifetime > 0;
6759 my $cache_lifetime = $rebuild ?
0 : $projlist_cache_lifetime;
6760 my $cache_file = "$cache_dir/$projlist_cache_name";
6766 if ($cache_lifetime && -f
$cache_file) {
6767 $cache_mtime = stat($cache_file)->mtime;
6768 $cache_dump = undef if $cache_mtime &&
6769 (!$cache_dump_mtime || $cache_dump_mtime != $cache_mtime);
6771 if (defined $cache_mtime && # caching is on and $cache_file exists
6772 $cache_mtime + $cache_lifetime*60 > $now &&
6773 ($cache_dump || ($cache_dump = git_retrieve_cache_file
($cache_file)))) {
6775 $cache_dump_mtime = $cache_mtime;
6776 $stale = $now - $cache_mtime;
6777 my $verify = ($action eq 'summary' || $action eq 'forks') &&
6778 gitweb_check_feature
('forks');
6779 @projects = git_filter_cached_projects
($cache_dump, $projlist, $verify);
6781 } else { # Cache miss.
6782 if (defined $cache_mtime) {
6783 # Postpone timeout by two minutes so that we get
6784 # enough time to do our job, or to be more exact
6785 # make cache expire after two minutes from now.
6786 my $time = $now - $cache_lifetime*60 + 120;
6787 utime $time, $time, $cache_file;
6789 my @all_projects = git_get_projects_list
();
6790 my %all_projects_filled = map { ( $_->{'path'} => $_ ) }
6791 fill_project_list_info_uncached
(\
@all_projects);
6792 map { $all_projects_filled{$_->{'path'}} = $_ }
6793 filter_forks_from_projects_list
([values(%all_projects_filled)])
6794 if gitweb_check_feature
('forks');
6795 $cache_dump = [[sort {$a->{'path'} cmp $b->{'path'}} values(%all_projects_filled)],
6796 \
%all_projects_filled];
6797 $cache_dump_mtime = git_store_cache_file
($cache_file, $cache_dump);
6798 @projects = git_filter_cached_projects
($cache_dump, $projlist);
6801 if ($cache_lifetime && $stale > 0) {
6802 print "<div class=\"stale_info\">Cached version (${stale}s old)</div>\n"
6803 unless $shown_stale_message;
6804 $shown_stale_message = 1;
6810 sub fill_project_list_info_uncached
{
6811 my ($projlist, @wanted_keys) = @_;
6813 my $filter_set = sub { return @_; };
6815 my %wanted_keys = map { $_ => 1 } @wanted_keys;
6816 $filter_set = sub { return grep { $wanted_keys{$_} } @_; };
6819 my $show_ctags = gitweb_check_feature
('ctags');
6821 foreach my $pr (@
$projlist) {
6822 if (project_info_needs_filling
($pr, $filter_set->('age_epoch'))) {
6823 my (@activity) = git_get_last_activity
($pr->{'path'});
6824 unless (@activity) {
6827 ($pr->{'age_epoch'}) = @activity;
6829 if (project_info_needs_filling
($pr, $filter_set->('descr', 'descr_long'))) {
6830 my $descr = git_get_project_description
($pr->{'path'}) || "";
6831 $descr = to_utf8
($descr);
6832 $pr->{'descr_long'} = $descr;
6833 $pr->{'descr'} = chop_str
($descr, $projects_list_description_width, 5);
6835 if (project_info_needs_filling
($pr, $filter_set->('owner'))) {
6836 $pr->{'owner'} = git_get_project_owner
("$pr->{'path'}") || "";
6839 project_info_needs_filling
($pr, $filter_set->('ctags'))) {
6840 $pr->{'ctags'} = git_get_project_ctags
($pr->{'path'});
6842 if ($projects_list_group_categories &&
6843 project_info_needs_filling
($pr, $filter_set->('category'))) {
6844 my $cat = git_get_project_category
($pr->{'path'}) ||
6845 $project_list_default_category;
6846 $pr->{'category'} = to_utf8
($cat);
6849 push @projects, $pr;
6855 sub sort_projects_list
{
6856 my ($projlist, $order) = @_;
6860 return sub { lc($a->{$key}) cmp lc($b->{$key}) };
6863 sub order_reverse_num_then_undef
{
6866 defined $a->{$key} ?
6867 (defined $b->{$key} ?
$b->{$key} <=> $a->{$key} : -1) :
6868 (defined $b->{$key} ?
1 : 0)
6873 project
=> order_str
('path'),
6874 descr
=> order_str
('descr_long'),
6875 owner
=> order_str
('owner'),
6876 age
=> order_reverse_num_then_undef
('age_epoch'),
6879 my $ordering = $orderings{$order};
6880 return defined $ordering ?
sort $ordering @
$projlist : @
$projlist;
6883 # returns a hash of categories, containing the list of project
6884 # belonging to each category
6885 sub build_projlist_by_category
{
6886 my ($projlist, $from, $to) = @_;
6889 $from = 0 unless defined $from;
6890 $to = $#$projlist if (!defined $to || $#$projlist < $to);
6892 for (my $i = $from; $i <= $to; $i++) {
6893 my $pr = $projlist->[$i];
6894 push @
{$categories{ $pr->{'category'} }}, $pr;
6897 return wantarray ?
%categories : \
%categories;
6900 # print 'sort by' <th> element, generating 'sort by $name' replay link
6901 # if that order is not selected
6903 print format_sort_th
(@_);
6906 sub format_sort_th
{
6907 my ($name, $order, $header) = @_;
6909 $header ||= ucfirst($name);
6911 if ($order eq $name) {
6912 $sort_th .= "<th>$header</th>\n";
6914 $sort_th .= "<th>" .
6915 $cgi->a({-href
=> href
(-replay
=>1, order
=>$name),
6916 -class => "header"}, $header) .
6923 sub git_project_list_rows
{
6924 my ($projlist, $from, $to, $check_forks) = @_;
6926 $from = 0 unless defined $from;
6927 $to = $#$projlist if (!defined $to || $#$projlist < $to);
6931 for (my $i = $from; $i <= $to; $i++) {
6932 my $pr = $projlist->[$i];
6935 print "<tr class=\"dark\">\n";
6937 print "<tr class=\"light\">\n";
6943 if ($pr->{'forks'}) {
6944 my $nforks = scalar @
{$pr->{'forks'}};
6945 my $s = $nforks == 1 ?
'' : 's';
6947 print $cgi->a({-href
=> href
(project
=>$pr->{'path'}, action
=>"forks"),
6948 -title
=> "$nforks fork$s"}, "+");
6950 print $cgi->span({-title
=> "$nforks fork$s"}, "+");
6955 my $path = $pr->{'path'};
6956 my $dotgit = $path =~ s/\.git$// ?
'.git' : '';
6957 print "<td>" . $cgi->a({-href
=> href
(project
=>$pr->{'path'}, action
=>"summary"),
6959 esc_html_match_hl
($path, $search_regexp).$dotgit) .
6961 "<td>" . $cgi->a({-href
=> href
(project
=>$pr->{'path'}, action
=>"summary"),
6963 -title
=> $pr->{'descr_long'}},
6965 ? esc_html_match_hl_chopped
($pr->{'descr_long'},
6966 $pr->{'descr'}, $search_regexp)
6967 : esc_html
($pr->{'descr'})) .
6969 unless ($omit_owner) {
6970 print "<td><i>" . ($owner_link_hook
6971 ?
$cgi->a({-href
=> $owner_link_hook->($pr->{'owner'}), -class => "list"},
6972 chop_and_escape_str
($pr->{'owner'}, 15))
6973 : chop_and_escape_str
($pr->{'owner'}, 15)) . "</i></td>\n";
6975 unless ($omit_age_column) {
6976 my ($age_epoch, $age_string) = ($pr->{'age_epoch'});
6977 $age_string = defined $age_epoch ? age_string
($age_epoch, $now) : "No commits";
6978 print "<td class=\"". age_class
($age_epoch, $now) . "\">" . $age_string . "</td>\n";
6980 print"<td class=\"link\">" .
6981 $cgi->a({-href
=> href
(project
=>$pr->{'path'}, action
=>"summary")}, "summary") . " | " .
6982 $cgi->a({-href
=> href
(project
=>$pr->{'path'}, action
=>"shortlog")}, "log") . " | " .
6983 $cgi->a({-href
=> href
(project
=>$pr->{'path'}, action
=>"tree")}, "tree") .
6984 ($pr->{'forks'} ?
" | " . $cgi->a({-href
=> href
(project
=>$pr->{'path'}, action
=>"forks")}, "forks") : '') .
6990 sub git_project_list_body
{
6991 # actually uses global variable $project
6992 my ($projlist, $order, $from, $to, $extra, $no_header, $ctags_action, $keep_top) = @_;
6993 my @projects = @
$projlist;
6995 my $check_forks = gitweb_check_feature
('forks');
6996 my $show_ctags = gitweb_check_feature
('ctags');
6997 my $tagfilter = $show_ctags ?
$input_params{'ctag_filter'} : undef;
6998 $check_forks = undef
6999 if ($tagfilter || $search_regexp);
7001 # filtering out forks before filling info allows us to do less work
7003 @projects = filter_forks_from_projects_list
(\
@projects);
7004 push @projects, { 'path' => "$project_filter.git" }
7005 if $project_filter && $keep_top && is_valid_project
("$project_filter.git");
7007 # search_projects_list pre-fills required info
7008 @projects = search_projects_list
(\
@projects,
7009 'search_regexp' => $search_regexp,
7010 'tagfilter' => $tagfilter)
7011 if ($tagfilter || $search_regexp);
7013 my @all_fields = ('descr', 'descr_long', 'ctags', 'category');
7014 push @all_fields, 'age_epoch' unless($omit_age_column);
7015 push @all_fields, 'owner' unless($omit_owner);
7016 @projects = fill_project_list_info
(\
@projects, @all_fields);
7018 $order ||= $default_projects_order;
7019 $from = 0 unless defined $from;
7020 $to = $#projects if (!defined $to || $#projects < $to);
7025 "<b>No such projects found</b><br />\n".
7026 "Click ".$cgi->a({-href
=>href
(project
=>undef,action
=>'project_list')},"here")." to view all projects<br />\n".
7027 "</center>\n<br />\n";
7031 @projects = sort_projects_list
(\
@projects, $order);
7034 my $ctags = git_gather_all_ctags
(\
@projects);
7035 my $cloud = git_populate_project_tagcloud
($ctags, $ctags_action||'project_list');
7036 print git_show_project_tagcloud
($cloud, 64);
7039 print "<table class=\"project_list\">\n";
7040 unless ($no_header) {
7043 print "<th></th>\n";
7045 print_sort_th
('project', $order, 'Project');
7046 print_sort_th
('descr', $order, 'Description');
7047 print_sort_th
('owner', $order, 'Owner') unless $omit_owner;
7048 print_sort_th
('age', $order, 'Last Change') unless $omit_age_column;
7049 print "<th></th>\n" . # for links
7053 if ($projects_list_group_categories) {
7054 # only display categories with projects in the $from-$to window
7055 @projects = sort {$a->{'category'} cmp $b->{'category'}} @projects[$from..$to];
7056 my %categories = build_projlist_by_category
(\
@projects, $from, $to);
7057 foreach my $cat (sort keys %categories) {
7058 unless ($cat eq "") {
7061 print "<td></td>\n";
7063 print "<td class=\"category\" colspan=\"5\">".esc_html
($cat)."</td>\n";
7067 git_project_list_rows
($categories{$cat}, undef, undef, $check_forks);
7070 git_project_list_rows
(\
@projects, $from, $to, $check_forks);
7073 if (defined $extra) {
7076 print "<td></td>\n";
7078 print "<td colspan=\"5\">$extra</td>\n" .
7085 # uses global variable $project
7086 my ($commitlist, $from, $to, $refs, $extra) = @_;
7088 $from = 0 unless defined $from;
7089 $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
7091 for (my $i = 0; $i <= $to; $i++) {
7092 my %co = %{$commitlist->[$i]};
7094 my $commit = $co{'id'};
7095 my $ref = format_ref_marker
($refs, $commit);
7096 git_print_header_div
('commit',
7097 "<span class=\"age\">$co{'age_string'}</span>" .
7098 esc_html
($co{'title'}),
7099 $commit, undef, $ref);
7100 print "<div class=\"title_text\">\n" .
7101 "<div class=\"log_link\">\n" .
7102 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$commit)}, "commit") .
7104 $cgi->a({-href
=> href
(action
=>"commitdiff", hash
=>$commit)}, "commitdiff") .
7106 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$commit, hash_base
=>$commit)}, "tree") .
7109 git_print_authorship
(\
%co, -tag
=> 'span');
7110 print "<br/>\n</div>\n";
7112 print "<div class=\"log_body\">\n";
7113 git_print_log
($co{'comment'}, -final_empty_line
=> 1);
7117 print "<div class=\"page_nav\">\n";
7123 sub git_shortlog_body
{
7124 # uses global variable $project
7125 my ($commitlist, $from, $to, $refs, $extra) = @_;
7127 $from = 0 unless defined $from;
7128 $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
7130 print "<table class=\"shortlog\">\n";
7132 for (my $i = $from; $i <= $to; $i++) {
7133 my %co = %{$commitlist->[$i]};
7134 my $commit = $co{'id'};
7135 my $ref = format_ref_marker
($refs, $commit);
7137 print "<tr class=\"dark\">\n";
7139 print "<tr class=\"light\">\n";
7142 # git_summary() used print "<td><i>$co{'age_string'}</i></td>\n" .
7143 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
7144 format_author_html
('td', \
%co, 10) . "<td>";
7145 print format_subject_html
($co{'title'}, $co{'title_short'},
7146 href
(action
=>"commit", hash
=>$commit), $ref);
7148 "<td class=\"link\">" .
7149 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$commit)}, "commit") . " | " .
7150 $cgi->a({-href
=> href
(action
=>"commitdiff", hash
=>$commit)}, "commitdiff") . " | " .
7151 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$commit, hash_base
=>$commit)}, "tree");
7152 my $snapshot_links = format_snapshot_links
($commit);
7153 if (defined $snapshot_links) {
7154 print " | " . $snapshot_links;
7159 if (defined $extra) {
7161 "<td colspan=\"4\">$extra</td>\n" .
7167 sub git_history_body
{
7168 # Warning: assumes constant type (blob or tree) during history
7169 my ($commitlist, $from, $to, $refs, $extra,
7170 $file_name, $file_hash, $ftype) = @_;
7172 $from = 0 unless defined $from;
7173 $to = $#{$commitlist} unless (defined $to && $to <= $#{$commitlist});
7175 print "<table class=\"history\">\n";
7177 for (my $i = $from; $i <= $to; $i++) {
7178 my %co = %{$commitlist->[$i]};
7182 my $commit = $co{'id'};
7184 my $ref = format_ref_marker
($refs, $commit);
7187 print "<tr class=\"dark\">\n";
7189 print "<tr class=\"light\">\n";
7192 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
7193 # shortlog: format_author_html('td', \%co, 10)
7194 format_author_html
('td', \
%co, 15, 3) . "<td>";
7195 # originally git_history used chop_str($co{'title'}, 50)
7196 print format_subject_html
($co{'title'}, $co{'title_short'},
7197 href
(action
=>"commit", hash
=>$commit), $ref);
7199 "<td class=\"link\">" .
7200 $cgi->a({-href
=> href
(action
=>$ftype, hash_base
=>$commit, file_name
=>$file_name)}, $ftype) . " | " .
7201 $cgi->a({-href
=> href
(action
=>"commitdiff", hash
=>$commit)}, "commitdiff");
7203 if ($ftype eq 'blob') {
7204 my $blob_current = $file_hash;
7205 my $blob_parent = git_get_hash_by_path
($commit, $file_name);
7206 if (defined $blob_current && defined $blob_parent &&
7207 $blob_current ne $blob_parent) {
7209 $cgi->a({-href
=> href
(action
=>"blobdiff",
7210 hash
=>$blob_current, hash_parent
=>$blob_parent,
7211 hash_base
=>$hash_base, hash_parent_base
=>$commit,
7212 file_name
=>$file_name)},
7219 if (defined $extra) {
7221 "<td colspan=\"4\">$extra</td>\n" .
7228 # uses global variable $project
7229 my ($taglist, $from, $to, $extra, $head_at, $full, $order) = @_;
7230 $from = 0 unless defined $from;
7231 $to = $#{$taglist} if (!defined $to || $#{$taglist} < $to);
7232 $order ||= $default_refs_order;
7234 print "<table class=\"tags\">\n";
7236 print "<tr class=\"tags_header\">\n";
7237 print_sort_th
('age', $order, 'Last Change');
7238 print_sort_th
('name', $order, 'Name');
7239 print "<th></th>\n" . # for comment
7240 "<th></th>\n" . # for tag
7241 "<th></th>\n" . # for links
7245 for (my $i = $from; $i <= $to; $i++) {
7246 my $entry = $taglist->[$i];
7248 my $comment = $tag{'subject'};
7250 if (defined $comment) {
7251 $comment_short = chop_str
($comment, 30, 5);
7253 my $curr = defined $head_at && $tag{'id'} eq $head_at;
7255 print "<tr class=\"dark\">\n";
7257 print "<tr class=\"light\">\n";
7260 if (defined $tag{'age'}) {
7261 print "<td><i>$tag{'age'}</i></td>\n";
7263 print "<td></td>\n";
7265 print(($curr ?
"<td class=\"current_head\">" : "<td>") .
7266 $cgi->a({-href
=> href
(action
=>$tag{'reftype'}, hash
=>$tag{'refid'}),
7267 -class => "list name"}, esc_html
($tag{'name'})) .
7270 if (defined $comment) {
7271 print format_subject_html
($comment, $comment_short,
7272 href
(action
=>"tag", hash
=>$tag{'id'}));
7275 "<td class=\"selflink\">";
7276 if ($tag{'type'} eq "tag") {
7277 print $cgi->a({-href
=> href
(action
=>"tag", hash
=>$tag{'id'})}, "tag");
7282 "<td class=\"link\">" . " | " .
7283 $cgi->a({-href
=> href
(action
=>$tag{'reftype'}, hash
=>$tag{'refid'})}, $tag{'reftype'});
7284 if ($tag{'reftype'} eq "commit") {
7285 print " | " . $cgi->a({-href
=> href
(action
=>"shortlog", hash
=>$tag{'fullname'})}, "log");
7286 print " | " . $cgi->a({-href
=> href
(action
=>"tree", hash
=>$tag{'fullname'})}, "tree") if $full;
7287 } elsif ($tag{'reftype'} eq "blob") {
7288 print " | " . $cgi->a({-href
=> href
(action
=>"blob_plain", hash
=>$tag{'refid'})}, "raw");
7293 if (defined $extra) {
7295 "<td colspan=\"5\">$extra</td>\n" .
7301 sub git_heads_body
{
7302 # uses global variable $project
7303 my ($headlist, $head_at, $from, $to, $extra) = @_;
7304 $from = 0 unless defined $from;
7305 $to = $#{$headlist} if (!defined $to || $#{$headlist} < $to);
7307 print "<table class=\"heads\">\n";
7309 for (my $i = $from; $i <= $to; $i++) {
7310 my $entry = $headlist->[$i];
7312 my $curr = defined $head_at && $ref{'id'} eq $head_at;
7314 print "<tr class=\"dark\">\n";
7316 print "<tr class=\"light\">\n";
7319 print "<td><i>$ref{'age'}</i></td>\n" .
7320 ($curr ?
"<td class=\"current_head\">" : "<td>") .
7321 $cgi->a({-href
=> href
(action
=>"shortlog", hash
=>$ref{'fullname'}),
7322 -class => "list name"},esc_html
($ref{'name'})) .
7324 "<td class=\"link\">" .
7325 $cgi->a({-href
=> href
(action
=>"shortlog", hash
=>$ref{'fullname'})}, "log") . " | " .
7326 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$ref{'fullname'}, hash_base
=>$ref{'fullname'})}, "tree") .
7330 if (defined $extra) {
7332 "<td colspan=\"3\">$extra</td>\n" .
7338 # Display a single remote block
7339 sub git_remote_block
{
7340 my ($remote, $rdata, $limit, $head) = @_;
7342 my $heads = $rdata->{'heads'};
7343 my $fetch = $rdata->{'fetch'};
7344 my $push = $rdata->{'push'};
7346 my $urls_table = "<table class=\"projects_list\">\n" ;
7348 if (defined $fetch) {
7349 if ($fetch eq $push) {
7350 $urls_table .= format_repo_url
("URL", $fetch);
7352 $urls_table .= format_repo_url
("Fetch URL", $fetch);
7353 $urls_table .= format_repo_url
("Push URL", $push) if defined $push;
7355 } elsif (defined $push) {
7356 $urls_table .= format_repo_url
("Push URL", $push);
7358 $urls_table .= format_repo_url
("", "No remote URL");
7361 $urls_table .= "</table>\n";
7364 if (defined $limit && $limit < @
$heads) {
7365 $dots = $cgi->a({-href
=> href
(action
=>"remotes", hash
=>$remote)}, "...");
7369 git_heads_body
($heads, $head, 0, $limit, $dots);
7372 # Display a list of remote names with the respective fetch and push URLs
7373 sub git_remotes_list
{
7374 my ($remotedata, $limit) = @_;
7375 print "<table class=\"heads\">\n";
7377 my @remotes = sort keys %$remotedata;
7379 my $limited = $limit && $limit < @remotes;
7381 $#remotes = $limit - 1 if $limited;
7383 while (my $remote = shift @remotes) {
7384 my $rdata = $remotedata->{$remote};
7385 my $fetch = $rdata->{'fetch'};
7386 my $push = $rdata->{'push'};
7388 print "<tr class=\"dark\">\n";
7390 print "<tr class=\"light\">\n";
7394 $cgi->a({-href
=> href
(action
=>'remotes', hash
=>$remote),
7395 -class=> "list name"},esc_html
($remote)) .
7397 print "<td class=\"link\">" .
7398 (defined $fetch ?
$cgi->a({-href
=> $fetch}, "fetch") : "fetch") .
7400 (defined $push ?
$cgi->a({-href
=> $push}, "push") : "push") .
7408 "<td colspan=\"3\">" .
7409 $cgi->a({-href
=> href
(action
=>"remotes")}, "...") .
7410 "</td>\n" . "</tr>\n";
7416 # Display remote heads grouped by remote, unless there are too many
7417 # remotes, in which case we only display the remote names
7418 sub git_remotes_body
{
7419 my ($remotedata, $limit, $head) = @_;
7420 if ($limit and $limit < keys %$remotedata) {
7421 git_remotes_list
($remotedata, $limit);
7423 fill_remote_heads
($remotedata);
7424 while (my ($remote, $rdata) = each %$remotedata) {
7425 git_print_section
({-class=>"remote", -id
=>$remote},
7426 ["remotes", $remote, $remote], sub {
7427 git_remote_block
($remote, $rdata, $limit, $head);
7433 sub git_search_message
{
7437 if ($searchtype eq 'commit') {
7438 $greptype = "--grep=";
7439 } elsif ($searchtype eq 'author') {
7440 $greptype = "--author=";
7441 } elsif ($searchtype eq 'committer') {
7442 $greptype = "--committer=";
7444 $greptype .= $searchtext;
7445 my @commitlist = parse_commits
($hash, 101, (100 * $page), undef,
7446 $greptype, '--regexp-ignore-case',
7447 $search_use_regexp ?
'--extended-regexp' : '--fixed-strings');
7449 my $paging_nav = '';
7452 $cgi->a({-href
=> href
(-replay
=>1, page
=>undef)},
7455 $cgi->a({-href
=> href
(-replay
=>1, page
=>$page-1),
7456 -accesskey
=> "p", -title
=> "Alt-p"}, "prev");
7458 $paging_nav .= "first · prev";
7461 if ($#commitlist >= 100) {
7463 $cgi->a({-href
=> href
(-replay
=>1, page
=>$page+1),
7464 -accesskey
=> "n", -title
=> "Alt-n"}, "next");
7465 $paging_nav .= " · $next_link";
7467 $paging_nav .= " · next";
7472 git_print_page_nav
('','', $hash,$co{'tree'},$hash, $paging_nav);
7473 git_print_header_div
('commit', esc_html
($co{'title'}), $hash);
7474 if ($page == 0 && !@commitlist) {
7475 print "<p>No match.</p>\n";
7477 git_search_grep_body
(\
@commitlist, 0, 99, $next_link);
7483 sub git_search_changes
{
7487 defined(my $fd = git_cmd_pipe
'--no-pager', 'log', @diff_opts,
7488 '--pretty=format:%H', '--no-abbrev', '--raw', "-S$searchtext",
7489 ($search_use_regexp ?
'--pickaxe-regex' : ()))
7490 or die_error
(500, "Open git-log failed");
7494 git_print_page_nav
('','', $hash,$co{'tree'},$hash);
7495 git_print_header_div
('commit', esc_html
($co{'title'}), $hash);
7497 print "<table class=\"pickaxe search\">\n";
7501 while (my $line = to_utf8
(scalar <$fd>)) {
7505 my %set = parse_difftree_raw_line
($line);
7506 if (defined $set{'commit'}) {
7507 # finish previous commit
7510 "<td class=\"link\">" .
7511 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$co{'id'})},
7514 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$co{'tree'},
7515 hash_base
=>$co{'id'})},
7522 print "<tr class=\"dark\">\n";
7524 print "<tr class=\"light\">\n";
7527 %co = parse_commit
($set{'commit'});
7528 my $author = chop_and_escape_str
($co{'author_name'}, 15, 5);
7529 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
7530 "<td><i>$author</i></td>\n" .
7532 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$co{'id'}),
7533 -class => "list subject"},
7534 chop_and_escape_str
($co{'title'}, 50) . "<br/>");
7535 } elsif (defined $set{'to_id'}) {
7536 next if ($set{'to_id'} =~ m/^0{40}$/);
7538 print $cgi->a({-href
=> href
(action
=>"blob", hash_base
=>$co{'id'},
7539 hash
=>$set{'to_id'}, file_name
=>$set{'to_file'}),
7541 "<span class=\"match\">" . esc_path
($set{'file'}) . "</span>") .
7547 # finish last commit (warning: repetition!)
7550 "<td class=\"link\">" .
7551 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$co{'id'})},
7554 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$co{'tree'},
7555 hash_base
=>$co{'id'})},
7566 sub git_search_files
{
7570 defined(my $fd = git_cmd_pipe
'grep', '-n', '-z',
7571 $search_use_regexp ?
('-E', '-i') : '-F',
7572 $searchtext, $co{'tree'})
7573 or die_error
(500, "Open git-grep failed");
7577 git_print_page_nav
('','', $hash,$co{'tree'},$hash);
7578 git_print_header_div
('commit', esc_html
($co{'title'}), $hash);
7580 print "<table class=\"grep_search\">\n";
7585 while (my $line = to_utf8
(scalar <$fd>)) {
7587 my ($file, $lno, $ltext, $binary);
7588 last if ($matches++ > 1000);
7589 if ($line =~ /^Binary file (.+) matches$/) {
7593 ($file, $lno, $ltext) = split(/\0/, $line, 3);
7594 $file =~ s/^$co{'tree'}://;
7596 if ($file ne $lastfile) {
7597 $lastfile and print "</td></tr>\n";
7599 print "<tr class=\"dark\">\n";
7601 print "<tr class=\"light\">\n";
7603 $file_href = href
(action
=>"blob", hash_base
=>$co{'id'},
7605 print "<td class=\"list\">".
7606 $cgi->a({-href
=> $file_href, -class => "list"}, esc_path
($file));
7607 print "</td><td>\n";
7611 print "<div class=\"binary\">Binary file</div>\n";
7613 $ltext = untabify
($ltext);
7614 if ($ltext =~ m/^(.*)($search_regexp)(.*)$/i) {
7615 $ltext = esc_html
($1, -nbsp
=>1);
7616 $ltext .= '<span class="match">';
7617 $ltext .= esc_html
($2, -nbsp
=>1);
7618 $ltext .= '</span>';
7619 $ltext .= esc_html
($3, -nbsp
=>1);
7621 $ltext = esc_html
($ltext, -nbsp
=>1);
7623 print "<div class=\"pre\">" .
7624 $cgi->a({-href
=> $file_href.'#l'.$lno,
7625 -class => "linenr"}, sprintf('%4i ', $lno)) .
7626 $ltext . "</div>\n";
7630 print "</td></tr>\n";
7631 if ($matches > 1000) {
7632 print "<div class=\"diff nodifferences\">Too many matches, listing trimmed</div>\n";
7635 print "<div class=\"diff nodifferences\">No matches found</div>\n";
7644 sub git_search_grep_body
{
7645 my ($commitlist, $from, $to, $extra) = @_;
7646 $from = 0 unless defined $from;
7647 $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
7649 print "<table class=\"commit_search\">\n";
7651 for (my $i = $from; $i <= $to; $i++) {
7652 my %co = %{$commitlist->[$i]};
7656 my $commit = $co{'id'};
7658 print "<tr class=\"dark\">\n";
7660 print "<tr class=\"light\">\n";
7663 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
7664 format_author_html
('td', \
%co, 15, 5) .
7666 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$co{'id'}),
7667 -class => "list subject"},
7668 chop_and_escape_str
($co{'title'}, 50) . "<br/>");
7669 my $comment = $co{'comment'};
7670 foreach my $line (@
$comment) {
7671 if ($line =~ m/^(.*?)($search_regexp)(.*)$/i) {
7672 my ($lead, $match, $trail) = ($1, $2, $3);
7673 $match = chop_str
($match, 70, 5, 'center');
7674 my $contextlen = int((80 - length($match))/2);
7675 $contextlen = 30 if ($contextlen > 30);
7676 $lead = chop_str
($lead, $contextlen, 10, 'left');
7677 $trail = chop_str
($trail, $contextlen, 10, 'right');
7679 $lead = esc_html
($lead);
7680 $match = esc_html
($match);
7681 $trail = esc_html
($trail);
7683 print "$lead<span class=\"match\">$match</span>$trail<br />";
7687 "<td class=\"link\">" .
7688 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$co{'id'})}, "commit") .
7690 $cgi->a({-href
=> href
(action
=>"commitdiff", hash
=>$co{'id'})}, "commitdiff") .
7692 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$co{'tree'}, hash_base
=>$co{'id'})}, "tree");
7696 if (defined $extra) {
7698 "<td colspan=\"3\">$extra</td>\n" .
7704 ## ======================================================================
7705 ## ======================================================================
7708 sub git_project_list_load
{
7709 my $empty_list_ok = shift;
7710 my $order = $input_params{'order'};
7711 if (defined $order && $order !~ m/none|project|descr|owner|age/) {
7712 die_error
(400, "Unknown order parameter");
7715 my @list = git_get_projects_list
($project_filter, $strict_export);
7716 if ($project_filter && (!@list || !gitweb_check_feature
('forks'))) {
7717 push @list, { 'path' => "$project_filter.git" }
7718 if is_valid_project
("$project_filter.git");
7721 die_error
(404, "No projects found") unless $empty_list_ok;
7724 return (\
@list, $order);
7728 my ($projlist, $order);
7730 if ($frontpage_no_project_list) {
7732 $project_filter = undef;
7734 ($projlist, $order) = git_project_list_load
(1);
7737 if (defined $home_text && -f
$home_text) {
7738 print "<div class=\"index_include\">\n";
7739 insert_file
($home_text);
7742 git_project_search_form
($searchtext, $search_use_regexp);
7743 if ($frontpage_no_project_list) {
7744 my $show_ctags = gitweb_check_feature
('ctags');
7745 if ($frontpage_no_project_list == 1 and $show_ctags) {
7746 my @projects = git_get_projects_list
($project_filter, $strict_export);
7747 @projects = filter_forks_from_projects_list
(\
@projects) if gitweb_check_feature
('forks');
7748 @projects = fill_project_list_info
(\
@projects, 'ctags');
7749 my $ctags = git_gather_all_ctags
(\
@projects);
7750 my $cloud = git_populate_project_tagcloud
($ctags, 'project_list');
7751 print git_show_project_tagcloud
($cloud, 64);
7754 git_project_list_body
($projlist, $order, undef, undef, undef, undef, undef, 1);
7759 sub git_project_list
{
7760 my ($projlist, $order) = git_project_list_load
();
7762 if (!$frontpage_no_project_list && defined $home_text && -f
$home_text) {
7763 print "<div class=\"index_include\">\n";
7764 insert_file
($home_text);
7767 git_project_search_form
();
7768 git_project_list_body
($projlist, $order, undef, undef, undef, undef, undef, 1);
7773 my $order = $input_params{'order'};
7774 if (defined $order && $order !~ m/none|project|descr|owner|age/) {
7775 die_error
(400, "Unknown order parameter");
7778 my $filter = $project;
7779 $filter =~ s/\.git$//;
7780 my @list = git_get_projects_list
($filter);
7782 die_error
(404, "No forks found");
7786 git_print_page_nav
('','');
7787 git_print_header_div
('summary', "$project forks");
7788 git_project_list_body
(\
@list, $order, undef, undef, undef, undef, 'forks');
7792 sub git_project_index
{
7793 my @projects = git_get_projects_list
($project_filter, $strict_export);
7795 die_error
(404, "No projects found");
7799 -type
=> 'text/plain',
7800 -charset
=> 'utf-8',
7801 -content_disposition
=> 'inline; filename="index.aux"');
7803 foreach my $pr (@projects) {
7804 if (!exists $pr->{'owner'}) {
7805 $pr->{'owner'} = git_get_project_owner
("$pr->{'path'}");
7808 my ($path, $owner) = ($pr->{'path'}, $pr->{'owner'});
7809 # quote as in CGI::Util::encode, but keep the slash, and use '+' for ' '
7810 $path =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf
("%%%02X", ord($1))/eg
;
7811 $owner =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf
("%%%02X", ord($1))/eg
;
7815 print "$path $owner\n";
7820 my $descr = git_get_project_description
($project) || "none";
7821 my %co = parse_commit
("HEAD");
7822 my %cd = %co ? parse_date
($co{'committer_epoch'}, $co{'committer_tz'}) : ();
7823 my $head = $co{'id'};
7824 my $remote_heads = gitweb_check_feature
('remote_heads');
7826 my $owner = git_get_project_owner
($project);
7827 my $homepage = git_get_project_config
('homepage');
7828 my $base_url = git_get_project_config
('baseurl');
7830 my $refs = git_get_references
();
7831 # These get_*_list functions return one more to allow us to see if
7832 # there are more ...
7833 my @taglist = git_get_tags_list
(16);
7834 my @headlist = git_get_heads_list
(16);
7835 my %remotedata = $remote_heads ? git_get_remotes_list
() : ();
7837 my $check_forks = gitweb_check_feature
('forks');
7840 # find forks of a project
7841 my $filter = $project;
7842 $filter =~ s/\.git$//;
7843 @forklist = git_get_projects_list
($filter);
7844 # filter out forks of forks
7845 @forklist = filter_forks_from_projects_list
(\
@forklist)
7850 git_print_page_nav
('summary','', $head);
7852 if ($check_forks and $project =~ m
#/#) {
7853 my $xproject = $project; $xproject =~ s
#/[^/]+$#.git#; #
7854 if (is_valid_project
($xproject)) {
7855 my $r = $cgi->a({-href
=> href
(project
=> $xproject, action
=> 'summary')}, $xproject);
7857 <div class="forkinfo">
7858 This project is a fork of the $r project. If you have that one
7859 already cloned locally, you can use
7860 <pre>git clone --reference /path/to/your/$xproject/incarnation mirror_URL</pre>
7861 to save bandwidth during cloning.
7867 print "<div class=\"title\"> </div>\n";
7868 print "<table class=\"projects_list\">\n" .
7869 "<tr id=\"metadata_desc\"><td>description</td><td>" . esc_html
($descr) . "</td></tr>\n";
7871 print "<tr id=\"metadata_homepage\"><td>homepage URL</td><td>" . $cgi->a({-href
=> $homepage}, $homepage) . "</td></tr>\n";
7874 print "<tr id=\"metadata_baseurl\"><td>repository URL</td><td>" . esc_html
($base_url) . "</td></tr>\n";
7876 if ($owner and not $omit_owner) {
7877 print "<tr id=\"metadata_owner\"><td>owner</td><td>" . ($owner_link_hook
7878 ?
$cgi->a({-href
=> $owner_link_hook->($owner)}, email_obfuscate
($owner))
7879 : email_obfuscate
($owner)) . "</td></tr>\n";
7881 if (defined $cd{'rfc2822'}) {
7882 print "<tr id=\"metadata_lchange\"><td>last change</td>" .
7883 "<td>".format_timestamp_html
(\
%cd)."</td></tr>\n";
7885 print format_lastrefresh_row
(), "\n";
7887 # use per project git URL list in $projectroot/$project/cloneurl
7888 # or make project git URL from git base URL and project name
7889 my $url_tag = $base_url ?
"mirror URL" : "URL";
7890 my $url_class = "metadata_url";
7891 my @url_list = git_get_project_url_list
($project);
7892 unless (@url_list) {
7893 @url_list = @git_base_url_list;
7894 if ((git_get_project_config
("showpush", '--bool')||'false') eq "true" ||
7895 -f
"$projectroot/$project/.nofetch") {
7896 my $pushidx = @url_list;
7897 foreach (@git_base_push_urls) {
7898 if ($https_hint_html && !ref($_) && $_ =~ /^https:/i) {
7899 push(@url_list, [$_, $https_hint_html]);
7901 push(@url_list, $_);
7904 if ($#url_list >= $pushidx) {
7905 my $pushtag = "push URL";
7906 my $classtag = "metadata_pushurl";
7907 if (ref($url_list[$pushidx])) {
7908 $url_list[$pushidx] = [
7909 ${$url_list[$pushidx]}[0],
7910 ${$url_list[$pushidx]}[1],
7914 $url_list[$pushidx] = [
7915 $url_list[$pushidx],
7922 push(@url_list, @git_base_mirror_urls);
7924 for (my $i=0; $i<=$#url_list; ++$i) {
7925 if (ref($url_list[$i])) {
7927 ${$url_list[$i]}[0] . "/$project",
7928 ${$url_list[$i]}[1],
7929 ${$url_list[$i]}[2],
7930 ${$url_list[$i]}[3]];
7932 $url_list[$i] .= "/$project";
7936 foreach (@url_list) {
7940 my $next_tag = undef;
7941 my $next_class = undef;
7944 $html_hint = " " . $$_[1] if defined($$_[1]);
7946 $next_class = $$_[3];
7950 next unless $git_url;
7951 $url_class = $next_class if $next_class;
7952 $url_tag = $next_tag if $next_tag;
7953 print "<tr class=\"$url_class\"><td>$url_tag</td><td>$git_url$html_hint</td></tr>\n";
7957 if (defined($git_base_bundles_url) && -d
"$projectroot/$project/bundles") {
7958 my $projname = $project;
7959 $projname =~ s
|^.*/||;
7960 my $url = "$git_base_bundles_url/$project/bundles";
7961 print format_repo_url
(
7963 "<a rel='nofollow' href='$url'>$projname downloadable bundles</a>");
7967 my $show_ctags = gitweb_check_feature
('ctags');
7969 my $ctags = git_get_project_ctags
($project);
7970 if (%$ctags || $show_ctags !~ /^\d+$/) {
7971 # without ability to add tags, don't show if there are none
7972 my $cloud = git_populate_project_tagcloud
($ctags, 'project_list');
7973 print "<tr id=\"metadata_ctags\">" .
7974 "<td style=\"vertical-align:middle\">content tags<br />";
7975 print "</td>\n<td>" unless %$ctags;
7976 print "<form action=\"$show_ctags\" method=\"post\" style=\"white-space:nowrap\">" .
7977 "<input type=\"hidden\" name=\"p\" value=\"$project\"/>" .
7978 "add: <input type=\"text\" name=\"t\" size=\"8\" /></form>"
7979 unless $show_ctags =~ /^\d+$/;
7980 print "</td>\n<td>" if %$ctags;
7981 print git_show_project_tagcloud
($cloud, 48)."</td>" .
7988 # If XSS prevention is on, we don't include README.html.
7989 # TODO: Allow a readme in some safe format.
7990 if (!$prevent_xss) {
7991 my $readme_name = "readme";
7993 if (-s
"$projectroot/$project/README.html") {
7994 $readme = collect_html_file
("$projectroot/$project/README.html");
7996 $readme = collect_output
($git_automatic_readme_html, "$projectroot/$project");
7997 if ($readme && $readme =~ /^<!-- README NAME: ((?:[^-]|(?:-(?!-)))+) -->/) {
7999 $readme =~ s/^<!--(?:[^-]|(?:-(?!-)))*-->\n?//;
8002 if (defined($readme)) {
8003 $readme =~ s/^\s+//s;
8004 $readme =~ s/\s+$//s;
8005 print "<div class=\"title\">$readme_name</div>\n",
8006 "<div class=\"readme\">\n",
8013 # we need to request one more than 16 (0..15) to check if
8015 my @commitlist = $head ? parse_commits
($head, 17) : ();
8017 git_print_header_div
('shortlog');
8018 git_shortlog_body
(\
@commitlist, 0, 15, $refs,
8019 $#commitlist <= 15 ?
undef :
8020 $cgi->a({-href
=> href
(action
=>"shortlog")}, "..."));
8024 git_print_header_div
('tags');
8025 git_tags_body
(\
@taglist, 0, 15,
8026 $#taglist <= 15 ?
undef :
8027 $cgi->a({-href
=> href
(action
=>"tags")}, "..."));
8031 git_print_header_div
('heads');
8032 git_heads_body
(\
@headlist, $head, 0, 15,
8033 $#headlist <= 15 ?
undef :
8034 $cgi->a({-href
=> href
(action
=>"heads")}, "..."));
8038 git_print_header_div
('remotes');
8039 git_remotes_body
(\
%remotedata, 15, $head);
8043 git_print_header_div
('forks');
8044 git_project_list_body
(\
@forklist, 'age', 0, 15,
8045 $#forklist <= 15 ?
undef :
8046 $cgi->a({-href
=> href
(action
=>"forks")}, "..."),
8047 'no_header', 'forks');
8054 my %tag = parse_tag
($hash);
8057 die_error
(404, "Unknown tag object");
8061 $fullhash = $hash if $hash =~ m/^[0-9a-fA-F]{40}$/;
8062 $fullhash = git_get_full_hash
($project, $hash) unless $fullhash;
8064 my $obj = $tag{'object'};
8066 if ($tag{'type'} eq 'commit') {
8067 git_print_page_nav
('','', $obj,undef,$obj);
8068 git_print_header_div
('commit', esc_html
($tag{'name'}), $hash);
8070 if ($tag{'type'} eq 'tree') {
8071 git_print_page_nav
('',['commit','commitdiff'], undef,undef,$obj);
8073 git_print_page_nav
('',['commit','commitdiff','tree'], undef,undef,undef);
8075 print "<div class=\"title\">".esc_html
($hash)."</div>\n";
8077 print "<div class=\"title_text\">\n" .
8078 "<table class=\"object_header\">\n" .
8079 "<tr><td>tag</td><td class=\"sha1\">$fullhash</td></tr>\n" .
8081 "<td>object</td>\n" .
8082 "<td>" . $cgi->a({-class => "list", -href
=> href
(action
=>$tag{'type'}, hash
=>$tag{'object'})},
8083 $tag{'object'}) . "</td>\n" .
8084 "<td class=\"link\">" . $cgi->a({-href
=> href
(action
=>$tag{'type'}, hash
=>$tag{'object'})},
8085 $tag{'type'}) . "</td>\n" .
8087 if (defined($tag{'author'})) {
8088 git_print_authorship_rows
(\
%tag, 'author');
8090 print "</table>\n\n" .
8092 print "<div class=\"page_body\">";
8093 my $comment = $tag{'comment'};
8094 foreach my $line (@
$comment) {
8096 print esc_html
($line, -nbsp
=>1) . "<br/>\n";
8102 sub git_blame_common
{
8103 my $format = shift || 'porcelain';
8104 if ($format eq 'porcelain' && $input_params{'javascript'}) {
8105 $format = 'incremental';
8106 $action = 'blame_incremental'; # for page title etc
8110 gitweb_check_feature
('blame')
8111 or die_error
(403, "Blame view not allowed");
8114 die_error
(400, "No file name given") unless $file_name;
8115 $hash_base ||= git_get_head_hash
($project);
8116 die_error
(404, "Couldn't find base commit") unless $hash_base;
8117 my %co = parse_commit
($hash_base)
8118 or die_error
(404, "Commit not found");
8120 if (!defined $hash) {
8121 $hash = git_get_hash_by_path
($hash_base, $file_name, "blob")
8122 or die_error
(404, "Error looking up file");
8124 $ftype = git_get_type
($hash);
8125 if ($ftype !~ "blob") {
8126 die_error
(400, "Object is not a blob");
8131 if ($format eq 'incremental') {
8132 # get file contents (as base)
8133 defined($fd = git_cmd_pipe
'cat-file', 'blob', $hash)
8134 or die_error
(500, "Open git-cat-file failed");
8135 } elsif ($format eq 'data') {
8136 # run git-blame --incremental
8137 defined($fd = git_cmd_pipe
"blame", "--incremental",
8138 $hash_base, "--", $file_name)
8139 or die_error
(500, "Open git-blame --incremental failed");
8141 # run git-blame --porcelain
8142 defined($fd = git_cmd_pipe
"blame", '-p',
8143 $hash_base, '--', $file_name)
8144 or die_error
(500, "Open git-blame --porcelain failed");
8147 # incremental blame data returns early
8148 if ($format eq 'data') {
8150 -type
=>"text/plain", -charset
=> "utf-8",
8151 -status
=> "200 OK");
8152 local $| = 1; # output autoflush
8157 or print "ERROR $!\n";
8160 if (defined $t0 && gitweb_check_feature
('timed')) {
8162 tv_interval
($t0, [ gettimeofday
() ]).
8163 ' '.$number_of_git_cmds;
8173 $cgi->a({-href
=> href
(action
=>"blob", -replay
=>1)},
8177 $cgi->a({-href
=> href
(action
=>"history", -replay
=>1)},
8180 $cgi->a({-href
=> href
(action
=>$action, file_name
=>$file_name)},
8182 git_print_page_nav
('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
8183 git_print_header_div
('commit', esc_html
($co{'title'}), $hash_base);
8184 git_print_page_path
($file_name, $ftype, $hash_base);
8187 if ($format eq 'incremental') {
8188 print "<noscript>\n<div class=\"error\"><center><b>\n".
8189 "This page requires JavaScript to run.\n Use ".
8190 $cgi->a({-href
=> href
(action
=>'blame',javascript
=>0,-replay
=>1)},
8193 "</b></center></div>\n</noscript>\n";
8195 print qq!<div id
="progress_bar" style
="width: 100%; background-color: yellow"></div
>\n!;
8198 print qq!<div
class="page_body">\n!;
8199 print qq!<div id
="progress_info">... / ...</div
>\n!
8200 if ($format eq 'incremental');
8201 print qq!<table id
="blame_table" class="blame" width
="100%">\n!.
8202 #qq!<col width="5.5em" /><col width="2.5em" /><col width="*" />\n!.
8204 qq!<tr
><th nowrap
="nowrap" style
="white-space:nowrap">!.
8205 qq!Commit
 <a href="javascript:extra_blame_columns()" id="columns_expander" !.
8206 qq!title
="toggles blame author information display">[+]</a></th
>!.
8207 qq!<th
class="extra_column">Author
</th><th class="extra_column">Date</th
>!.
8208 qq!<th
>Line
</th><th width="100%">Data</th
></tr
>\n!.
8212 my @rev_color = qw(light dark);
8213 my $num_colors = scalar(@rev_color);
8214 my $current_color = 0;
8216 if ($format eq 'incremental') {
8217 my $color_class = $rev_color[$current_color];
8222 while (my $line = to_utf8
(scalar <$fd>)) {
8226 print qq!<tr id
="l$linenr" class="$color_class">!.
8227 qq!<td
class="sha1"><a href
=""> </a></td
>!.
8228 qq!<td
class="extra_column" nowrap
="nowrap"></td
>!.
8229 qq!<td
class="extra_column" nowrap
="nowrap"></td
>!.
8230 qq!<td
class="linenr">!.
8231 qq!<a
class="linenr" href
="">$linenr</a></td
>!;
8232 print qq!<td
class="pre">! . esc_html
($line) . "</td>\n";
8236 } else { # porcelain, i.e. ordinary blame
8237 my %metainfo = (); # saves information about commits
8241 while (my $line = to_utf8
(scalar <$fd>)) {
8243 # the header: <SHA-1> <src lineno> <dst lineno> [<lines in group>]
8244 # no <lines in group> for subsequent lines in group of lines
8245 my ($full_rev, $orig_lineno, $lineno, $group_size) =
8246 ($line =~ /^([0-9a-f]{40}) (\d+) (\d+)(?: (\d+))?$/);
8247 if (!exists $metainfo{$full_rev}) {
8248 $metainfo{$full_rev} = { 'nprevious' => 0 };
8250 my $meta = $metainfo{$full_rev};
8252 while ($data = to_utf8
(scalar <$fd>)) {
8254 last if ($data =~ s/^\t//); # contents of line
8255 if ($data =~ /^(\S+)(?: (.*))?$/) {
8256 $meta->{$1} = $2 unless exists $meta->{$1};
8258 if ($data =~ /^previous /) {
8259 $meta->{'nprevious'}++;
8262 my $short_rev = substr($full_rev, 0, 8);
8263 my $author = $meta->{'author'};
8265 parse_date
($meta->{'author-time'}, $meta->{'author-tz'});
8266 my $date = $date{'iso-tz'};
8268 $current_color = ($current_color + 1) % $num_colors;
8270 my $tr_class = $rev_color[$current_color];
8271 $tr_class .= ' boundary' if (exists $meta->{'boundary'});
8272 $tr_class .= ' no-previous' if ($meta->{'nprevious'} == 0);
8273 $tr_class .= ' multiple-previous' if ($meta->{'nprevious'} > 1);
8274 print "<tr id=\"l$lineno\" class=\"$tr_class\">\n";
8276 my $rowspan = $group_size > 1 ?
" rowspan=\"$group_size\"" : "";
8277 print "<td class=\"sha1\"";
8278 print " title=\"". esc_html
($author) . ", $date\"";
8280 print $cgi->a({-href
=> href
(action
=>"commit",
8282 file_name
=>$file_name)},
8283 esc_html
($short_rev));
8284 if ($group_size >= 2) {
8285 my @author_initials = ($author =~ /\b([[:upper:]])\B/g);
8286 if (@author_initials) {
8288 esc_html
(join('', @author_initials));
8293 print "<td class=\"extra_column\" nowrap=\"nowrap\"$rowspan>". esc_html
($author) . "</td>";
8294 print "<td class=\"extra_column\" nowrap=\"nowrap\"$rowspan>". $date . "</td>";
8296 # 'previous' <sha1 of parent commit> <filename at commit>
8297 if (exists $meta->{'previous'} &&
8298 $meta->{'previous'} =~ /^([a-fA-F0-9]{40}) (.*)$/) {
8299 $meta->{'parent'} = $1;
8300 $meta->{'file_parent'} = unquote
($2);
8303 exists($meta->{'parent'}) ?
8304 $meta->{'parent'} : $full_rev;
8305 my $linenr_filename =
8306 exists($meta->{'file_parent'}) ?
8307 $meta->{'file_parent'} : unquote
($meta->{'filename'});
8308 my $blamed = href
(action
=> 'blame',
8309 file_name
=> $linenr_filename,
8310 hash_base
=> $linenr_commit);
8311 print "<td class=\"linenr\">";
8312 print $cgi->a({ -href
=> "$blamed#l$orig_lineno",
8313 -class => "linenr" },
8316 print "<td class=\"pre\">" . esc_html
($data) . "</td>\n";
8324 "</table>\n"; # class="blame"
8325 print "</div>\n"; # class="blame_body"
8327 or print "Reading blob failed\n";
8336 sub git_blame_incremental
{
8337 git_blame_common
('incremental');
8340 sub git_blame_data
{
8341 git_blame_common
('data');
8345 my $head = git_get_head_hash
($project);
8347 git_print_page_nav
('','', $head,undef,$head,format_ref_views
('tags'));
8348 git_print_header_div
('summary', $project);
8350 my @tagslist = git_get_tags_list
();
8352 git_tags_body
(\
@tagslist);
8358 my $order = $input_params{'order'};
8359 if (defined $order && $order !~ m/age|name/) {
8360 die_error
(400, "Unknown order parameter");
8363 my $head = git_get_head_hash
($project);
8365 git_print_page_nav
('','', $head,undef,$head,format_ref_views
('refs'));
8366 git_print_header_div
('summary', $project);
8368 my @refslist = git_get_tags_list
(undef, 1, $order);
8370 git_tags_body
(\
@refslist, undef, undef, undef, $head, 1, $order);
8376 my $head = git_get_head_hash
($project);
8378 git_print_page_nav
('','', $head,undef,$head,format_ref_views
('heads'));
8379 git_print_header_div
('summary', $project);
8381 my @headslist = git_get_heads_list
();
8383 git_heads_body
(\
@headslist, $head);
8388 # used both for single remote view and for list of all the remotes
8390 gitweb_check_feature
('remote_heads')
8391 or die_error
(403, "Remote heads view is disabled");
8393 my $head = git_get_head_hash
($project);
8394 my $remote = $input_params{'hash'};
8396 my $remotedata = git_get_remotes_list
($remote);
8397 die_error
(500, "Unable to get remote information") unless defined $remotedata;
8399 unless (%$remotedata) {
8400 die_error
(404, defined $remote ?
8401 "Remote $remote not found" :
8402 "No remotes found");
8405 git_header_html
(undef, undef, -action_extra
=> $remote);
8406 git_print_page_nav
('', '', $head, undef, $head,
8407 format_ref_views
($remote ?
'' : 'remotes'));
8409 fill_remote_heads
($remotedata);
8410 if (defined $remote) {
8411 git_print_header_div
('remotes', "$remote remote for $project");
8412 git_remote_block
($remote, $remotedata->{$remote}, undef, $head);
8414 git_print_header_div
('summary', "$project remotes");
8415 git_remotes_body
($remotedata, undef, $head);
8421 sub git_blob_plain
{
8425 if (!defined $hash) {
8426 if (defined $file_name) {
8427 my $base = $hash_base || git_get_head_hash
($project);
8428 $hash = git_get_hash_by_path
($base, $file_name, "blob")
8429 or die_error
(404, "Cannot find file");
8431 die_error
(400, "No file name defined");
8433 } elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) {
8434 # blobs defined by non-textual hash id's can be cached
8438 defined(my $fd = git_cmd_pipe
"cat-file", "blob", $hash)
8439 or die_error
(500, "Open git-cat-file blob '$hash' failed");
8442 # content-type (can include charset)
8444 ($type, $leader) = blob_contenttype
($fd, $file_name, $type);
8446 # "save as" filename, even when no $file_name is given
8447 my $save_as = "$hash";
8448 if (defined $file_name) {
8449 $save_as = $file_name;
8450 } elsif ($type =~ m/^text\//) {
8454 # With XSS prevention on, blobs of all types except a few known safe
8455 # ones are served with "Content-Disposition: attachment" to make sure
8456 # they don't run in our security domain. For certain image types,
8457 # blob view writes an <img> tag referring to blob_plain view, and we
8458 # want to be sure not to break that by serving the image as an
8459 # attachment (though Firefox 3 doesn't seem to care).
8460 my $sandbox = $prevent_xss &&
8461 $type !~ m!^(?:text/[a-z]+|image/(?:gif|png|jpeg))(?:[ ;]|$)!;
8463 # serve text/* as text/plain
8465 ($type =~ m!^text/[a-z]+\b(.*)$! ||
8466 ($type =~ m!^[a-z]+/[a-z]\+xml\b(.*)$! && -T
$fd))) {
8468 $rest = defined $rest ?
$rest : '';
8469 $type = "text/plain$rest";
8474 -expires
=> $expires,
8475 -content_disposition
=>
8476 ($sandbox ?
'attachment' : 'inline')
8477 . '; filename="' . $save_as . '"');
8478 binmode STDOUT
, ':raw';
8480 print $leader if defined $leader;
8482 while (read($fd, $buf, 32768)) {
8485 binmode STDOUT
, ':utf8'; # as set at the beginning of gitweb.cgi
8493 if (!defined $hash) {
8494 if (defined $file_name) {
8495 my $base = $hash_base || git_get_head_hash
($project);
8496 $hash = git_get_hash_by_path
($base, $file_name, "blob")
8497 or die_error
(404, "Cannot find file");
8499 die_error
(400, "No file name defined");
8501 } elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) {
8502 # blobs defined by non-textual hash id's can be cached
8505 my $fullhash = git_get_full_hash
($project, "$hash^{blob}");
8506 die_error
(404, "No such blob") unless defined($fullhash);
8508 my $have_blame = gitweb_check_feature
('blame');
8509 defined(my $fd = git_cmd_pipe
"cat-file", "blob", $fullhash)
8510 or die_error
(500, "Couldn't cat $file_name, $hash");
8512 my $mimetype = blob_mimetype
($fd, $file_name);
8513 # use 'blob_plain' (aka 'raw') view for files that cannot be displayed
8514 if ($mimetype !~ m!^(?:text/|image/(?:gif|png|jpeg)$)! && -B
$fd) {
8516 return git_blob_plain
($mimetype);
8518 # we can have blame only for text/* mimetype
8519 $have_blame &&= ($mimetype =~ m!^text/!);
8521 my $highlight = gitweb_check_feature
('highlight') && defined $highlight_bin;
8522 my $syntax = guess_file_syntax
($fd, $mimetype, $file_name) if $highlight;
8523 my $highlight_mode_active;
8524 ($fd, $highlight_mode_active) = run_highlighter
($fd, $syntax) if $syntax;
8526 git_header_html
(undef, $expires);
8527 my $formats_nav = '';
8528 if (defined $hash_base && (my %co = parse_commit
($hash_base))) {
8529 if (defined $file_name) {
8532 $cgi->a({-href
=> href
(action
=>"blame", -replay
=>1),
8533 -class => "blamelink"},
8538 $cgi->a({-href
=> href
(action
=>"history", -replay
=>1)},
8541 $cgi->a({-href
=> href
(action
=>"blob_plain", -replay
=>1)},
8544 $cgi->a({-href
=> href
(action
=>"blob",
8545 hash_base
=>"HEAD", file_name
=>$file_name)},
8549 $cgi->a({-href
=> href
(action
=>"blob_plain", -replay
=>1)},
8552 git_print_page_nav
('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
8553 git_print_header_div
('commit', esc_html
($co{'title'}), $hash_base);
8555 git_print_page_nav
('',['commit','commitdiff','tree'], undef,undef,undef);
8556 print "<div class=\"title\">".esc_html
($hash)."</div>\n";
8558 git_print_page_path
($file_name, "blob", $hash_base);
8559 print "<div class=\"title_text\">\n" .
8560 "<table class=\"object_header\">\n";
8561 print "<tr><td>blob</td><td class=\"sha1\">$fullhash</td></tr>\n";
8564 print "<div class=\"page_body\">\n";
8565 if ($mimetype =~ m!^image/!) {
8566 print qq!<img
class="blob" type
="!.esc_attr($mimetype).qq!"!;
8568 print qq! alt
="!.esc_attr($file_name).qq!" title
="!.esc_attr($file_name).qq!"!;
8571 href(action=>"blob_plain
", hash=>$hash,
8572 hash_base=>$hash_base, file_name=>$file_name) .
8574 close $fd; # ignore likely EPIPE error from child
8577 while (my $line = to_utf8
(scalar <$fd>)) {
8580 $line = untabify
($line);
8581 printf qq!<div
class="pre"><a id
="l%i" href
="%s#l%i" class="linenr">%4i </a>%s</div
>\n!,
8582 $nr, esc_attr
(href
(-replay
=> 1)), $nr, $nr,
8583 $highlight_mode_active ? sanitize
($line) : esc_html
($line, -nbsp
=>1);
8586 or print "Reading blob failed.\n";
8593 if (!defined $hash_base) {
8594 $hash_base = "HEAD";
8596 if (!defined $hash) {
8597 if (defined $file_name) {
8598 $hash = git_get_hash_by_path
($hash_base, $file_name, "tree");
8603 die_error
(404, "No such tree") unless defined($hash);
8604 my $fullhash = git_get_full_hash
($project, "$hash^{tree}");
8605 die_error
(404, "No such tree") unless defined($fullhash);
8607 my $show_sizes = gitweb_check_feature
('show-sizes');
8608 my $have_blame = gitweb_check_feature
('blame');
8613 defined(my $fd = git_cmd_pipe
"ls-tree", '-z',
8614 ($show_sizes ?
'-l' : ()), @extra_options, $fullhash)
8615 or die_error
(500, "Open git-ls-tree failed");
8616 @entries = map { chomp; to_utf8
($_) } <$fd>;
8618 or die_error
(404, "Reading tree failed");
8623 if (defined $hash_base && (my %co = parse_commit
($hash_base))) {
8624 my $refs = git_get_references
();
8625 my $ref = format_ref_marker
($refs, $co{'id'});
8627 if (defined $file_name) {
8629 $cgi->a({-href
=> href
(action
=>"history", -replay
=>1)},
8631 $cgi->a({-href
=> href
(action
=>"tree",
8632 hash_base
=>"HEAD", file_name
=>$file_name)},
8635 my $snapshot_links = format_snapshot_links
($hash);
8636 if (defined $snapshot_links) {
8637 # FIXME: Should be available when we have no hash base as well.
8638 push @views_nav, $snapshot_links;
8640 git_print_page_nav
('tree','', $hash_base, undef, undef,
8641 join(' | ', @views_nav));
8642 git_print_header_div
('commit', esc_html
($co{'title'}), $hash_base, undef, $ref);
8644 git_print_page_nav
('tree',['commit','commitdiff'], undef,undef,$hash_base);
8646 print "<div class=\"title\">".esc_html
($hash)."</div>\n";
8648 if (defined $file_name) {
8649 $basedir = $file_name;
8650 if ($basedir ne '' && substr($basedir, -1) ne '/') {
8653 git_print_page_path
($file_name, 'tree', $hash_base);
8655 print "<div class=\"title_text\">\n" .
8656 "<table class=\"object_header\">\n";
8657 print "<tr><td>tree</td><td class=\"sha1\">$fullhash</td></tr>\n";
8660 print "<div class=\"page_body\">\n";
8661 print "<table class=\"tree\">\n";
8663 # '..' (top directory) link if possible
8664 if (defined $hash_base &&
8665 defined $file_name && $file_name =~ m![^/]+$!) {
8667 print "<tr class=\"dark\">\n";
8669 print "<tr class=\"light\">\n";
8673 my $up = $file_name;
8674 $up =~ s!/?[^/]+$!!;
8675 undef $up unless $up;
8676 # based on git_print_tree_entry
8677 print '<td class="mode">' . mode_str
('040000') . "</td>\n";
8678 print '<td class="size"> </td>'."\n" if $show_sizes;
8679 print '<td class="list">';
8680 print $cgi->a({-href
=> href
(action
=>"tree",
8681 hash_base
=>$hash_base,
8685 print "<td class=\"link\"></td>\n";
8689 foreach my $line (@entries) {
8690 my %t = parse_ls_tree_line
($line, -z
=> 1, -l
=> $show_sizes);
8693 print "<tr class=\"dark\">\n";
8695 print "<tr class=\"light\">\n";
8699 git_print_tree_entry
(\
%t, $basedir, $hash_base, $have_blame);
8703 print "</table>\n" .
8708 sub sanitize_for_filename
{
8712 $name =~ s/[^[:alnum:]_.-]//g;
8718 my ($project, $hash) = @_;
8720 # path/to/project.git -> project
8721 # path/to/project/.git -> project
8722 my $name = to_utf8
($project);
8723 $name =~ s
,([^/])/*\
.git
$,$1,;
8724 $name = sanitize_for_filename
(basename
($name));
8727 if ($hash =~ /^[0-9a-fA-F]+$/) {
8728 # shorten SHA-1 hash
8729 my $full_hash = git_get_full_hash
($project, $hash);
8730 if ($full_hash =~ /^$hash/ && length($hash) > 7) {
8731 $ver = git_get_short_hash
($project, $hash);
8733 } elsif ($hash =~ m!^refs/tags/(.*)$!) {
8734 # tags don't need shortened SHA-1 hash
8737 # branches and other need shortened SHA-1 hash
8738 my $strip_refs = join '|', map { quotemeta } get_branch_refs
();
8739 if ($hash =~ m!^refs/($strip_refs|remotes)/(.*)$!) {
8740 my $ref_dir = (defined $1) ?
$1 : '';
8743 $ref_dir = sanitize_for_filename
($ref_dir);
8744 # for refs neither in heads nor remotes we want to
8745 # add a ref dir to archive name
8746 if ($ref_dir ne '' and $ref_dir ne 'heads' and $ref_dir ne 'remotes') {
8747 $ver = $ref_dir . '-' . $ver;
8750 $ver .= '-' . git_get_short_hash
($project, $hash);
8752 # special case of sanitization for filename - we change
8753 # slashes to dots instead of dashes
8754 # in case of hierarchical branch names
8756 $ver =~ s/[^[:alnum:]_.-]//g;
8758 # name = project-version_string
8759 $name = "$name-$ver";
8761 return wantarray ?
($name, $name) : $name;
8764 sub exit_if_unmodified_since
{
8765 my ($latest_epoch) = @_;
8768 my $if_modified = $cgi->http('IF_MODIFIED_SINCE');
8769 if (defined $if_modified) {
8771 if (eval { require HTTP
::Date
; 1; }) {
8772 $since = HTTP
::Date
::str2time
($if_modified);
8773 } elsif (eval { require Time
::ParseDate
; 1; }) {
8774 $since = Time
::ParseDate
::parsedate
($if_modified, GMT
=> 1);
8776 if (defined $since && $latest_epoch <= $since) {
8777 my %latest_date = parse_date
($latest_epoch);
8779 -last_modified
=> $latest_date{'rfc2822'},
8780 -status
=> '304 Not Modified');
8787 my $format = $input_params{'snapshot_format'};
8788 if (!@snapshot_fmts) {
8789 die_error
(403, "Snapshots not allowed");
8791 # default to first supported snapshot format
8792 $format ||= $snapshot_fmts[0];
8793 if ($format !~ m/^[a-z0-9]+$/) {
8794 die_error
(400, "Invalid snapshot format parameter");
8795 } elsif (!exists($known_snapshot_formats{$format})) {
8796 die_error
(400, "Unknown snapshot format");
8797 } elsif ($known_snapshot_formats{$format}{'disabled'}) {
8798 die_error
(403, "Snapshot format not allowed");
8799 } elsif (!grep($_ eq $format, @snapshot_fmts)) {
8800 die_error
(403, "Unsupported snapshot format");
8803 my $type = git_get_type
("$hash^{}");
8805 die_error
(404, 'Object does not exist');
8806 } elsif ($type eq 'blob') {
8807 die_error
(400, 'Object is not a tree-ish');
8810 my ($name, $prefix) = snapshot_name
($project, $hash);
8811 my $filename = "$name$known_snapshot_formats{$format}{'suffix'}";
8813 my %co = parse_commit
($hash);
8814 exit_if_unmodified_since
($co{'committer_epoch'}) if %co;
8817 git_cmd
(), 'archive',
8818 "--format=$known_snapshot_formats{$format}{'format'}",
8819 "--prefix=$prefix/", $hash);
8820 if (exists $known_snapshot_formats{$format}{'compressor'}) {
8821 @cmd = ($posix_shell_bin, '-c', quote_command
(@cmd) .
8822 ' | ' . quote_command
(@
{$known_snapshot_formats{$format}{'compressor'}}));
8825 $filename =~ s/(["\\])/\\$1/g;
8828 %latest_date = parse_date
($co{'committer_epoch'}, $co{'committer_tz'});
8832 -type
=> $known_snapshot_formats{$format}{'type'},
8833 -content_disposition
=> 'inline; filename="' . $filename . '"',
8834 %co ?
(-last_modified
=> $latest_date{'rfc2822'}) : (),
8835 -status
=> '200 OK');
8837 defined(my $fd = cmd_pipe
@cmd)
8838 or die_error
(500, "Execute git-archive failed");
8840 binmode STDOUT
, ':raw';
8843 while (read($fd, $buf, 32768)) {
8846 binmode STDOUT
, ':utf8'; # as set at the beginning of gitweb.cgi
8851 sub git_log_generic
{
8852 my ($fmt_name, $body_subr, $base, $parent, $file_name, $file_hash) = @_;
8854 my $head = git_get_head_hash
($project);
8855 if (!defined $base) {
8858 if (!defined $page) {
8861 my $refs = git_get_references
();
8863 my $commit_hash = $base;
8864 if (defined $parent) {
8865 $commit_hash = "$parent..$base";
8868 parse_commits
($commit_hash, 101, (100 * $page),
8869 defined $file_name ?
($file_name, "--full-history") : ());
8872 if (!defined $file_hash && defined $file_name) {
8873 # some commits could have deleted file in question,
8874 # and not have it in tree, but one of them has to have it
8875 for (my $i = 0; $i < @commitlist; $i++) {
8876 $file_hash = git_get_hash_by_path
($commitlist[$i]{'id'}, $file_name);
8877 last if defined $file_hash;
8880 if (defined $file_hash) {
8881 $ftype = git_get_type
($file_hash);
8883 if (defined $file_name && !defined $ftype) {
8884 die_error
(500, "Unknown type of object");
8887 if (defined $file_name) {
8888 %co = parse_commit
($base)
8889 or die_error
(404, "Unknown commit object");
8893 my $paging_nav = format_log_nav
($fmt_name, $page, $#commitlist >= 100);
8895 if ($#commitlist >= 100) {
8897 $cgi->a({-href
=> href
(-replay
=>1, page
=>$page+1),
8898 -accesskey
=> "n", -title
=> "Alt-n"}, "next");
8900 my ($patch_max) = gitweb_get_feature
('patches');
8901 if ($patch_max && !defined $file_name) {
8902 if ($patch_max < 0 || @commitlist <= $patch_max) {
8903 $paging_nav .= " · " .
8904 $cgi->a({-href
=> href
(action
=>"patches", -replay
=>1)},
8910 local $action = 'log';
8913 git_print_page_nav
($fmt_name,'', $hash,$hash,$hash, $paging_nav);
8914 if (defined $file_name) {
8915 git_print_header_div
('commit', esc_html
($co{'title'}), $base);
8917 git_print_header_div
('summary', $project)
8919 git_print_page_path
($file_name, $ftype, $hash_base)
8920 if (defined $file_name);
8922 $body_subr->(\
@commitlist, 0, 99, $refs, $next_link,
8923 $file_name, $file_hash, $ftype);
8929 git_log_generic
('log', \
&git_log_body
,
8930 $hash, $hash_parent);
8934 $hash ||= $hash_base || "HEAD";
8935 my %co = parse_commit
($hash)
8936 or die_error
(404, "Unknown commit object");
8938 my $parent = $co{'parent'};
8939 my $parents = $co{'parents'}; # listref
8941 # we need to prepare $formats_nav before any parameter munging
8943 if (!defined $parent) {
8945 $formats_nav .= '(initial)';
8946 } elsif (@
$parents == 1) {
8947 # single parent commit
8950 $cgi->a({-href
=> href
(action
=>"commit",
8952 esc_html
(substr($parent, 0, 7))) .
8959 $cgi->a({-href
=> href
(action
=>"commit",
8961 esc_html
(substr($_, 0, 7)));
8965 if (gitweb_check_feature
('patches') && @
$parents <= 1) {
8966 $formats_nav .= " | " .
8967 $cgi->a({-href
=> href
(action
=>"patch", -replay
=>1)},
8971 if (!defined $parent) {
8975 defined(my $fd = git_cmd_pipe
"diff-tree", '-r', "--no-commit-id",
8977 (@
$parents <= 1 ?
$parent : '-c'),
8979 or die_error
(500, "Open git-diff-tree failed");
8980 @difftree = map { chomp; to_utf8
($_) } <$fd>;
8981 close $fd or die_error
(404, "Reading git-diff-tree failed");
8983 # non-textual hash id's can be cached
8985 if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
8988 my $refs = git_get_references
();
8989 my $ref = format_ref_marker
($refs, $co{'id'});
8991 git_header_html
(undef, $expires);
8992 git_print_page_nav
('commit', '',
8993 $hash, $co{'tree'}, $hash,
8996 if (defined $co{'parent'}) {
8997 git_print_header_div
('commitdiff', esc_html
($co{'title'}), $hash, undef, $ref);
8999 git_print_header_div
('tree', esc_html
($co{'title'}), $co{'tree'}, $hash, $ref);
9001 print "<div class=\"title_text\">\n" .
9002 "<table class=\"object_header\">\n";
9003 print "<tr><td>commit</td><td class=\"sha1\">$co{'id'}</td></tr>\n";
9004 git_print_authorship_rows
(\
%co);
9007 "<td class=\"sha1\">" .
9008 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$co{'tree'}, hash_base
=>$hash),
9009 class => "list"}, $co{'tree'}) .
9011 "<td class=\"link\">" .
9012 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$co{'tree'}, hash_base
=>$hash)},
9014 my $snapshot_links = format_snapshot_links
($hash);
9015 if (defined $snapshot_links) {
9016 print " | " . $snapshot_links;
9021 foreach my $par (@
$parents) {
9024 "<td class=\"sha1\">" .
9025 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$par),
9026 class => "list"}, $par) .
9028 "<td class=\"link\">" .
9029 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$par)}, "commit") .
9031 $cgi->a({-href
=> href
(action
=>"commitdiff", hash
=>$hash, hash_parent
=>$par)}, "diff") .
9038 print "<div class=\"page_body\">\n";
9039 git_print_log
($co{'comment'});
9042 git_difftree_body
(\
@difftree, $hash, @
$parents);
9048 # object is defined by:
9049 # - hash or hash_base alone
9050 # - hash_base and file_name
9053 # - hash or hash_base alone
9054 if ($hash || ($hash_base && !defined $file_name)) {
9055 my $object_id = $hash || $hash_base;
9057 defined(my $fd = git_cmd_pipe
'cat-file', '-t', $object_id)
9058 or die_error
(404, "Object does not exist");
9060 defined $type && chomp $type;
9062 or die_error
(404, "Object does not exist");
9064 # - hash_base and file_name
9065 } elsif ($hash_base && defined $file_name) {
9066 $file_name =~ s
,/+$,,;
9068 system(git_cmd
(), "cat-file", '-e', $hash_base) == 0
9069 or die_error
(404, "Base object does not exist");
9071 # here errors should not happen
9072 defined(my $fd = git_cmd_pipe
"ls-tree", $hash_base, "--", $file_name)
9073 or die_error
(500, "Open git-ls-tree failed");
9074 my $line = to_utf8
(scalar <$fd>);
9077 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'
9078 unless ($line && $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t/) {
9079 die_error
(404, "File or directory for given base does not exist");
9084 die_error
(400, "Not enough information to find object");
9087 print $cgi->redirect(-uri
=> href
(action
=>$type, -full
=>1,
9088 hash
=>$hash, hash_base
=>$hash_base,
9089 file_name
=>$file_name),
9090 -status
=> '302 Found');
9094 my $format = shift || 'html';
9095 my $diff_style = $input_params{'diff_style'} || 'inline';
9102 # preparing $fd and %diffinfo for git_patchset_body
9104 if (defined $hash_base && defined $hash_parent_base) {
9105 if (defined $file_name) {
9107 defined($fd = git_cmd_pipe
"diff-tree", '-r', @diff_opts,
9108 $hash_parent_base, $hash_base,
9109 "--", (defined $file_parent ?
$file_parent : ()), $file_name)
9110 or die_error
(500, "Open git-diff-tree failed");
9111 @difftree = map { chomp; to_utf8
($_) } <$fd>;
9113 or die_error
(404, "Reading git-diff-tree failed");
9115 or die_error
(404, "Blob diff not found");
9117 } elsif (defined $hash &&
9118 $hash =~ /[0-9a-fA-F]{40}/) {
9119 # try to find filename from $hash
9121 # read filtered raw output
9122 defined($fd = git_cmd_pipe
"diff-tree", '-r', @diff_opts,
9123 $hash_parent_base, $hash_base, "--")
9124 or die_error
(500, "Open git-diff-tree failed");
9126 # ':100644 100644 03b21826... 3b93d5e7... M ls-files.c'
9128 grep { /^:[0-7]{6} [0-7]{6} [0-9a-fA-F]{40} $hash/ }
9129 map { chomp; to_utf8
($_) } <$fd>;
9131 or die_error
(404, "Reading git-diff-tree failed");
9133 or die_error
(404, "Blob diff not found");
9136 die_error
(400, "Missing one of the blob diff parameters");
9139 if (@difftree > 1) {
9140 die_error
(400, "Ambiguous blob diff specification");
9143 %diffinfo = parse_difftree_raw_line
($difftree[0]);
9144 $file_parent ||= $diffinfo{'from_file'} || $file_name;
9145 $file_name ||= $diffinfo{'to_file'};
9147 $hash_parent ||= $diffinfo{'from_id'};
9148 $hash ||= $diffinfo{'to_id'};
9150 # non-textual hash id's can be cached
9151 if ($hash_base =~ m/^[0-9a-fA-F]{40}$/ &&
9152 $hash_parent_base =~ m/^[0-9a-fA-F]{40}$/) {
9157 defined($fd = git_cmd_pipe
"diff-tree", '-r', @diff_opts,
9158 '-p', ($format eq 'html' ?
"--full-index" : ()),
9159 $hash_parent_base, $hash_base,
9160 "--", (defined $file_parent ?
$file_parent : ()), $file_name)
9161 or die_error
(500, "Open git-diff-tree failed");
9164 # old/legacy style URI -- not generated anymore since 1.4.3.
9166 die_error
('404 Not Found', "Missing one of the blob diff parameters")
9170 if ($format eq 'html') {
9172 $cgi->a({-href
=> href
(action
=>"blobdiff_plain", -replay
=>1)},
9174 $formats_nav .= diff_style_nav
($diff_style);
9175 git_header_html
(undef, $expires);
9176 if (defined $hash_base && (my %co = parse_commit
($hash_base))) {
9177 git_print_page_nav
('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
9178 git_print_header_div
('commit', esc_html
($co{'title'}), $hash_base);
9180 print "<div class=\"page_nav\"><br/>$formats_nav<br/></div>\n";
9181 print "<div class=\"title\">".esc_html
("$hash vs $hash_parent")."</div>\n";
9183 if (defined $file_name) {
9184 git_print_page_path
($file_name, "blob", $hash_base);
9186 print "<div class=\"page_path\"></div>\n";
9189 } elsif ($format eq 'plain') {
9191 -type
=> 'text/plain',
9192 -charset
=> 'utf-8',
9193 -expires
=> $expires,
9194 -content_disposition
=> 'inline; filename="' . "$file_name" . '.patch"');
9196 print "X-Git-Url: " . $cgi->self_url() . "\n\n";
9199 die_error
(400, "Unknown blobdiff format");
9203 if ($format eq 'html') {
9204 print "<div class=\"page_body\">\n";
9206 git_patchset_body
($fd, $diff_style,
9207 [ \
%diffinfo ], $hash_base, $hash_parent_base);
9210 print "</div>\n"; # class="page_body"
9214 while (my $line = to_utf8
(scalar <$fd>)) {
9215 $line =~ s!a/($hash|$hash_parent)!'a/'.esc_path($diffinfo{'from_file'})!eg;
9216 $line =~ s!b/($hash|$hash_parent)!'b/'.esc_path($diffinfo{'to_file'})!eg;
9220 last if $line =~ m!^\+\+\+!;
9229 sub git_blobdiff_plain
{
9230 git_blobdiff
('plain');
9233 # assumes that it is added as later part of already existing navigation,
9234 # so it returns "| foo | bar" rather than just "foo | bar"
9235 sub diff_style_nav
{
9236 my ($diff_style, $is_combined) = @_;
9237 $diff_style ||= 'inline';
9239 return "" if ($is_combined);
9241 my @styles = (inline
=> 'inline', 'sidebyside' => 'side by side');
9242 my %styles = @styles;
9244 @styles[ map { $_ * 2 } 0..$#styles/2 ];
9249 $_ eq $diff_style ?
$styles{$_} :
9250 $cgi->a({-href
=> href
(-replay
=>1, diff_style
=> $_)}, $styles{$_})
9254 sub git_commitdiff
{
9256 my $format = $params{-format
} || 'html';
9257 my $diff_style = $input_params{'diff_style'} || 'inline';
9259 my ($patch_max) = gitweb_get_feature
('patches');
9260 if ($format eq 'patch') {
9261 die_error
(403, "Patch view not allowed") unless $patch_max;
9264 $hash ||= $hash_base || "HEAD";
9265 my %co = parse_commit
($hash)
9266 or die_error
(404, "Unknown commit object");
9268 # choose format for commitdiff for merge
9269 if (! defined $hash_parent && @
{$co{'parents'}} > 1) {
9270 $hash_parent = '--cc';
9272 # we need to prepare $formats_nav before almost any parameter munging
9274 if ($format eq 'html') {
9276 $cgi->a({-href
=> href
(action
=>"commitdiff_plain", -replay
=>1)},
9278 if ($patch_max && @
{$co{'parents'}} <= 1) {
9279 $formats_nav .= " | " .
9280 $cgi->a({-href
=> href
(action
=>"patch", -replay
=>1)},
9283 $formats_nav .= diff_style_nav
($diff_style, @
{$co{'parents'}} > 1);
9285 if (defined $hash_parent &&
9286 $hash_parent ne '-c' && $hash_parent ne '--cc') {
9287 # commitdiff with two commits given
9288 my $hash_parent_short = $hash_parent;
9289 if ($hash_parent =~ m/^[0-9a-fA-F]{40}$/) {
9290 $hash_parent_short = substr($hash_parent, 0, 7);
9294 for (my $i = 0; $i < @
{$co{'parents'}}; $i++) {
9295 if ($co{'parents'}[$i] eq $hash_parent) {
9296 $formats_nav .= ' parent ' . ($i+1);
9300 $formats_nav .= ': ' .
9301 $cgi->a({-href
=> href
(-replay
=>1,
9302 hash
=>$hash_parent, hash_base
=>undef)},
9303 esc_html
($hash_parent_short)) .
9305 } elsif (!$co{'parent'}) {
9307 $formats_nav .= ' (initial)';
9308 } elsif (scalar @
{$co{'parents'}} == 1) {
9309 # single parent commit
9312 $cgi->a({-href
=> href
(-replay
=>1,
9313 hash
=>$co{'parent'}, hash_base
=>undef)},
9314 esc_html
(substr($co{'parent'}, 0, 7))) .
9318 if ($hash_parent eq '--cc') {
9319 $formats_nav .= ' | ' .
9320 $cgi->a({-href
=> href
(-replay
=>1,
9321 hash
=>$hash, hash_parent
=>'-c')},
9323 } else { # $hash_parent eq '-c'
9324 $formats_nav .= ' | ' .
9325 $cgi->a({-href
=> href
(-replay
=>1,
9326 hash
=>$hash, hash_parent
=>'--cc')},
9332 $cgi->a({-href
=> href
(-replay
=>1,
9333 hash
=>$_, hash_base
=>undef)},
9334 esc_html
(substr($_, 0, 7)));
9335 } @
{$co{'parents'}} ) .
9340 my $hash_parent_param = $hash_parent;
9341 if (!defined $hash_parent_param) {
9342 # --cc for multiple parents, --root for parentless
9343 $hash_parent_param =
9344 @
{$co{'parents'}} > 1 ?
'--cc' : $co{'parent'} || '--root';
9350 if ($format eq 'html') {
9351 defined($fd = git_cmd_pipe
"diff-tree", '-r', @diff_opts,
9352 "--no-commit-id", "--patch-with-raw", "--full-index",
9353 $hash_parent_param, $hash, "--")
9354 or die_error
(500, "Open git-diff-tree failed");
9356 while (my $line = to_utf8
(scalar <$fd>)) {
9358 # empty line ends raw part of diff-tree output
9360 push @difftree, scalar parse_difftree_raw_line
($line);
9363 } elsif ($format eq 'plain') {
9364 defined($fd = git_cmd_pipe
"diff-tree", '-r', @diff_opts,
9365 '-p', $hash_parent_param, $hash, "--")
9366 or die_error
(500, "Open git-diff-tree failed");
9367 } elsif ($format eq 'patch') {
9368 # For commit ranges, we limit the output to the number of
9369 # patches specified in the 'patches' feature.
9370 # For single commits, we limit the output to a single patch,
9371 # diverging from the git-format-patch default.
9372 my @commit_spec = ();
9374 if ($patch_max > 0) {
9375 push @commit_spec, "-$patch_max";
9377 push @commit_spec, '-n', "$hash_parent..$hash";
9379 if ($params{-single
}) {
9380 push @commit_spec, '-1';
9382 if ($patch_max > 0) {
9383 push @commit_spec, "-$patch_max";
9385 push @commit_spec, "-n";
9387 push @commit_spec, '--root', $hash;
9389 defined($fd = git_cmd_pipe
"format-patch", @diff_opts,
9390 '--encoding=utf8', '--stdout', @commit_spec)
9391 or die_error
(500, "Open git-format-patch failed");
9393 die_error
(400, "Unknown commitdiff format");
9396 # non-textual hash id's can be cached
9398 if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
9402 # write commit message
9403 if ($format eq 'html') {
9404 my $refs = git_get_references
();
9405 my $ref = format_ref_marker
($refs, $co{'id'});
9407 git_header_html
(undef, $expires);
9408 git_print_page_nav
('commitdiff','', $hash,$co{'tree'},$hash, $formats_nav);
9409 git_print_header_div
('commit', esc_html
($co{'title'}), $hash, undef, $ref);
9410 print "<div class=\"title_text\">\n" .
9411 "<table class=\"object_header\">\n";
9412 git_print_authorship_rows
(\
%co);
9415 print "<div class=\"page_body\">\n";
9416 if (@
{$co{'comment'}} > 1) {
9417 print "<div class=\"log\">\n";
9418 git_print_log
($co{'comment'}, -final_empty_line
=> 1, -remove_title
=> 1);
9419 print "</div>\n"; # class="log"
9422 } elsif ($format eq 'plain') {
9423 my $refs = git_get_references
("tags");
9424 my $tagname = git_get_rev_name_tags
($hash);
9425 my $filename = basename
($project) . "-$hash.patch";
9428 -type
=> 'text/plain',
9429 -charset
=> 'utf-8',
9430 -expires
=> $expires,
9431 -content_disposition
=> 'inline; filename="' . "$filename" . '"');
9432 my %ad = parse_date
($co{'author_epoch'}, $co{'author_tz'});
9433 print "From: " . to_utf8
($co{'author'}) . "\n";
9434 print "Date: $ad{'rfc2822'} ($ad{'tz_local'})\n";
9435 print "Subject: " . to_utf8
($co{'title'}) . "\n";
9437 print "X-Git-Tag: $tagname\n" if $tagname;
9438 print "X-Git-Url: " . $cgi->self_url() . "\n\n";
9440 foreach my $line (@
{$co{'comment'}}) {
9441 print to_utf8
($line) . "\n";
9444 } elsif ($format eq 'patch') {
9445 my $filename = basename
($project) . "-$hash.patch";
9448 -type
=> 'text/plain',
9449 -charset
=> 'utf-8',
9450 -expires
=> $expires,
9451 -content_disposition
=> 'inline; filename="' . "$filename" . '"');
9455 if ($format eq 'html') {
9456 my $use_parents = !defined $hash_parent ||
9457 $hash_parent eq '-c' || $hash_parent eq '--cc';
9458 git_difftree_body
(\
@difftree, $hash,
9459 $use_parents ? @
{$co{'parents'}} : $hash_parent);
9462 git_patchset_body
($fd, $diff_style,
9464 $use_parents ? @
{$co{'parents'}} : $hash_parent);
9466 print "</div>\n"; # class="page_body"
9469 } elsif ($format eq 'plain') {
9474 or print "Reading git-diff-tree failed\n";
9475 } elsif ($format eq 'patch') {
9480 or print "Reading git-format-patch failed\n";
9484 sub git_commitdiff_plain
{
9485 git_commitdiff
(-format
=> 'plain');
9488 # format-patch-style patches
9490 git_commitdiff
(-format
=> 'patch', -single
=> 1);
9494 git_commitdiff
(-format
=> 'patch');
9498 git_log_generic
('history', \
&git_history_body
,
9499 $hash_base, $hash_parent_base,
9504 $searchtype ||= 'commit';
9506 # check if appropriate features are enabled
9507 gitweb_check_feature
('search')
9508 or die_error
(403, "Search is disabled");
9509 if ($searchtype eq 'pickaxe') {
9510 # pickaxe may take all resources of your box and run for several minutes
9511 # with every query - so decide by yourself how public you make this feature
9512 gitweb_check_feature
('pickaxe')
9513 or die_error
(403, "Pickaxe search is disabled");
9515 if ($searchtype eq 'grep') {
9516 # grep search might be potentially CPU-intensive, too
9517 gitweb_check_feature
('grep')
9518 or die_error
(403, "Grep search is disabled");
9520 if ($search_use_regexp) {
9521 # regular expression search can be disabled to avoid potentially
9522 # malicious regular expressions
9523 gitweb_check_feature
('regexp')
9524 or die_error
(403, "Regular expression search is disabled");
9527 if (!defined $searchtext) {
9528 die_error
(400, "Text field is empty");
9530 if (!defined $hash) {
9531 $hash = git_get_head_hash
($project);
9533 my %co = parse_commit
($hash);
9535 die_error
(404, "Unknown commit object");
9537 if (!defined $page) {
9541 if ($searchtype eq 'commit' ||
9542 $searchtype eq 'author' ||
9543 $searchtype eq 'committer') {
9544 git_search_message
(%co);
9545 } elsif ($searchtype eq 'pickaxe') {
9546 git_search_changes
(%co);
9547 } elsif ($searchtype eq 'grep') {
9548 git_search_files
(%co);
9550 die_error
(400, "Unknown search type");
9554 sub git_search_help
{
9556 git_print_page_nav
('','', $hash,$hash,$hash);
9558 <p><strong>Pattern</strong> is by default a normal string that is matched precisely (but without
9559 regard to case, except in the case of pickaxe). However, when you check the <em>re</em> checkbox,
9560 the pattern entered is recognized as the POSIX extended
9561 <a href="http://en.wikipedia.org/wiki/Regular_expression">regular expression</a> (also case
9564 <dt><b>commit</b></dt>
9565 <dd>The commit messages and authorship information will be scanned for the given pattern.</dd>
9567 my $have_grep = gitweb_check_feature
('grep');
9570 <dt><b>grep</b></dt>
9571 <dd>All files in the currently selected tree (HEAD unless you are explicitly browsing
9572 a different one) are searched for the given pattern. On large trees, this search can take
9573 a while and put some strain on the server, so please use it with some consideration. Note that
9574 due to git-grep peculiarity, currently if regexp mode is turned off, the matches are
9575 case-sensitive.</dd>
9579 <dt><b>author</b></dt>
9580 <dd>Name and e-mail of the change author and date of birth of the patch will be scanned for the given pattern.</dd>
9581 <dt><b>committer</b></dt>
9582 <dd>Name and e-mail of the committer and date of commit will be scanned for the given pattern.</dd>
9584 my $have_pickaxe = gitweb_check_feature
('pickaxe');
9585 if ($have_pickaxe) {
9587 <dt><b>pickaxe</b></dt>
9588 <dd>All commits that caused the string to appear or disappear from any file (changes that
9589 added, removed or "modified" the string) will be listed. This search can take a while and
9590 takes a lot of strain on the server, so please use it wisely. Note that since you may be
9591 interested even in changes just changing the case as well, this search is case sensitive.</dd>
9599 git_log_generic
('shortlog', \
&git_shortlog_body
,
9600 $hash, $hash_parent);
9603 ## ......................................................................
9604 ## feeds (RSS, Atom; OPML)
9607 my $format = shift || 'atom';
9608 my $have_blame = gitweb_check_feature
('blame');
9610 # Atom: http://www.atomenabled.org/developers/syndication/
9611 # RSS: http://www.notestips.com/80256B3A007F2692/1/NAMO5P9UPQ
9612 if ($format ne 'rss' && $format ne 'atom') {
9613 die_error
(400, "Unknown web feed format");
9616 # log/feed of current (HEAD) branch, log of given branch, history of file/directory
9617 my $head = $hash || 'HEAD';
9618 my @commitlist = parse_commits
($head, 150, 0, $file_name);
9622 my $content_type = "application/$format+xml";
9623 if (defined $cgi->http('HTTP_ACCEPT') &&
9624 $cgi->Accept('text/xml') > $cgi->Accept($content_type)) {
9625 # browser (feed reader) prefers text/xml
9626 $content_type = 'text/xml';
9628 if (defined($commitlist[0])) {
9629 %latest_commit = %{$commitlist[0]};
9630 my $latest_epoch = $latest_commit{'committer_epoch'};
9631 exit_if_unmodified_since
($latest_epoch);
9632 %latest_date = parse_date
($latest_epoch, $latest_commit{'committer_tz'});
9635 -type
=> $content_type,
9636 -charset
=> 'utf-8',
9637 %latest_date ?
(-last_modified
=> $latest_date{'rfc2822'}) : (),
9638 -status
=> '200 OK');
9640 # Optimization: skip generating the body if client asks only
9641 # for Last-Modified date.
9642 return if ($cgi->request_method() eq 'HEAD');
9645 my $title = "$site_name - $project/$action";
9646 my $feed_type = 'log';
9647 if (defined $hash) {
9648 $title .= " - '$hash'";
9649 $feed_type = 'branch log';
9650 if (defined $file_name) {
9651 $title .= " :: $file_name";
9652 $feed_type = 'history';
9654 } elsif (defined $file_name) {
9655 $title .= " - $file_name";
9656 $feed_type = 'history';
9658 $title .= " $feed_type";
9659 $title = esc_html
($title);
9660 my $descr = git_get_project_description
($project);
9661 if (defined $descr) {
9662 $descr = esc_html
($descr);
9664 $descr = "$project " .
9665 ($format eq 'rss' ?
'RSS' : 'Atom') .
9668 my $owner = git_get_project_owner
($project);
9669 $owner = esc_html
($owner);
9673 if (defined $file_name) {
9674 $alt_url = href
(-full
=>1, action
=>"history", hash
=>$hash, file_name
=>$file_name);
9675 } elsif (defined $hash) {
9676 $alt_url = href
(-full
=>1, action
=>"log", hash
=>$hash);
9678 $alt_url = href
(-full
=>1, action
=>"summary");
9680 print qq!<?xml version
="1.0" encoding
="utf-8"?
>\n!;
9681 if ($format eq 'rss') {
9683 <rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
9686 print "<title>$title</title>\n" .
9687 "<link>$alt_url</link>\n" .
9688 "<description>$descr</description>\n" .
9689 "<language>en</language>\n" .
9690 # project owner is responsible for 'editorial' content
9691 "<managingEditor>$owner</managingEditor>\n";
9692 if (defined $logo || defined $favicon) {
9693 # prefer the logo to the favicon, since RSS
9694 # doesn't allow both
9695 my $img = esc_url
($logo || $favicon);
9697 "<url>$img</url>\n" .
9698 "<title>$title</title>\n" .
9699 "<link>$alt_url</link>\n" .
9703 print "<pubDate>$latest_date{'rfc2822'}</pubDate>\n";
9704 print "<lastBuildDate>$latest_date{'rfc2822'}</lastBuildDate>\n";
9706 print "<generator>gitweb v.$version/$git_version</generator>\n";
9707 } elsif ($format eq 'atom') {
9709 <feed xmlns="http://www.w3.org/2005/Atom">
9711 print "<title>$title</title>\n" .
9712 "<subtitle>$descr</subtitle>\n" .
9713 '<link rel="alternate" type="text/html" href="' .
9714 $alt_url . '" />' . "\n" .
9715 '<link rel="self" type="' . $content_type . '" href="' .
9716 $cgi->self_url() . '" />' . "\n" .
9717 "<id>" . href
(-full
=>1) . "</id>\n" .
9718 # use project owner for feed author
9719 '<author><name>'. email_obfuscate
($owner) . '</name></author>\n';
9720 if (defined $favicon) {
9721 print "<icon>" . esc_url
($favicon) . "</icon>\n";
9723 if (defined $logo) {
9724 # not twice as wide as tall: 72 x 27 pixels
9725 print "<logo>" . esc_url
($logo) . "</logo>\n";
9727 if (! %latest_date) {
9728 # dummy date to keep the feed valid until commits trickle in:
9729 print "<updated>1970-01-01T00:00:00Z</updated>\n";
9731 print "<updated>$latest_date{'iso-8601'}</updated>\n";
9733 print "<generator version='$version/$git_version'>gitweb</generator>\n";
9737 for (my $i = 0; $i <= $#commitlist; $i++) {
9738 my %co = %{$commitlist[$i]};
9739 my $commit = $co{'id'};
9740 # we read 150, we always show 30 and the ones more recent than 48 hours
9741 if (($i >= 20) && ((time - $co{'author_epoch'}) > 48*60*60)) {
9744 my %cd = parse_date
($co{'author_epoch'}, $co{'author_tz'});
9746 # get list of changed files
9747 defined(my $fd = git_cmd_pipe
"diff-tree", '-r', @diff_opts,
9748 $co{'parent'} || "--root",
9749 $co{'id'}, "--", (defined $file_name ?
$file_name : ()))
9751 my @difftree = map { chomp; to_utf8
($_) } <$fd>;
9755 # print element (entry, item)
9756 my $co_url = href
(-full
=>1, action
=>"commitdiff", hash
=>$commit);
9757 if ($format eq 'rss') {
9759 "<title>" . esc_html
($co{'title'}) . "</title>\n" .
9760 "<author>" . esc_html
($co{'author'}) . "</author>\n" .
9761 "<pubDate>$cd{'rfc2822'}</pubDate>\n" .
9762 "<guid isPermaLink=\"true\">$co_url</guid>\n" .
9763 "<link>$co_url</link>\n" .
9764 "<description>" . esc_html
($co{'title'}) . "</description>\n" .
9765 "<content:encoded>" .
9767 } elsif ($format eq 'atom') {
9769 "<title type=\"html\">" . esc_html
($co{'title'}) . "</title>\n" .
9770 "<updated>$cd{'iso-8601'}</updated>\n" .
9772 " <name>" . esc_html
($co{'author_name'}) . "</name>\n";
9773 if ($co{'author_email'}) {
9774 print " <email>" . esc_html
($co{'author_email'}) . "</email>\n";
9776 print "</author>\n" .
9777 # use committer for contributor
9779 " <name>" . esc_html
($co{'committer_name'}) . "</name>\n";
9780 if ($co{'committer_email'}) {
9781 print " <email>" . esc_html
($co{'committer_email'}) . "</email>\n";
9783 print "</contributor>\n" .
9784 "<published>$cd{'iso-8601'}</published>\n" .
9785 "<link rel=\"alternate\" type=\"text/html\" href=\"$co_url\" />\n" .
9786 "<id>$co_url</id>\n" .
9787 "<content type=\"xhtml\" xml:base=\"" . esc_url
($my_url) . "\">\n" .
9788 "<div xmlns=\"http://www.w3.org/1999/xhtml\">\n";
9790 my $comment = $co{'comment'};
9792 foreach my $line (@
$comment) {
9793 $line = esc_html
($line);
9796 print "</pre><ul>\n";
9797 foreach my $difftree_line (@difftree) {
9798 my %difftree = parse_difftree_raw_line
($difftree_line);
9799 next if !$difftree{'from_id'};
9801 my $file = $difftree{'file'} || $difftree{'to_file'};
9805 $cgi->a({-href
=> href
(-full
=>1, action
=>"blobdiff",
9806 hash
=>$difftree{'to_id'}, hash_parent
=>$difftree{'from_id'},
9807 hash_base
=>$co{'id'}, hash_parent_base
=>$co{'parent'},
9808 file_name
=>$file, file_parent
=>$difftree{'from_file'}),
9809 -title
=> "diff"}, 'D');
9811 print $cgi->a({-href
=> href
(-full
=>1, action
=>"blame",
9812 file_name
=>$file, hash_base
=>$commit),
9813 -class => "blamelink",
9814 -title
=> "blame"}, 'B');
9816 # if this is not a feed of a file history
9817 if (!defined $file_name || $file_name ne $file) {
9818 print $cgi->a({-href
=> href
(-full
=>1, action
=>"history",
9819 file_name
=>$file, hash
=>$commit),
9820 -title
=> "history"}, 'H');
9822 $file = esc_path
($file);
9826 if ($format eq 'rss') {
9827 print "</ul>]]>\n" .
9828 "</content:encoded>\n" .
9830 } elsif ($format eq 'atom') {
9831 print "</ul>\n</div>\n" .
9838 if ($format eq 'rss') {
9839 print "</channel>\n</rss>\n";
9840 } elsif ($format eq 'atom') {
9854 my @list = git_get_projects_list
($project_filter, $strict_export);
9856 die_error
(404, "No projects found");
9860 -type
=> 'text/xml',
9861 -charset
=> 'utf-8',
9862 -content_disposition
=> 'inline; filename="opml.xml"');
9864 my $title = esc_html
($site_name);
9865 my $filter = " within subdirectory ";
9866 if (defined $project_filter) {
9867 $filter .= esc_html
($project_filter);
9872 <?xml version="1.0" encoding="utf-8"?>
9873 <opml version="1.0">
9875 <title>$title OPML Export$filter</title>
9878 <outline text="git RSS feeds">
9881 foreach my $pr (@list) {
9883 my $head = git_get_head_hash
($proj{'path'});
9884 if (!defined $head) {
9887 $git_dir = "$projectroot/$proj{'path'}";
9888 my %co = parse_commit
($head);
9893 my $path = esc_html
(chop_str
($proj{'path'}, 25, 5));
9894 my $rss = href
('project' => $proj{'path'}, 'action' => 'rss', -full
=> 1);
9895 my $html = href
('project' => $proj{'path'}, 'action' => 'summary', -full
=> 1);
9896 print "<outline type=\"rss\" text=\"$path\" title=\"$path\" xmlUrl=\"$rss\" htmlUrl=\"$html\"/>\n";