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 sub evaluate_git_version
{
1077 our $git_version = $version;
1081 if (defined $maxload && get_loadavg
() > $maxload) {
1082 die_error
(503, "The load average on the server is too high");
1086 # ======================================================================
1087 # input validation and dispatch
1089 # input parameters can be collected from a variety of sources (presently, CGI
1090 # and PATH_INFO), so we define an %input_params hash that collects them all
1091 # together during validation: this allows subsequent uses (e.g. href()) to be
1092 # agnostic of the parameter origin
1094 our %input_params = ();
1096 # input parameters are stored with the long parameter name as key. This will
1097 # also be used in the href subroutine to convert parameters to their CGI
1098 # equivalent, and since the href() usage is the most frequent one, we store
1099 # the name -> CGI key mapping here, instead of the reverse.
1101 # XXX: Warning: If you touch this, check the search form for updating,
1104 our @cgi_param_mapping = (
1108 file_parent
=> "fp",
1110 hash_parent
=> "hp",
1112 hash_parent_base
=> "hpb",
1117 snapshot_format
=> "sf",
1119 extra_options
=> "opt",
1120 search_use_regexp
=> "sr",
1123 project_filter
=> "pf",
1124 # this must be last entry (for manipulation from JavaScript)
1127 our %cgi_param_mapping = @cgi_param_mapping;
1129 # we will also need to know the possible actions, for validation
1131 "blame" => \
&git_blame
,
1132 "blame_incremental" => \
&git_blame_incremental
,
1133 "blame_data" => \
&git_blame_data
,
1134 "blobdiff" => \
&git_blobdiff
,
1135 "blobdiff_plain" => \
&git_blobdiff_plain
,
1136 "blob" => \
&git_blob
,
1137 "blob_plain" => \
&git_blob_plain
,
1138 "commitdiff" => \
&git_commitdiff
,
1139 "commitdiff_plain" => \
&git_commitdiff_plain
,
1140 "commit" => \
&git_commit
,
1141 "forks" => \
&git_forks
,
1142 "heads" => \
&git_heads
,
1143 "history" => \
&git_history
,
1145 "patch" => \
&git_patch
,
1146 "patches" => \
&git_patches
,
1147 "refs" => \
&git_refs
,
1148 "remotes" => \
&git_remotes
,
1150 "atom" => \
&git_atom
,
1151 "search" => \
&git_search
,
1152 "search_help" => \
&git_search_help
,
1153 "shortlog" => \
&git_shortlog
,
1154 "summary" => \
&git_summary
,
1156 "tags" => \
&git_tags
,
1157 "tree" => \
&git_tree
,
1158 "snapshot" => \
&git_snapshot
,
1159 "object" => \
&git_object
,
1160 # those below don't need $project
1161 "opml" => \
&git_opml
,
1162 "frontpage" => \
&git_frontpage
,
1163 "project_list" => \
&git_project_list
,
1164 "project_index" => \
&git_project_index
,
1167 # the only actions we will allow to be cached
1168 my %supported_cache_actions;
1169 BEGIN {%supported_cache_actions = map {( $_ => 1 )} qw(summary)}
1171 # finally, we have the hash of allowed extra_options for the commands that
1173 our %allowed_options = (
1174 "--no-merges" => [ qw(rss atom log shortlog history) ],
1177 # fill %input_params with the CGI parameters. All values except for 'opt'
1178 # should be single values, but opt can be an array. We should probably
1179 # build an array of parameters that can be multi-valued, but since for the time
1180 # being it's only this one, we just single it out
1181 sub evaluate_query_params
{
1184 while (my ($name, $symbol) = each %cgi_param_mapping) {
1185 if ($symbol eq 'opt') {
1186 $input_params{$name} = [ map { decode_utf8
($_) } $cgi->multi_param($symbol) ];
1188 $input_params{$name} = decode_utf8
($cgi->param($symbol));
1192 # Backwards compatibility - by_tag= <=> t=
1193 if ($input_params{'ctag'}) {
1194 $input_params{'ctag_filter'} = $input_params{'ctag'};
1198 # now read PATH_INFO and update the parameter list for missing parameters
1199 sub evaluate_path_info
{
1200 return if defined $input_params{'project'};
1201 return if !$path_info;
1202 $path_info =~ s
,^/+,,;
1203 return if !$path_info;
1205 # find which part of PATH_INFO is project
1206 my $project = $path_info;
1207 $project =~ s
,/+$,,;
1208 while ($project && !check_head_link
("$projectroot/$project")) {
1209 $project =~ s
,/*[^/]*$,,;
1211 return unless $project;
1212 $input_params{'project'} = $project;
1214 # do not change any parameters if an action is given using the query string
1215 return if $input_params{'action'};
1216 $path_info =~ s
,^\Q
$project\E
/*,,;
1218 # next, check if we have an action
1219 my $action = $path_info;
1220 $action =~ s
,/.*$,,;
1221 if (exists $actions{$action}) {
1222 $path_info =~ s
,^$action/*,,;
1223 $input_params{'action'} = $action;
1226 # list of actions that want hash_base instead of hash, but can have no
1227 # pathname (f) parameter
1233 # we want to catch, among others
1234 # [$hash_parent_base[:$file_parent]..]$hash_parent[:$file_name]
1235 my ($parentrefname, $parentpathname, $refname, $pathname) =
1236 ($path_info =~ /^(?:(.+?)(?::(.+))?\.\.)?([^:]+?)?(?::(.+))?$/);
1238 # first, analyze the 'current' part
1239 if (defined $pathname) {
1240 # we got "branch:filename" or "branch:dir/"
1241 # we could use git_get_type(branch:pathname), but:
1242 # - it needs $git_dir
1243 # - it does a git() call
1244 # - the convention of terminating directories with a slash
1245 # makes it superfluous
1246 # - embedding the action in the PATH_INFO would make it even
1248 $pathname =~ s
,^/+,,;
1249 if (!$pathname || substr($pathname, -1) eq "/") {
1250 $input_params{'action'} ||= "tree";
1251 $pathname =~ s
,/$,,;
1253 # the default action depends on whether we had parent info
1255 if ($parentrefname) {
1256 $input_params{'action'} ||= "blobdiff_plain";
1258 $input_params{'action'} ||= "blob_plain";
1261 $input_params{'hash_base'} ||= $refname;
1262 $input_params{'file_name'} ||= $pathname;
1263 } elsif (defined $refname) {
1264 # we got "branch". In this case we have to choose if we have to
1265 # set hash or hash_base.
1267 # Most of the actions without a pathname only want hash to be
1268 # set, except for the ones specified in @wants_base that want
1269 # hash_base instead. It should also be noted that hand-crafted
1270 # links having 'history' as an action and no pathname or hash
1271 # set will fail, but that happens regardless of PATH_INFO.
1272 if (defined $parentrefname) {
1273 # if there is parent let the default be 'shortlog' action
1274 # (for http://git.example.com/repo.git/A..B links); if there
1275 # is no parent, dispatch will detect type of object and set
1276 # action appropriately if required (if action is not set)
1277 $input_params{'action'} ||= "shortlog";
1279 if ($input_params{'action'} &&
1280 grep { $_ eq $input_params{'action'} } @wants_base) {
1281 $input_params{'hash_base'} ||= $refname;
1283 $input_params{'hash'} ||= $refname;
1287 # next, handle the 'parent' part, if present
1288 if (defined $parentrefname) {
1289 # a missing pathspec defaults to the 'current' filename, allowing e.g.
1290 # someproject/blobdiff/oldrev..newrev:/filename
1291 if ($parentpathname) {
1292 $parentpathname =~ s
,^/+,,;
1293 $parentpathname =~ s
,/$,,;
1294 $input_params{'file_parent'} ||= $parentpathname;
1296 $input_params{'file_parent'} ||= $input_params{'file_name'};
1298 # we assume that hash_parent_base is wanted if a path was specified,
1299 # or if the action wants hash_base instead of hash
1300 if (defined $input_params{'file_parent'} ||
1301 grep { $_ eq $input_params{'action'} } @wants_base) {
1302 $input_params{'hash_parent_base'} ||= $parentrefname;
1304 $input_params{'hash_parent'} ||= $parentrefname;
1308 # for the snapshot action, we allow URLs in the form
1309 # $project/snapshot/$hash.ext
1310 # where .ext determines the snapshot and gets removed from the
1311 # passed $refname to provide the $hash.
1313 # To be able to tell that $refname includes the format extension, we
1314 # require the following two conditions to be satisfied:
1315 # - the hash input parameter MUST have been set from the $refname part
1316 # of the URL (i.e. they must be equal)
1317 # - the snapshot format MUST NOT have been defined already (e.g. from
1319 # It's also useless to try any matching unless $refname has a dot,
1320 # so we check for that too
1321 if (defined $input_params{'action'} &&
1322 $input_params{'action'} eq 'snapshot' &&
1323 defined $refname && index($refname, '.') != -1 &&
1324 $refname eq $input_params{'hash'} &&
1325 !defined $input_params{'snapshot_format'}) {
1326 # We loop over the known snapshot formats, checking for
1327 # extensions. Allowed extensions are both the defined suffix
1328 # (which includes the initial dot already) and the snapshot
1329 # format key itself, with a prepended dot
1330 while (my ($fmt, $opt) = each %known_snapshot_formats) {
1331 my $hash = $refname;
1332 unless ($hash =~ s/(\Q$opt->{'suffix'}\E|\Q.$fmt\E)$//) {
1336 # a valid suffix was found, so set the snapshot format
1337 # and reset the hash parameter
1338 $input_params{'snapshot_format'} = $fmt;
1339 $input_params{'hash'} = $hash;
1340 # we also set the format suffix to the one requested
1341 # in the URL: this way a request for e.g. .tgz returns
1342 # a .tgz instead of a .tar.gz
1343 $known_snapshot_formats{$fmt}{'suffix'} = $sfx;
1349 our ($action, $project, $file_name, $file_parent, $hash, $hash_parent, $hash_base,
1350 $hash_parent_base, @extra_options, $page, $searchtype, $search_use_regexp,
1351 $searchtext, $search_regexp, $project_filter);
1352 sub evaluate_and_validate_params
{
1353 our $action = $input_params{'action'};
1354 if (defined $action) {
1355 if (!is_valid_action
($action)) {
1356 die_error
(400, "Invalid action parameter");
1360 # parameters which are pathnames
1361 our $project = $input_params{'project'};
1362 if (defined $project) {
1363 if (!is_valid_project
($project)) {
1365 die_error
(404, "No such project");
1369 our $project_filter = $input_params{'project_filter'};
1370 if (defined $project_filter) {
1371 if (!is_valid_pathname
($project_filter)) {
1372 die_error
(404, "Invalid project_filter parameter");
1376 our $file_name = $input_params{'file_name'};
1377 if (defined $file_name) {
1378 if (!is_valid_pathname
($file_name)) {
1379 die_error
(400, "Invalid file parameter");
1383 our $file_parent = $input_params{'file_parent'};
1384 if (defined $file_parent) {
1385 if (!is_valid_pathname
($file_parent)) {
1386 die_error
(400, "Invalid file parent parameter");
1390 # parameters which are refnames
1391 our $hash = $input_params{'hash'};
1392 if (defined $hash) {
1393 if (!is_valid_refname
($hash)) {
1394 die_error
(400, "Invalid hash parameter");
1398 our $hash_parent = $input_params{'hash_parent'};
1399 if (defined $hash_parent) {
1400 if (!is_valid_refname
($hash_parent)) {
1401 die_error
(400, "Invalid hash parent parameter");
1405 our $hash_base = $input_params{'hash_base'};
1406 if (defined $hash_base) {
1407 if (!is_valid_refname
($hash_base)) {
1408 die_error
(400, "Invalid hash base parameter");
1412 our @extra_options = @
{$input_params{'extra_options'}};
1413 # @extra_options is always defined, since it can only be (currently) set from
1414 # CGI, and $cgi->param() returns the empty array in array context if the param
1416 foreach my $opt (@extra_options) {
1417 if (not exists $allowed_options{$opt}) {
1418 die_error
(400, "Invalid option parameter");
1420 if (not grep(/^$action$/, @
{$allowed_options{$opt}})) {
1421 die_error
(400, "Invalid option parameter for this action");
1425 our $hash_parent_base = $input_params{'hash_parent_base'};
1426 if (defined $hash_parent_base) {
1427 if (!is_valid_refname
($hash_parent_base)) {
1428 die_error
(400, "Invalid hash parent base parameter");
1433 our $page = $input_params{'page'};
1434 if (defined $page) {
1435 if ($page =~ m/[^0-9]/) {
1436 die_error
(400, "Invalid page parameter");
1440 our $searchtype = $input_params{'searchtype'};
1441 if (defined $searchtype) {
1442 if ($searchtype =~ m/[^a-z]/) {
1443 die_error
(400, "Invalid searchtype parameter");
1447 our $search_use_regexp = $input_params{'search_use_regexp'};
1449 our $searchtext = $input_params{'searchtext'};
1450 our $search_regexp = undef;
1451 if (defined $searchtext) {
1452 if (length($searchtext) < 2) {
1453 die_error
(403, "At least two characters are required for search parameter");
1455 if ($search_use_regexp) {
1456 $search_regexp = $searchtext;
1457 if (!eval { qr/$search_regexp/; 1; }) {
1458 (my $error = $@
) =~ s/ at \S+ line \d+.*\n?//;
1459 die_error
(400, "Invalid search regexp '$search_regexp'",
1463 $search_regexp = quotemeta $searchtext;
1468 # path to the current git repository
1470 sub evaluate_git_dir
{
1471 our $git_dir = $project ?
"$projectroot/$project" : undef;
1474 our (@snapshot_fmts, $git_avatar, @extra_branch_refs);
1475 sub configure_gitweb_features
{
1476 # list of supported snapshot formats
1477 our @snapshot_fmts = gitweb_get_feature
('snapshot');
1478 @snapshot_fmts = filter_snapshot_fmts
(@snapshot_fmts);
1480 # check that the avatar feature is set to a known provider name,
1481 # and for each provider check if the dependencies are satisfied.
1482 # if the provider name is invalid or the dependencies are not met,
1483 # reset $git_avatar to the empty string.
1484 our ($git_avatar) = gitweb_get_feature
('avatar');
1485 if ($git_avatar eq 'gravatar') {
1486 $git_avatar = '' unless (eval { require Digest
::MD5
; 1; });
1487 } elsif ($git_avatar eq 'picon') {
1493 our @extra_branch_refs = gitweb_get_feature
('extra-branch-refs');
1494 @extra_branch_refs = filter_and_validate_refs
(@extra_branch_refs);
1497 sub get_branch_refs
{
1498 return ('heads', @extra_branch_refs);
1501 # custom error handler: 'die <message>' is Internal Server Error
1502 sub handle_errors_html
{
1503 my $msg = shift; # it is already HTML escaped
1505 # to avoid infinite loop where error occurs in die_error,
1506 # change handler to default handler, disabling handle_errors_html
1507 set_message
("Error occurred when inside die_error:\n$msg");
1509 # you cannot jump out of die_error when called as error handler;
1510 # the subroutine set via CGI::Carp::set_message is called _after_
1511 # HTTP headers are already written, so it cannot write them itself
1512 die_error
(undef, undef, $msg, -error_handler
=> 1, -no_http_header
=> 1);
1514 set_message
(\
&handle_errors_html
);
1516 our $shown_stale_message = 0;
1517 our $cache_dump = undef;
1518 our $cache_dump_mtime = undef;
1521 my $cache_mode_active;
1523 if (!defined $action) {
1524 if (defined $hash) {
1525 $action = git_get_type
($hash);
1526 $action or die_error
(404, "Object does not exist");
1527 } elsif (defined $hash_base && defined $file_name) {
1528 $action = git_get_type
("$hash_base:$file_name");
1529 $action or die_error
(404, "File or directory does not exist");
1530 } elsif (defined $project) {
1531 $action = 'summary';
1533 $action = 'frontpage';
1536 if (!defined($actions{$action})) {
1537 die_error
(400, "Unknown action");
1539 if ($action !~ m/^(?:opml|frontpage|project_list|project_index)$/ &&
1541 die_error
(400, "Project needed");
1544 my $cached_page = $supported_cache_actions{$action}
1545 ? cached_action_page
($action)
1547 goto DUMPCACHE
if $cached_page;
1548 local *SAVEOUT
= *STDOUT
;
1549 $cache_mode_active = $supported_cache_actions{$action}
1550 ? cached_action_start
($action)
1553 configure_gitweb_features
();
1554 $actions{$action}->();
1556 return unless $cache_mode_active;
1558 $cached_page = cached_action_finish
($action);
1563 $cache_mode_active = 0;
1564 # Avoid any extra unwanted encoding steps as $cached_page is raw bytes
1565 binmode STDOUT
, ':raw';
1566 our $fcgi_raw_mode = 1;
1567 print expand_gitweb_pi
($cached_page, time);
1568 binmode STDOUT
, ':utf8'; # as set at the beginning of gitweb.cgi
1573 our $t0 = [ gettimeofday
() ]
1575 our $number_of_git_cmds = 0;
1578 our $first_request = 1;
1579 our $evaluate_uri_force = undef;
1583 # Only allow GET and HEAD methods
1584 if (!$ENV{'REQUEST_METHOD'} || ($ENV{'REQUEST_METHOD'} ne 'GET' && $ENV{'REQUEST_METHOD'} ne 'HEAD')) {
1586 Status: 405 Method Not Allowed
1587 Content-Type: text/plain
1590 405 Method Not Allowed
1596 &$evaluate_uri_force() if $evaluate_uri_force;
1597 if ($per_request_config) {
1598 if (ref($per_request_config) eq 'CODE') {
1599 $per_request_config->();
1600 } elsif (!$first_request) {
1601 evaluate_gitweb_config
();
1602 evaluate_email_obfuscate
();
1607 # $projectroot and $projects_list might be set in gitweb config file
1608 $projects_list ||= $projectroot;
1610 evaluate_query_params
();
1611 evaluate_path_info
();
1612 evaluate_and_validate_params
();
1618 our $is_last_request = sub { 1 };
1619 our ($pre_dispatch_hook, $post_dispatch_hook, $pre_listen_hook);
1623 our $fcgi_nproc_active = 0;
1624 our $fcgi_raw_mode = 0;
1627 my $stdinfno = fileno STDIN
;
1628 return 0 unless defined $stdinfno && $stdinfno == 0;
1629 return 0 unless getsockname STDIN
;
1630 return 0 if getpeername STDIN
;
1631 return $!{ENOTCONN
}?
1:0;
1633 sub configure_as_fcgi
{
1634 return if $fcgi_mode;
1639 # We have gone to great effort to make sure that all incoming data has
1640 # been converted from whatever format it was in into UTF-8. We have
1641 # even taken care to make sure the output handle is in ':utf8' mode.
1642 # Now along comes FCGI and blows it with:
1644 # Use of wide characters in FCGI::Stream::PRINT is deprecated
1645 # and will stop wprking[sic] in a future version of FCGI
1647 # To fix this we replace FCGI::Stream::PRINT with our own routine that
1648 # first encodes everything and then calls the original routine, but
1649 # not if $fcgi_raw_mode is true (then we just call the original routine).
1651 # Note that we could do this by using utf8::is_utf8 to check instead
1652 # of having a $fcgi_raw_mode global, but that would be slower to run
1653 # the test on each element and much slower than skipping the conversion
1654 # entirely when we know we're outputting raw bytes.
1655 my $orig = \
&FCGI
::Stream
::PRINT
;
1656 undef *FCGI
::Stream
::PRINT
;
1657 *FCGI
::Stream
::PRINT
= sub {
1658 @_ = (shift, map {my $x=$_; utf8
::encode
($x); $x} @_)
1659 unless $fcgi_raw_mode;
1663 our $CGI = 'CGI::Fast';
1667 my $request_number = 0;
1668 # let each child service 100 requests
1669 our $is_last_request = sub { ++$request_number >= 100 };
1672 my $script_name = $ENV{'SCRIPT_NAME'} || $ENV{'SCRIPT_FILENAME'} || __FILE__
;
1674 if $script_name =~ /\.fcgi$/ || ($auto_fcgi && is_fcgi
());
1676 my $nproc_sub = sub {
1677 my ($arg, $val) = @_;
1678 return unless eval { require FCGI
::ProcManager
; 1; };
1679 $fcgi_nproc_active = 1;
1680 my $proc_manager = FCGI
::ProcManager
->new({
1681 n_processes
=> $val,
1683 our $pre_listen_hook = sub { $proc_manager->pm_manage() };
1684 our $pre_dispatch_hook = sub { $proc_manager->pm_pre_dispatch() };
1685 our $post_dispatch_hook = sub { $proc_manager->pm_post_dispatch() };
1688 require Getopt
::Long
;
1689 Getopt
::Long
::GetOptions
(
1690 'fastcgi|fcgi|f' => \
&configure_as_fcgi
,
1691 'nproc|n=i' => $nproc_sub,
1694 if (!$fcgi_nproc_active && defined $ENV{'GITWEB_FCGI_NPROC'} && $ENV{'GITWEB_FCGI_NPROC'} =~ /^\d+$/) {
1695 &$nproc_sub('nproc', $ENV{'GITWEB_FCGI_NPROC'});
1699 # Any "our" variable that could possibly influence correct handling of
1700 # a CGI request MUST be reset in this subroutine
1701 sub _reset_globals
{
1702 # Note that $t0 and $number_of_git_commands are handled by reset_timer
1703 our %input_params = ();
1704 our ($action, $project, $file_name, $file_parent, $hash, $hash_parent, $hash_base,
1705 $hash_parent_base, @extra_options, $page, $searchtype, $search_use_regexp,
1706 $searchtext, $search_regexp, $project_filter) = ();
1707 our $git_dir = undef;
1708 our (@snapshot_fmts, $git_avatar, @extra_branch_refs) = ();
1709 our %avatar_cache = ();
1710 our $config_file = '';
1712 our $gitweb_project_owner = undef;
1713 our $shown_stale_message = 0;
1714 our $fcgi_raw_mode = 0;
1715 keys %known_snapshot_formats; # reset 'each' iterator
1719 evaluate_gitweb_config
();
1720 evaluate_encoding
();
1721 evaluate_email_obfuscate
();
1722 evaluate_git_version
();
1723 my ($ml, $mi, $bu, $hl, $subroutine) = ($my_url, $my_uri, $base_url, $home_link, '');
1724 $subroutine .= '$my_url = $ml;' if defined $my_url && $my_url ne '';
1725 $subroutine .= '$my_uri = $mi;' if defined $my_uri; # this one can be ""
1726 $subroutine .= '$base_url = $bu;' if defined $base_url && $base_url ne '';
1727 $subroutine .= '$home_link = $hl;' if defined $home_link && $home_link ne '';
1728 $evaluate_uri_force = eval "sub {$subroutine}" if $subroutine;
1732 $pre_listen_hook->()
1733 if $pre_listen_hook;
1736 while ($cgi = $CGI->new()) {
1737 $pre_dispatch_hook->()
1738 if $pre_dispatch_hook;
1740 # most globals can simply be reset
1743 # evaluate_path_info corrupts %known_snapshot_formats
1744 # so we need a deepish copy of it -- note that
1745 # _reset_globals already took care of resetting its
1746 # hash iterator that evaluate_path_info also leaves
1747 # in an indeterminate state
1749 while (my ($k,$v) = each(%known_snapshot_formats)) {
1750 $formats{$k} = {%{$known_snapshot_formats{$k}}};
1752 local *known_snapshot_formats
= \
%formats;
1754 eval {run_request
()};
1756 $post_dispatch_hook->()
1757 if $post_dispatch_hook;
1760 last REQUEST
if ($is_last_request->());
1768 if (defined caller) {
1769 # wrapped in a subroutine processing requests,
1770 # e.g. mod_perl with ModPerl::Registry, or PSGI with Plack::App::WrapCGI
1773 # pure CGI script, serving single request
1777 ## ======================================================================
1780 # possible values of extra options
1781 # -full => 0|1 - use absolute/full URL ($my_uri/$my_url as base)
1782 # -replay => 1 - start from a current view (replay with modifications)
1783 # -path_info => 0|1 - don't use/use path_info URL (if possible)
1784 # -anchor => ANCHOR - add #ANCHOR to end of URL, implies -replay if used alone
1787 # default is to use -absolute url() i.e. $my_uri
1788 my $href = $params{-full
} ?
$my_url : $my_uri;
1790 # implicit -replay, must be first of implicit params
1791 $params{-replay
} = 1 if (keys %params == 1 && $params{-anchor
});
1793 $params{'project'} = $project unless exists $params{'project'};
1795 if ($params{-replay
}) {
1796 while (my ($name, $symbol) = each %cgi_param_mapping) {
1797 if (!exists $params{$name}) {
1798 $params{$name} = $input_params{$name};
1803 my $use_pathinfo = gitweb_check_feature
('pathinfo');
1804 if (defined $params{'project'} &&
1805 (exists $params{-path_info
} ?
$params{-path_info
} : $use_pathinfo)) {
1806 # try to put as many parameters as possible in PATH_INFO:
1809 # - hash_parent or hash_parent_base:/file_parent
1810 # - hash or hash_base:/filename
1811 # - the snapshot_format as an appropriate suffix
1813 # When the script is the root DirectoryIndex for the domain,
1814 # $href here would be something like http://gitweb.example.com/
1815 # Thus, we strip any trailing / from $href, to spare us double
1816 # slashes in the final URL
1819 # Then add the project name, if present
1820 $href .= "/".esc_path_info
($params{'project'});
1821 delete $params{'project'};
1823 # since we destructively absorb parameters, we keep this
1824 # boolean that remembers if we're handling a snapshot
1825 my $is_snapshot = $params{'action'} eq 'snapshot';
1827 # Summary just uses the project path URL, any other action is
1829 if (defined $params{'action'}) {
1830 $href .= "/".esc_path_info
($params{'action'})
1831 unless $params{'action'} eq 'summary';
1832 delete $params{'action'};
1835 # Next, we put hash_parent_base:/file_parent..hash_base:/file_name,
1836 # stripping nonexistent or useless pieces
1837 $href .= "/" if ($params{'hash_base'} || $params{'hash_parent_base'}
1838 || $params{'hash_parent'} || $params{'hash'});
1839 if (defined $params{'hash_base'}) {
1840 if (defined $params{'hash_parent_base'}) {
1841 $href .= esc_path_info
($params{'hash_parent_base'});
1842 # skip the file_parent if it's the same as the file_name
1843 if (defined $params{'file_parent'}) {
1844 if (defined $params{'file_name'} && $params{'file_parent'} eq $params{'file_name'}) {
1845 delete $params{'file_parent'};
1846 } elsif ($params{'file_parent'} !~ /\.\./) {
1847 $href .= ":/".esc_path_info
($params{'file_parent'});
1848 delete $params{'file_parent'};
1852 delete $params{'hash_parent'};
1853 delete $params{'hash_parent_base'};
1854 } elsif (defined $params{'hash_parent'}) {
1855 $href .= esc_path_info
($params{'hash_parent'}). "..";
1856 delete $params{'hash_parent'};
1859 $href .= esc_path_info
($params{'hash_base'});
1860 if (defined $params{'file_name'} && $params{'file_name'} !~ /\.\./) {
1861 $href .= ":/".esc_path_info
($params{'file_name'});
1862 delete $params{'file_name'};
1864 delete $params{'hash'};
1865 delete $params{'hash_base'};
1866 } elsif (defined $params{'hash'}) {
1867 $href .= esc_path_info
($params{'hash'});
1868 delete $params{'hash'};
1871 # If the action was a snapshot, we can absorb the
1872 # snapshot_format parameter too
1874 my $fmt = $params{'snapshot_format'};
1875 # snapshot_format should always be defined when href()
1876 # is called, but just in case some code forgets, we
1877 # fall back to the default
1878 $fmt ||= $snapshot_fmts[0];
1879 $href .= $known_snapshot_formats{$fmt}{'suffix'};
1880 delete $params{'snapshot_format'};
1884 # now encode the parameters explicitly
1886 for (my $i = 0; $i < @cgi_param_mapping; $i += 2) {
1887 my ($name, $symbol) = ($cgi_param_mapping[$i], $cgi_param_mapping[$i+1]);
1888 if (defined $params{$name}) {
1889 if (ref($params{$name}) eq "ARRAY") {
1890 foreach my $par (@
{$params{$name}}) {
1891 push @result, $symbol . "=" . esc_param
($par);
1894 push @result, $symbol . "=" . esc_param
($params{$name});
1898 $href .= "?" . join(';', @result) if scalar @result;
1900 # final transformation: trailing spaces must be escaped (URI-encoded)
1901 $href =~ s/(\s+)$/CGI::escape($1)/e;
1903 if ($params{-anchor
}) {
1904 $href .= "#".esc_param
($params{-anchor
});
1911 ## ======================================================================
1912 ## validation, quoting/unquoting and escaping
1914 sub is_valid_action
{
1916 return undef unless exists $actions{$input};
1920 sub is_valid_project
{
1923 return unless defined $input;
1924 if (!is_valid_pathname
($input) ||
1925 !(-d
"$projectroot/$input") ||
1926 !check_export_ok
("$projectroot/$input") ||
1927 ($strict_export && !project_in_list
($input))) {
1934 sub is_valid_pathname
{
1937 return undef unless defined $input;
1938 # no '.' or '..' as elements of path, i.e. no '.' or '..'
1939 # at the beginning, at the end, and between slashes.
1940 # also this catches doubled slashes
1941 if ($input =~ m!(^|/)(|\.|\.\.)(/|$)!) {
1944 # no null characters
1945 if ($input =~ m!\0!) {
1951 sub is_valid_ref_format
{
1954 return undef unless defined $input;
1955 # restrictions on ref name according to git-check-ref-format
1956 if ($input =~ m!(/\.|\.\.|[\000-\040\177 ~^:?*\[]|/$)!) {
1962 sub is_valid_refname
{
1965 return undef unless defined $input;
1966 # textual hashes are O.K.
1967 if ($input =~ m/^[0-9a-fA-F]{40}$/) {
1970 # allow repeated trailing '[~^]n*' suffix(es)
1971 $input =~ s/^([^~^]+)(?:[~^]\d*)+$/$1/;
1972 # it must be correct pathname
1973 is_valid_pathname
($input) or return undef;
1974 # check git-check-ref-format restrictions
1975 is_valid_ref_format
($input) or return undef;
1979 # decode sequences of octets in utf8 into Perl's internal form,
1980 # which is utf-8 with utf8 flag set if needed. gitweb writes out
1981 # in utf-8 thanks to "binmode STDOUT, ':utf8'" at beginning
1984 return undef unless defined $str;
1986 if (utf8
::is_utf8
($str) || utf8
::decode
($str)) {
1989 return $encode_object->decode($str, Encode
::FB_DEFAULT
);
1993 # quote unsafe chars, but keep the slash, even when it's not
1994 # correct, but quoted slashes look too horrible in bookmarks
1997 return undef unless defined $str;
1998 $str =~ s/([^A-Za-z0-9\-_.~()\/:@ ]+)/CGI
::escape
($1)/eg
;
2003 # the quoting rules for path_info fragment are slightly different
2006 return undef unless defined $str;
2008 # path_info doesn't treat '+' as space (specially), but '?' must be escaped
2009 $str =~ s/([^A-Za-z0-9\-_.~();\/;:@&= +]+)/CGI
::escape
($1)/eg
;
2014 # quote unsafe chars in whole URL, so some characters cannot be quoted
2017 return undef unless defined $str;
2018 $str =~ s/([^A-Za-z0-9\-_.~();\/;?:@&= ]+)/CGI
::escape
($1)/eg
;
2023 # quote unsafe characters in HTML attributes
2026 # for XHTML conformance escaping '"' to '"' is not enough
2027 return esc_html
(@_);
2030 # replace invalid utf8 character with SUBSTITUTION sequence
2035 return undef unless defined $str;
2037 $str = to_utf8
($str);
2038 $str = $cgi->escapeHTML($str);
2039 if ($opts{'-nbsp'}) {
2040 $str =~ s/ / /g;
2043 $str =~ s
|([[:cntrl
:]])|(($1 ne "\t") ? quot_cec
($1) : $1)|eg
;
2047 # quote control characters and escape filename to HTML
2052 return undef unless defined $str;
2054 $str = to_utf8
($str);
2055 $str = $cgi->escapeHTML($str);
2056 if ($opts{'-nbsp'}) {
2057 $str =~ s/ / /g;
2060 $str =~ s
|([[:cntrl
:]])|quot_cec
($1)|eg
;
2064 # Sanitize for use in XHTML + application/xml+xhtm (valid XML 1.0)
2068 return undef unless defined $str;
2070 $str = to_utf8
($str);
2072 $str =~ s
|([[:cntrl
:]])|(index("\t\n\r", $1) != -1 ?
$1 : quot_cec
($1))|eg
;
2076 # Make control characters "printable", using character escape codes (CEC)
2080 my %es = ( # character escape codes, aka escape sequences
2081 "\t" => '\t', # tab (HT)
2082 "\n" => '\n', # line feed (LF)
2083 "\r" => '\r', # carrige return (CR)
2084 "\f" => '\f', # form feed (FF)
2085 "\b" => '\b', # backspace (BS)
2086 "\a" => '\a', # alarm (bell) (BEL)
2087 "\e" => '\e', # escape (ESC)
2088 "\013" => '\v', # vertical tab (VT)
2089 "\000" => '\0', # nul character (NUL)
2091 my $chr = ( (exists $es{$cntrl})
2093 : sprintf('\x%02x', ord($cntrl)) );
2094 if ($opts{-nohtml
}) {
2097 return "<span class=\"cntrl\">$chr</span>";
2101 # Alternatively use unicode control pictures codepoints,
2102 # Unicode "printable representation" (PR)
2107 my $chr = sprintf('&#%04d;', 0x2400+ord($cntrl));
2108 if ($opts{-nohtml
}) {
2111 return "<span class=\"cntrl\">$chr</span>";
2115 # git may return quoted and escaped filenames
2121 my %es = ( # character escape codes, aka escape sequences
2122 't' => "\t", # tab (HT, TAB)
2123 'n' => "\n", # newline (NL)
2124 'r' => "\r", # return (CR)
2125 'f' => "\f", # form feed (FF)
2126 'b' => "\b", # backspace (BS)
2127 'a' => "\a", # alarm (bell) (BEL)
2128 'e' => "\e", # escape (ESC)
2129 'v' => "\013", # vertical tab (VT)
2132 if ($seq =~ m/^[0-7]{1,3}$/) {
2133 # octal char sequence
2134 return chr(oct($seq));
2135 } elsif (exists $es{$seq}) {
2136 # C escape sequence, aka character escape code
2139 # quoted ordinary character
2143 if ($str =~ m/^"(.*)"$/) {
2146 $str =~ s/\\([^0-7]|[0-7]{1,3})/unq($1)/eg;
2151 # escape tabs (convert tabs to spaces)
2155 while ((my $pos = index($line, "\t")) != -1) {
2156 if (my $count = (8 - ($pos % 8))) {
2157 my $spaces = ' ' x
$count;
2158 $line =~ s/\t/$spaces/;
2165 sub project_in_list
{
2166 my $project = shift;
2167 my @list = git_get_projects_list
();
2168 return @list && scalar(grep { $_->{'path'} eq $project } @list);
2171 sub cached_page_precondition_check
{
2174 $action eq 'summary' &&
2175 $projlist_cache_lifetime > 0 &&
2176 gitweb_check_feature
('forks');
2178 # Note that ALL the 'forkchange' logic is in this function.
2179 # It does NOT belong in cached_action_page NOR in cached_action_start
2180 # NOR in cached_action_finish. None of those functions should know anything
2181 # about nor do anything to any of the 'forkchange'/"$action.forkchange" files.
2183 # besides the basic 'changed' "$action.changed" check, we may only use
2184 # a summary cache if:
2186 # 1) we are not using a project list cache file
2188 # 2) we are not using the 'forks' feature
2190 # 3) there is no 'forkchange' nor "$action.forkchange" file in $html_cache_dir
2192 # 4) there is no cache file ("$cache_dir/$projlist_cache_name")
2194 # 5) the OLDER of 'forkchange'/"$action.forkchange" is NEWER than the cache file
2196 # Otherwise we must re-generate the cache because we've had a fork change
2197 # (either a fork was added or a fork was removed) AND the change has been
2198 # picked up in the cache file AND we've not got that in our cached copy
2200 # For (5) regenerating the cached page wouldn't get us anything if the project
2201 # cache file is older than the 'forkchange'/"$action.forkchange" because the
2202 # forks information comes from the project cache file and it's clearly not
2203 # picked up the changes yet so we may continue to use a cached page until it does.
2205 my $htmlcd = "$projectroot/$project/$html_cache_dir";
2206 my $fc_mt = (stat("$htmlcd/forkchange"))[9];
2207 my $afc_mt = (stat("$htmlcd/$action.forkchange"))[9];
2208 return 1 unless defined($fc_mt) || defined($afc_mt);
2209 my $prj_mt = (stat("$cache_dir/$projlist_cache_name"))[9];
2210 return 1 unless $prj_mt;
2211 my $old_mt = $fc_mt;
2212 $old_mt = $afc_mt if !defined($old_mt) || (defined($afc_mt) && $afc_mt < $old_mt);
2213 return 1 if $old_mt > $prj_mt;
2215 # We're going to regenerate the cached page because we know the project cache
2216 # has new fork information that we cannot possibly have in our cached copy.
2218 # However, if both 'forkchange' and "$action.forkchange" exist and one of
2219 # them is older than the project cache and one of them is newer, we still
2220 # need to regenerate the page cache, but we will also need to do it again
2221 # in the future because there's yet another fork update not yet in the cache.
2223 # So we make sure to touch "$action.changed" to force a cache regeneration
2224 # and then we remove either or both of 'forkchange'/"$action.forkchange" if
2225 # they're older than the project cache (they've served their purpose, we're
2226 # forcing a page regeneration by touching "$action.changed" but the project
2227 # cache was rebuilt since then so there are no more pending fork updates to
2228 # pick up in the future and they need to go).
2230 # For best results, the external code that touches 'forkchange' should always
2231 # touch 'forkchange' and additionally touch 'summary.forkchange' but only
2232 # if it does not already exist. That way the cached page will be regenerated
2233 # each time it's requested and ANY fork updates are available in the proj
2234 # cache rather than waiting until they all are before updating.
2236 # Note that we take a shortcut here and will zap 'forkchange' since we know
2237 # that it only affects the 'summary' cache. If, in the future, it affects
2238 # other cache types, it will first need to be propogated down to
2239 # "$action.forkchange" for those types before we zap it.
2242 open $fd, '>', "$htmlcd/$action.changed" and close $fd;
2243 $fc_mt=undef, unlink "$htmlcd/forkchange" if defined $fc_mt && $fc_mt < $prj_mt;
2244 $afc_mt=undef, unlink "$htmlcd/$action.forkchange" if defined $afc_mt && $afc_mt < $prj_mt;
2246 # Now we propagate 'forkchange' to "$action.forkchange" if we have the
2247 # one and not the other.
2249 if (defined $fc_mt && ! defined $afc_mt) {
2250 open $fd, '>', "$htmlcd/$action.forkchange" and close $fd;
2251 -e
"$htmlcd/$action.forkchange" and
2252 utime($fc_mt, $fc_mt, "$htmlcd/$action.forkchange") and
2253 unlink "$htmlcd/forkchange";
2259 sub cached_action_page
{
2262 return undef unless $action && $html_cache_actions{$action} && $html_cache_dir;
2263 my $htmlcd = "$projectroot/$project/$html_cache_dir";
2264 return undef if -e
"$htmlcd/changed" || -e
"$htmlcd/$action.changed";
2265 return undef unless cached_page_precondition_check
($action);
2266 open my $fd, '<', "$htmlcd/$action" or return undef;
2269 my $cached_page = <$fd>;
2270 close $fd or return undef;
2271 return $cached_page;
2274 package Git
::Gitweb
::CacheFile
;
2277 use POSIX
qw(:fcntl_h);
2279 my $cachefile = shift;
2281 sysopen(my $self, $cachefile, O_WRONLY
|O_CREAT
|O_EXCL
, 0664)
2283 $$self->{'cachefile'} = $cachefile;
2284 $$self->{'opened'} = 1;
2285 $$self->{'contents'} = '';
2286 return bless $self, $class;
2291 if ($$self->{'opened'}) {
2292 $$self->{'opened'} = 0;
2293 my $result = close $self;
2294 unlink $$self->{'cachefile'} unless $result;
2302 if ($$self->{'opened'}) {
2303 $self->CLOSE() and unlink $$self->{'cachefile'};
2309 @_ = (map {my $x=$_; utf8
::encode
($x); $x} @_) unless $fcgi_raw_mode;
2310 print $self @_ if $$self->{'opened'};
2311 $$self->{'contents'} .= join('', @_);
2317 my $template = shift;
2318 return $self->PRINT(sprintf $template, @_);
2323 return $$self->{'contents'};
2328 # Caller is responsible for preserving STDOUT beforehand if needed
2329 sub cached_action_start
{
2332 return undef unless $html_cache_actions{$action} && $html_cache_dir;
2333 my $htmlcd = "$projectroot/$project/$html_cache_dir";
2334 return undef unless -d
$htmlcd;
2335 if (-e
"$htmlcd/changed") {
2336 foreach my $cacheable (keys(%html_cache_actions)) {
2337 next unless $supported_cache_actions{$cacheable} &&
2338 $html_cache_actions{$cacheable};
2340 open $fd, '>', "$htmlcd/$cacheable.changed"
2343 unlink "$htmlcd/changed";
2346 tie
*CACHEFILE
, 'Git::Gitweb::CacheFile', "$htmlcd/$action.lock" or return undef;
2347 *STDOUT
= *CACHEFILE
;
2348 unlink "$htmlcd/$action", "$htmlcd/$action.changed";
2352 # Caller is responsible for restoring STDOUT afterward if needed
2353 sub cached_action_finish
{
2358 my $obj = tied *STDOUT
;
2359 return undef unless ref($obj) eq 'Git::Gitweb::CacheFile';
2360 my $cached_page = $obj->contents;
2361 (my $result = close(STDOUT
)) or warn "couldn't close cache file on STDOUT: $!";
2362 # Do not leave STDOUT file descriptor invalid!
2364 open(NULL
, '>', File
::Spec
->devnull) or die "couldn't open NULL to devnull: $!";
2366 return $cached_page unless $result;
2367 my $htmlcd = "$projectroot/$project/$html_cache_dir";
2368 return $cached_page unless -d
$htmlcd;
2369 unlink "$htmlcd/$action.lock" unless rename "$htmlcd/$action.lock", "$htmlcd/$action";
2370 return $cached_page;
2374 BEGIN {%expand_pi_subs = (
2375 'age_string' => \
&age_string
,
2376 'age_string_date' => \
&age_string_date
,
2377 'age_string_age' => \
&age_string_age
,
2378 'compute_timed_interval' => \
&compute_timed_interval
,
2379 'compute_commands_count' => \
&compute_commands_count
,
2380 'format_lastrefresh_row' => \
&format_lastrefresh_row
,
2383 # Expands any <?gitweb...> processing instructions and returns the result
2384 sub expand_gitweb_pi
{
2387 my @time_now = gettimeofday
();
2388 $page =~ s
{<\?gitweb
(?
:\s
+([^\s
>]+)([^>]*))?\s
*\?>}
2390 (ref($expand_pi_subs{$1}) eq 'CODE' ?
2391 $expand_pi_subs{$1}->(split(' ',$2), @time_now) :
2397 ## ----------------------------------------------------------------------
2398 ## HTML aware string manipulation
2400 # Try to chop given string on a word boundary between position
2401 # $len and $len+$add_len. If there is no word boundary there,
2402 # chop at $len+$add_len. Do not chop if chopped part plus ellipsis
2403 # (marking chopped part) would be longer than given string.
2407 my $add_len = shift || 10;
2408 my $where = shift || 'right'; # 'left' | 'center' | 'right'
2410 # Make sure perl knows it is utf8 encoded so we don't
2411 # cut in the middle of a utf8 multibyte char.
2412 $str = to_utf8
($str);
2414 # allow only $len chars, but don't cut a word if it would fit in $add_len
2415 # if it doesn't fit, cut it if it's still longer than the dots we would add
2416 # remove chopped character entities entirely
2418 # when chopping in the middle, distribute $len into left and right part
2419 # return early if chopping wouldn't make string shorter
2420 if ($where eq 'center') {
2421 return $str if ($len + 5 >= length($str)); # filler is length 5
2424 return $str if ($len + 4 >= length($str)); # filler is length 4
2427 # regexps: ending and beginning with word part up to $add_len
2428 my $endre = qr/.{$len}\w{0,$add_len}/;
2429 my $begre = qr/\w{0,$add_len}.{$len}/;
2431 if ($where eq 'left') {
2432 $str =~ m/^(.*?)($begre)$/;
2433 my ($lead, $body) = ($1, $2);
2434 if (length($lead) > 4) {
2437 return "$lead$body";
2439 } elsif ($where eq 'center') {
2440 $str =~ m/^($endre)(.*)$/;
2441 my ($left, $str) = ($1, $2);
2442 $str =~ m/^(.*?)($begre)$/;
2443 my ($mid, $right) = ($1, $2);
2444 if (length($mid) > 5) {
2447 return "$left$mid$right";
2450 $str =~ m/^($endre)(.*)$/;
2453 if (length($tail) > 4) {
2456 return "$body$tail";
2460 # pass-through email filter, obfuscating it when possible
2461 sub email_obfuscate
{
2465 $str = $email->escape_html($str);
2466 # Stock HTML::Email::Obfuscate version likes to produce
2468 $str =~ s
#<(/?)B>#<$1b>#g;
2471 $str = esc_html
($str);
2472 $str =~ s/@/@/;
2477 # takes the same arguments as chop_str, but also wraps a <span> around the
2478 # result with a title attribute if it does get chopped. Additionally, the
2479 # string is HTML-escaped.
2480 sub chop_and_escape_str
{
2483 my $chopped = chop_str
(@_);
2484 $str = to_utf8
($str);
2485 if ($chopped eq $str) {
2486 return email_obfuscate
($chopped);
2489 $str =~ s/[[:cntrl:]]/?/g;
2490 return $cgi->span({-title
=>$str}, email_obfuscate
($chopped));
2494 # Highlight selected fragments of string, using given CSS class,
2495 # and escape HTML. It is assumed that fragments do not overlap.
2496 # Regions are passed as list of pairs (array references).
2498 # Example: esc_html_hl_regions("foobar", "mark", [ 0, 3 ]) returns
2499 # '<span class="mark">foo</span>bar'
2500 sub esc_html_hl_regions
{
2501 my ($str, $css_class, @sel) = @_;
2502 my %opts = grep { ref($_) ne 'ARRAY' } @sel;
2503 @sel = grep { ref($_) eq 'ARRAY' } @sel;
2504 return esc_html
($str, %opts) unless @sel;
2510 my ($begin, $end) = @
$s;
2512 # Don't create empty <span> elements.
2513 next if $end <= $begin;
2515 my $escaped = esc_html
(substr($str, $begin, $end - $begin),
2518 $out .= esc_html
(substr($str, $pos, $begin - $pos), %opts)
2519 if ($begin - $pos > 0);
2520 $out .= $cgi->span({-class => $css_class}, $escaped);
2524 $out .= esc_html
(substr($str, $pos), %opts)
2525 if ($pos < length($str));
2530 # return positions of beginning and end of each match
2532 my ($str, $regexp) = @_;
2533 return unless (defined $str && defined $regexp);
2536 while ($str =~ /$regexp/g) {
2537 push @matches, [$-[0], $+[0]];
2542 # highlight match (if any), and escape HTML
2543 sub esc_html_match_hl
{
2544 my ($str, $regexp) = @_;
2545 return esc_html
($str) unless defined $regexp;
2547 my @matches = matchpos_list
($str, $regexp);
2548 return esc_html
($str) unless @matches;
2550 return esc_html_hl_regions
($str, 'match', @matches);
2554 # highlight match (if any) of shortened string, and escape HTML
2555 sub esc_html_match_hl_chopped
{
2556 my ($str, $chopped, $regexp) = @_;
2557 return esc_html_match_hl
($str, $regexp) unless defined $chopped;
2559 my @matches = matchpos_list
($str, $regexp);
2560 return esc_html
($chopped) unless @matches;
2562 # filter matches so that we mark chopped string
2563 my $tail = "... "; # see chop_str
2564 unless ($chopped =~ s/\Q$tail\E$//) {
2567 my $chop_len = length($chopped);
2568 my $tail_len = length($tail);
2571 for my $m (@matches) {
2572 if ($m->[0] > $chop_len) {
2573 push @filtered, [ $chop_len, $chop_len + $tail_len ] if ($tail_len > 0);
2575 } elsif ($m->[1] > $chop_len) {
2576 push @filtered, [ $m->[0], $chop_len + $tail_len ];
2582 return esc_html_hl_regions
($chopped . $tail, 'match', @filtered);
2585 ## ----------------------------------------------------------------------
2586 ## functions returning short strings
2588 # CSS class for given age epoch value (in seconds)
2589 # and reference time (optional, defaults to now) as second value
2591 my ($age_epoch, $time_now) = @_;
2592 return "noage" unless defined $age_epoch;
2593 defined $time_now or $time_now = time;
2594 my $age = $time_now - $age_epoch;
2596 if ($age < 60*60*2) {
2598 } elsif ($age < 60*60*24*2) {
2605 # convert age epoch in seconds to "nn units ago" string
2606 # reference time used is now unless second argument passed in
2607 # to get the old behavior, pass 0 as the first argument and
2608 # the time in seconds as the second
2610 my ($age_epoch, $time_now) = @_;
2611 return "unknown" unless defined $age_epoch;
2612 return "<?gitweb age_string $age_epoch?>" if $cache_mode_active;
2613 defined $time_now or $time_now = time;
2614 my $age = $time_now - $age_epoch;
2617 if ($age > 60*60*24*365*2) {
2618 $age_str = (int $age/60/60/24/365);
2619 $age_str .= " years ago";
2620 } elsif ($age > 60*60*24*(365/12)*2) {
2621 $age_str = int $age/60/60/24/(365/12);
2622 $age_str .= " months ago";
2623 } elsif ($age > 60*60*24*7*2) {
2624 $age_str = int $age/60/60/24/7;
2625 $age_str .= " weeks ago";
2626 } elsif ($age > 60*60*24*2) {
2627 $age_str = int $age/60/60/24;
2628 $age_str .= " days ago";
2629 } elsif ($age > 60*60*2) {
2630 $age_str = int $age/60/60;
2631 $age_str .= " hours ago";
2632 } elsif ($age > 60*2) {
2633 $age_str = int $age/60;
2634 $age_str .= " min ago";
2635 } elsif ($age > 2) {
2636 $age_str = int $age;
2637 $age_str .= " sec ago";
2639 $age_str .= " right now";
2644 # returns age_string if the age is <= 2 weeks otherwise an absolute date
2645 # this is typically shown to the user directly with the age_string_age as a title
2646 sub age_string_date
{
2647 my ($age_epoch, $time_now) = @_;
2648 return "unknown" unless defined $age_epoch;
2649 return "<?gitweb age_string_date $age_epoch?>" if $cache_mode_active;
2650 defined $time_now or $time_now = time;
2651 my $age = $time_now - $age_epoch;
2653 if ($age > 60*60*24*7*2) {
2654 my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($age_epoch);
2655 return sprintf "%4i-%02u-%02i", 1900 + $year, $mon+1, $mday;
2657 return age_string
($age_epoch, $time_now);
2661 # returns an absolute date if the age is <= 2 weeks otherwise age_string
2662 # this is typically used for the 'title' attribute so it will show as a tooltip
2663 sub age_string_age
{
2664 my ($age_epoch, $time_now) = @_;
2665 return "unknown" unless defined $age_epoch;
2666 return "<?gitweb age_string_age $age_epoch?>" if $cache_mode_active;
2667 defined $time_now or $time_now = time;
2668 my $age = $time_now - $age_epoch;
2670 if ($age > 60*60*24*7*2) {
2671 return age_string
($age_epoch, $time_now);
2673 my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($age_epoch);
2674 return sprintf "%4i-%02u-%02i", 1900 + $year, $mon+1, $mday;
2679 S_IFINVALID
=> 0030000,
2680 S_IFGITLINK
=> 0160000,
2683 # submodule/subproject, a commit object reference
2687 return (($mode & S_IFMT
) == S_IFGITLINK
)
2690 # convert file mode in octal to symbolic file mode string
2692 my $mode = oct shift;
2694 if (S_ISGITLINK
($mode)) {
2695 return 'm---------';
2696 } elsif (S_ISDIR
($mode & S_IFMT
)) {
2697 return 'drwxr-xr-x';
2698 } elsif (S_ISLNK
($mode)) {
2699 return 'lrwxrwxrwx';
2700 } elsif (S_ISREG
($mode)) {
2701 # git cares only about the executable bit
2702 if ($mode & S_IXUSR
) {
2703 return '-rwxr-xr-x';
2705 return '-rw-r--r--';
2708 return '----------';
2712 # convert file mode in octal to file type string
2716 if ($mode !~ m/^[0-7]+$/) {
2722 if (S_ISGITLINK
($mode)) {
2724 } elsif (S_ISDIR
($mode & S_IFMT
)) {
2726 } elsif (S_ISLNK
($mode)) {
2728 } elsif (S_ISREG
($mode)) {
2735 # convert file mode in octal to file type description string
2736 sub file_type_long
{
2739 if ($mode !~ m/^[0-7]+$/) {
2745 if (S_ISGITLINK
($mode)) {
2747 } elsif (S_ISDIR
($mode & S_IFMT
)) {
2749 } elsif (S_ISLNK
($mode)) {
2751 } elsif (S_ISREG
($mode)) {
2752 if ($mode & S_IXUSR
) {
2753 return "executable";
2763 ## ----------------------------------------------------------------------
2764 ## functions returning short HTML fragments, or transforming HTML fragments
2765 ## which don't belong to other sections
2767 # format line of commit message.
2768 sub format_log_line_html
{
2771 $line = esc_html
($line, -nbsp
=>1);
2772 $line =~ s
{\b([0-9a
-fA
-F
]{8,40})\b}{
2773 $cgi->a({-href
=> href
(action
=>"object", hash
=>$1),
2774 -class => "text"}, $1);
2775 }eg
unless $line =~ /^\s*git-svn-id:/;
2780 # format marker of refs pointing to given object
2782 # the destination action is chosen based on object type and current context:
2783 # - for annotated tags, we choose the tag view unless it's the current view
2784 # already, in which case we go to shortlog view
2785 # - for other refs, we keep the current view if we're in history, shortlog or
2786 # log view, and select shortlog otherwise
2787 sub format_ref_marker
{
2788 my ($refs, $id) = @_;
2791 if (defined $refs->{$id}) {
2792 foreach my $ref (@
{$refs->{$id}}) {
2793 # this code exploits the fact that non-lightweight tags are the
2794 # only indirect objects, and that they are the only objects for which
2795 # we want to use tag instead of shortlog as action
2796 my ($type, $name) = qw();
2797 my $indirect = ($ref =~ s/\^\{\}$//);
2798 # e.g. tags/v2.6.11 or heads/next
2799 if ($ref =~ m!^(.*?)s?/(.*)$!) {
2808 $class .= " indirect" if $indirect;
2810 my $dest_action = "shortlog";
2813 $dest_action = "tag" unless $action eq "tag";
2814 } elsif ($action =~ /^(history|(short)?log)$/) {
2815 $dest_action = $action;
2819 $dest .= "refs/" unless $ref =~ m
!^refs
/!;
2822 my $link = $cgi->a({
2824 action
=>$dest_action,
2826 )}, esc_html
($name));
2828 $markers .= "<span class=\"".esc_attr
($class)."\" title=\"".esc_attr
($ref)."\">" .
2834 return '<span class="refs">'. $markers . '</span>';
2840 # format, perhaps shortened and with markers, title line
2841 sub format_subject_html
{
2842 my ($long, $short, $href, $extra) = @_;
2843 $extra = '' unless defined($extra);
2845 if (length($short) < length($long)) {
2847 $long =~ s/[[:cntrl:]]/?/g;
2848 return $cgi->a({-href
=> $href, -class => "list subject",
2849 -title
=> to_utf8
($long)},
2850 esc_html
($short)) . $extra;
2852 return $cgi->a({-href
=> $href, -class => "list subject"},
2853 esc_html
($long)) . $extra;
2857 # Rather than recomputing the url for an email multiple times, we cache it
2858 # after the first hit. This gives a visible benefit in views where the avatar
2859 # for the same email is used repeatedly (e.g. shortlog).
2860 # The cache is shared by all avatar engines (currently gravatar only), which
2861 # are free to use it as preferred. Since only one avatar engine is used for any
2862 # given page, there's no risk for cache conflicts.
2863 our %avatar_cache = ();
2865 # Compute the picon url for a given email, by using the picon search service over at
2866 # http://www.cs.indiana.edu/picons/search.html
2868 my $email = lc shift;
2869 if (!$avatar_cache{$email}) {
2870 my ($user, $domain) = split('@', $email);
2871 $avatar_cache{$email} =
2872 "//www.cs.indiana.edu/cgi-pub/kinzler/piconsearch.cgi/" .
2874 "users+domains+unknown/up/single";
2876 return $avatar_cache{$email};
2879 # Compute the gravatar url for a given email, if it's not in the cache already.
2880 # Gravatar stores only the part of the URL before the size, since that's the
2881 # one computationally more expensive. This also allows reuse of the cache for
2882 # different sizes (for this particular engine).
2884 my $email = lc shift;
2886 $avatar_cache{$email} ||=
2887 "//www.gravatar.com/avatar/" .
2888 Digest
::MD5
::md5_hex
($email) . "?s=";
2889 return $avatar_cache{$email} . $size;
2892 # Insert an avatar for the given $email at the given $size if the feature
2894 sub git_get_avatar
{
2895 my ($email, %opts) = @_;
2896 my $pre_white = ($opts{-pad_before
} ?
" " : "");
2897 my $post_white = ($opts{-pad_after
} ?
" " : "");
2898 $opts{-size
} ||= 'default';
2899 my $size = $avatar_size{$opts{-size
}} || $avatar_size{'default'};
2901 if ($git_avatar eq 'gravatar') {
2902 $url = gravatar_url
($email, $size);
2903 } elsif ($git_avatar eq 'picon') {
2904 $url = picon_url
($email);
2906 # Other providers can be added by extending the if chain, defining $url
2907 # as needed. If no variant puts something in $url, we assume avatars
2908 # are completely disabled/unavailable.
2911 "<img width=\"$size\" " .
2912 "class=\"avatar\" " .
2913 "src=\"".esc_url
($url)."\" " .
2921 sub format_search_author
{
2922 my ($author, $searchtype, $displaytext) = @_;
2923 my $have_search = gitweb_check_feature
('search');
2927 if ($searchtype eq 'author') {
2928 $performed = "authored";
2929 } elsif ($searchtype eq 'committer') {
2930 $performed = "committed";
2933 return $cgi->a({-href
=> href
(action
=>"search", hash
=>$hash,
2934 searchtext
=>$author,
2935 searchtype
=>$searchtype), class=>"list",
2936 title
=>"Search for commits $performed by $author"},
2940 return $displaytext;
2944 # format the author name of the given commit with the given tag
2945 # the author name is chopped and escaped according to the other
2946 # optional parameters (see chop_str).
2947 sub format_author_html
{
2950 my $author = chop_and_escape_str
($co->{'author_name'}, @_);
2951 return "<$tag class=\"author\">" .
2952 format_search_author
($co->{'author_name'}, "author",
2953 git_get_avatar
($co->{'author_email'}, -pad_after
=> 1) .
2958 # format git diff header line, i.e. "diff --(git|combined|cc) ..."
2959 sub format_git_diff_header_line
{
2961 my $diffinfo = shift;
2962 my ($from, $to) = @_;
2964 if ($diffinfo->{'nparents'}) {
2966 $line =~ s!^(diff (.*?) )"?.*$!$1!;
2967 if ($to->{'href'}) {
2968 $line .= $cgi->a({-href
=> $to->{'href'}, -class => "path"},
2969 esc_path
($to->{'file'}));
2970 } else { # file was deleted (no href)
2971 $line .= esc_path
($to->{'file'});
2975 $line =~ s!^(diff (.*?) )"?a/.*$!$1!;
2976 if ($from->{'href'}) {
2977 $line .= $cgi->a({-href
=> $from->{'href'}, -class => "path"},
2978 'a/' . esc_path
($from->{'file'}));
2979 } else { # file was added (no href)
2980 $line .= 'a/' . esc_path
($from->{'file'});
2983 if ($to->{'href'}) {
2984 $line .= $cgi->a({-href
=> $to->{'href'}, -class => "path"},
2985 'b/' . esc_path
($to->{'file'}));
2986 } else { # file was deleted
2987 $line .= 'b/' . esc_path
($to->{'file'});
2991 return "<div class=\"diff header\">$line</div>\n";
2994 # format extended diff header line, before patch itself
2995 sub format_extended_diff_header_line
{
2997 my $diffinfo = shift;
2998 my ($from, $to) = @_;
3001 if ($line =~ s!^((copy|rename) from ).*$!$1! && $from->{'href'}) {
3002 $line .= $cgi->a({-href
=>$from->{'href'}, -class=>"path"},
3003 esc_path
($from->{'file'}));
3005 if ($line =~ s!^((copy|rename) to ).*$!$1! && $to->{'href'}) {
3006 $line .= $cgi->a({-href
=>$to->{'href'}, -class=>"path"},
3007 esc_path
($to->{'file'}));
3009 # match single <mode>
3010 if ($line =~ m/\s(\d{6})$/) {
3011 $line .= '<span class="info"> (' .
3012 file_type_long
($1) .
3016 if ($line =~ m/^index [0-9a-fA-F]{40},[0-9a-fA-F]{40}/) {
3017 # can match only for combined diff
3019 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
3020 if ($from->{'href'}[$i]) {
3021 $line .= $cgi->a({-href
=>$from->{'href'}[$i],
3023 substr($diffinfo->{'from_id'}[$i],0,7));
3028 $line .= ',' if ($i < $diffinfo->{'nparents'} - 1);
3031 if ($to->{'href'}) {
3032 $line .= $cgi->a({-href
=>$to->{'href'}, -class=>"hash"},
3033 substr($diffinfo->{'to_id'},0,7));
3038 } elsif ($line =~ m/^index [0-9a-fA-F]{40}..[0-9a-fA-F]{40}/) {
3039 # can match only for ordinary diff
3040 my ($from_link, $to_link);
3041 if ($from->{'href'}) {
3042 $from_link = $cgi->a({-href
=>$from->{'href'}, -class=>"hash"},
3043 substr($diffinfo->{'from_id'},0,7));
3045 $from_link = '0' x
7;
3047 if ($to->{'href'}) {
3048 $to_link = $cgi->a({-href
=>$to->{'href'}, -class=>"hash"},
3049 substr($diffinfo->{'to_id'},0,7));
3053 my ($from_id, $to_id) = ($diffinfo->{'from_id'}, $diffinfo->{'to_id'});
3054 $line =~ s!$from_id\.\.$to_id!$from_link..$to_link!;
3057 return $line . "<br/>\n";
3060 # format from-file/to-file diff header
3061 sub format_diff_from_to_header
{
3062 my ($from_line, $to_line, $diffinfo, $from, $to, @parents) = @_;
3067 #assert($line =~ m/^---/) if DEBUG;
3068 # no extra formatting for "^--- /dev/null"
3069 if (! $diffinfo->{'nparents'}) {
3070 # ordinary (single parent) diff
3071 if ($line =~ m!^--- "?a/!) {
3072 if ($from->{'href'}) {
3074 $cgi->a({-href
=>$from->{'href'}, -class=>"path"},
3075 esc_path
($from->{'file'}));
3078 esc_path
($from->{'file'});
3081 $result .= qq!<div
class="diff from_file">$line</div
>\n!;
3084 # combined diff (merge commit)
3085 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
3086 if ($from->{'href'}[$i]) {
3088 $cgi->a({-href
=>href
(action
=>"blobdiff",
3089 hash_parent
=>$diffinfo->{'from_id'}[$i],
3090 hash_parent_base
=>$parents[$i],
3091 file_parent
=>$from->{'file'}[$i],
3092 hash
=>$diffinfo->{'to_id'},
3094 file_name
=>$to->{'file'}),
3096 -title
=>"diff" . ($i+1)},
3099 $cgi->a({-href
=>$from->{'href'}[$i], -class=>"path"},
3100 esc_path
($from->{'file'}[$i]));
3102 $line = '--- /dev/null';
3104 $result .= qq!<div
class="diff from_file">$line</div
>\n!;
3109 #assert($line =~ m/^\+\+\+/) if DEBUG;
3110 # no extra formatting for "^+++ /dev/null"
3111 if ($line =~ m!^\+\+\+ "?b/!) {
3112 if ($to->{'href'}) {
3114 $cgi->a({-href
=>$to->{'href'}, -class=>"path"},
3115 esc_path
($to->{'file'}));
3118 esc_path
($to->{'file'});
3121 $result .= qq!<div
class="diff to_file">$line</div
>\n!;
3126 # create note for patch simplified by combined diff
3127 sub format_diff_cc_simplified
{
3128 my ($diffinfo, @parents) = @_;
3131 $result .= "<div class=\"diff header\">" .
3133 if (!is_deleted
($diffinfo)) {
3134 $result .= $cgi->a({-href
=> href
(action
=>"blob",
3136 hash
=>$diffinfo->{'to_id'},
3137 file_name
=>$diffinfo->{'to_file'}),
3139 esc_path
($diffinfo->{'to_file'}));
3141 $result .= esc_path
($diffinfo->{'to_file'});
3143 $result .= "</div>\n" . # class="diff header"
3144 "<div class=\"diff nodifferences\">" .
3146 "</div>\n"; # class="diff nodifferences"
3151 sub diff_line_class
{
3152 my ($line, $from, $to) = @_;
3157 if ($from && $to && ref($from->{'href'}) eq "ARRAY") {
3158 $num_sign = scalar @
{$from->{'href'}};
3161 my @diff_line_classifier = (
3162 { regexp
=> qr/^\@\@{$num_sign} /, class => "chunk_header"},
3163 { regexp
=> qr/^\\/, class => "incomplete" },
3164 { regexp
=> qr/^ {$num_sign}/, class => "ctx" },
3165 # classifier for context must come before classifier add/rem,
3166 # or we would have to use more complicated regexp, for example
3167 # qr/(?= {0,$m}\+)[+ ]{$num_sign}/, where $m = $num_sign - 1;
3168 { regexp
=> qr/^[+ ]{$num_sign}/, class => "add" },
3169 { regexp
=> qr/^[- ]{$num_sign}/, class => "rem" },
3171 for my $clsfy (@diff_line_classifier) {
3172 return $clsfy->{'class'}
3173 if ($line =~ $clsfy->{'regexp'});
3180 # assumes that $from and $to are defined and correctly filled,
3181 # and that $line holds a line of chunk header for unified diff
3182 sub format_unidiff_chunk_header
{
3183 my ($line, $from, $to) = @_;
3185 my ($from_text, $from_start, $from_lines, $to_text, $to_start, $to_lines, $section) =
3186 $line =~ m/^\@{2} (-(\d+)(?:,(\d+))?) (\+(\d+)(?:,(\d+))?) \@{2}(.*)$/;
3188 $from_lines = 0 unless defined $from_lines;
3189 $to_lines = 0 unless defined $to_lines;
3191 if ($from->{'href'}) {
3192 $from_text = $cgi->a({-href
=>"$from->{'href'}#l$from_start",
3193 -class=>"list"}, $from_text);
3195 if ($to->{'href'}) {
3196 $to_text = $cgi->a({-href
=>"$to->{'href'}#l$to_start",
3197 -class=>"list"}, $to_text);
3199 $line = "<span class=\"chunk_info\">@@ $from_text $to_text @@</span>" .
3200 "<span class=\"section\">" . esc_html
($section, -nbsp
=>1) . "</span>";
3204 # assumes that $from and $to are defined and correctly filled,
3205 # and that $line holds a line of chunk header for combined diff
3206 sub format_cc_diff_chunk_header
{
3207 my ($line, $from, $to) = @_;
3209 my ($prefix, $ranges, $section) = $line =~ m/^(\@+) (.*?) \@+(.*)$/;
3210 my (@from_text, @from_start, @from_nlines, $to_text, $to_start, $to_nlines);
3212 @from_text = split(' ', $ranges);
3213 for (my $i = 0; $i < @from_text; ++$i) {
3214 ($from_start[$i], $from_nlines[$i]) =
3215 (split(',', substr($from_text[$i], 1)), 0);
3218 $to_text = pop @from_text;
3219 $to_start = pop @from_start;
3220 $to_nlines = pop @from_nlines;
3222 $line = "<span class=\"chunk_info\">$prefix ";
3223 for (my $i = 0; $i < @from_text; ++$i) {
3224 if ($from->{'href'}[$i]) {
3225 $line .= $cgi->a({-href
=>"$from->{'href'}[$i]#l$from_start[$i]",
3226 -class=>"list"}, $from_text[$i]);
3228 $line .= $from_text[$i];
3232 if ($to->{'href'}) {
3233 $line .= $cgi->a({-href
=>"$to->{'href'}#l$to_start",
3234 -class=>"list"}, $to_text);
3238 $line .= " $prefix</span>" .
3239 "<span class=\"section\">" . esc_html
($section, -nbsp
=>1) . "</span>";
3243 # process patch (diff) line (not to be used for diff headers),
3244 # returning HTML-formatted (but not wrapped) line.
3245 # If the line is passed as a reference, it is treated as HTML and not
3247 sub format_diff_line
{
3248 my ($line, $diff_class, $from, $to) = @_;
3254 $line = untabify
($line);
3256 if ($from && $to && $line =~ m/^\@{2} /) {
3257 $line = format_unidiff_chunk_header
($line, $from, $to);
3258 } elsif ($from && $to && $line =~ m/^\@{3}/) {
3259 $line = format_cc_diff_chunk_header
($line, $from, $to);
3261 $line = esc_html
($line, -nbsp
=>1);
3265 my $diff_classes = "diff diff_body";
3266 $diff_classes .= " $diff_class" if ($diff_class);
3267 $line = "<div class=\"$diff_classes\">$line</div>\n";
3272 # Generates undef or something like "_snapshot_" or "snapshot (_tbz2_ _zip_)",
3273 # linked. Pass the hash of the tree/commit to snapshot.
3274 sub format_snapshot_links
{
3276 my $num_fmts = @snapshot_fmts;
3277 if ($num_fmts > 1) {
3278 # A parenthesized list of links bearing format names.
3279 # e.g. "snapshot (_tar.gz_ _zip_)"
3280 return "snapshot (" . join(' ', map
3287 }, $known_snapshot_formats{$_}{'display'})
3288 , @snapshot_fmts) . ")";
3289 } elsif ($num_fmts == 1) {
3290 # A single "snapshot" link whose tooltip bears the format name.
3292 my ($fmt) = @snapshot_fmts;
3298 snapshot_format
=>$fmt
3300 -title
=> "in format: $known_snapshot_formats{$fmt}{'display'}"
3302 } else { # $num_fmts == 0
3307 ## ......................................................................
3308 ## functions returning values to be passed, perhaps after some
3309 ## transformation, to other functions; e.g. returning arguments to href()
3311 # returns hash to be passed to href to generate gitweb URL
3312 # in -title key it returns description of link
3314 my $format = shift || 'Atom';
3315 my %res = (action
=> lc($format));
3316 my $matched_ref = 0;
3318 # feed links are possible only for project views
3319 return unless (defined $project);
3320 # some views should link to OPML, or to generic project feed,
3321 # or don't have specific feed yet (so they should use generic)
3322 return if (!$action || $action =~ /^(?:tags|heads|forks|tag|search)$/x);
3325 # branches refs uses 'refs/' + $get_branch_refs()[x] + '/' prefix
3326 # (fullname) to differentiate from tag links; this also makes
3327 # possible to detect branch links
3328 for my $ref (get_branch_refs
()) {
3329 if ((defined $hash_base && $hash_base =~ m!^refs/\Q$ref\E/(.*)$!) ||
3330 (defined $hash && $hash =~ m!^refs/\Q$ref\E/(.*)$!)) {
3332 $matched_ref = $ref;
3336 # find log type for feed description (title)
3338 if (defined $file_name) {
3339 $type = "history of $file_name";
3340 $type .= "/" if ($action eq 'tree');
3341 $type .= " on '$branch'" if (defined $branch);
3343 $type = "log of $branch" if (defined $branch);
3346 $res{-title
} = $type;
3347 $res{'hash'} = (defined $branch ?
"refs/$matched_ref/$branch" : undef);
3348 $res{'file_name'} = $file_name;
3353 ## ----------------------------------------------------------------------
3354 ## git utility subroutines, invoking git commands
3356 # returns path to the core git executable and the --git-dir parameter as list
3358 $number_of_git_cmds++;
3359 return $GIT, '--git-dir='.$git_dir;
3362 # opens a "-|" cmd pipe handle with 2>/dev/null and returns it
3365 # In order to be compatible with FCGI mode we must use POSIX
3366 # and access the STDERR_FILENO file descriptor directly
3368 use POSIX
qw(STDERR_FILENO dup dup2);
3370 open(my $null, '>', File
::Spec
->devnull) or die "couldn't open devnull: $!";
3371 (my $saveerr = dup
(STDERR_FILENO
)) or die "couldn't dup STDERR: $!";
3372 my $dup2ok = dup2
(fileno($null), STDERR_FILENO
);
3373 close($null) or !$dup2ok or die "couldn't close NULL: $!";
3374 $dup2ok or POSIX
::close($saveerr), die "couldn't dup NULL to STDERR: $!";
3375 my $result = open(my $fd, "-|", @_);
3376 $dup2ok = dup2
($saveerr, STDERR_FILENO
);
3377 POSIX
::close($saveerr) or !$dup2ok or die "couldn't close SAVEERR: $!";
3378 $dup2ok or die "couldn't dup SAVERR to STDERR: $!";
3380 return $result ?
$fd : undef;
3383 # opens a "-|" git_cmd pipe handle with 2>/dev/null and returns it
3385 return cmd_pipe git_cmd
(), @_;
3388 # quote the given arguments for passing them to the shell
3389 # quote_command("command", "arg 1", "arg with ' and ! characters")
3390 # => "'command' 'arg 1' 'arg with '\'' and '\!' characters'"
3391 # Try to avoid using this function wherever possible.
3394 map { my $a = $_; $a =~ s/(['!])/'\\$1'/g; "'$a'" } @_ );
3397 # get HEAD ref of given project as hash
3398 sub git_get_head_hash
{
3399 return git_get_full_hash
(shift, 'HEAD');
3402 sub git_get_full_hash
{
3403 return git_get_hash
(@_);
3406 sub git_get_short_hash
{
3407 return git_get_hash
(@_, '--short=7');
3411 my ($project, $hash, @options) = @_;
3412 my $o_git_dir = $git_dir;
3414 $git_dir = "$projectroot/$project";
3415 if (defined(my $fd = git_cmd_pipe
'rev-parse',
3416 '--verify', '-q', @options, $hash)) {
3418 chomp $retval if defined $retval;
3421 if (defined $o_git_dir) {
3422 $git_dir = $o_git_dir;
3427 # get type of given object
3431 defined(my $fd = git_cmd_pipe
"cat-file", '-t', $hash) or return;
3433 close $fd or return;
3438 # repository configuration
3439 our $config_file = '';
3442 # store multiple values for single key as anonymous array reference
3443 # single values stored directly in the hash, not as [ <value> ]
3444 sub hash_set_multi
{
3445 my ($hash, $key, $value) = @_;
3447 if (!exists $hash->{$key}) {
3448 $hash->{$key} = $value;
3449 } elsif (!ref $hash->{$key}) {
3450 $hash->{$key} = [ $hash->{$key}, $value ];
3452 push @
{$hash->{$key}}, $value;
3456 # return hash of git project configuration
3457 # optionally limited to some section, e.g. 'gitweb'
3458 sub git_parse_project_config
{
3459 my $section_regexp = shift;
3464 defined(my $fh = git_cmd_pipe
"config", '-z', '-l')
3467 while (my $keyval = to_utf8
(scalar <$fh>)) {
3469 my ($key, $value) = split(/\n/, $keyval, 2);
3471 hash_set_multi
(\
%config, $key, $value)
3472 if (!defined $section_regexp || $key =~ /^(?:$section_regexp)\./o);
3479 # convert config value to boolean: 'true' or 'false'
3480 # no value, number > 0, 'true' and 'yes' values are true
3481 # rest of values are treated as false (never as error)
3482 sub config_to_bool
{
3485 return 1 if !defined $val; # section.key
3487 # strip leading and trailing whitespace
3491 return (($val =~ /^\d+$/ && $val) || # section.key = 1
3492 ($val =~ /^(?:true|yes)$/i)); # section.key = true
3495 # convert config value to simple decimal number
3496 # an optional value suffix of 'k', 'm', or 'g' will cause the value
3497 # to be multiplied by 1024, 1048576, or 1073741824
3501 # strip leading and trailing whitespace
3505 if (my ($num, $unit) = ($val =~ /^([0-9]*)([kmg])$/i)) {
3507 # unknown unit is treated as 1
3508 return $num * ($unit eq 'g' ?
1073741824 :
3509 $unit eq 'm' ?
1048576 :
3510 $unit eq 'k' ?
1024 : 1);
3515 # convert config value to array reference, if needed
3516 sub config_to_multi
{
3519 return ref($val) ?
$val : (defined($val) ?
[ $val ] : []);
3522 sub git_get_project_config
{
3523 my ($key, $type) = @_;
3525 return unless defined $git_dir;
3528 return unless ($key);
3529 # only subsection, if exists, is case sensitive,
3530 # and not lowercased by 'git config -z -l'
3531 if (my ($hi, $mi, $lo) = ($key =~ /^([^.]*)\.(.*)\.([^.]*)$/)) {
3533 $key = join(".", lc($hi), $mi, lc($lo));
3534 return if ($lo =~ /\W/ || $hi =~ /\W/);
3538 return if ($key =~ /\W/);
3540 $key =~ s/^gitweb\.//;
3543 if (defined $type) {
3546 unless ($type eq 'bool' || $type eq 'int');
3550 if (!defined $config_file ||
3551 $config_file ne "$git_dir/config") {
3552 %config = git_parse_project_config
('gitweb');
3553 $config_file = "$git_dir/config";
3556 # check if config variable (key) exists
3557 return unless exists $config{"gitweb.$key"};
3560 if (!defined $type) {
3561 return $config{"gitweb.$key"};
3562 } elsif ($type eq 'bool') {
3563 # backward compatibility: 'git config --bool' returns true/false
3564 return config_to_bool
($config{"gitweb.$key"}) ?
'true' : 'false';
3565 } elsif ($type eq 'int') {
3566 return config_to_int
($config{"gitweb.$key"});
3568 return $config{"gitweb.$key"};
3571 # get hash of given path at given ref
3572 sub git_get_hash_by_path
{
3574 my $path = shift || return undef;
3579 defined(my $fd = git_cmd_pipe
"ls-tree", $base, "--", $path)
3580 or die_error
(500, "Open git-ls-tree failed");
3581 my $line = to_utf8
(scalar <$fd>);
3582 close $fd or return undef;
3584 if (!defined $line) {
3585 # there is no tree or hash given by $path at $base
3589 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'
3590 $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t/;
3591 if (defined $type && $type ne $2) {
3592 # type doesn't match
3598 # get path of entry with given hash at given tree-ish (ref)
3599 # used to get 'from' filename for combined diff (merge commit) for renames
3600 sub git_get_path_by_hash
{
3601 my $base = shift || return;
3602 my $hash = shift || return;
3606 defined(my $fd = git_cmd_pipe
"ls-tree", '-r', '-t', '-z', $base)
3608 while (my $line = to_utf8
(scalar <$fd>)) {
3611 #'040000 tree 595596a6a9117ddba9fe379b6b012b558bac8423 gitweb'
3612 #'100644 blob e02e90f0429be0d2a69b76571101f20b8f75530f gitweb/README'
3613 if ($line =~ m/(?:[0-9]+) (?:.+) $hash\t(.+)$/) {
3622 ## ......................................................................
3623 ## git utility functions, directly accessing git repository
3625 # get the value of config variable either from file named as the variable
3626 # itself in the repository ($GIT_DIR/$name file), or from gitweb.$name
3627 # configuration variable in the repository config file.
3628 sub git_get_file_or_project_config
{
3629 my ($path, $name) = @_;
3631 $git_dir = "$projectroot/$path";
3632 open my $fd, '<', "$git_dir/$name"
3633 or return git_get_project_config
($name);
3634 my $conf = to_utf8
(scalar <$fd>);
3636 if (defined $conf) {
3642 sub git_get_project_description
{
3644 return git_get_file_or_project_config
($path, 'description');
3647 sub git_get_project_category
{
3649 return git_get_file_or_project_config
($path, 'category');
3653 # supported formats:
3654 # * $GIT_DIR/ctags/<tagname> file (in 'ctags' subdirectory)
3655 # - if its contents is a number, use it as tag weight,
3656 # - otherwise add a tag with weight 1
3657 # * $GIT_DIR/ctags file, each line is a tag (with weight 1)
3658 # the same value multiple times increases tag weight
3659 # * `gitweb.ctag' multi-valued repo config variable
3660 sub git_get_project_ctags
{
3661 my $project = shift;
3664 $git_dir = "$projectroot/$project";
3665 if (opendir my $dh, "$git_dir/ctags") {
3666 my @files = grep { -f
$_ } map { "$git_dir/ctags/$_" } readdir($dh);
3667 foreach my $tagfile (@files) {
3668 open my $ct, '<', $tagfile
3674 (my $ctag = $tagfile) =~ s
#.*/##;
3675 $ctag = to_utf8
($ctag);
3676 if ($val =~ /^\d+$/) {
3677 $ctags->{$ctag} = $val;
3679 $ctags->{$ctag} = 1;
3684 } elsif (open my $fh, '<', "$git_dir/ctags") {
3685 while (my $line = to_utf8
(scalar <$fh>)) {
3687 $ctags->{$line}++ if $line;
3692 my $taglist = config_to_multi
(git_get_project_config
('ctag'));
3693 foreach my $tag (@
$taglist) {
3701 # return hash, where keys are content tags ('ctags'),
3702 # and values are sum of weights of given tag in every project
3703 sub git_gather_all_ctags
{
3704 my $projects = shift;
3707 foreach my $p (@
$projects) {
3708 foreach my $ct (keys %{$p->{'ctags'}}) {
3709 $ctags->{$ct} += $p->{'ctags'}->{$ct};
3716 sub git_populate_project_tagcloud
{
3717 my ($ctags, $action) = @_;
3719 # First, merge different-cased tags; tags vote on casing
3721 foreach (keys %$ctags) {
3722 $ctags_lc{lc $_}->{count
} += $ctags->{$_};
3723 if (not $ctags_lc{lc $_}->{topcount
}
3724 or $ctags_lc{lc $_}->{topcount
} < $ctags->{$_}) {
3725 $ctags_lc{lc $_}->{topcount
} = $ctags->{$_};
3726 $ctags_lc{lc $_}->{topname
} = $_;
3731 my $matched = $input_params{'ctag_filter'};
3732 if (eval { require HTML
::TagCloud
; 1; }) {
3733 $cloud = HTML
::TagCloud
->new;
3734 foreach my $ctag (sort keys %ctags_lc) {
3735 # Pad the title with spaces so that the cloud looks
3737 my $title = esc_html
($ctags_lc{$ctag}->{topname
});
3738 $title =~ s/ / /g;
3739 $title =~ s/^/ /g;
3740 $title =~ s/$/ /g;
3741 if (defined $matched && $matched eq $ctag) {
3742 $title = qq(<span
class="match">$title</span
>);
3744 $cloud->add($title, href
(-replay
=>1, action
=>$action, ctag_filter
=>$ctag),
3745 $ctags_lc{$ctag}->{count
});
3749 foreach my $ctag (keys %ctags_lc) {
3750 my $title = esc_html
($ctags_lc{$ctag}->{topname
}, -nbsp
=>1);
3751 if (defined $matched && $matched eq $ctag) {
3752 $title = qq(<span
class="match">$title</span
>);
3754 $cloud->{$ctag}{count
} = $ctags_lc{$ctag}->{count
};
3755 $cloud->{$ctag}{ctag
} =
3756 $cgi->a({-href
=>href
(-replay
=>1, action
=>$action, ctag_filter
=>$ctag)}, $title);
3762 sub git_show_project_tagcloud
{
3763 my ($cloud, $count) = @_;
3764 if (ref $cloud eq 'HTML::TagCloud') {
3765 return $cloud->html_and_css($count);
3767 my @tags = sort { $cloud->{$a}->{'count'} <=> $cloud->{$b}->{'count'} } keys %$cloud;
3769 '<div id="htmltagcloud"'.($project ?
'' : ' align="center"').'>' .
3771 $cloud->{$_}->{'ctag'}
3772 } splice(@tags, 0, $count)) .
3777 sub git_get_project_url_list
{
3780 $git_dir = "$projectroot/$path";
3781 open my $fd, '<', "$git_dir/cloneurl"
3782 or return wantarray ?
3783 @
{ config_to_multi
(git_get_project_config
('url')) } :
3784 config_to_multi
(git_get_project_config
('url'));
3785 my @git_project_url_list = map { chomp; to_utf8
($_) } <$fd>;
3788 return wantarray ?
@git_project_url_list : \
@git_project_url_list;
3791 sub git_get_projects_list
{
3792 my $filter = shift || '';
3793 my $paranoid = shift;
3796 if (-d
$projects_list) {
3797 # search in directory
3798 my $dir = $projects_list;
3799 # remove the trailing "/"
3801 my $pfxlen = length("$dir");
3802 my $pfxdepth = ($dir =~ tr!/!!);
3803 # when filtering, search only given subdirectory
3804 if ($filter && !$paranoid) {
3810 follow_fast
=> 1, # follow symbolic links
3811 follow_skip
=> 2, # ignore duplicates
3812 dangling_symlinks
=> 0, # ignore dangling symlinks, silently
3815 our $project_maxdepth;
3817 # skip project-list toplevel, if we get it.
3818 return if (m!^[/.]$!);
3819 # only directories can be git repositories
3820 return unless (-d
$_);
3821 # don't traverse too deep (Find is super slow on os x)
3822 # $project_maxdepth excludes depth of $projectroot
3823 if (($File::Find
::name
=~ tr!/!!) - $pfxdepth > $project_maxdepth) {
3824 $File::Find
::prune
= 1;
3828 my $path = substr($File::Find
::name
, $pfxlen + 1);
3829 # paranoidly only filter here
3830 if ($paranoid && $filter && $path !~ m!^\Q$filter\E/!) {
3833 # we check related file in $projectroot
3834 if (check_export_ok
("$projectroot/$path")) {
3835 push @list, { path
=> $path };
3836 $File::Find
::prune
= 1;
3841 } elsif (-f
$projects_list) {
3842 # read from file(url-encoded):
3843 # 'git%2Fgit.git Linus+Torvalds'
3844 # 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin'
3845 # 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman'
3846 open my $fd, '<', $projects_list or return;
3848 while (my $line = <$fd>) {
3850 my ($path, $owner) = split ' ', $line;
3851 $path = unescape
($path);
3852 $owner = unescape
($owner);
3853 if (!defined $path) {
3856 # if $filter is rpovided, check if $path begins with $filter
3857 if ($filter && $path !~ m!^\Q$filter\E/!) {
3860 if (check_export_ok
("$projectroot/$path")) {
3865 $pr->{'owner'} = to_utf8
($owner);
3875 # written with help of Tree::Trie module (Perl Artistic License, GPL compatibile)
3876 # as side effects it sets 'forks' field to list of forks for forked projects
3877 sub filter_forks_from_projects_list
{
3878 my $projects = shift;
3880 my %trie; # prefix tree of directories (path components)
3881 # generate trie out of those directories that might contain forks
3882 foreach my $pr (@
$projects) {
3883 my $path = $pr->{'path'};
3884 $path =~ s/\.git$//; # forks of 'repo.git' are in 'repo/' directory
3885 next if ($path =~ m!/$!); # skip non-bare repositories, e.g. 'repo/.git'
3886 next unless ($path); # skip '.git' repository: tests, git-instaweb
3887 next unless (-d
"$projectroot/$path"); # containing directory exists
3888 $pr->{'forks'} = []; # there can be 0 or more forks of project
3891 my @dirs = split('/', $path);
3892 # walk the trie, until either runs out of components or out of trie
3894 while (scalar @dirs &&
3895 exists($ref->{$dirs[0]})) {
3896 $ref = $ref->{shift @dirs};
3898 # create rest of trie structure from rest of components
3899 foreach my $dir (@dirs) {
3900 $ref = $ref->{$dir} = {};
3902 # create end marker, store $pr as a data
3903 $ref->{''} = $pr if (!exists $ref->{''});
3906 # filter out forks, by finding shortest prefix match for paths
3909 foreach my $pr (@
$projects) {
3913 foreach my $dir (split('/', $pr->{'path'})) {
3914 if (exists $ref->{''}) {
3915 # found [shortest] prefix, is a fork - skip it
3916 push @
{$ref->{''}{'forks'}}, $pr;
3919 if (!exists $ref->{$dir}) {
3920 # not in trie, cannot have prefix, not a fork
3921 push @filtered, $pr;
3924 # If the dir is there, we just walk one step down the trie.
3925 $ref = $ref->{$dir};
3927 # we ran out of trie
3928 # (shouldn't happen: it's either no match, or end marker)
3929 push @filtered, $pr;
3935 # note: fill_project_list_info must be run first,
3936 # for 'descr_long' and 'ctags' to be filled
3937 sub search_projects_list
{
3938 my ($projlist, %opts) = @_;
3939 my $tagfilter = $opts{'tagfilter'};
3940 my $search_re = $opts{'search_regexp'};
3943 unless ($tagfilter || $search_re);
3945 # searching projects require filling to be run before it;
3946 fill_project_list_info
($projlist,
3947 $tagfilter ?
'ctags' : (),
3948 $search_re ?
('path', 'descr') : ());
3951 foreach my $pr (@
$projlist) {
3954 next unless ref($pr->{'ctags'}) eq 'HASH';
3956 grep { lc($_) eq lc($tagfilter) } keys %{$pr->{'ctags'}};
3960 my $path = $pr->{'path'};
3961 $path =~ s/\.git$//; # should not be included in search
3963 $path =~ /$search_re/ ||
3964 $pr->{'descr_long'} =~ /$search_re/;
3967 push @projects, $pr;
3973 our $gitweb_project_owner = undef;
3974 sub git_get_project_list_from_file
{
3976 return if (defined $gitweb_project_owner);
3978 $gitweb_project_owner = {};
3979 # read from file (url-encoded):
3980 # 'git%2Fgit.git Linus+Torvalds'
3981 # 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin'
3982 # 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman'
3983 if (-f
$projects_list) {
3984 open(my $fd, '<', $projects_list);
3985 while (my $line = <$fd>) {
3987 my ($pr, $ow) = split ' ', $line;
3988 $pr = unescape
($pr);
3989 $ow = unescape
($ow);
3990 $gitweb_project_owner->{$pr} = to_utf8
($ow);
3996 sub git_get_project_owner
{
4000 return undef unless $proj;
4001 $git_dir = "$projectroot/$proj";
4003 if (defined $project && $proj eq $project) {
4004 $owner = git_get_project_config
('owner');
4006 if (!defined $owner && !defined $gitweb_project_owner) {
4007 git_get_project_list_from_file
();
4009 if (!defined $owner && exists $gitweb_project_owner->{$proj}) {
4010 $owner = $gitweb_project_owner->{$proj};
4012 if (!defined $owner && (!defined $project || $proj ne $project)) {
4013 $owner = git_get_project_config
('owner');
4015 if (!defined $owner) {
4016 $owner = get_file_owner
("$git_dir");
4022 sub parse_activity_date
{
4025 if ($dstr =~ /^\s*([-+]?\d+)(?:\s+([-+]\d{4}))?\s*$/) {
4029 if ($dstr =~ /^\s*(\d{4})-(\d{2})-(\d{2})[Tt _](\d{1,2}):(\d{2}):(\d{2})(?:[ _]?([Zz]|(?:[-+]\d{1,2}:?\d{2})))?\s*$/) {
4030 my ($Y,$m,$d,$H,$M,$S,$z) = ($1,$2,$3,$4,$5,$6,$7||'');
4031 my $seconds = timegm
(0+$S, 0+$M, 0+$H, 0+$d, $m-1, $Y-1900);
4032 defined($z) && $z ne '' or $z = 'Z';
4034 substr($z,1,0) = '0' if length($z) == 4;
4036 if (uc($z) ne 'Z') {
4037 $off = 60 * (60 * (0+substr($z,1,2)) + (0+substr($z,3,2)));
4038 $off = -$off if substr($z,0,1) eq '-';
4040 return $seconds - $off;
4045 # If $quick is true only look at $lastactivity_file
4046 sub git_get_last_activity
{
4047 my ($path, $quick) = @_;
4050 $git_dir = "$projectroot/$path";
4051 if ($lastactivity_file && open($fd, "<", "$git_dir/$lastactivity_file")) {
4052 my $activity = <$fd>;
4054 return (undef) unless defined $activity;
4056 return (undef) if $activity eq '';
4057 if (my $timestamp = parse_activity_date
($activity)) {
4058 return ($timestamp);
4061 return (undef) if $quick;
4062 defined($fd = git_cmd_pipe
'for-each-ref',
4063 '--format=%(committer)',
4064 '--sort=-committerdate',
4066 map { "refs/$_" } get_branch_refs
()) or return;
4067 my $most_recent = <$fd>;
4068 close $fd or return (undef);
4069 if (defined $most_recent &&
4070 $most_recent =~ / (\d+) [-+][01]\d\d\d$/) {
4072 return ($timestamp);
4077 # Implementation note: when a single remote is wanted, we cannot use 'git
4078 # remote show -n' because that command always work (assuming it's a remote URL
4079 # if it's not defined), and we cannot use 'git remote show' because that would
4080 # try to make a network roundtrip. So the only way to find if that particular
4081 # remote is defined is to walk the list provided by 'git remote -v' and stop if
4082 # and when we find what we want.
4083 sub git_get_remotes_list
{
4087 my $fd = git_cmd_pipe
'remote', '-v';
4089 while (my $remote = to_utf8
(scalar <$fd>)) {
4091 $remote =~ s!\t(.*?)\s+\((\w+)\)$!!;
4092 next if $wanted and not $remote eq $wanted;
4093 my ($url, $key) = ($1, $2);
4095 $remotes{$remote} ||= { 'heads' => [] };
4096 $remotes{$remote}{$key} = $url;
4098 close $fd or return;
4099 return wantarray ?
%remotes : \
%remotes;
4102 # Takes a hash of remotes as first parameter and fills it by adding the
4103 # available remote heads for each of the indicated remotes.
4104 sub fill_remote_heads
{
4105 my $remotes = shift;
4106 my @heads = map { "remotes/$_" } keys %$remotes;
4107 my @remoteheads = git_get_heads_list
(undef, @heads);
4108 foreach my $remote (keys %$remotes) {
4109 $remotes->{$remote}{'heads'} = [ grep {
4110 $_->{'name'} =~ s!^$remote/!!
4115 sub git_get_references
{
4116 my $type = shift || "";
4118 # 5dc01c595e6c6ec9ccda4f6f69c131c0dd945f8c refs/tags/v2.6.11
4119 # c39ae07f393806ccf406ef966e9a15afc43cc36a refs/tags/v2.6.11^{}
4120 defined(my $fd = git_cmd_pipe
"show-ref", "--dereference",
4121 ($type ?
("--", "refs/$type") : ())) # use -- <pattern> if $type
4124 while (my $line = to_utf8
(scalar <$fd>)) {
4126 if ($line =~ m!^([0-9a-fA-F]{40})\srefs/($type.*)$!) {
4127 if (defined $refs{$1}) {
4128 push @
{$refs{$1}}, $2;
4134 close $fd or return;
4138 sub git_get_rev_name_tags
{
4139 my $hash = shift || return undef;
4141 defined(my $fd = git_cmd_pipe
"name-rev", "--tags", $hash)
4143 my $name_rev = to_utf8
(scalar <$fd>);
4146 if ($name_rev =~ m
|^$hash tags
/(.*)$|) {
4149 # catches also '$hash undefined' output
4154 ## ----------------------------------------------------------------------
4155 ## parse to hash functions
4159 my $tz = shift || "-0000";
4162 my @months = ("Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec");
4163 my @days = ("Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat");
4164 my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($epoch);
4165 $date{'hour'} = $hour;
4166 $date{'minute'} = $min;
4167 $date{'mday'} = $mday;
4168 $date{'day'} = $days[$wday];
4169 $date{'month'} = $months[$mon];
4170 $date{'rfc2822'} = sprintf "%s, %d %s %4d %02d:%02d:%02d +0000",
4171 $days[$wday], $mday, $months[$mon], 1900+$year, $hour ,$min, $sec;
4172 $date{'mday-time'} = sprintf "%d %s %02d:%02d",
4173 $mday, $months[$mon], $hour ,$min;
4174 $date{'iso-8601'} = sprintf "%04d-%02d-%02dT%02d:%02d:%02dZ",
4175 1900+$year, 1+$mon, $mday, $hour ,$min, $sec;
4177 my ($tz_sign, $tz_hour, $tz_min) =
4178 ($tz =~ m/^([-+])(\d\d)(\d\d)$/);
4179 $tz_sign = ($tz_sign eq '-' ?
-1 : +1);
4180 my $local = $epoch + $tz_sign*((($tz_hour*60) + $tz_min)*60);
4181 ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($local);
4182 $date{'hour_local'} = $hour;
4183 $date{'minute_local'} = $min;
4184 $date{'mday_local'} = $mday;
4185 $date{'tz_local'} = $tz;
4186 $date{'iso-tz'} = sprintf("%04d-%02d-%02d %02d:%02d:%02d %s",
4187 1900+$year, $mon+1, $mday,
4188 $hour, $min, $sec, $tz);
4192 sub parse_file_date
{
4194 my $mtime = (stat("$projectroot/$project/$file"))[9];
4195 return () unless defined $mtime;
4196 my $tzoffset = timegm
((localtime($mtime))[0..5]) - $mtime;
4198 if ($tzoffset <= 0) {
4202 $tzoffset = int($tzoffset/60);
4203 $tzstring .= sprintf("%02d%02d", int($tzoffset/60), $tzoffset%60);
4204 return parse_date
($mtime, $tzstring);
4212 defined(my $fd = git_cmd_pipe
"cat-file", "tag", $tag_id) or return;
4213 $tag{'id'} = $tag_id;
4214 while (my $line = to_utf8
(scalar <$fd>)) {
4216 if ($line =~ m/^object ([0-9a-fA-F]{40})$/) {
4217 $tag{'object'} = $1;
4218 } elsif ($line =~ m/^type (.+)$/) {
4220 } elsif ($line =~ m/^tag (.+)$/) {
4222 } elsif ($line =~ m/^tagger (.*) ([0-9]+) (.*)$/) {
4223 $tag{'author'} = $1;
4224 $tag{'author_epoch'} = $2;
4225 $tag{'author_tz'} = $3;
4226 if ($tag{'author'} =~ m/^([^<]+) <([^>]*)>/) {
4227 $tag{'author_name'} = $1;
4228 $tag{'author_email'} = $2;
4230 $tag{'author_name'} = $tag{'author'};
4232 } elsif ($line =~ m/--BEGIN/) {
4233 push @comment, $line;
4235 } elsif ($line eq "") {
4239 push @comment, map(to_utf8
($_), <$fd>);
4240 $tag{'comment'} = \
@comment;
4241 close $fd or return;
4242 if (!defined $tag{'name'}) {
4248 sub parse_commit_text
{
4249 my ($commit_text, $withparents) = @_;
4250 my @commit_lines = split '\n', $commit_text;
4253 pop @commit_lines; # Remove '\0'
4255 if (! @commit_lines) {
4259 my $header = shift @commit_lines;
4260 if ($header !~ m/^[0-9a-fA-F]{40}/) {
4263 ($co{'id'}, my @parents) = split ' ', $header;
4264 while (my $line = shift @commit_lines) {
4265 last if $line eq "\n";
4266 if ($line =~ m/^tree ([0-9a-fA-F]{40})$/) {
4268 } elsif ((!defined $withparents) && ($line =~ m/^parent ([0-9a-fA-F]{40})$/)) {
4270 } elsif ($line =~ m/^author (.*) ([0-9]+) (.*)$/) {
4271 $co{'author'} = to_utf8
($1);
4272 $co{'author_epoch'} = $2;
4273 $co{'author_tz'} = $3;
4274 if ($co{'author'} =~ m/^([^<]+) <([^>]*)>/) {
4275 $co{'author_name'} = $1;
4276 $co{'author_email'} = $2;
4278 $co{'author_name'} = $co{'author'};
4280 } elsif ($line =~ m/^committer (.*) ([0-9]+) (.*)$/) {
4281 $co{'committer'} = to_utf8
($1);
4282 $co{'committer_epoch'} = $2;
4283 $co{'committer_tz'} = $3;
4284 if ($co{'committer'} =~ m/^([^<]+) <([^>]*)>/) {
4285 $co{'committer_name'} = $1;
4286 $co{'committer_email'} = $2;
4288 $co{'committer_name'} = $co{'committer'};
4292 if (!defined $co{'tree'}) {
4295 $co{'parents'} = \
@parents;
4296 $co{'parent'} = $parents[0];
4298 @commit_lines = map to_utf8
($_), @commit_lines;
4299 foreach my $title (@commit_lines) {
4302 $co{'title'} = chop_str
($title, 80, 5);
4303 # remove leading stuff of merges to make the interesting part visible
4304 if (length($title) > 50) {
4305 $title =~ s/^Automatic //;
4306 $title =~ s/^merge (of|with) /Merge ... /i;
4307 if (length($title) > 50) {
4308 $title =~ s/(http|rsync):\/\///;
4310 if (length($title) > 50) {
4311 $title =~ s/(master|www|rsync)\.//;
4313 if (length($title) > 50) {
4314 $title =~ s/kernel.org:?//;
4316 if (length($title) > 50) {
4317 $title =~ s/\/pub\/scm//;
4320 $co{'title_short'} = chop_str
($title, 50, 5);
4324 if (! defined $co{'title'} || $co{'title'} eq "") {
4325 $co{'title'} = $co{'title_short'} = '(no commit message)';
4327 # remove added spaces
4328 foreach my $line (@commit_lines) {
4331 $co{'comment'} = \
@commit_lines;
4333 my $age_epoch = $co{'committer_epoch'};
4334 $co{'age_epoch'} = $age_epoch;
4335 my $time_now = time;
4336 $co{'age_string'} = age_string
($age_epoch, $time_now);
4337 $co{'age_string_date'} = age_string_date
($age_epoch, $time_now);
4338 $co{'age_string_age'} = age_string_age
($age_epoch, $time_now);
4343 my ($commit_id) = @_;
4348 defined(my $fd = git_cmd_pipe
"rev-list",
4354 or die_error
(500, "Open git-rev-list failed");
4355 %co = parse_commit_text
(<$fd>, 1);
4362 my ($commit_id, $maxcount, $skip, $filename, @args) = @_;
4370 defined(my $fd = git_cmd_pipe
"rev-list",
4373 ("--max-count=" . $maxcount),
4374 ("--skip=" . $skip),
4378 ($filename ?
($filename) : ()))
4379 or die_error
(500, "Open git-rev-list failed");
4380 while (my $line = <$fd>) {
4381 my %co = parse_commit_text
($line);
4386 return wantarray ?
@cos : \
@cos;
4389 # parse line of git-diff-tree "raw" output
4390 sub parse_difftree_raw_line
{
4394 # ':100644 100644 03b218260e99b78c6df0ed378e59ed9205ccc96d 3b93d5e7cc7f7dd4ebed13a5cc1a4ad976fc94d8 M ls-files.c'
4395 # ':100644 100644 7f9281985086971d3877aca27704f2aaf9c448ce bc190ebc71bbd923f2b728e505408f5e54bd073a M rev-tree.c'
4396 if ($line =~ m/^:([0-7]{6}) ([0-7]{6}) ([0-9a-fA-F]{40}) ([0-9a-fA-F]{40}) (.)([0-9]{0,3})\t(.*)$/) {
4397 $res{'from_mode'} = $1;
4398 $res{'to_mode'} = $2;
4399 $res{'from_id'} = $3;
4401 $res{'status'} = $5;
4402 $res{'similarity'} = $6;
4403 if ($res{'status'} eq 'R' || $res{'status'} eq 'C') { # renamed or copied
4404 ($res{'from_file'}, $res{'to_file'}) = map { unquote
($_) } split("\t", $7);
4406 $res{'from_file'} = $res{'to_file'} = $res{'file'} = unquote
($7);
4409 # '::100755 100755 100755 60e79ca1b01bc8b057abe17ddab484699a7f5fdb 94067cc5f73388f33722d52ae02f44692bc07490 94067cc5f73388f33722d52ae02f44692bc07490 MR git-gui/git-gui.sh'
4410 # combined diff (for merge commit)
4411 elsif ($line =~ s/^(::+)((?:[0-7]{6} )+)((?:[0-9a-fA-F]{40} )+)([a-zA-Z]+)\t(.*)$//) {
4412 $res{'nparents'} = length($1);
4413 $res{'from_mode'} = [ split(' ', $2) ];
4414 $res{'to_mode'} = pop @
{$res{'from_mode'}};
4415 $res{'from_id'} = [ split(' ', $3) ];
4416 $res{'to_id'} = pop @
{$res{'from_id'}};
4417 $res{'status'} = [ split('', $4) ];
4418 $res{'to_file'} = unquote
($5);
4420 # 'c512b523472485aef4fff9e57b229d9d243c967f'
4421 elsif ($line =~ m/^([0-9a-fA-F]{40})$/) {
4422 $res{'commit'} = $1;
4425 return wantarray ?
%res : \
%res;
4428 # wrapper: return parsed line of git-diff-tree "raw" output
4429 # (the argument might be raw line, or parsed info)
4430 sub parsed_difftree_line
{
4431 my $line_or_ref = shift;
4433 if (ref($line_or_ref) eq "HASH") {
4434 # pre-parsed (or generated by hand)
4435 return $line_or_ref;
4437 return parse_difftree_raw_line
($line_or_ref);
4441 # parse line of git-ls-tree output
4442 sub parse_ls_tree_line
{
4448 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa 16717 panic.c'
4449 $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40}) +(-|[0-9]+)\t(.+)$/s;
4458 $res{'name'} = unquote
($5);
4461 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'
4462 $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t(.+)$/s;
4470 $res{'name'} = unquote
($4);
4474 return wantarray ?
%res : \
%res;
4477 # generates _two_ hashes, references to which are passed as 2 and 3 argument
4478 sub parse_from_to_diffinfo
{
4479 my ($diffinfo, $from, $to, @parents) = @_;
4481 if ($diffinfo->{'nparents'}) {
4483 $from->{'file'} = [];
4484 $from->{'href'} = [];
4485 fill_from_file_info
($diffinfo, @parents)
4486 unless exists $diffinfo->{'from_file'};
4487 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
4488 $from->{'file'}[$i] =
4489 defined $diffinfo->{'from_file'}[$i] ?
4490 $diffinfo->{'from_file'}[$i] :
4491 $diffinfo->{'to_file'};
4492 if ($diffinfo->{'status'}[$i] ne "A") { # not new (added) file
4493 $from->{'href'}[$i] = href
(action
=>"blob",
4494 hash_base
=>$parents[$i],
4495 hash
=>$diffinfo->{'from_id'}[$i],
4496 file_name
=>$from->{'file'}[$i]);
4498 $from->{'href'}[$i] = undef;
4502 # ordinary (not combined) diff
4503 $from->{'file'} = $diffinfo->{'from_file'};
4504 if ($diffinfo->{'status'} ne "A") { # not new (added) file
4505 $from->{'href'} = href
(action
=>"blob", hash_base
=>$hash_parent,
4506 hash
=>$diffinfo->{'from_id'},
4507 file_name
=>$from->{'file'});
4509 delete $from->{'href'};
4513 $to->{'file'} = $diffinfo->{'to_file'};
4514 if (!is_deleted
($diffinfo)) { # file exists in result
4515 $to->{'href'} = href
(action
=>"blob", hash_base
=>$hash,
4516 hash
=>$diffinfo->{'to_id'},
4517 file_name
=>$to->{'file'});
4519 delete $to->{'href'};
4523 ## ......................................................................
4524 ## parse to array of hashes functions
4526 sub git_get_heads_list
{
4527 my ($limit, @classes) = @_;
4528 @classes = get_branch_refs
() unless @classes;
4529 my @patterns = map { "refs/$_" } @classes;
4532 defined(my $fd = git_cmd_pipe
'for-each-ref',
4533 ($limit ?
'--count='.($limit+1) : ()), '--sort=-committerdate',
4534 '--format=%(objectname) %(refname) %(subject)%00%(committer)',
4537 while (my $line = to_utf8
(scalar <$fd>)) {
4541 my ($refinfo, $committerinfo) = split(/\0/, $line);
4542 my ($hash, $name, $title) = split(' ', $refinfo, 3);
4543 my ($committer, $epoch, $tz) =
4544 ($committerinfo =~ /^(.*) ([0-9]+) (.*)$/);
4545 $ref_item{'fullname'} = $name;
4546 my $strip_refs = join '|', map { quotemeta } get_branch_refs
();
4547 $name =~ s!^refs/($strip_refs|remotes)/!!;
4548 $ref_item{'name'} = $name;
4549 # for refs neither in 'heads' nor 'remotes' we want to
4550 # show their ref dir
4551 my $ref_dir = (defined $1) ?
$1 : '';
4552 if ($ref_dir ne '' and $ref_dir ne 'heads' and $ref_dir ne 'remotes') {
4553 $ref_item{'name'} .= ' (' . $ref_dir . ')';
4556 $ref_item{'id'} = $hash;
4557 $ref_item{'title'} = $title || '(no commit message)';
4558 $ref_item{'epoch'} = $epoch;
4560 $ref_item{'age'} = age_string
($ref_item{'epoch'});
4562 $ref_item{'age'} = "unknown";
4565 push @headslist, \
%ref_item;
4569 return wantarray ?
@headslist : \
@headslist;
4572 sub git_get_tags_list
{
4575 my $all = shift || 0;
4576 my $order = shift || $default_refs_order;
4577 my $sortkey = $all && $order eq 'name' ?
'refname' : '-creatordate';
4579 defined(my $fd = git_cmd_pipe
'for-each-ref',
4580 ($limit ?
'--count='.($limit+1) : ()), "--sort=$sortkey",
4581 '--format=%(objectname) %(objecttype) %(refname) '.
4582 '%(*objectname) %(*objecttype) %(subject)%00%(creator)',
4583 ($all ?
'refs' : 'refs/tags'))
4585 while (my $line = to_utf8
(scalar <$fd>)) {
4589 my ($refinfo, $creatorinfo) = split(/\0/, $line);
4590 my ($id, $type, $name, $refid, $reftype, $title) = split(' ', $refinfo, 6);
4591 my ($creator, $epoch, $tz) =
4592 ($creatorinfo =~ /^(.*) ([0-9]+) (.*)$/);
4593 $ref_item{'fullname'} = $name;
4594 $name =~ s!^refs/!! if $all;
4595 $name =~ s!^refs/tags/!! unless $all;
4597 $ref_item{'type'} = $type;
4598 $ref_item{'id'} = $id;
4599 $ref_item{'name'} = $name;
4600 if ($type eq "tag") {
4601 $ref_item{'subject'} = $title;
4602 $ref_item{'reftype'} = $reftype;
4603 $ref_item{'refid'} = $refid;
4605 $ref_item{'reftype'} = $type;
4606 $ref_item{'refid'} = $id;
4609 if ($type eq "tag" || $type eq "commit") {
4610 $ref_item{'epoch'} = $epoch;
4612 $ref_item{'age'} = age_string
($ref_item{'epoch'});
4614 $ref_item{'age'} = "unknown";
4618 push @tagslist, \
%ref_item;
4622 return wantarray ?
@tagslist : \
@tagslist;
4625 ## ----------------------------------------------------------------------
4626 ## filesystem-related functions
4628 sub get_file_owner
{
4631 my ($dev, $ino, $mode, $nlink, $st_uid, $st_gid, $rdev, $size) = stat($path);
4632 my ($name, $passwd, $uid, $gid, $quota, $comment, $gcos, $dir, $shell) = getpwuid($st_uid);
4633 if (!defined $gcos) {
4637 $owner =~ s/[,;].*$//;
4638 return to_utf8
($owner);
4641 # assume that file exists
4643 my $filename = shift;
4645 open my $fd, '<', $filename;
4652 # return undef on failure
4653 sub collect_output
{
4654 defined(my $fd = cmd_pipe
@_) or return undef;
4659 my $result = join('', map({ to_utf8
($_) } <$fd>));
4660 close $fd or return undef;
4664 # return undef on failure
4665 # return '' if only comments
4666 sub collect_html_file
{
4667 my $filename = shift;
4669 open my $fd, '<', $filename or return undef;
4670 my $result = join('', map({ to_utf8
($_) } <$fd>));
4671 close $fd or return undef;
4672 return undef unless defined($result);
4674 $test =~ s/<!--(?:[^-]|(?:-(?!-)))*-->//gs;
4676 return $test eq '' ?
'' : $result;
4679 ## ......................................................................
4680 ## mimetype related functions
4682 sub mimetype_guess_file
{
4683 my $filename = shift;
4684 my $mimemap = shift;
4685 my $rawmode = shift;
4686 -r
$mimemap or return undef;
4689 open(my $mh, '<', $mimemap) or return undef;
4691 next if m/^#/; # skip comments
4692 my ($mimetype, @exts) = split(/\s+/);
4693 foreach my $ext (@exts) {
4694 $mimemap{$ext} = $mimetype;
4700 $ext = $1 if $filename =~ /\.([^.]*)$/;
4701 $ans = $mimemap{$ext} if $ext;
4704 $ans = 'text/html' if $l eq 'application/xhtml+xml';
4706 $ans = 'text/xml' if $l =~ m
!^application
/[^\s
:;,=]+\
+xml
$! ||
4707 $l eq 'image/svg+xml' ||
4708 $l eq 'application/xml-dtd' ||
4709 $l eq 'application/xml-external-parsed-entity';
4715 sub mimetype_guess
{
4716 my $filename = shift;
4717 my $rawmode = shift;
4719 $filename =~ /\./ or return undef;
4721 if ($mimetypes_file) {
4722 my $file = $mimetypes_file;
4723 if ($file !~ m!^/!) { # if it is relative path
4724 # it is relative to project
4725 $file = "$projectroot/$project/$file";
4727 $mime = mimetype_guess_file
($filename, $file, $rawmode);
4729 $mime ||= mimetype_guess_file
($filename, '/etc/mime.types', $rawmode);
4735 my $filename = shift;
4736 my $rawmode = shift;
4739 # The -T/-B file operators produce the wrong result unless a perlio
4740 # layer is present when the file handle is a pipe that delivers less
4741 # than 512 bytes of data before reaching EOF.
4743 # If we are running in a Perl that uses the stdio layer rather than the
4744 # unix+perlio layers we will end up adding a perlio layer on top of the
4745 # stdio layer and get a second level of buffering. This is harmless
4746 # and it makes the -T/-B file operators work properly in all cases.
4748 binmode $fd, ":perlio" or die_error
(500, "Adding perlio layer failed")
4749 unless grep /^perlio$/, PerlIO
::get_layers
($fd);
4751 $mime = mimetype_guess
($filename, $rawmode) if defined $filename;
4753 if (!$mime && $filename) {
4754 if ($filename =~ m/\.html?$/i) {
4755 $mime = 'text/html';
4756 } elsif ($filename =~ m/\.xht(?:ml)?$/i) {
4757 $mime = 'text/html';
4758 } elsif ($filename =~ m/\.te?xt?$/i) {
4759 $mime = 'text/plain';
4760 } elsif ($filename =~ m/\.(?:markdown|md)$/i) {
4761 $mime = 'text/plain';
4762 } elsif ($filename =~ m/\.png$/i) {
4763 $mime = 'image/png';
4764 } elsif ($filename =~ m/\.gif$/i) {
4765 $mime = 'image/gif';
4766 } elsif ($filename =~ m/\.jpe?g$/i) {
4767 $mime = 'image/jpeg';
4768 } elsif ($filename =~ m/\.svgz?$/i) {
4769 $mime = 'image/svg+xml';
4774 return $default_blob_plain_mimetype || 'application/octet-stream' unless $fd || $mime;
4776 $mime = -T
$fd ?
'text/plain' : 'application/octet-stream' unless $mime;
4784 return scalar($data =~ /^[\x00-\x7f]*$/);
4789 return utf8
::decode
($data);
4792 sub extract_html_charset
{
4793 return undef unless $_[0] && "$_[0]</head>" =~ m
#<head(?:\s+[^>]*)?(?<!/)>(.*?)</head\s*>#is;
4795 return $2 if $head =~ m
#<meta\s+charset\s*=\s*(['"])\s*([a-z0-9(:)_.+-]+)\s*\1\s*/?>#is;
4796 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) {
4797 my %kv = (lc($1) => $3, lc($4) => $6);
4798 my ($he, $c) = (lc($kv{'http-equiv'}), $kv{'content'});
4799 return $1 if $he && $c && $he eq 'content-type' &&
4800 $c =~ m!\s*text/html\s*;\s*charset\s*=\s*([a-z0-9(:)_.+-]+)\s*$!is;
4805 sub blob_contenttype
{
4806 my ($fd, $file_name, $type) = @_;
4808 $type ||= blob_mimetype
($fd, $file_name, 1);
4809 return $type unless $type =~ m!^text/.+!i;
4810 my ($leader, $charset, $htmlcharset);
4811 if ($fd && read($fd, $leader, 32768)) {{
4812 $charset='US-ASCII' if is_ascii
($leader);
4813 return ("$type; charset=UTF-8", $leader) if !$charset && is_valid_utf8
($leader);
4814 $charset='ISO-8859-1' unless $charset;
4815 $htmlcharset = extract_html_charset
($leader) if $type eq 'text/html';
4816 if ($htmlcharset && $charset ne 'US-ASCII') {
4817 $htmlcharset = undef if $htmlcharset =~ /^(?:utf-8|us-ascii)$/i
4820 return ("$type; charset=$htmlcharset", $leader) if $htmlcharset;
4821 my $defcharset = $default_text_plain_charset || '';
4822 $defcharset =~ s/^\s+//;
4823 $defcharset =~ s/\s+$//;
4824 $defcharset = '' if $charset && $charset ne 'US-ASCII' && $defcharset =~ /^(?:utf-8|us-ascii)$/i;
4825 return ("$type; charset=" . ($defcharset || 'ISO-8859-1'), $leader);
4828 # peek the first upto 128 bytes off a file handle
4836 return '' unless $fd && read($fd, $prefix128, 128);
4838 # In the general case, we're guaranteed only to be able to ungetc one
4839 # character (provided, of course, we actually got a character first).
4843 # 1) we are dealing with a :perlio layer since blob_mimetype will have
4844 # already been called at least once on the file handle before us
4846 # 2) we have an $fd positioned at the start of the input stream and
4847 # therefore know we were positioned at a buffer boundary before
4848 # reading the initial upto 128 bytes
4850 # 3) the buffer size is at least 512 bytes
4852 # 4) we are careful to only unget raw bytes
4854 # 5) we are attempting to unget exactly the same number of bytes we got
4856 # Given the above conditions we will ALWAYS be able to safely unget
4857 # the $prefix128 value we just got.
4859 # In fact, we could read up to 511 bytes and still be sure.
4860 # (Reading 512 might pop us into the next internal buffer, but probably
4861 # not since that could break the always able to unget at least the one
4862 # you just got guarantee.)
4864 map {$fd->ungetc(ord($_))} reverse(split //, $prefix128);
4869 # guess file syntax for syntax highlighting; return undef if no highlighting
4870 # the name of syntax can (in the future) depend on syntax highlighter used
4871 sub guess_file_syntax
{
4872 my ($fd, $mimetype, $file_name) = @_;
4873 return undef unless $fd && defined $file_name &&
4874 defined $mimetype && $mimetype =~ m!^text/.+!i;
4875 my $basename = basename
($file_name, '.in');
4876 return $highlight_basename{$basename}
4877 if exists $highlight_basename{$basename};
4879 # Peek to see if there's a shebang or xml line.
4880 # We always operate on bytes when testing this.
4883 my $shebang = peek128bytes
($fd);
4884 if (length($shebang) >= 4 && $shebang =~ /^#!/) { # 4 would be '#!/x'
4885 foreach my $key (keys %highlight_shebang) {
4886 my $ar = ref($highlight_shebang{$key}) ?
4887 $highlight_shebang{$key} :
4888 [$highlight_shebang{key
}];
4889 map {return $key if $shebang =~ /$_/} @
$ar;
4892 return 'xml' if $shebang =~ m!^\s*<\?xml\s!; # "xml" must be lowercase
4895 $basename =~ /\.([^.]*)$/;
4896 my $ext = $1 or return undef;
4897 return $highlight_ext{$ext}
4898 if exists $highlight_ext{$ext};
4903 # run highlighter and return FD of its output,
4904 # or return original FD if no highlighting
4905 sub run_highlighter
{
4906 my ($fd, $syntax) = @_;
4907 return $fd unless $fd && !eof($fd) && defined $highlight_bin && defined $syntax;
4909 defined(my $hifd = cmd_pipe
$posix_shell_bin, '-c',
4910 quote_command
(git_cmd
(), "cat-file", "blob", $hash)." | ".
4911 $to_utf8_pipe_command.
4912 quote_command
($highlight_bin).
4913 " --replace-tabs=8 --fragment --syntax $syntax")
4914 or die_error
(500, "Couldn't open file or run syntax highlighter");
4916 # just in case, should not happen as we tested !eof($fd) above
4917 return $fd if close($hifd);
4920 !$! or die_error
(500, "Couldn't close syntax highighter pipe");
4922 # leaving us with the only possibility a non-zero exit status (possibly a signal);
4923 # instead of dying horribly on this, just skip the highlighting
4924 # but do output a message about it to STDERR that will end up in the log
4925 print STDERR
"warning: skipping failed highlight for --syntax $syntax: ".
4926 sprintf("child exit status 0x%x\n", $?
);
4933 ## ======================================================================
4934 ## functions printing HTML: header, footer, error page
4936 sub get_page_title
{
4937 my $title = to_utf8
($site_name);
4939 unless (defined $project) {
4940 if (defined $project_filter) {
4941 $title .= " - projects in '" . esc_path
($project_filter) . "'";
4945 $title .= " - " . to_utf8
($project);
4947 return $title unless (defined $action);
4948 my $action_print = $action eq 'blame_incremental' ?
'blame' : $action;
4949 $title .= "/$action_print"; # $action is US-ASCII (7bit ASCII)
4951 return $title unless (defined $file_name);
4952 $title .= " - " . esc_path
($file_name);
4953 if ($action eq "tree" && $file_name !~ m
|/$|) {
4960 sub get_content_type_html
{
4961 # We do not ever emit application/xhtml+xml since that gives us
4962 # no benefits and it makes many browsers (e.g. Firefox) exceedingly
4963 # strict, which is troublesome for example when showing user-supplied
4964 # README.html files.
4968 sub print_feed_meta
{
4969 if (defined $project) {
4970 my %href_params = get_feed_info
();
4971 if (!exists $href_params{'-title'}) {
4972 $href_params{'-title'} = 'log';
4975 foreach my $format (qw(RSS Atom)) {
4976 my $type = lc($format);
4978 '-rel' => 'alternate',
4979 '-title' => esc_attr
("$project - $href_params{'-title'} - $format feed"),
4980 '-type' => "application/$type+xml"
4983 $href_params{'extra_options'} = undef;
4984 $href_params{'action'} = $type;
4985 $link_attr{'-href'} = href
(%href_params);
4987 "rel=\"$link_attr{'-rel'}\" ".
4988 "title=\"$link_attr{'-title'}\" ".
4989 "href=\"$link_attr{'-href'}\" ".
4990 "type=\"$link_attr{'-type'}\" ".
4993 $href_params{'extra_options'} = '--no-merges';
4994 $link_attr{'-href'} = href
(%href_params);
4995 $link_attr{'-title'} .= ' (no merges)';
4997 "rel=\"$link_attr{'-rel'}\" ".
4998 "title=\"$link_attr{'-title'}\" ".
4999 "href=\"$link_attr{'-href'}\" ".
5000 "type=\"$link_attr{'-type'}\" ".
5005 printf('<link rel="alternate" title="%s projects list" '.
5006 'href="%s" type="text/plain; charset=utf-8" />'."\n",
5007 esc_attr
($site_name), href
(project
=>undef, action
=>"project_index"));
5008 printf('<link rel="alternate" title="%s projects feeds" '.
5009 'href="%s" type="text/x-opml" />'."\n",
5010 esc_attr
($site_name), href
(project
=>undef, action
=>"opml"));
5014 sub print_header_links
{
5017 # print out each stylesheet that exist, providing backwards capability
5018 # for those people who defined $stylesheet in a config file
5019 if (defined $stylesheet) {
5020 print '<link rel="stylesheet" type="text/css" href="'.esc_url
($stylesheet).'"/>'."\n";
5022 foreach my $stylesheet (@stylesheets) {
5023 next unless $stylesheet;
5024 print '<link rel="stylesheet" type="text/css" href="'.esc_url
($stylesheet).'"/>'."\n";
5028 if ($status eq '200 OK');
5029 if (defined $favicon) {
5030 print qq(<link rel
="shortcut icon" href
=").esc_url($favicon).qq(" type
="image/png" />\n);
5034 sub print_nav_breadcrumbs_path
{
5035 my $dirprefix = undef;
5036 while (my $part = shift) {
5037 $dirprefix .= "/" if defined $dirprefix;
5038 $dirprefix .= $part;
5039 print $cgi->a({-href
=> href
(project
=> undef,
5040 project_filter
=> $dirprefix,
5041 action
=> "project_list")},
5042 esc_html
($part)) . " / ";
5046 sub print_nav_breadcrumbs
{
5049 for my $crumb (@extra_breadcrumbs, [ $home_link_str => $home_link ]) {
5050 print $cgi->a({-href
=> esc_url
($crumb->[1])}, $crumb->[0]) . " / ";
5052 if (defined $project) {
5053 my @dirname = split '/', $project;
5054 my $projectbasename = pop @dirname;
5055 print_nav_breadcrumbs_path
(@dirname);
5056 print $cgi->a({-href
=> href
(action
=>"summary")}, esc_html
($projectbasename));
5057 if (defined $action) {
5058 my $action_print = $action ;
5059 $action_print = 'blame' if $action_print eq 'blame_incremental';
5060 if (defined $opts{-action_extra
}) {
5061 $action_print = $cgi->a({-href
=> href
(action
=>$action)},
5064 print " / $action_print";
5066 if (defined $opts{-action_extra
}) {
5067 print " / $opts{-action_extra}";
5070 } elsif (defined $project_filter) {
5071 print_nav_breadcrumbs_path
(split '/', $project_filter);
5075 sub print_search_form
{
5076 if (!defined $searchtext) {
5080 if (defined $hash_base) {
5081 $search_hash = $hash_base;
5082 } elsif (defined $hash) {
5083 $search_hash = $hash;
5085 $search_hash = "HEAD";
5087 # We can't use href() here because we need to encode the
5088 # URL parameters into the form, not into the action link.
5089 my $action = $my_uri;
5090 my $use_pathinfo = gitweb_check_feature
('pathinfo');
5091 if ($use_pathinfo) {
5092 # See notes about doubled / in href()
5094 $action .= "/".esc_path_info
($project);
5096 print $cgi->start_form(-method
=> "get", -action
=> $action) .
5097 "<div class=\"search\">\n" .
5099 $cgi->input({-name
=>"p", -value
=>$project, -type
=>"hidden"}) . "\n") .
5100 $cgi->input({-name
=>"a", -value
=>"search", -type
=>"hidden"}) . "\n" .
5101 $cgi->input({-name
=>"h", -value
=>$search_hash, -type
=>"hidden"}) . "\n" .
5102 $cgi->popup_menu(-name
=> 'st', -default => 'commit',
5103 -values => ['commit', 'grep', 'author', 'committer', 'pickaxe']) .
5104 " " . $cgi->a({-href
=> href
(action
=>"search_help"),
5105 -title
=> "search help" }, "?") . " search:\n",
5106 $cgi->textfield(-name
=> "s", -value
=> $searchtext, -override
=> 1) . "\n" .
5107 "<span title=\"Extended regular expression\">" .
5108 $cgi->checkbox(-name
=> 'sr', -value
=> 1, -label
=> 're',
5109 -checked
=> $search_use_regexp) .
5112 $cgi->end_form() . "\n";
5115 sub git_header_html
{
5116 my $status = shift || "200 OK";
5117 my $expires = shift;
5120 my $title = get_page_title
();
5121 my $content_type = get_content_type_html
();
5122 print $cgi->header(-type
=>$content_type, -charset
=> 'utf-8',
5123 -status
=> $status, -expires
=> $expires)
5124 unless ($opts{'-no_http_header'});
5125 my $mod_perl_version = $ENV{'MOD_PERL'} ?
" $ENV{'MOD_PERL'}" : '';
5127 <?xml version="1.0" encoding="utf-8"?>
5128 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
5129 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US" lang="en-US">
5130 <!-- git web interface version $version, (C) 2005-2006, Kay Sievers <kay.sievers\@vrfy.org>, Christian Gierke -->
5131 <!-- git core binaries version $git_version -->
5133 <meta http-equiv="content-type" content="$content_type; charset=utf-8"/>
5134 <meta name="generator" content="gitweb/$version git/$git_version$mod_perl_version"/>
5135 <meta name="robots" content="index, nofollow"/>
5136 <title>$title</title>
5137 <script type="text/javascript">/* <![CDATA[ */
5138 function fixBlameLinks() {
5139 var allLinks = document.getElementsByTagName("a");
5140 for (var i = 0; i < allLinks.length; i++) {
5141 var link = allLinks.item(i);
5142 if (link.className == 'blamelink')
5143 link.href = link.href.replace("/blame/", "/blame_incremental/");
5148 # the stylesheet, favicon etc urls won't work correctly with path_info
5149 # unless we set the appropriate base URL
5150 if ($ENV{'PATH_INFO'}) {
5151 print "<base href=\"".esc_url
($base_url)."\" />\n";
5153 print_header_links
($status);
5155 if (defined $site_html_head_string) {
5156 print to_utf8
($site_html_head_string);
5162 if (defined $site_header && -f
$site_header) {
5163 insert_file
($site_header);
5166 print "<div class=\"page_header\">\n";
5167 if (defined $logo) {
5168 print $cgi->a({-href
=> esc_url
($logo_url),
5169 -title
=> $logo_label},
5170 $cgi->img({-src
=> esc_url
($logo),
5171 -width
=> 72, -height
=> 27,
5173 -class => "logo"}));
5175 print_nav_breadcrumbs
(%opts);
5178 my $have_search = gitweb_check_feature
('search');
5179 if (defined $project && $have_search) {
5180 print_search_form
();
5184 sub compute_timed_interval
{
5185 return "<?gitweb compute_timed_interval?>" if $cache_mode_active;
5186 return tv_interval
($t0, [ gettimeofday
() ]);
5189 sub compute_commands_count
{
5190 return "<?gitweb compute_commands_count?>" if $cache_mode_active;
5191 my $s = $number_of_git_cmds == 1 ?
'' : 's';
5192 return '<span id="generating_cmd">'.
5193 $number_of_git_cmds.
5194 "</span> git command$s";
5197 sub git_footer_html
{
5198 my $feed_class = 'rss_logo';
5200 print "<div class=\"page_footer\">\n";
5201 if (defined $project) {
5202 my $descr = git_get_project_description
($project);
5203 if (defined $descr) {
5204 print "<div class=\"page_footer_text\">" . esc_html
($descr) . "</div>\n";
5207 my %href_params = get_feed_info
();
5208 if (!%href_params) {
5209 $feed_class .= ' generic';
5211 $href_params{'-title'} ||= 'log';
5213 foreach my $format (qw(RSS Atom)) {
5214 $href_params{'action'} = lc($format);
5215 print $cgi->a({-href
=> href
(%href_params),
5216 -title
=> "$href_params{'-title'} $format feed",
5217 -class => $feed_class}, $format)."\n";
5221 print $cgi->a({-href
=> href
(project
=>undef, action
=>"opml",
5222 project_filter
=> $project_filter),
5223 -class => $feed_class}, "OPML") . " ";
5224 print $cgi->a({-href
=> href
(project
=>undef, action
=>"project_index",
5225 project_filter
=> $project_filter),
5226 -class => $feed_class}, "TXT") . "\n";
5228 print "</div>\n"; # class="page_footer"
5230 if (defined $t0 && gitweb_check_feature
('timed')) {
5231 print "<div id=\"generating_info\">\n";
5232 print 'This page took '.
5233 '<span id="generating_time" class="time_span">'.
5234 compute_timed_interval
().
5237 compute_commands_count
().
5239 print "</div>\n"; # class="page_footer"
5242 if (defined $site_footer && -f
$site_footer) {
5243 insert_file
($site_footer);
5246 print qq!<script type
="text/javascript" src
="!.esc_url($javascript).qq!"></script
>\n!;
5247 if (defined $action &&
5248 $action eq 'blame_incremental') {
5249 print qq!<script type
="text/javascript">\n!.
5250 qq!startBlame
("!. href(action=>"blame_data
", -replay=>1) .qq!",\n!.
5251 qq! "!. href() .qq!");\n!.
5254 my ($jstimezone, $tz_cookie, $datetime_class) =
5255 gitweb_get_feature
('javascript-timezone');
5257 print qq!<script type
="text/javascript">\n!.
5258 qq!window
.onload
= function
() {\n!;
5259 if (gitweb_check_feature
('blame_incremental')) {
5260 print qq! fixBlameLinks
();\n!;
5262 if (gitweb_check_feature
('javascript-actions')) {
5263 print qq! fixLinks
();\n!;
5265 if ($jstimezone && $tz_cookie && $datetime_class) {
5266 print qq! var tz_cookie
= { name
: '$tz_cookie', expires
: 14, path
: '/' };\n!. # in days
5267 qq! onloadTZSetup
('$jstimezone', tz_cookie
, '$datetime_class');\n!;
5277 # die_error(<http_status_code>, <error_message>[, <detailed_html_description>])
5278 # Example: die_error(404, 'Hash not found')
5279 # By convention, use the following status codes (as defined in RFC 2616):
5280 # 400: Invalid or missing CGI parameters, or
5281 # requested object exists but has wrong type.
5282 # 403: Requested feature (like "pickaxe" or "snapshot") not enabled on
5283 # this server or project.
5284 # 404: Requested object/revision/project doesn't exist.
5285 # 500: The server isn't configured properly, or
5286 # an internal error occurred (e.g. failed assertions caused by bugs), or
5287 # an unknown error occurred (e.g. the git binary died unexpectedly).
5288 # 503: The server is currently unavailable (because it is overloaded,
5289 # or down for maintenance). Generally, this is a temporary state.
5291 my $status = shift || 500;
5292 my $error = esc_html
(shift) || "Internal Server Error";
5296 my %http_responses = (
5297 400 => '400 Bad Request',
5298 403 => '403 Forbidden',
5299 404 => '404 Not Found',
5300 500 => '500 Internal Server Error',
5301 503 => '503 Service Unavailable',
5303 git_header_html
($http_responses{$status}, undef, %opts);
5305 <div class="page_body">
5310 if (defined $extra) {
5318 unless ($opts{'-error_handler'});
5321 ## ----------------------------------------------------------------------
5322 ## functions printing or outputting HTML: navigation
5324 sub git_print_page_nav
{
5325 my ($current, $suppress, $head, $treehead, $treebase, $extra) = @_;
5326 $extra = '' if !defined $extra; # pager or formats
5328 my @navs = qw(summary log commit commitdiff tree refs);
5331 if (ref($suppress) eq 'ARRAY') {
5332 %omit = map { ($_ => 1) } @
$suppress;
5334 %omit = ($suppress => 1);
5336 @navs = grep { !$omit{$_} } @navs;
5339 my %arg = map { $_ => {action
=>$_} } @navs;
5340 if (defined $head) {
5341 for (qw(commit commitdiff)) {
5342 $arg{$_}{'hash'} = $head;
5344 if ($current =~ m/^(tree | log | shortlog | commit | commitdiff | search)$/x) {
5345 $arg{'log'}{'hash'} = $head;
5349 $arg{'log'}{'action'} = 'shortlog';
5350 if ($current eq 'log') {
5351 $current = 'shortlog';
5352 } elsif ($current eq 'shortlog') {
5355 $arg{'tree'}{'hash'} = $treehead if defined $treehead;
5356 $arg{'tree'}{'hash_base'} = $treebase if defined $treebase;
5358 my @actions = gitweb_get_feature
('actions');
5359 my $escname = $project;
5360 $escname =~ s/[+]/%2B/g;
5363 'n' => $project, # project name
5364 'f' => $git_dir, # project path within filesystem
5365 'h' => $treehead || '', # current hash ('h' parameter)
5366 'b' => $treebase || '', # hash base ('hb' parameter)
5367 'e' => $escname, # project name with '+' escaped
5370 my ($label, $link, $pos) = splice(@actions,0,3);
5372 @navs = map { $_ eq $pos ?
($_, $label) : $_ } @navs;
5374 $link =~ s/%([%nfhbe])/$repl{$1}/g;
5375 $arg{$label}{'_href'} = $link;
5378 print "<div class=\"page_nav\">\n" .
5380 map { $_ eq $current ?
5381 $_ : $cgi->a({-href
=> ($arg{$_}{_href
} ?
$arg{$_}{_href
} : href
(%{$arg{$_}}))}, "$_")
5383 print "<br/>\n$extra<br/>\n" .
5387 # returns a submenu for the nagivation of the refs views (tags, heads,
5388 # remotes) with the current view disabled and the remotes view only
5389 # available if the feature is enabled
5390 sub format_ref_views
{
5392 my @ref_views = qw{tags heads
};
5393 push @ref_views, 'remotes' if gitweb_check_feature
('remote_heads');
5394 return join " | ", map {
5395 $_ eq $current ?
$_ :
5396 $cgi->a({-href
=> href
(action
=>$_)}, $_)
5400 sub format_paging_nav
{
5401 my ($action, $page, $has_next_link) = @_;
5407 $cgi->a({-href
=> href
(-replay
=>1, page
=>undef)}, "first") .
5409 $cgi->a({-href
=> href
(-replay
=>1, page
=>$page-1),
5410 -accesskey
=> "p", -title
=> "Alt-p"}, "prev");
5412 $paging_nav .= "first · prev";
5415 if ($has_next_link) {
5416 $paging_nav .= " · " .
5417 $cgi->a({-href
=> href
(-replay
=>1, page
=>$page+1),
5418 -accesskey
=> "n", -title
=> "Alt-n"}, "next");
5420 $paging_nav .= " · next";
5426 sub format_log_nav
{
5427 my ($action, $page, $has_next_link) = @_;
5430 if ($action eq 'shortlog') {
5431 $paging_nav .= 'shortlog';
5433 $paging_nav .= $cgi->a({-href
=> href
(action
=>'shortlog', -replay
=>1)}, 'shortlog');
5435 $paging_nav .= ' | ';
5436 if ($action eq 'log') {
5437 $paging_nav .= 'fulllog';
5439 $paging_nav .= $cgi->a({-href
=> href
(action
=>'log', -replay
=>1)}, 'fulllog');
5442 $paging_nav .= " | " . format_paging_nav
($action, $page, $has_next_link);
5446 ## ......................................................................
5447 ## functions printing or outputting HTML: div
5449 sub git_print_header_div
{
5450 my ($action, $title, $hash, $hash_base, $extra) = @_;
5452 defined $extra or $extra = '';
5454 $args{'action'} = $action;
5455 $args{'hash'} = $hash if $hash;
5456 $args{'hash_base'} = $hash_base if $hash_base;
5458 my $link1 = $cgi->a({-href
=> href
(%args), -class => "title"},
5459 $title ?
$title : $action);
5460 my $link2 = $cgi->a({-href
=> href
(%args), -class => "cover"}, "");
5461 print "<div class=\"header\">\n" . '<span class="title">' .
5462 $link1 . $extra . $link2 . '</span>' . "\n</div>\n";
5465 sub format_repo_url
{
5466 my ($name, $url) = @_;
5467 return "<tr class=\"metadata_url\"><td>$name</td><td>$url</td></tr>\n";
5470 # Group output by placing it in a DIV element and adding a header.
5471 # Options for start_div() can be provided by passing a hash reference as the
5472 # first parameter to the function.
5473 # Options to git_print_header_div() can be provided by passing an array
5474 # reference. This must follow the options to start_div if they are present.
5475 # The content can be a scalar, which is output as-is, a scalar reference, which
5476 # is output after html escaping, an IO handle passed either as *handle or
5477 # *handle{IO}, or a function reference. In the latter case all following
5478 # parameters will be taken as argument to the content function call.
5479 sub git_print_section
{
5480 my ($div_args, $header_args, $content);
5482 if (ref($arg) eq 'HASH') {
5486 if (ref($arg) eq 'ARRAY') {
5487 $header_args = $arg;
5492 print $cgi->start_div($div_args);
5493 git_print_header_div
(@
$header_args);
5495 if (ref($content) eq 'CODE') {
5497 } elsif (ref($content) eq 'SCALAR') {
5498 print esc_html
($$content);
5499 } elsif (ref($content) eq 'GLOB' or ref($content) eq 'IO::Handle') {
5500 while (<$content>) {
5503 } elsif (!ref($content) && defined($content)) {
5507 print $cgi->end_div;
5510 sub format_timestamp_html
{
5512 my $useatnight = shift;
5513 defined($useatnight) or $useatnight = 1;
5514 my $strtime = $date->{'rfc2822'};
5516 my (undef, undef, $datetime_class) =
5517 gitweb_get_feature
('javascript-timezone');
5518 if ($datetime_class) {
5519 $strtime = qq!<span
class="$datetime_class">$strtime</span
>!;
5522 my $localtime_format = '(%d %02d:%02d %s)';
5523 if ($useatnight && $date->{'hour_local'} < 6) {
5524 $localtime_format = '(%d <span class="atnight">%02d:%02d</span> %s)';
5527 sprintf($localtime_format, $date->{'mday_local'},
5528 $date->{'hour_local'}, $date->{'minute_local'}, $date->{'tz_local'});
5533 sub format_lastrefresh_row
{
5534 return "<?gitweb format_lastrefresh_row?>" if $cache_mode_active;
5535 my %rd = parse_file_date
('.last_refresh');
5536 if (defined $rd{'rfc2822'}) {
5537 return "<tr id=\"metadata_lrefresh\"><td>last refresh</td>" .
5538 "<td>".format_timestamp_html
(\
%rd,0)."</td></tr>";
5543 # Outputs the author name and date in long form
5544 sub git_print_authorship
{
5547 my $tag = $opts{-tag
} || 'div';
5548 my $author = $co->{'author_name'};
5550 my %ad = parse_date
($co->{'author_epoch'}, $co->{'author_tz'});
5551 print "<$tag class=\"author_date\">" .
5552 format_search_author
($author, "author", esc_html
($author)) .
5553 " [".format_timestamp_html
(\
%ad)."]".
5554 git_get_avatar
($co->{'author_email'}, -pad_before
=> 1) .
5558 # Outputs table rows containing the full author or committer information,
5559 # in the format expected for 'commit' view (& similar).
5560 # Parameters are a commit hash reference, followed by the list of people
5561 # to output information for. If the list is empty it defaults to both
5562 # author and committer.
5563 sub git_print_authorship_rows
{
5565 # too bad we can't use @people = @_ || ('author', 'committer')
5567 @people = ('author', 'committer') unless @people;
5568 foreach my $who (@people) {
5569 my %wd = parse_date
($co->{"${who}_epoch"}, $co->{"${who}_tz"});
5570 print "<tr><td>$who</td><td>" .
5571 format_search_author
($co->{"${who}_name"}, $who,
5572 esc_html
($co->{"${who}_name"})) . " " .
5573 format_search_author
($co->{"${who}_email"}, $who,
5574 esc_html
("<" . $co->{"${who}_email"} . ">")) .
5575 "</td><td rowspan=\"2\">" .
5576 git_get_avatar
($co->{"${who}_email"}, -size
=> 'double') .
5580 format_timestamp_html
(\
%wd) .
5586 sub git_print_page_path
{
5592 print "<div class=\"page_path\">";
5593 print $cgi->a({-href
=> href
(action
=>"tree", hash_base
=>$hb),
5594 -title
=> 'tree root'}, to_utf8
("[$project]"));
5596 if (defined $name) {
5597 my @dirname = split '/', $name;
5598 my $basename = pop @dirname;
5601 foreach my $dir (@dirname) {
5602 $fullname .= ($fullname ?
'/' : '') . $dir;
5603 print $cgi->a({-href
=> href
(action
=>"tree", file_name
=>$fullname,
5605 -title
=> $fullname}, esc_path
($dir));
5608 if (defined $type && $type eq 'blob') {
5609 print $cgi->a({-href
=> href
(action
=>"blob_plain", file_name
=>$file_name,
5611 -title
=> $name}, esc_path
($basename));
5612 } elsif (defined $type && $type eq 'tree') {
5613 print $cgi->a({-href
=> href
(action
=>"tree", file_name
=>$file_name,
5615 -title
=> $name}, esc_path
($basename));
5618 print esc_path
($basename);
5621 print "<br/></div>\n";
5628 if ($opts{'-remove_title'}) {
5629 # remove title, i.e. first line of log
5632 # remove leading empty lines
5633 while (defined $log->[0] && $log->[0] eq "") {
5638 my $skip_blank_line = 0;
5639 foreach my $line (@
$log) {
5640 if ($line =~ m/^\s*([A-Z][-A-Za-z]*-[Bb]y|C[Cc]): /) {
5641 if (! $opts{'-remove_signoff'}) {
5642 print "<span class=\"signoff\">" . esc_html
($line) . "</span><br/>\n";
5643 $skip_blank_line = 1;
5648 if ($line =~ m
,\s
*([a
-z
]*link): (https?
://\S
+),i
) {
5649 if (! $opts{'-remove_signoff'}) {
5650 print "<span class=\"signoff\">" . esc_html
($1) . ": " .
5651 "<a href=\"" . esc_html
($2) . "\">" . esc_html
($2) . "</a>" .
5653 $skip_blank_line = 1;
5658 # print only one empty line
5659 # do not print empty line after signoff
5661 next if ($skip_blank_line);
5662 $skip_blank_line = 1;
5664 $skip_blank_line = 0;
5667 print format_log_line_html
($line) . "<br/>\n";
5670 if ($opts{'-final_empty_line'}) {
5671 # end with single empty line
5672 print "<br/>\n" unless $skip_blank_line;
5676 # return link target (what link points to)
5677 sub git_get_link_target
{
5682 defined(my $fd = git_cmd_pipe
"cat-file", "blob", $hash)
5686 $link_target = to_utf8
(scalar <$fd>);
5691 return $link_target;
5694 # given link target, and the directory (basedir) the link is in,
5695 # return target of link relative to top directory (top tree);
5696 # return undef if it is not possible (including absolute links).
5697 sub normalize_link_target
{
5698 my ($link_target, $basedir) = @_;
5700 # absolute symlinks (beginning with '/') cannot be normalized
5701 return if (substr($link_target, 0, 1) eq '/');
5703 # normalize link target to path from top (root) tree (dir)
5706 $path = $basedir . '/' . $link_target;
5708 # we are in top (root) tree (dir)
5709 $path = $link_target;
5712 # remove //, /./, and /../
5714 foreach my $part (split('/', $path)) {
5715 # discard '.' and ''
5716 next if (!$part || $part eq '.');
5718 if ($part eq '..') {
5722 # link leads outside repository (outside top dir)
5726 push @path_parts, $part;
5729 $path = join('/', @path_parts);
5734 # print tree entry (row of git_tree), but without encompassing <tr> element
5735 sub git_print_tree_entry
{
5736 my ($t, $basedir, $hash_base, $have_blame) = @_;
5739 $base_key{'hash_base'} = $hash_base if defined $hash_base;
5741 # The format of a table row is: mode list link. Where mode is
5742 # the mode of the entry, list is the name of the entry, an href,
5743 # and link is the action links of the entry.
5745 print "<td class=\"mode\">" . mode_str
($t->{'mode'}) . "</td>\n";
5746 if (exists $t->{'size'}) {
5747 print "<td class=\"size\">$t->{'size'}</td>\n";
5749 if ($t->{'type'} eq "blob") {
5750 print "<td class=\"list\">" .
5751 $cgi->a({-href
=> href
(action
=>"blob", hash
=>$t->{'hash'},
5752 file_name
=>"$basedir$t->{'name'}", %base_key),
5753 -class => "list"}, esc_path
($t->{'name'}));
5754 if (S_ISLNK
(oct $t->{'mode'})) {
5755 my $link_target = git_get_link_target
($t->{'hash'});
5757 my $norm_target = normalize_link_target
($link_target, $basedir);
5758 if (defined $norm_target) {
5760 $cgi->a({-href
=> href
(action
=>"object", hash_base
=>$hash_base,
5761 file_name
=>$norm_target),
5762 -title
=> $norm_target}, esc_path
($link_target));
5764 print " -> " . esc_path
($link_target);
5769 print "<td class=\"link\">";
5770 print $cgi->a({-href
=> href
(action
=>"blob", hash
=>$t->{'hash'},
5771 file_name
=>"$basedir$t->{'name'}", %base_key)},
5775 $cgi->a({-href
=> href
(action
=>"blame", hash
=>$t->{'hash'},
5776 file_name
=>"$basedir$t->{'name'}", %base_key),
5777 -class => "blamelink"},
5780 if (defined $hash_base) {
5782 $cgi->a({-href
=> href
(action
=>"history", hash_base
=>$hash_base,
5783 hash
=>$t->{'hash'}, file_name
=>"$basedir$t->{'name'}")},
5787 $cgi->a({-href
=> href
(action
=>"blob_plain", hash_base
=>$hash_base,
5788 file_name
=>"$basedir$t->{'name'}")},
5792 } elsif ($t->{'type'} eq "tree") {
5793 print "<td class=\"list\">";
5794 print $cgi->a({-href
=> href
(action
=>"tree", hash
=>$t->{'hash'},
5795 file_name
=>"$basedir$t->{'name'}",
5797 esc_path
($t->{'name'}));
5799 print "<td class=\"link\">";
5800 print $cgi->a({-href
=> href
(action
=>"tree", hash
=>$t->{'hash'},
5801 file_name
=>"$basedir$t->{'name'}",
5804 if (defined $hash_base) {
5806 $cgi->a({-href
=> href
(action
=>"history", hash_base
=>$hash_base,
5807 file_name
=>"$basedir$t->{'name'}")},
5812 # unknown object: we can only present history for it
5813 # (this includes 'commit' object, i.e. submodule support)
5814 print "<td class=\"list\">" .
5815 esc_path
($t->{'name'}) .
5817 print "<td class=\"link\">";
5818 if (defined $hash_base) {
5819 print $cgi->a({-href
=> href
(action
=>"history",
5820 hash_base
=>$hash_base,
5821 file_name
=>"$basedir$t->{'name'}")},
5828 ## ......................................................................
5829 ## functions printing large fragments of HTML
5831 # get pre-image filenames for merge (combined) diff
5832 sub fill_from_file_info
{
5833 my ($diff, @parents) = @_;
5835 $diff->{'from_file'} = [ ];
5836 $diff->{'from_file'}[$diff->{'nparents'} - 1] = undef;
5837 for (my $i = 0; $i < $diff->{'nparents'}; $i++) {
5838 if ($diff->{'status'}[$i] eq 'R' ||
5839 $diff->{'status'}[$i] eq 'C') {
5840 $diff->{'from_file'}[$i] =
5841 git_get_path_by_hash
($parents[$i], $diff->{'from_id'}[$i]);
5848 # is current raw difftree line of file deletion
5850 my $diffinfo = shift;
5852 return $diffinfo->{'to_id'} eq ('0' x
40);
5855 # does patch correspond to [previous] difftree raw line
5856 # $diffinfo - hashref of parsed raw diff format
5857 # $patchinfo - hashref of parsed patch diff format
5858 # (the same keys as in $diffinfo)
5859 sub is_patch_split
{
5860 my ($diffinfo, $patchinfo) = @_;
5862 return defined $diffinfo && defined $patchinfo
5863 && $diffinfo->{'to_file'} eq $patchinfo->{'to_file'};
5867 sub git_difftree_body
{
5868 my ($difftree, $hash, @parents) = @_;
5869 my ($parent) = $parents[0];
5870 my $have_blame = gitweb_check_feature
('blame');
5871 print "<div class=\"list_head\">\n";
5872 if ($#{$difftree} > 10) {
5873 print(($#{$difftree} + 1) . " files changed:\n");
5877 print "<table class=\"" .
5878 (@parents > 1 ?
"combined " : "") .
5881 # header only for combined diff in 'commitdiff' view
5882 my $has_header = @
$difftree && @parents > 1 && $action eq 'commitdiff';
5885 print "<thead><tr>\n" .
5886 "<th></th><th></th>\n"; # filename, patchN link
5887 for (my $i = 0; $i < @parents; $i++) {
5888 my $par = $parents[$i];
5890 $cgi->a({-href
=> href
(action
=>"commitdiff",
5891 hash
=>$hash, hash_parent
=>$par),
5892 -title
=> 'commitdiff to parent number ' .
5893 ($i+1) . ': ' . substr($par,0,7)},
5897 print "</tr></thead>\n<tbody>\n";
5902 foreach my $line (@
{$difftree}) {
5903 my $diff = parsed_difftree_line
($line);
5906 print "<tr class=\"dark\">\n";
5908 print "<tr class=\"light\">\n";
5912 if (exists $diff->{'nparents'}) { # combined diff
5914 fill_from_file_info
($diff, @parents)
5915 unless exists $diff->{'from_file'};
5917 if (!is_deleted
($diff)) {
5918 # file exists in the result (child) commit
5920 $cgi->a({-href
=> href
(action
=>"blob", hash
=>$diff->{'to_id'},
5921 file_name
=>$diff->{'to_file'},
5923 -class => "list"}, esc_path
($diff->{'to_file'})) .
5927 esc_path
($diff->{'to_file'}) .
5931 if ($action eq 'commitdiff') {
5934 print "<td class=\"link\">" .
5935 $cgi->a({-href
=> href
(-anchor
=>"patch$patchno")},
5941 my $has_history = 0;
5942 my $not_deleted = 0;
5943 for (my $i = 0; $i < $diff->{'nparents'}; $i++) {
5944 my $hash_parent = $parents[$i];
5945 my $from_hash = $diff->{'from_id'}[$i];
5946 my $from_path = $diff->{'from_file'}[$i];
5947 my $status = $diff->{'status'}[$i];
5949 $has_history ||= ($status ne 'A');
5950 $not_deleted ||= ($status ne 'D');
5952 if ($status eq 'A') {
5953 print "<td class=\"link\" align=\"right\"> | </td>\n";
5954 } elsif ($status eq 'D') {
5955 print "<td class=\"link\">" .
5956 $cgi->a({-href
=> href
(action
=>"blob",
5959 file_name
=>$from_path)},
5963 if ($diff->{'to_id'} eq $from_hash) {
5964 print "<td class=\"link nochange\">";
5966 print "<td class=\"link\">";
5968 print $cgi->a({-href
=> href
(action
=>"blobdiff",
5969 hash
=>$diff->{'to_id'},
5970 hash_parent
=>$from_hash,
5972 hash_parent_base
=>$hash_parent,
5973 file_name
=>$diff->{'to_file'},
5974 file_parent
=>$from_path)},
5980 print "<td class=\"link\">";
5982 print $cgi->a({-href
=> href
(action
=>"blob",
5983 hash
=>$diff->{'to_id'},
5984 file_name
=>$diff->{'to_file'},
5987 print " | " if ($has_history);
5990 print $cgi->a({-href
=> href
(action
=>"history",
5991 file_name
=>$diff->{'to_file'},
5998 next; # instead of 'else' clause, to avoid extra indent
6000 # else ordinary diff
6002 my ($to_mode_oct, $to_mode_str, $to_file_type);
6003 my ($from_mode_oct, $from_mode_str, $from_file_type);
6004 if ($diff->{'to_mode'} ne ('0' x
6)) {
6005 $to_mode_oct = oct $diff->{'to_mode'};
6006 if (S_ISREG
($to_mode_oct)) { # only for regular file
6007 $to_mode_str = sprintf("%04o", $to_mode_oct & 0777); # permission bits
6009 $to_file_type = file_type
($diff->{'to_mode'});
6011 if ($diff->{'from_mode'} ne ('0' x
6)) {
6012 $from_mode_oct = oct $diff->{'from_mode'};
6013 if (S_ISREG
($from_mode_oct)) { # only for regular file
6014 $from_mode_str = sprintf("%04o", $from_mode_oct & 0777); # permission bits
6016 $from_file_type = file_type
($diff->{'from_mode'});
6019 if ($diff->{'status'} eq "A") { # created
6020 my $mode_chng = "<span class=\"file_status new\">[new $to_file_type";
6021 $mode_chng .= " with mode: $to_mode_str" if $to_mode_str;
6022 $mode_chng .= "]</span>";
6024 print $cgi->a({-href
=> href
(action
=>"blob", hash
=>$diff->{'to_id'},
6025 hash_base
=>$hash, file_name
=>$diff->{'file'}),
6026 -class => "list"}, esc_path
($diff->{'file'}));
6028 print "<td>$mode_chng</td>\n";
6029 print "<td class=\"link\">";
6030 if ($action eq 'commitdiff') {
6033 print $cgi->a({-href
=> href
(-anchor
=>"patch$patchno")},
6037 print $cgi->a({-href
=> href
(action
=>"blob", hash
=>$diff->{'to_id'},
6038 hash_base
=>$hash, file_name
=>$diff->{'file'})},
6042 } elsif ($diff->{'status'} eq "D") { # deleted
6043 my $mode_chng = "<span class=\"file_status deleted\">[deleted $from_file_type]</span>";
6045 print $cgi->a({-href
=> href
(action
=>"blob", hash
=>$diff->{'from_id'},
6046 hash_base
=>$parent, file_name
=>$diff->{'file'}),
6047 -class => "list"}, esc_path
($diff->{'file'}));
6049 print "<td>$mode_chng</td>\n";
6050 print "<td class=\"link\">";
6051 if ($action eq 'commitdiff') {
6054 print $cgi->a({-href
=> href
(-anchor
=>"patch$patchno")},
6058 print $cgi->a({-href
=> href
(action
=>"blob", hash
=>$diff->{'from_id'},
6059 hash_base
=>$parent, file_name
=>$diff->{'file'})},
6062 print $cgi->a({-href
=> href
(action
=>"blame", hash_base
=>$parent,
6063 file_name
=>$diff->{'file'}),
6064 -class => "blamelink"},
6067 print $cgi->a({-href
=> href
(action
=>"history", hash_base
=>$parent,
6068 file_name
=>$diff->{'file'})},
6072 } elsif ($diff->{'status'} eq "M" || $diff->{'status'} eq "T") { # modified, or type changed
6073 my $mode_chnge = "";
6074 if ($diff->{'from_mode'} != $diff->{'to_mode'}) {
6075 $mode_chnge = "<span class=\"file_status mode_chnge\">[changed";
6076 if ($from_file_type ne $to_file_type) {
6077 $mode_chnge .= " from $from_file_type to $to_file_type";
6079 if (($from_mode_oct & 0777) != ($to_mode_oct & 0777)) {
6080 if ($from_mode_str && $to_mode_str) {
6081 $mode_chnge .= " mode: $from_mode_str->$to_mode_str";
6082 } elsif ($to_mode_str) {
6083 $mode_chnge .= " mode: $to_mode_str";
6086 $mode_chnge .= "]</span>\n";
6089 print $cgi->a({-href
=> href
(action
=>"blob", hash
=>$diff->{'to_id'},
6090 hash_base
=>$hash, file_name
=>$diff->{'file'}),
6091 -class => "list"}, esc_path
($diff->{'file'}));
6093 print "<td>$mode_chnge</td>\n";
6094 print "<td class=\"link\">";
6095 if ($action eq 'commitdiff') {
6098 print $cgi->a({-href
=> href
(-anchor
=>"patch$patchno")},
6101 } elsif ($diff->{'to_id'} ne $diff->{'from_id'}) {
6102 # "commit" view and modified file (not onlu mode changed)
6103 print $cgi->a({-href
=> href
(action
=>"blobdiff",
6104 hash
=>$diff->{'to_id'}, hash_parent
=>$diff->{'from_id'},
6105 hash_base
=>$hash, hash_parent_base
=>$parent,
6106 file_name
=>$diff->{'file'})},
6110 print $cgi->a({-href
=> href
(action
=>"blob", hash
=>$diff->{'to_id'},
6111 hash_base
=>$hash, file_name
=>$diff->{'file'})},
6114 print $cgi->a({-href
=> href
(action
=>"blame", hash_base
=>$hash,
6115 file_name
=>$diff->{'file'}),
6116 -class => "blamelink"},
6119 print $cgi->a({-href
=> href
(action
=>"history", hash_base
=>$hash,
6120 file_name
=>$diff->{'file'})},
6124 } elsif ($diff->{'status'} eq "R" || $diff->{'status'} eq "C") { # renamed or copied
6125 my %status_name = ('R' => 'moved', 'C' => 'copied');
6126 my $nstatus = $status_name{$diff->{'status'}};
6128 if ($diff->{'from_mode'} != $diff->{'to_mode'}) {
6129 # mode also for directories, so we cannot use $to_mode_str
6130 $mode_chng = sprintf(", mode: %04o", $to_mode_oct & 0777);
6133 $cgi->a({-href
=> href
(action
=>"blob", hash_base
=>$hash,
6134 hash
=>$diff->{'to_id'}, file_name
=>$diff->{'to_file'}),
6135 -class => "list"}, esc_path
($diff->{'to_file'})) . "</td>\n" .
6136 "<td><span class=\"file_status $nstatus\">[$nstatus from " .
6137 $cgi->a({-href
=> href
(action
=>"blob", hash_base
=>$parent,
6138 hash
=>$diff->{'from_id'}, file_name
=>$diff->{'from_file'}),
6139 -class => "list"}, esc_path
($diff->{'from_file'})) .
6140 " with " . (int $diff->{'similarity'}) . "% similarity$mode_chng]</span></td>\n" .
6141 "<td class=\"link\">";
6142 if ($action eq 'commitdiff') {
6145 print $cgi->a({-href
=> href
(-anchor
=>"patch$patchno")},
6148 } elsif ($diff->{'to_id'} ne $diff->{'from_id'}) {
6149 # "commit" view and modified file (not only pure rename or copy)
6150 print $cgi->a({-href
=> href
(action
=>"blobdiff",
6151 hash
=>$diff->{'to_id'}, hash_parent
=>$diff->{'from_id'},
6152 hash_base
=>$hash, hash_parent_base
=>$parent,
6153 file_name
=>$diff->{'to_file'}, file_parent
=>$diff->{'from_file'})},
6157 print $cgi->a({-href
=> href
(action
=>"blob", hash
=>$diff->{'to_id'},
6158 hash_base
=>$parent, file_name
=>$diff->{'to_file'})},
6161 print $cgi->a({-href
=> href
(action
=>"blame", hash_base
=>$hash,
6162 file_name
=>$diff->{'to_file'}),
6163 -class => "blamelink"},
6166 print $cgi->a({-href
=> href
(action
=>"history", hash_base
=>$hash,
6167 file_name
=>$diff->{'to_file'})},
6171 } # we should not encounter Unmerged (U) or Unknown (X) status
6174 print "</tbody>" if $has_header;
6178 # Print context lines and then rem/add lines in a side-by-side manner.
6179 sub print_sidebyside_diff_lines
{
6180 my ($ctx, $rem, $add) = @_;
6182 # print context block before add/rem block
6185 '<div class="chunk_block ctx">',
6186 '<div class="old">',
6189 '<div class="new">',
6198 '<div class="chunk_block rem">',
6199 '<div class="old">',
6206 '<div class="chunk_block add">',
6207 '<div class="new">',
6213 '<div class="chunk_block chg">',
6214 '<div class="old">',
6217 '<div class="new">',
6224 # Print context lines and then rem/add lines in inline manner.
6225 sub print_inline_diff_lines
{
6226 my ($ctx, $rem, $add) = @_;
6228 print @
$ctx, @
$rem, @
$add;
6231 # Format removed and added line, mark changed part and HTML-format them.
6232 # Implementation is based on contrib/diff-highlight
6233 sub format_rem_add_lines_pair
{
6234 my ($rem, $add, $num_parents) = @_;
6236 # We need to untabify lines before split()'ing them;
6237 # otherwise offsets would be invalid.
6240 $rem = untabify
($rem);
6241 $add = untabify
($add);
6243 my @rem = split(//, $rem);
6244 my @add = split(//, $add);
6245 my ($esc_rem, $esc_add);
6246 # Ignore leading +/- characters for each parent.
6247 my ($prefix_len, $suffix_len) = ($num_parents, 0);
6248 my ($prefix_has_nonspace, $suffix_has_nonspace);
6250 my $shorter = (@rem < @add) ?
@rem : @add;
6251 while ($prefix_len < $shorter) {
6252 last if ($rem[$prefix_len] ne $add[$prefix_len]);
6254 $prefix_has_nonspace = 1 if ($rem[$prefix_len] !~ /\s/);
6258 while ($prefix_len + $suffix_len < $shorter) {
6259 last if ($rem[-1 - $suffix_len] ne $add[-1 - $suffix_len]);
6261 $suffix_has_nonspace = 1 if ($rem[-1 - $suffix_len] !~ /\s/);
6265 # Mark lines that are different from each other, but have some common
6266 # part that isn't whitespace. If lines are completely different, don't
6267 # mark them because that would make output unreadable, especially if
6268 # diff consists of multiple lines.
6269 if ($prefix_has_nonspace || $suffix_has_nonspace) {
6270 $esc_rem = esc_html_hl_regions
($rem, 'marked',
6271 [$prefix_len, @rem - $suffix_len], -nbsp
=>1);
6272 $esc_add = esc_html_hl_regions
($add, 'marked',
6273 [$prefix_len, @add - $suffix_len], -nbsp
=>1);
6275 $esc_rem = esc_html
($rem, -nbsp
=>1);
6276 $esc_add = esc_html
($add, -nbsp
=>1);
6279 return format_diff_line
(\
$esc_rem, 'rem'),
6280 format_diff_line
(\
$esc_add, 'add');
6283 # HTML-format diff context, removed and added lines.
6284 sub format_ctx_rem_add_lines
{
6285 my ($ctx, $rem, $add, $num_parents) = @_;
6286 my (@new_ctx, @new_rem, @new_add);
6287 my $can_highlight = 0;
6288 my $is_combined = ($num_parents > 1);
6290 # Highlight if every removed line has a corresponding added line.
6291 if (@
$add > 0 && @
$add == @
$rem) {
6294 # Highlight lines in combined diff only if the chunk contains
6295 # diff between the same version, e.g.
6302 # Otherwise the highlightling would be confusing.
6304 for (my $i = 0; $i < @
$add; $i++) {
6305 my $prefix_rem = substr($rem->[$i], 0, $num_parents);
6306 my $prefix_add = substr($add->[$i], 0, $num_parents);
6308 $prefix_rem =~ s/-/+/g;
6310 if ($prefix_rem ne $prefix_add) {
6318 if ($can_highlight) {
6319 for (my $i = 0; $i < @
$add; $i++) {
6320 my ($line_rem, $line_add) = format_rem_add_lines_pair
(
6321 $rem->[$i], $add->[$i], $num_parents);
6322 push @new_rem, $line_rem;
6323 push @new_add, $line_add;
6326 @new_rem = map { format_diff_line
($_, 'rem') } @
$rem;
6327 @new_add = map { format_diff_line
($_, 'add') } @
$add;
6330 @new_ctx = map { format_diff_line
($_, 'ctx') } @
$ctx;
6332 return (\
@new_ctx, \
@new_rem, \
@new_add);
6335 # Print context lines and then rem/add lines.
6336 sub print_diff_lines
{
6337 my ($ctx, $rem, $add, $diff_style, $num_parents) = @_;
6338 my $is_combined = $num_parents > 1;
6340 ($ctx, $rem, $add) = format_ctx_rem_add_lines
($ctx, $rem, $add,
6343 if ($diff_style eq 'sidebyside' && !$is_combined) {
6344 print_sidebyside_diff_lines
($ctx, $rem, $add);
6346 # default 'inline' style and unknown styles
6347 print_inline_diff_lines
($ctx, $rem, $add);
6351 sub print_diff_chunk
{
6352 my ($diff_style, $num_parents, $from, $to, @chunk) = @_;
6353 my (@ctx, @rem, @add);
6355 # The class of the previous line.
6356 my $prev_class = '';
6358 return unless @chunk;
6360 # incomplete last line might be among removed or added lines,
6361 # or both, or among context lines: find which
6362 for (my $i = 1; $i < @chunk; $i++) {
6363 if ($chunk[$i][0] eq 'incomplete') {
6364 $chunk[$i][0] = $chunk[$i-1][0];
6369 push @chunk, ["", ""];
6371 foreach my $line_info (@chunk) {
6372 my ($class, $line) = @
$line_info;
6374 # print chunk headers
6375 if ($class && $class eq 'chunk_header') {
6376 print format_diff_line
($line, $class, $from, $to);
6380 ## print from accumulator when have some add/rem lines or end
6381 # of chunk (flush context lines), or when have add and rem
6382 # lines and new block is reached (otherwise add/rem lines could
6384 if (!$class || ((@rem || @add) && $class eq 'ctx') ||
6385 (@rem && @add && $class ne $prev_class)) {
6386 print_diff_lines
(\
@ctx, \
@rem, \
@add,
6387 $diff_style, $num_parents);
6388 @ctx = @rem = @add = ();
6391 ## adding lines to accumulator
6394 # rem, add or change
6395 if ($class eq 'rem') {
6397 } elsif ($class eq 'add') {
6401 if ($class eq 'ctx') {
6405 $prev_class = $class;
6409 sub git_patchset_body
{
6410 my ($fd, $diff_style, $difftree, $hash, @hash_parents) = @_;
6411 my ($hash_parent) = $hash_parents[0];
6413 my $is_combined = (@hash_parents > 1);
6415 my $patch_number = 0;
6420 my @chunk; # for side-by-side diff
6422 print "<div class=\"patchset\">\n";
6424 # skip to first patch
6425 while ($patch_line = to_utf8
(scalar <$fd>)) {
6428 last if ($patch_line =~ m/^diff /);
6432 while ($patch_line) {
6434 # parse "git diff" header line
6435 if ($patch_line =~ m/^diff --git (\"(?:[^\\\"]*(?:\\.[^\\\"]*)*)\"|[^ "]*) (.*)$/) {
6436 # $1 is from_name, which we do not use
6437 $to_name = unquote
($2);
6438 $to_name =~ s!^b/!!;
6439 } elsif ($patch_line =~ m/^diff --(cc|combined) ("?.*"?)$/) {
6440 # $1 is 'cc' or 'combined', which we do not use
6441 $to_name = unquote
($2);
6446 # check if current patch belong to current raw line
6447 # and parse raw git-diff line if needed
6448 if (is_patch_split
($diffinfo, { 'to_file' => $to_name })) {
6449 # this is continuation of a split patch
6450 print "<div class=\"patch cont\">\n";
6452 # advance raw git-diff output if needed
6453 $patch_idx++ if defined $diffinfo;
6455 # read and prepare patch information
6456 $diffinfo = parsed_difftree_line
($difftree->[$patch_idx]);
6458 # compact combined diff output can have some patches skipped
6459 # find which patch (using pathname of result) we are at now;
6461 while ($to_name ne $diffinfo->{'to_file'}) {
6462 print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n" .
6463 format_diff_cc_simplified
($diffinfo, @hash_parents) .
6464 "</div>\n"; # class="patch"
6469 last if $patch_idx > $#$difftree;
6470 $diffinfo = parsed_difftree_line
($difftree->[$patch_idx]);
6474 # modifies %from, %to hashes
6475 parse_from_to_diffinfo
($diffinfo, \
%from, \
%to, @hash_parents);
6477 # this is first patch for raw difftree line with $patch_idx index
6478 # we index @$difftree array from 0, but number patches from 1
6479 print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n";
6483 #assert($patch_line =~ m/^diff /) if DEBUG;
6484 #assert($patch_line !~ m!$/$!) if DEBUG; # is chomp-ed
6486 # print "git diff" header
6487 print format_git_diff_header_line
($patch_line, $diffinfo,
6490 # print extended diff header
6491 print "<div class=\"diff extended_header\">\n";
6493 while ($patch_line = to_utf8
(scalar<$fd>)) {
6496 last EXTENDED_HEADER
if ($patch_line =~ m/^--- |^diff /);
6498 print format_extended_diff_header_line
($patch_line, $diffinfo,
6501 print "</div>\n"; # class="diff extended_header"
6503 # from-file/to-file diff header
6504 if (! $patch_line) {
6505 print "</div>\n"; # class="patch"
6508 next PATCH
if ($patch_line =~ m/^diff /);
6509 #assert($patch_line =~ m/^---/) if DEBUG;
6511 my $last_patch_line = $patch_line;
6512 $patch_line = to_utf8
(scalar <$fd>);
6514 #assert($patch_line =~ m/^\+\+\+/) if DEBUG;
6516 print format_diff_from_to_header
($last_patch_line, $patch_line,
6517 $diffinfo, \
%from, \
%to,
6522 while ($patch_line = to_utf8
(scalar <$fd>)) {
6525 next PATCH
if ($patch_line =~ m/^diff /);
6527 my $class = diff_line_class
($patch_line, \
%from, \
%to);
6529 if ($class eq 'chunk_header') {
6530 print_diff_chunk
($diff_style, scalar @hash_parents, \
%from, \
%to, @chunk);
6534 push @chunk, [ $class, $patch_line ];
6539 print_diff_chunk
($diff_style, scalar @hash_parents, \
%from, \
%to, @chunk);
6542 print "</div>\n"; # class="patch"
6545 # for compact combined (--cc) format, with chunk and patch simplification
6546 # the patchset might be empty, but there might be unprocessed raw lines
6547 for (++$patch_idx if $patch_number > 0;
6548 $patch_idx < @
$difftree;
6550 # read and prepare patch information
6551 $diffinfo = parsed_difftree_line
($difftree->[$patch_idx]);
6553 # generate anchor for "patch" links in difftree / whatchanged part
6554 print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n" .
6555 format_diff_cc_simplified
($diffinfo, @hash_parents) .
6556 "</div>\n"; # class="patch"
6561 if ($patch_number == 0) {
6562 if (@hash_parents > 1) {
6563 print "<div class=\"diff nodifferences\">Trivial merge</div>\n";
6565 print "<div class=\"diff nodifferences\">No differences found</div>\n";
6569 print "</div>\n"; # class="patchset"
6572 # . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
6574 sub git_project_search_form
{
6575 my ($searchtext, $search_use_regexp) = @_;
6578 if ($project_filter) {
6579 $limit = " in '$project_filter'";
6582 print "<div class=\"projsearch\">\n";
6583 print $cgi->start_form(-method
=> 'get', -action
=> $my_uri) .
6584 $cgi->hidden(-name
=> 'a', -value
=> 'project_list') . "\n";
6585 print $cgi->hidden(-name
=> 'pf', -value
=> $project_filter). "\n"
6586 if (defined $project_filter);
6587 print $cgi->textfield(-name
=> 's', -value
=> $searchtext,
6588 -title
=> "Search project by name and description$limit",
6589 -size
=> 60) . "\n" .
6590 "<span title=\"Extended regular expression\">" .
6591 $cgi->checkbox(-name
=> 'sr', -value
=> 1, -label
=> 're',
6592 -checked
=> $search_use_regexp) .
6594 $cgi->submit(-name
=> 'btnS', -value
=> 'Search') .
6595 $cgi->end_form() . "\n" .
6596 "<span class=\"projectlist_link\">" .
6597 $cgi->a({-href
=> href
(project
=> undef, searchtext
=> undef,
6598 action
=> 'project_list',
6599 project_filter
=> $project_filter)},
6600 esc_html
("List all projects$limit")) . "</span><br />\n";
6601 print "<span class=\"projectlist_link\">" .
6602 $cgi->a({-href
=> href
(project
=> undef, searchtext
=> undef,
6603 action
=> 'project_list',
6604 project_filter
=> undef)},
6605 esc_html
("List all projects")) . "</span>\n" if $project_filter;
6609 # entry for given @keys needs filling if at least one of keys in list
6610 # is not present in %$project_info
6611 sub project_info_needs_filling
{
6612 my ($project_info, @keys) = @_;
6614 # return List::MoreUtils::any { !exists $project_info->{$_} } @keys;
6615 foreach my $key (@keys) {
6616 if (!exists $project_info->{$key}) {
6623 sub git_cache_file_format
{
6624 return GITWEB_CACHE_FORMAT
.
6625 (gitweb_check_feature
('forks') ?
" (forks)" : "");
6628 sub git_retrieve_cache_file
{
6629 my $cache_file = shift;
6631 use Storable
qw(retrieve);
6633 if ((my $dump = eval { retrieve
($cache_file) })) {
6635 ref($dump) eq 'ARRAY' &&
6637 ref($$dump[1]) eq 'ARRAY' &&
6638 @
{$$dump[1]} == 2 &&
6639 ref(${$$dump[1]}[0]) eq 'ARRAY' &&
6640 ref(${$$dump[1]}[1]) eq 'HASH' &&
6641 $$dump[0] eq git_cache_file_format
();
6647 sub git_store_cache_file
{
6648 my ($cache_file, $cachedata) = @_;
6650 use File
::Basename
qw(dirname);
6652 use POSIX
qw(:fcntl_h);
6653 use Storable
qw(store_fd);
6656 my $cache_d = dirname
($cache_file);
6658 umask($mask & ~0070) if $cache_grpshared;
6659 if ((-d
$cache_d || mkdir($cache_d, $cache_grpshared ?
0770 : 0700)) &&
6660 sysopen(my $fd, "$cache_file.lock", O_WRONLY
|O_CREAT
|O_EXCL
, $cache_grpshared ?
0660 : 0600)) {
6661 store_fd
([git_cache_file_format
(), $cachedata], $fd);
6663 rename "$cache_file.lock", $cache_file;
6664 $result = stat($cache_file)->mtime;
6666 umask($mask) if $cache_grpshared;
6670 sub verify_cached_project
{
6671 my ($hashref, $path) = @_;
6672 return undef unless $path;
6673 delete $$hashref{$path}, return undef unless is_valid_project
($path);
6674 return $$hashref{$path} if exists $$hashref{$path};
6676 # A valid project was requested but it's not yet in the cache
6677 # Manufacture a minimal project entry (path, name, description)
6678 # Also provide age, but only if it's available via $lastactivity_file
6680 my %proj = ('path' => $path);
6681 my $val = git_get_project_description
($path);
6682 defined $val or $val = '';
6683 $proj{'descr_long'} = $val;
6684 $proj{'descr'} = chop_str
($val, $projects_list_description_width, 5);
6685 unless ($omit_owner) {
6686 $val = git_get_project_owner
($path);
6687 defined $val or $val = '';
6688 $proj{'owner'} = $val;
6690 unless ($omit_age_column) {
6691 ($val) = git_get_last_activity
($path, 1);
6692 $proj{'age_epoch'} = $val if defined $val;
6694 $$hashref{$path} = \
%proj;
6698 sub git_filter_cached_projects
{
6699 my ($cache, $projlist, $verify) = @_;
6700 my $hashref = $$cache[1];
6702 sub {verify_cached_project
($hashref, $_[0])} :
6703 sub {$$hashref{$_[0]}};
6705 my $c = &$sub($_->{'path'});
6706 defined $c ?
($_ = $c) : ()
6710 # fills project list info (age, description, owner, category, forks, etc.)
6711 # for each project in the list, removing invalid projects from
6712 # returned list, or fill only specified info.
6714 # Invalid projects are removed from the returned list if and only if you
6715 # ask 'age_epoch' to be filled, because they are the only fields
6716 # that run unconditionally git command that requires repository, and
6717 # therefore do always check if project repository is invalid.
6720 # * fill_project_list_info(\@project_list, 'descr_long', 'ctags')
6721 # ensures that 'descr_long' and 'ctags' fields are filled
6722 # * @project_list = fill_project_list_info(\@project_list)
6723 # ensures that all fields are filled (and invalid projects removed)
6725 # NOTE: modifies $projlist, but does not remove entries from it
6726 sub fill_project_list_info
{
6727 my ($projlist, @wanted_keys) = @_;
6729 my $rebuild = @wanted_keys && $wanted_keys[0] eq 'rebuild-cache' && shift @wanted_keys;
6730 return fill_project_list_info_uncached
($projlist, @wanted_keys)
6731 unless $projlist_cache_lifetime && $projlist_cache_lifetime > 0;
6735 my $cache_lifetime = $rebuild ?
0 : $projlist_cache_lifetime;
6736 my $cache_file = "$cache_dir/$projlist_cache_name";
6742 if ($cache_lifetime && -f
$cache_file) {
6743 $cache_mtime = stat($cache_file)->mtime;
6744 $cache_dump = undef if $cache_mtime &&
6745 (!$cache_dump_mtime || $cache_dump_mtime != $cache_mtime);
6747 if (defined $cache_mtime && # caching is on and $cache_file exists
6748 $cache_mtime + $cache_lifetime*60 > $now &&
6749 ($cache_dump || ($cache_dump = git_retrieve_cache_file
($cache_file)))) {
6751 $cache_dump_mtime = $cache_mtime;
6752 $stale = $now - $cache_mtime;
6753 my $verify = ($action eq 'summary' || $action eq 'forks') &&
6754 gitweb_check_feature
('forks');
6755 @projects = git_filter_cached_projects
($cache_dump, $projlist, $verify);
6757 } else { # Cache miss.
6758 if (defined $cache_mtime) {
6759 # Postpone timeout by two minutes so that we get
6760 # enough time to do our job, or to be more exact
6761 # make cache expire after two minutes from now.
6762 my $time = $now - $cache_lifetime*60 + 120;
6763 utime $time, $time, $cache_file;
6765 my @all_projects = git_get_projects_list
();
6766 my %all_projects_filled = map { ( $_->{'path'} => $_ ) }
6767 fill_project_list_info_uncached
(\
@all_projects);
6768 map { $all_projects_filled{$_->{'path'}} = $_ }
6769 filter_forks_from_projects_list
([values(%all_projects_filled)])
6770 if gitweb_check_feature
('forks');
6771 $cache_dump = [[sort {$a->{'path'} cmp $b->{'path'}} values(%all_projects_filled)],
6772 \
%all_projects_filled];
6773 $cache_dump_mtime = git_store_cache_file
($cache_file, $cache_dump);
6774 @projects = git_filter_cached_projects
($cache_dump, $projlist);
6777 if ($cache_lifetime && $stale > 0) {
6778 print "<div class=\"stale_info\">Cached version (${stale}s old)</div>\n"
6779 unless $shown_stale_message;
6780 $shown_stale_message = 1;
6786 sub fill_project_list_info_uncached
{
6787 my ($projlist, @wanted_keys) = @_;
6789 my $filter_set = sub { return @_; };
6791 my %wanted_keys = map { $_ => 1 } @wanted_keys;
6792 $filter_set = sub { return grep { $wanted_keys{$_} } @_; };
6795 my $show_ctags = gitweb_check_feature
('ctags');
6797 foreach my $pr (@
$projlist) {
6798 if (project_info_needs_filling
($pr, $filter_set->('age_epoch'))) {
6799 my (@activity) = git_get_last_activity
($pr->{'path'});
6800 unless (@activity) {
6803 ($pr->{'age_epoch'}) = @activity;
6805 if (project_info_needs_filling
($pr, $filter_set->('descr', 'descr_long'))) {
6806 my $descr = git_get_project_description
($pr->{'path'}) || "";
6807 $descr = to_utf8
($descr);
6808 $pr->{'descr_long'} = $descr;
6809 $pr->{'descr'} = chop_str
($descr, $projects_list_description_width, 5);
6811 if (project_info_needs_filling
($pr, $filter_set->('owner'))) {
6812 $pr->{'owner'} = git_get_project_owner
("$pr->{'path'}") || "";
6815 project_info_needs_filling
($pr, $filter_set->('ctags'))) {
6816 $pr->{'ctags'} = git_get_project_ctags
($pr->{'path'});
6818 if ($projects_list_group_categories &&
6819 project_info_needs_filling
($pr, $filter_set->('category'))) {
6820 my $cat = git_get_project_category
($pr->{'path'}) ||
6821 $project_list_default_category;
6822 $pr->{'category'} = to_utf8
($cat);
6825 push @projects, $pr;
6831 sub sort_projects_list
{
6832 my ($projlist, $order) = @_;
6836 return sub { lc($a->{$key}) cmp lc($b->{$key}) };
6839 sub order_reverse_num_then_undef
{
6842 defined $a->{$key} ?
6843 (defined $b->{$key} ?
$b->{$key} <=> $a->{$key} : -1) :
6844 (defined $b->{$key} ?
1 : 0)
6849 project
=> order_str
('path'),
6850 descr
=> order_str
('descr_long'),
6851 owner
=> order_str
('owner'),
6852 age
=> order_reverse_num_then_undef
('age_epoch'),
6855 my $ordering = $orderings{$order};
6856 return defined $ordering ?
sort $ordering @
$projlist : @
$projlist;
6859 # returns a hash of categories, containing the list of project
6860 # belonging to each category
6861 sub build_projlist_by_category
{
6862 my ($projlist, $from, $to) = @_;
6865 $from = 0 unless defined $from;
6866 $to = $#$projlist if (!defined $to || $#$projlist < $to);
6868 for (my $i = $from; $i <= $to; $i++) {
6869 my $pr = $projlist->[$i];
6870 push @
{$categories{ $pr->{'category'} }}, $pr;
6873 return wantarray ?
%categories : \
%categories;
6876 # print 'sort by' <th> element, generating 'sort by $name' replay link
6877 # if that order is not selected
6879 print format_sort_th
(@_);
6882 sub format_sort_th
{
6883 my ($name, $order, $header) = @_;
6885 $header ||= ucfirst($name);
6887 if ($order eq $name) {
6888 $sort_th .= "<th>$header</th>\n";
6890 $sort_th .= "<th>" .
6891 $cgi->a({-href
=> href
(-replay
=>1, order
=>$name),
6892 -class => "header"}, $header) .
6899 sub git_project_list_rows
{
6900 my ($projlist, $from, $to, $check_forks) = @_;
6902 $from = 0 unless defined $from;
6903 $to = $#$projlist if (!defined $to || $#$projlist < $to);
6907 for (my $i = $from; $i <= $to; $i++) {
6908 my $pr = $projlist->[$i];
6911 print "<tr class=\"dark\">\n";
6913 print "<tr class=\"light\">\n";
6919 if ($pr->{'forks'}) {
6920 my $nforks = scalar @
{$pr->{'forks'}};
6921 my $s = $nforks == 1 ?
'' : 's';
6923 print $cgi->a({-href
=> href
(project
=>$pr->{'path'}, action
=>"forks"),
6924 -title
=> "$nforks fork$s"}, "+");
6926 print $cgi->span({-title
=> "$nforks fork$s"}, "+");
6931 my $path = $pr->{'path'};
6932 my $dotgit = $path =~ s/\.git$// ?
'.git' : '';
6933 print "<td>" . $cgi->a({-href
=> href
(project
=>$pr->{'path'}, action
=>"summary"),
6935 esc_html_match_hl
($path, $search_regexp).$dotgit) .
6937 "<td>" . $cgi->a({-href
=> href
(project
=>$pr->{'path'}, action
=>"summary"),
6939 -title
=> $pr->{'descr_long'}},
6941 ? esc_html_match_hl_chopped
($pr->{'descr_long'},
6942 $pr->{'descr'}, $search_regexp)
6943 : esc_html
($pr->{'descr'})) .
6945 unless ($omit_owner) {
6946 print "<td><i>" . ($owner_link_hook
6947 ?
$cgi->a({-href
=> $owner_link_hook->($pr->{'owner'}), -class => "list"},
6948 chop_and_escape_str
($pr->{'owner'}, 15))
6949 : chop_and_escape_str
($pr->{'owner'}, 15)) . "</i></td>\n";
6951 unless ($omit_age_column) {
6952 my ($age_epoch, $age_string) = ($pr->{'age_epoch'});
6953 $age_string = defined $age_epoch ? age_string
($age_epoch, $now) : "No commits";
6954 print "<td class=\"". age_class
($age_epoch, $now) . "\">" . $age_string . "</td>\n";
6956 print"<td class=\"link\">" .
6957 $cgi->a({-href
=> href
(project
=>$pr->{'path'}, action
=>"summary")}, "summary") . " | " .
6958 $cgi->a({-href
=> href
(project
=>$pr->{'path'}, action
=>"shortlog")}, "log") . " | " .
6959 $cgi->a({-href
=> href
(project
=>$pr->{'path'}, action
=>"tree")}, "tree") .
6960 ($pr->{'forks'} ?
" | " . $cgi->a({-href
=> href
(project
=>$pr->{'path'}, action
=>"forks")}, "forks") : '') .
6966 sub git_project_list_body
{
6967 # actually uses global variable $project
6968 my ($projlist, $order, $from, $to, $extra, $no_header, $ctags_action, $keep_top) = @_;
6969 my @projects = @
$projlist;
6971 my $check_forks = gitweb_check_feature
('forks');
6972 my $show_ctags = gitweb_check_feature
('ctags');
6973 my $tagfilter = $show_ctags ?
$input_params{'ctag_filter'} : undef;
6974 $check_forks = undef
6975 if ($tagfilter || $search_regexp);
6977 # filtering out forks before filling info allows us to do less work
6979 @projects = filter_forks_from_projects_list
(\
@projects);
6980 push @projects, { 'path' => "$project_filter.git" }
6981 if $project_filter && $keep_top && is_valid_project
("$project_filter.git");
6983 # search_projects_list pre-fills required info
6984 @projects = search_projects_list
(\
@projects,
6985 'search_regexp' => $search_regexp,
6986 'tagfilter' => $tagfilter)
6987 if ($tagfilter || $search_regexp);
6989 my @all_fields = ('descr', 'descr_long', 'ctags', 'category');
6990 push @all_fields, 'age_epoch' unless($omit_age_column);
6991 push @all_fields, 'owner' unless($omit_owner);
6992 @projects = fill_project_list_info
(\
@projects, @all_fields);
6994 $order ||= $default_projects_order;
6995 $from = 0 unless defined $from;
6996 $to = $#projects if (!defined $to || $#projects < $to);
7001 "<b>No such projects found</b><br />\n".
7002 "Click ".$cgi->a({-href
=>href
(project
=>undef,action
=>'project_list')},"here")." to view all projects<br />\n".
7003 "</center>\n<br />\n";
7007 @projects = sort_projects_list
(\
@projects, $order);
7010 my $ctags = git_gather_all_ctags
(\
@projects);
7011 my $cloud = git_populate_project_tagcloud
($ctags, $ctags_action||'project_list');
7012 print git_show_project_tagcloud
($cloud, 64);
7015 print "<table class=\"project_list\">\n";
7016 unless ($no_header) {
7019 print "<th></th>\n";
7021 print_sort_th
('project', $order, 'Project');
7022 print_sort_th
('descr', $order, 'Description');
7023 print_sort_th
('owner', $order, 'Owner') unless $omit_owner;
7024 print_sort_th
('age', $order, 'Last Change') unless $omit_age_column;
7025 print "<th></th>\n" . # for links
7029 if ($projects_list_group_categories) {
7030 # only display categories with projects in the $from-$to window
7031 @projects = sort {$a->{'category'} cmp $b->{'category'}} @projects[$from..$to];
7032 my %categories = build_projlist_by_category
(\
@projects, $from, $to);
7033 foreach my $cat (sort keys %categories) {
7034 unless ($cat eq "") {
7037 print "<td></td>\n";
7039 print "<td class=\"category\" colspan=\"5\">".esc_html
($cat)."</td>\n";
7043 git_project_list_rows
($categories{$cat}, undef, undef, $check_forks);
7046 git_project_list_rows
(\
@projects, $from, $to, $check_forks);
7049 if (defined $extra) {
7052 print "<td></td>\n";
7054 print "<td colspan=\"5\">$extra</td>\n" .
7061 # uses global variable $project
7062 my ($commitlist, $from, $to, $refs, $extra) = @_;
7064 $from = 0 unless defined $from;
7065 $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
7067 for (my $i = 0; $i <= $to; $i++) {
7068 my %co = %{$commitlist->[$i]};
7070 my $commit = $co{'id'};
7071 my $ref = format_ref_marker
($refs, $commit);
7072 git_print_header_div
('commit',
7073 "<span class=\"age\">$co{'age_string'}</span>" .
7074 esc_html
($co{'title'}),
7075 $commit, undef, $ref);
7076 print "<div class=\"title_text\">\n" .
7077 "<div class=\"log_link\">\n" .
7078 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$commit)}, "commit") .
7080 $cgi->a({-href
=> href
(action
=>"commitdiff", hash
=>$commit)}, "commitdiff") .
7082 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$commit, hash_base
=>$commit)}, "tree") .
7085 git_print_authorship
(\
%co, -tag
=> 'span');
7086 print "<br/>\n</div>\n";
7088 print "<div class=\"log_body\">\n";
7089 git_print_log
($co{'comment'}, -final_empty_line
=> 1);
7093 print "<div class=\"page_nav\">\n";
7099 sub git_shortlog_body
{
7100 # uses global variable $project
7101 my ($commitlist, $from, $to, $refs, $extra) = @_;
7103 $from = 0 unless defined $from;
7104 $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
7106 print "<table class=\"shortlog\">\n";
7108 for (my $i = $from; $i <= $to; $i++) {
7109 my %co = %{$commitlist->[$i]};
7110 my $commit = $co{'id'};
7111 my $ref = format_ref_marker
($refs, $commit);
7113 print "<tr class=\"dark\">\n";
7115 print "<tr class=\"light\">\n";
7118 # git_summary() used print "<td><i>$co{'age_string'}</i></td>\n" .
7119 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
7120 format_author_html
('td', \
%co, 10) . "<td>";
7121 print format_subject_html
($co{'title'}, $co{'title_short'},
7122 href
(action
=>"commit", hash
=>$commit), $ref);
7124 "<td class=\"link\">" .
7125 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$commit)}, "commit") . " | " .
7126 $cgi->a({-href
=> href
(action
=>"commitdiff", hash
=>$commit)}, "commitdiff") . " | " .
7127 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$commit, hash_base
=>$commit)}, "tree");
7128 my $snapshot_links = format_snapshot_links
($commit);
7129 if (defined $snapshot_links) {
7130 print " | " . $snapshot_links;
7135 if (defined $extra) {
7137 "<td colspan=\"4\">$extra</td>\n" .
7143 sub git_history_body
{
7144 # Warning: assumes constant type (blob or tree) during history
7145 my ($commitlist, $from, $to, $refs, $extra,
7146 $file_name, $file_hash, $ftype) = @_;
7148 $from = 0 unless defined $from;
7149 $to = $#{$commitlist} unless (defined $to && $to <= $#{$commitlist});
7151 print "<table class=\"history\">\n";
7153 for (my $i = $from; $i <= $to; $i++) {
7154 my %co = %{$commitlist->[$i]};
7158 my $commit = $co{'id'};
7160 my $ref = format_ref_marker
($refs, $commit);
7163 print "<tr class=\"dark\">\n";
7165 print "<tr class=\"light\">\n";
7168 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
7169 # shortlog: format_author_html('td', \%co, 10)
7170 format_author_html
('td', \
%co, 15, 3) . "<td>";
7171 # originally git_history used chop_str($co{'title'}, 50)
7172 print format_subject_html
($co{'title'}, $co{'title_short'},
7173 href
(action
=>"commit", hash
=>$commit), $ref);
7175 "<td class=\"link\">" .
7176 $cgi->a({-href
=> href
(action
=>$ftype, hash_base
=>$commit, file_name
=>$file_name)}, $ftype) . " | " .
7177 $cgi->a({-href
=> href
(action
=>"commitdiff", hash
=>$commit)}, "commitdiff");
7179 if ($ftype eq 'blob') {
7180 my $blob_current = $file_hash;
7181 my $blob_parent = git_get_hash_by_path
($commit, $file_name);
7182 if (defined $blob_current && defined $blob_parent &&
7183 $blob_current ne $blob_parent) {
7185 $cgi->a({-href
=> href
(action
=>"blobdiff",
7186 hash
=>$blob_current, hash_parent
=>$blob_parent,
7187 hash_base
=>$hash_base, hash_parent_base
=>$commit,
7188 file_name
=>$file_name)},
7195 if (defined $extra) {
7197 "<td colspan=\"4\">$extra</td>\n" .
7204 # uses global variable $project
7205 my ($taglist, $from, $to, $extra, $head_at, $full, $order) = @_;
7206 $from = 0 unless defined $from;
7207 $to = $#{$taglist} if (!defined $to || $#{$taglist} < $to);
7208 $order ||= $default_refs_order;
7210 print "<table class=\"tags\">\n";
7212 print "<tr class=\"tags_header\">\n";
7213 print_sort_th
('age', $order, 'Last Change');
7214 print_sort_th
('name', $order, 'Name');
7215 print "<th></th>\n" . # for comment
7216 "<th></th>\n" . # for tag
7217 "<th></th>\n" . # for links
7221 for (my $i = $from; $i <= $to; $i++) {
7222 my $entry = $taglist->[$i];
7224 my $comment = $tag{'subject'};
7226 if (defined $comment) {
7227 $comment_short = chop_str
($comment, 30, 5);
7229 my $curr = defined $head_at && $tag{'id'} eq $head_at;
7231 print "<tr class=\"dark\">\n";
7233 print "<tr class=\"light\">\n";
7236 if (defined $tag{'age'}) {
7237 print "<td><i>$tag{'age'}</i></td>\n";
7239 print "<td></td>\n";
7241 print(($curr ?
"<td class=\"current_head\">" : "<td>") .
7242 $cgi->a({-href
=> href
(action
=>$tag{'reftype'}, hash
=>$tag{'refid'}),
7243 -class => "list name"}, esc_html
($tag{'name'})) .
7246 if (defined $comment) {
7247 print format_subject_html
($comment, $comment_short,
7248 href
(action
=>"tag", hash
=>$tag{'id'}));
7251 "<td class=\"selflink\">";
7252 if ($tag{'type'} eq "tag") {
7253 print $cgi->a({-href
=> href
(action
=>"tag", hash
=>$tag{'id'})}, "tag");
7258 "<td class=\"link\">" . " | " .
7259 $cgi->a({-href
=> href
(action
=>$tag{'reftype'}, hash
=>$tag{'refid'})}, $tag{'reftype'});
7260 if ($tag{'reftype'} eq "commit") {
7261 print " | " . $cgi->a({-href
=> href
(action
=>"shortlog", hash
=>$tag{'fullname'})}, "log");
7262 print " | " . $cgi->a({-href
=> href
(action
=>"tree", hash
=>$tag{'fullname'})}, "tree") if $full;
7263 } elsif ($tag{'reftype'} eq "blob") {
7264 print " | " . $cgi->a({-href
=> href
(action
=>"blob_plain", hash
=>$tag{'refid'})}, "raw");
7269 if (defined $extra) {
7271 "<td colspan=\"5\">$extra</td>\n" .
7277 sub git_heads_body
{
7278 # uses global variable $project
7279 my ($headlist, $head_at, $from, $to, $extra) = @_;
7280 $from = 0 unless defined $from;
7281 $to = $#{$headlist} if (!defined $to || $#{$headlist} < $to);
7283 print "<table class=\"heads\">\n";
7285 for (my $i = $from; $i <= $to; $i++) {
7286 my $entry = $headlist->[$i];
7288 my $curr = defined $head_at && $ref{'id'} eq $head_at;
7290 print "<tr class=\"dark\">\n";
7292 print "<tr class=\"light\">\n";
7295 print "<td><i>$ref{'age'}</i></td>\n" .
7296 ($curr ?
"<td class=\"current_head\">" : "<td>") .
7297 $cgi->a({-href
=> href
(action
=>"shortlog", hash
=>$ref{'fullname'}),
7298 -class => "list name"},esc_html
($ref{'name'})) .
7300 "<td class=\"link\">" .
7301 $cgi->a({-href
=> href
(action
=>"shortlog", hash
=>$ref{'fullname'})}, "log") . " | " .
7302 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$ref{'fullname'}, hash_base
=>$ref{'fullname'})}, "tree") .
7306 if (defined $extra) {
7308 "<td colspan=\"3\">$extra</td>\n" .
7314 # Display a single remote block
7315 sub git_remote_block
{
7316 my ($remote, $rdata, $limit, $head) = @_;
7318 my $heads = $rdata->{'heads'};
7319 my $fetch = $rdata->{'fetch'};
7320 my $push = $rdata->{'push'};
7322 my $urls_table = "<table class=\"projects_list\">\n" ;
7324 if (defined $fetch) {
7325 if ($fetch eq $push) {
7326 $urls_table .= format_repo_url
("URL", $fetch);
7328 $urls_table .= format_repo_url
("Fetch URL", $fetch);
7329 $urls_table .= format_repo_url
("Push URL", $push) if defined $push;
7331 } elsif (defined $push) {
7332 $urls_table .= format_repo_url
("Push URL", $push);
7334 $urls_table .= format_repo_url
("", "No remote URL");
7337 $urls_table .= "</table>\n";
7340 if (defined $limit && $limit < @
$heads) {
7341 $dots = $cgi->a({-href
=> href
(action
=>"remotes", hash
=>$remote)}, "...");
7345 git_heads_body
($heads, $head, 0, $limit, $dots);
7348 # Display a list of remote names with the respective fetch and push URLs
7349 sub git_remotes_list
{
7350 my ($remotedata, $limit) = @_;
7351 print "<table class=\"heads\">\n";
7353 my @remotes = sort keys %$remotedata;
7355 my $limited = $limit && $limit < @remotes;
7357 $#remotes = $limit - 1 if $limited;
7359 while (my $remote = shift @remotes) {
7360 my $rdata = $remotedata->{$remote};
7361 my $fetch = $rdata->{'fetch'};
7362 my $push = $rdata->{'push'};
7364 print "<tr class=\"dark\">\n";
7366 print "<tr class=\"light\">\n";
7370 $cgi->a({-href
=> href
(action
=>'remotes', hash
=>$remote),
7371 -class=> "list name"},esc_html
($remote)) .
7373 print "<td class=\"link\">" .
7374 (defined $fetch ?
$cgi->a({-href
=> $fetch}, "fetch") : "fetch") .
7376 (defined $push ?
$cgi->a({-href
=> $push}, "push") : "push") .
7384 "<td colspan=\"3\">" .
7385 $cgi->a({-href
=> href
(action
=>"remotes")}, "...") .
7386 "</td>\n" . "</tr>\n";
7392 # Display remote heads grouped by remote, unless there are too many
7393 # remotes, in which case we only display the remote names
7394 sub git_remotes_body
{
7395 my ($remotedata, $limit, $head) = @_;
7396 if ($limit and $limit < keys %$remotedata) {
7397 git_remotes_list
($remotedata, $limit);
7399 fill_remote_heads
($remotedata);
7400 while (my ($remote, $rdata) = each %$remotedata) {
7401 git_print_section
({-class=>"remote", -id
=>$remote},
7402 ["remotes", $remote, $remote], sub {
7403 git_remote_block
($remote, $rdata, $limit, $head);
7409 sub git_search_message
{
7413 if ($searchtype eq 'commit') {
7414 $greptype = "--grep=";
7415 } elsif ($searchtype eq 'author') {
7416 $greptype = "--author=";
7417 } elsif ($searchtype eq 'committer') {
7418 $greptype = "--committer=";
7420 $greptype .= $searchtext;
7421 my @commitlist = parse_commits
($hash, 101, (100 * $page), undef,
7422 $greptype, '--regexp-ignore-case',
7423 $search_use_regexp ?
'--extended-regexp' : '--fixed-strings');
7425 my $paging_nav = '';
7428 $cgi->a({-href
=> href
(-replay
=>1, page
=>undef)},
7431 $cgi->a({-href
=> href
(-replay
=>1, page
=>$page-1),
7432 -accesskey
=> "p", -title
=> "Alt-p"}, "prev");
7434 $paging_nav .= "first · prev";
7437 if ($#commitlist >= 100) {
7439 $cgi->a({-href
=> href
(-replay
=>1, page
=>$page+1),
7440 -accesskey
=> "n", -title
=> "Alt-n"}, "next");
7441 $paging_nav .= " · $next_link";
7443 $paging_nav .= " · next";
7448 git_print_page_nav
('','', $hash,$co{'tree'},$hash, $paging_nav);
7449 git_print_header_div
('commit', esc_html
($co{'title'}), $hash);
7450 if ($page == 0 && !@commitlist) {
7451 print "<p>No match.</p>\n";
7453 git_search_grep_body
(\
@commitlist, 0, 99, $next_link);
7459 sub git_search_changes
{
7463 defined(my $fd = git_cmd_pipe
'--no-pager', 'log', @diff_opts,
7464 '--pretty=format:%H', '--no-abbrev', '--raw', "-S$searchtext",
7465 ($search_use_regexp ?
'--pickaxe-regex' : ()))
7466 or die_error
(500, "Open git-log failed");
7470 git_print_page_nav
('','', $hash,$co{'tree'},$hash);
7471 git_print_header_div
('commit', esc_html
($co{'title'}), $hash);
7473 print "<table class=\"pickaxe search\">\n";
7477 while (my $line = to_utf8
(scalar <$fd>)) {
7481 my %set = parse_difftree_raw_line
($line);
7482 if (defined $set{'commit'}) {
7483 # finish previous commit
7486 "<td class=\"link\">" .
7487 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$co{'id'})},
7490 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$co{'tree'},
7491 hash_base
=>$co{'id'})},
7498 print "<tr class=\"dark\">\n";
7500 print "<tr class=\"light\">\n";
7503 %co = parse_commit
($set{'commit'});
7504 my $author = chop_and_escape_str
($co{'author_name'}, 15, 5);
7505 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
7506 "<td><i>$author</i></td>\n" .
7508 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$co{'id'}),
7509 -class => "list subject"},
7510 chop_and_escape_str
($co{'title'}, 50) . "<br/>");
7511 } elsif (defined $set{'to_id'}) {
7512 next if ($set{'to_id'} =~ m/^0{40}$/);
7514 print $cgi->a({-href
=> href
(action
=>"blob", hash_base
=>$co{'id'},
7515 hash
=>$set{'to_id'}, file_name
=>$set{'to_file'}),
7517 "<span class=\"match\">" . esc_path
($set{'file'}) . "</span>") .
7523 # finish last commit (warning: repetition!)
7526 "<td class=\"link\">" .
7527 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$co{'id'})},
7530 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$co{'tree'},
7531 hash_base
=>$co{'id'})},
7542 sub git_search_files
{
7546 defined(my $fd = git_cmd_pipe
'grep', '-n', '-z',
7547 $search_use_regexp ?
('-E', '-i') : '-F',
7548 $searchtext, $co{'tree'})
7549 or die_error
(500, "Open git-grep failed");
7553 git_print_page_nav
('','', $hash,$co{'tree'},$hash);
7554 git_print_header_div
('commit', esc_html
($co{'title'}), $hash);
7556 print "<table class=\"grep_search\">\n";
7561 while (my $line = to_utf8
(scalar <$fd>)) {
7563 my ($file, $lno, $ltext, $binary);
7564 last if ($matches++ > 1000);
7565 if ($line =~ /^Binary file (.+) matches$/) {
7569 ($file, $lno, $ltext) = split(/\0/, $line, 3);
7570 $file =~ s/^$co{'tree'}://;
7572 if ($file ne $lastfile) {
7573 $lastfile and print "</td></tr>\n";
7575 print "<tr class=\"dark\">\n";
7577 print "<tr class=\"light\">\n";
7579 $file_href = href
(action
=>"blob", hash_base
=>$co{'id'},
7581 print "<td class=\"list\">".
7582 $cgi->a({-href
=> $file_href, -class => "list"}, esc_path
($file));
7583 print "</td><td>\n";
7587 print "<div class=\"binary\">Binary file</div>\n";
7589 $ltext = untabify
($ltext);
7590 if ($ltext =~ m/^(.*)($search_regexp)(.*)$/i) {
7591 $ltext = esc_html
($1, -nbsp
=>1);
7592 $ltext .= '<span class="match">';
7593 $ltext .= esc_html
($2, -nbsp
=>1);
7594 $ltext .= '</span>';
7595 $ltext .= esc_html
($3, -nbsp
=>1);
7597 $ltext = esc_html
($ltext, -nbsp
=>1);
7599 print "<div class=\"pre\">" .
7600 $cgi->a({-href
=> $file_href.'#l'.$lno,
7601 -class => "linenr"}, sprintf('%4i ', $lno)) .
7602 $ltext . "</div>\n";
7606 print "</td></tr>\n";
7607 if ($matches > 1000) {
7608 print "<div class=\"diff nodifferences\">Too many matches, listing trimmed</div>\n";
7611 print "<div class=\"diff nodifferences\">No matches found</div>\n";
7620 sub git_search_grep_body
{
7621 my ($commitlist, $from, $to, $extra) = @_;
7622 $from = 0 unless defined $from;
7623 $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
7625 print "<table class=\"commit_search\">\n";
7627 for (my $i = $from; $i <= $to; $i++) {
7628 my %co = %{$commitlist->[$i]};
7632 my $commit = $co{'id'};
7634 print "<tr class=\"dark\">\n";
7636 print "<tr class=\"light\">\n";
7639 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
7640 format_author_html
('td', \
%co, 15, 5) .
7642 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$co{'id'}),
7643 -class => "list subject"},
7644 chop_and_escape_str
($co{'title'}, 50) . "<br/>");
7645 my $comment = $co{'comment'};
7646 foreach my $line (@
$comment) {
7647 if ($line =~ m/^(.*?)($search_regexp)(.*)$/i) {
7648 my ($lead, $match, $trail) = ($1, $2, $3);
7649 $match = chop_str
($match, 70, 5, 'center');
7650 my $contextlen = int((80 - length($match))/2);
7651 $contextlen = 30 if ($contextlen > 30);
7652 $lead = chop_str
($lead, $contextlen, 10, 'left');
7653 $trail = chop_str
($trail, $contextlen, 10, 'right');
7655 $lead = esc_html
($lead);
7656 $match = esc_html
($match);
7657 $trail = esc_html
($trail);
7659 print "$lead<span class=\"match\">$match</span>$trail<br />";
7663 "<td class=\"link\">" .
7664 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$co{'id'})}, "commit") .
7666 $cgi->a({-href
=> href
(action
=>"commitdiff", hash
=>$co{'id'})}, "commitdiff") .
7668 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$co{'tree'}, hash_base
=>$co{'id'})}, "tree");
7672 if (defined $extra) {
7674 "<td colspan=\"3\">$extra</td>\n" .
7680 ## ======================================================================
7681 ## ======================================================================
7684 sub git_project_list_load
{
7685 my $empty_list_ok = shift;
7686 my $order = $input_params{'order'};
7687 if (defined $order && $order !~ m/none|project|descr|owner|age/) {
7688 die_error
(400, "Unknown order parameter");
7691 my @list = git_get_projects_list
($project_filter, $strict_export);
7692 if ($project_filter && (!@list || !gitweb_check_feature
('forks'))) {
7693 push @list, { 'path' => "$project_filter.git" }
7694 if is_valid_project
("$project_filter.git");
7697 die_error
(404, "No projects found") unless $empty_list_ok;
7700 return (\
@list, $order);
7704 my ($projlist, $order);
7706 if ($frontpage_no_project_list) {
7708 $project_filter = undef;
7710 ($projlist, $order) = git_project_list_load
(1);
7713 if (defined $home_text && -f
$home_text) {
7714 print "<div class=\"index_include\">\n";
7715 insert_file
($home_text);
7718 git_project_search_form
($searchtext, $search_use_regexp);
7719 if ($frontpage_no_project_list) {
7720 my $show_ctags = gitweb_check_feature
('ctags');
7721 if ($frontpage_no_project_list == 1 and $show_ctags) {
7722 my @projects = git_get_projects_list
($project_filter, $strict_export);
7723 @projects = filter_forks_from_projects_list
(\
@projects) if gitweb_check_feature
('forks');
7724 @projects = fill_project_list_info
(\
@projects, 'ctags');
7725 my $ctags = git_gather_all_ctags
(\
@projects);
7726 my $cloud = git_populate_project_tagcloud
($ctags, 'project_list');
7727 print git_show_project_tagcloud
($cloud, 64);
7730 git_project_list_body
($projlist, $order, undef, undef, undef, undef, undef, 1);
7735 sub git_project_list
{
7736 my ($projlist, $order) = git_project_list_load
();
7738 if (!$frontpage_no_project_list && defined $home_text && -f
$home_text) {
7739 print "<div class=\"index_include\">\n";
7740 insert_file
($home_text);
7743 git_project_search_form
();
7744 git_project_list_body
($projlist, $order, undef, undef, undef, undef, undef, 1);
7749 my $order = $input_params{'order'};
7750 if (defined $order && $order !~ m/none|project|descr|owner|age/) {
7751 die_error
(400, "Unknown order parameter");
7754 my $filter = $project;
7755 $filter =~ s/\.git$//;
7756 my @list = git_get_projects_list
($filter);
7758 die_error
(404, "No forks found");
7762 git_print_page_nav
('','');
7763 git_print_header_div
('summary', "$project forks");
7764 git_project_list_body
(\
@list, $order, undef, undef, undef, undef, 'forks');
7768 sub git_project_index
{
7769 my @projects = git_get_projects_list
($project_filter, $strict_export);
7771 die_error
(404, "No projects found");
7775 -type
=> 'text/plain',
7776 -charset
=> 'utf-8',
7777 -content_disposition
=> 'inline; filename="index.aux"');
7779 foreach my $pr (@projects) {
7780 if (!exists $pr->{'owner'}) {
7781 $pr->{'owner'} = git_get_project_owner
("$pr->{'path'}");
7784 my ($path, $owner) = ($pr->{'path'}, $pr->{'owner'});
7785 # quote as in CGI::Util::encode, but keep the slash, and use '+' for ' '
7786 $path =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf
("%%%02X", ord($1))/eg
;
7787 $owner =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf
("%%%02X", ord($1))/eg
;
7791 print "$path $owner\n";
7796 my $descr = git_get_project_description
($project) || "none";
7797 my %co = parse_commit
("HEAD");
7798 my %cd = %co ? parse_date
($co{'committer_epoch'}, $co{'committer_tz'}) : ();
7799 my $head = $co{'id'};
7800 my $remote_heads = gitweb_check_feature
('remote_heads');
7802 my $owner = git_get_project_owner
($project);
7803 my $homepage = git_get_project_config
('homepage');
7804 my $base_url = git_get_project_config
('baseurl');
7806 my $refs = git_get_references
();
7807 # These get_*_list functions return one more to allow us to see if
7808 # there are more ...
7809 my @taglist = git_get_tags_list
(16);
7810 my @headlist = git_get_heads_list
(16);
7811 my %remotedata = $remote_heads ? git_get_remotes_list
() : ();
7813 my $check_forks = gitweb_check_feature
('forks');
7816 # find forks of a project
7817 my $filter = $project;
7818 $filter =~ s/\.git$//;
7819 @forklist = git_get_projects_list
($filter);
7820 # filter out forks of forks
7821 @forklist = filter_forks_from_projects_list
(\
@forklist)
7826 git_print_page_nav
('summary','', $head);
7828 if ($check_forks and $project =~ m
#/#) {
7829 my $xproject = $project; $xproject =~ s
#/[^/]+$#.git#; #
7830 if (is_valid_project
($xproject)) {
7831 my $r = $cgi->a({-href
=> href
(project
=> $xproject, action
=> 'summary')}, $xproject);
7833 <div class="forkinfo">
7834 This project is a fork of the $r project. If you have that one
7835 already cloned locally, you can use
7836 <pre>git clone --reference /path/to/your/$xproject/incarnation mirror_URL</pre>
7837 to save bandwidth during cloning.
7843 print "<div class=\"title\"> </div>\n";
7844 print "<table class=\"projects_list\">\n" .
7845 "<tr id=\"metadata_desc\"><td>description</td><td>" . esc_html
($descr) . "</td></tr>\n";
7847 print "<tr id=\"metadata_homepage\"><td>homepage URL</td><td>" . $cgi->a({-href
=> $homepage}, $homepage) . "</td></tr>\n";
7850 print "<tr id=\"metadata_baseurl\"><td>repository URL</td><td>" . esc_html
($base_url) . "</td></tr>\n";
7852 if ($owner and not $omit_owner) {
7853 print "<tr id=\"metadata_owner\"><td>owner</td><td>" . ($owner_link_hook
7854 ?
$cgi->a({-href
=> $owner_link_hook->($owner)}, email_obfuscate
($owner))
7855 : email_obfuscate
($owner)) . "</td></tr>\n";
7857 if (defined $cd{'rfc2822'}) {
7858 print "<tr id=\"metadata_lchange\"><td>last change</td>" .
7859 "<td>".format_timestamp_html
(\
%cd)."</td></tr>\n";
7861 print format_lastrefresh_row
(), "\n";
7863 # use per project git URL list in $projectroot/$project/cloneurl
7864 # or make project git URL from git base URL and project name
7865 my $url_tag = $base_url ?
"mirror URL" : "URL";
7866 my $url_class = "metadata_url";
7867 my @url_list = git_get_project_url_list
($project);
7868 unless (@url_list) {
7869 @url_list = @git_base_url_list;
7870 if ((git_get_project_config
("showpush", '--bool')||'false') eq "true" ||
7871 -f
"$projectroot/$project/.nofetch") {
7872 my $pushidx = @url_list;
7873 foreach (@git_base_push_urls) {
7874 if ($https_hint_html && !ref($_) && $_ =~ /^https:/i) {
7875 push(@url_list, [$_, $https_hint_html]);
7877 push(@url_list, $_);
7880 if ($#url_list >= $pushidx) {
7881 my $pushtag = "push URL";
7882 my $classtag = "metadata_pushurl";
7883 if (ref($url_list[$pushidx])) {
7884 $url_list[$pushidx] = [
7885 ${$url_list[$pushidx]}[0],
7886 ${$url_list[$pushidx]}[1],
7890 $url_list[$pushidx] = [
7891 $url_list[$pushidx],
7898 push(@url_list, @git_base_mirror_urls);
7900 for (my $i=0; $i<=$#url_list; ++$i) {
7901 if (ref($url_list[$i])) {
7903 ${$url_list[$i]}[0] . "/$project",
7904 ${$url_list[$i]}[1],
7905 ${$url_list[$i]}[2],
7906 ${$url_list[$i]}[3]];
7908 $url_list[$i] .= "/$project";
7912 foreach (@url_list) {
7916 my $next_tag = undef;
7917 my $next_class = undef;
7920 $html_hint = " " . $$_[1] if defined($$_[1]);
7922 $next_class = $$_[3];
7926 next unless $git_url;
7927 $url_class = $next_class if $next_class;
7928 $url_tag = $next_tag if $next_tag;
7929 print "<tr class=\"$url_class\"><td>$url_tag</td><td>$git_url$html_hint</td></tr>\n";
7933 if (defined($git_base_bundles_url) && -d
"$projectroot/$project/bundles") {
7934 my $projname = $project;
7935 $projname =~ s
|^.*/||;
7936 my $url = "$git_base_bundles_url/$project/bundles";
7937 print format_repo_url
(
7939 "<a rel='nofollow' href='$url'>$projname downloadable bundles</a>");
7943 my $show_ctags = gitweb_check_feature
('ctags');
7945 my $ctags = git_get_project_ctags
($project);
7946 if (%$ctags || $show_ctags !~ /^\d+$/) {
7947 # without ability to add tags, don't show if there are none
7948 my $cloud = git_populate_project_tagcloud
($ctags, 'project_list');
7949 print "<tr id=\"metadata_ctags\">" .
7950 "<td style=\"vertical-align:middle\">content tags<br />";
7951 print "</td>\n<td>" unless %$ctags;
7952 print "<form action=\"$show_ctags\" method=\"post\" style=\"white-space:nowrap\">" .
7953 "<input type=\"hidden\" name=\"p\" value=\"$project\"/>" .
7954 "add: <input type=\"text\" name=\"t\" size=\"8\" /></form>"
7955 unless $show_ctags =~ /^\d+$/;
7956 print "</td>\n<td>" if %$ctags;
7957 print git_show_project_tagcloud
($cloud, 48)."</td>" .
7964 # If XSS prevention is on, we don't include README.html.
7965 # TODO: Allow a readme in some safe format.
7966 if (!$prevent_xss) {
7967 my $readme_name = "readme";
7969 if (-s
"$projectroot/$project/README.html") {
7970 $readme = collect_html_file
("$projectroot/$project/README.html");
7972 $readme = collect_output
($git_automatic_readme_html, "$projectroot/$project");
7973 if ($readme && $readme =~ /^<!-- README NAME: ((?:[^-]|(?:-(?!-)))+) -->/) {
7975 $readme =~ s/^<!--(?:[^-]|(?:-(?!-)))*-->\n?//;
7978 if (defined($readme)) {
7979 $readme =~ s/^\s+//s;
7980 $readme =~ s/\s+$//s;
7981 print "<div class=\"title\">$readme_name</div>\n",
7982 "<div class=\"readme\">\n",
7989 # we need to request one more than 16 (0..15) to check if
7991 my @commitlist = $head ? parse_commits
($head, 17) : ();
7993 git_print_header_div
('shortlog');
7994 git_shortlog_body
(\
@commitlist, 0, 15, $refs,
7995 $#commitlist <= 15 ?
undef :
7996 $cgi->a({-href
=> href
(action
=>"shortlog")}, "..."));
8000 git_print_header_div
('tags');
8001 git_tags_body
(\
@taglist, 0, 15,
8002 $#taglist <= 15 ?
undef :
8003 $cgi->a({-href
=> href
(action
=>"tags")}, "..."));
8007 git_print_header_div
('heads');
8008 git_heads_body
(\
@headlist, $head, 0, 15,
8009 $#headlist <= 15 ?
undef :
8010 $cgi->a({-href
=> href
(action
=>"heads")}, "..."));
8014 git_print_header_div
('remotes');
8015 git_remotes_body
(\
%remotedata, 15, $head);
8019 git_print_header_div
('forks');
8020 git_project_list_body
(\
@forklist, 'age', 0, 15,
8021 $#forklist <= 15 ?
undef :
8022 $cgi->a({-href
=> href
(action
=>"forks")}, "..."),
8023 'no_header', 'forks');
8030 my %tag = parse_tag
($hash);
8033 die_error
(404, "Unknown tag object");
8037 $fullhash = $hash if $hash =~ m/^[0-9a-fA-F]{40}$/;
8038 $fullhash = git_get_full_hash
($project, $hash) unless $fullhash;
8040 my $obj = $tag{'object'};
8042 if ($tag{'type'} eq 'commit') {
8043 git_print_page_nav
('','', $obj,undef,$obj);
8044 git_print_header_div
('commit', esc_html
($tag{'name'}), $hash);
8046 if ($tag{'type'} eq 'tree') {
8047 git_print_page_nav
('',['commit','commitdiff'], undef,undef,$obj);
8049 git_print_page_nav
('',['commit','commitdiff','tree'], undef,undef,undef);
8051 print "<div class=\"title\">".esc_html
($hash)."</div>\n";
8053 print "<div class=\"title_text\">\n" .
8054 "<table class=\"object_header\">\n" .
8055 "<tr><td>tag</td><td class=\"sha1\">$fullhash</td></tr>\n" .
8057 "<td>object</td>\n" .
8058 "<td>" . $cgi->a({-class => "list", -href
=> href
(action
=>$tag{'type'}, hash
=>$tag{'object'})},
8059 $tag{'object'}) . "</td>\n" .
8060 "<td class=\"link\">" . $cgi->a({-href
=> href
(action
=>$tag{'type'}, hash
=>$tag{'object'})},
8061 $tag{'type'}) . "</td>\n" .
8063 if (defined($tag{'author'})) {
8064 git_print_authorship_rows
(\
%tag, 'author');
8066 print "</table>\n\n" .
8068 print "<div class=\"page_body\">";
8069 my $comment = $tag{'comment'};
8070 foreach my $line (@
$comment) {
8072 print esc_html
($line, -nbsp
=>1) . "<br/>\n";
8078 sub git_blame_common
{
8079 my $format = shift || 'porcelain';
8080 if ($format eq 'porcelain' && $input_params{'javascript'}) {
8081 $format = 'incremental';
8082 $action = 'blame_incremental'; # for page title etc
8086 gitweb_check_feature
('blame')
8087 or die_error
(403, "Blame view not allowed");
8090 die_error
(400, "No file name given") unless $file_name;
8091 $hash_base ||= git_get_head_hash
($project);
8092 die_error
(404, "Couldn't find base commit") unless $hash_base;
8093 my %co = parse_commit
($hash_base)
8094 or die_error
(404, "Commit not found");
8096 if (!defined $hash) {
8097 $hash = git_get_hash_by_path
($hash_base, $file_name, "blob")
8098 or die_error
(404, "Error looking up file");
8100 $ftype = git_get_type
($hash);
8101 if ($ftype !~ "blob") {
8102 die_error
(400, "Object is not a blob");
8107 if ($format eq 'incremental') {
8108 # get file contents (as base)
8109 defined($fd = git_cmd_pipe
'cat-file', 'blob', $hash)
8110 or die_error
(500, "Open git-cat-file failed");
8111 } elsif ($format eq 'data') {
8112 # run git-blame --incremental
8113 defined($fd = git_cmd_pipe
"blame", "--incremental",
8114 $hash_base, "--", $file_name)
8115 or die_error
(500, "Open git-blame --incremental failed");
8117 # run git-blame --porcelain
8118 defined($fd = git_cmd_pipe
"blame", '-p',
8119 $hash_base, '--', $file_name)
8120 or die_error
(500, "Open git-blame --porcelain failed");
8123 # incremental blame data returns early
8124 if ($format eq 'data') {
8126 -type
=>"text/plain", -charset
=> "utf-8",
8127 -status
=> "200 OK");
8128 local $| = 1; # output autoflush
8133 or print "ERROR $!\n";
8136 if (defined $t0 && gitweb_check_feature
('timed')) {
8138 tv_interval
($t0, [ gettimeofday
() ]).
8139 ' '.$number_of_git_cmds;
8149 $cgi->a({-href
=> href
(action
=>"blob", -replay
=>1)},
8153 $cgi->a({-href
=> href
(action
=>"history", -replay
=>1)},
8156 $cgi->a({-href
=> href
(action
=>$action, file_name
=>$file_name)},
8158 git_print_page_nav
('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
8159 git_print_header_div
('commit', esc_html
($co{'title'}), $hash_base);
8160 git_print_page_path
($file_name, $ftype, $hash_base);
8163 if ($format eq 'incremental') {
8164 print "<noscript>\n<div class=\"error\"><center><b>\n".
8165 "This page requires JavaScript to run.\n Use ".
8166 $cgi->a({-href
=> href
(action
=>'blame',javascript
=>0,-replay
=>1)},
8169 "</b></center></div>\n</noscript>\n";
8171 print qq!<div id
="progress_bar" style
="width: 100%; background-color: yellow"></div
>\n!;
8174 print qq!<div
class="page_body">\n!;
8175 print qq!<div id
="progress_info">... / ...</div
>\n!
8176 if ($format eq 'incremental');
8177 print qq!<table id
="blame_table" class="blame" width
="100%">\n!.
8178 #qq!<col width="5.5em" /><col width="2.5em" /><col width="*" />\n!.
8180 qq!<tr
><th nowrap
="nowrap" style
="white-space:nowrap">!.
8181 qq!Commit
 <a href="javascript:extra_blame_columns()" id="columns_expander" !.
8182 qq!title
="toggles blame author information display">[+]</a></th
>!.
8183 qq!<th
class="extra_column">Author
</th><th class="extra_column">Date</th
>!.
8184 qq!<th
>Line
</th><th width="100%">Data</th
></tr
>\n!.
8188 my @rev_color = qw(light dark);
8189 my $num_colors = scalar(@rev_color);
8190 my $current_color = 0;
8192 if ($format eq 'incremental') {
8193 my $color_class = $rev_color[$current_color];
8198 while (my $line = to_utf8
(scalar <$fd>)) {
8202 print qq!<tr id
="l$linenr" class="$color_class">!.
8203 qq!<td
class="sha1"><a href
=""> </a></td
>!.
8204 qq!<td
class="extra_column" nowrap
="nowrap"></td
>!.
8205 qq!<td
class="extra_column" nowrap
="nowrap"></td
>!.
8206 qq!<td
class="linenr">!.
8207 qq!<a
class="linenr" href
="">$linenr</a></td
>!;
8208 print qq!<td
class="pre">! . esc_html
($line) . "</td>\n";
8212 } else { # porcelain, i.e. ordinary blame
8213 my %metainfo = (); # saves information about commits
8217 while (my $line = to_utf8
(scalar <$fd>)) {
8219 # the header: <SHA-1> <src lineno> <dst lineno> [<lines in group>]
8220 # no <lines in group> for subsequent lines in group of lines
8221 my ($full_rev, $orig_lineno, $lineno, $group_size) =
8222 ($line =~ /^([0-9a-f]{40}) (\d+) (\d+)(?: (\d+))?$/);
8223 if (!exists $metainfo{$full_rev}) {
8224 $metainfo{$full_rev} = { 'nprevious' => 0 };
8226 my $meta = $metainfo{$full_rev};
8228 while ($data = to_utf8
(scalar <$fd>)) {
8230 last if ($data =~ s/^\t//); # contents of line
8231 if ($data =~ /^(\S+)(?: (.*))?$/) {
8232 $meta->{$1} = $2 unless exists $meta->{$1};
8234 if ($data =~ /^previous /) {
8235 $meta->{'nprevious'}++;
8238 my $short_rev = substr($full_rev, 0, 8);
8239 my $author = $meta->{'author'};
8241 parse_date
($meta->{'author-time'}, $meta->{'author-tz'});
8242 my $date = $date{'iso-tz'};
8244 $current_color = ($current_color + 1) % $num_colors;
8246 my $tr_class = $rev_color[$current_color];
8247 $tr_class .= ' boundary' if (exists $meta->{'boundary'});
8248 $tr_class .= ' no-previous' if ($meta->{'nprevious'} == 0);
8249 $tr_class .= ' multiple-previous' if ($meta->{'nprevious'} > 1);
8250 print "<tr id=\"l$lineno\" class=\"$tr_class\">\n";
8252 my $rowspan = $group_size > 1 ?
" rowspan=\"$group_size\"" : "";
8253 print "<td class=\"sha1\"";
8254 print " title=\"". esc_html
($author) . ", $date\"";
8256 print $cgi->a({-href
=> href
(action
=>"commit",
8258 file_name
=>$file_name)},
8259 esc_html
($short_rev));
8260 if ($group_size >= 2) {
8261 my @author_initials = ($author =~ /\b([[:upper:]])\B/g);
8262 if (@author_initials) {
8264 esc_html
(join('', @author_initials));
8269 print "<td class=\"extra_column\" nowrap=\"nowrap\"$rowspan>". esc_html
($author) . "</td>";
8270 print "<td class=\"extra_column\" nowrap=\"nowrap\"$rowspan>". $date . "</td>";
8272 # 'previous' <sha1 of parent commit> <filename at commit>
8273 if (exists $meta->{'previous'} &&
8274 $meta->{'previous'} =~ /^([a-fA-F0-9]{40}) (.*)$/) {
8275 $meta->{'parent'} = $1;
8276 $meta->{'file_parent'} = unquote
($2);
8279 exists($meta->{'parent'}) ?
8280 $meta->{'parent'} : $full_rev;
8281 my $linenr_filename =
8282 exists($meta->{'file_parent'}) ?
8283 $meta->{'file_parent'} : unquote
($meta->{'filename'});
8284 my $blamed = href
(action
=> 'blame',
8285 file_name
=> $linenr_filename,
8286 hash_base
=> $linenr_commit);
8287 print "<td class=\"linenr\">";
8288 print $cgi->a({ -href
=> "$blamed#l$orig_lineno",
8289 -class => "linenr" },
8292 print "<td class=\"pre\">" . esc_html
($data) . "</td>\n";
8300 "</table>\n"; # class="blame"
8301 print "</div>\n"; # class="blame_body"
8303 or print "Reading blob failed\n";
8312 sub git_blame_incremental
{
8313 git_blame_common
('incremental');
8316 sub git_blame_data
{
8317 git_blame_common
('data');
8321 my $head = git_get_head_hash
($project);
8323 git_print_page_nav
('','', $head,undef,$head,format_ref_views
('tags'));
8324 git_print_header_div
('summary', $project);
8326 my @tagslist = git_get_tags_list
();
8328 git_tags_body
(\
@tagslist);
8334 my $order = $input_params{'order'};
8335 if (defined $order && $order !~ m/age|name/) {
8336 die_error
(400, "Unknown order parameter");
8339 my $head = git_get_head_hash
($project);
8341 git_print_page_nav
('','', $head,undef,$head,format_ref_views
('refs'));
8342 git_print_header_div
('summary', $project);
8344 my @refslist = git_get_tags_list
(undef, 1, $order);
8346 git_tags_body
(\
@refslist, undef, undef, undef, $head, 1, $order);
8352 my $head = git_get_head_hash
($project);
8354 git_print_page_nav
('','', $head,undef,$head,format_ref_views
('heads'));
8355 git_print_header_div
('summary', $project);
8357 my @headslist = git_get_heads_list
();
8359 git_heads_body
(\
@headslist, $head);
8364 # used both for single remote view and for list of all the remotes
8366 gitweb_check_feature
('remote_heads')
8367 or die_error
(403, "Remote heads view is disabled");
8369 my $head = git_get_head_hash
($project);
8370 my $remote = $input_params{'hash'};
8372 my $remotedata = git_get_remotes_list
($remote);
8373 die_error
(500, "Unable to get remote information") unless defined $remotedata;
8375 unless (%$remotedata) {
8376 die_error
(404, defined $remote ?
8377 "Remote $remote not found" :
8378 "No remotes found");
8381 git_header_html
(undef, undef, -action_extra
=> $remote);
8382 git_print_page_nav
('', '', $head, undef, $head,
8383 format_ref_views
($remote ?
'' : 'remotes'));
8385 fill_remote_heads
($remotedata);
8386 if (defined $remote) {
8387 git_print_header_div
('remotes', "$remote remote for $project");
8388 git_remote_block
($remote, $remotedata->{$remote}, undef, $head);
8390 git_print_header_div
('summary', "$project remotes");
8391 git_remotes_body
($remotedata, undef, $head);
8397 sub git_blob_plain
{
8401 if (!defined $hash) {
8402 if (defined $file_name) {
8403 my $base = $hash_base || git_get_head_hash
($project);
8404 $hash = git_get_hash_by_path
($base, $file_name, "blob")
8405 or die_error
(404, "Cannot find file");
8407 die_error
(400, "No file name defined");
8409 } elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) {
8410 # blobs defined by non-textual hash id's can be cached
8414 defined(my $fd = git_cmd_pipe
"cat-file", "blob", $hash)
8415 or die_error
(500, "Open git-cat-file blob '$hash' failed");
8418 # content-type (can include charset)
8420 ($type, $leader) = blob_contenttype
($fd, $file_name, $type);
8422 # "save as" filename, even when no $file_name is given
8423 my $save_as = "$hash";
8424 if (defined $file_name) {
8425 $save_as = $file_name;
8426 } elsif ($type =~ m/^text\//) {
8430 # With XSS prevention on, blobs of all types except a few known safe
8431 # ones are served with "Content-Disposition: attachment" to make sure
8432 # they don't run in our security domain. For certain image types,
8433 # blob view writes an <img> tag referring to blob_plain view, and we
8434 # want to be sure not to break that by serving the image as an
8435 # attachment (though Firefox 3 doesn't seem to care).
8436 my $sandbox = $prevent_xss &&
8437 $type !~ m!^(?:text/[a-z]+|image/(?:gif|png|jpeg))(?:[ ;]|$)!;
8439 # serve text/* as text/plain
8441 ($type =~ m!^text/[a-z]+\b(.*)$! ||
8442 ($type =~ m!^[a-z]+/[a-z]\+xml\b(.*)$! && -T
$fd))) {
8444 $rest = defined $rest ?
$rest : '';
8445 $type = "text/plain$rest";
8450 -expires
=> $expires,
8451 -content_disposition
=>
8452 ($sandbox ?
'attachment' : 'inline')
8453 . '; filename="' . $save_as . '"');
8454 binmode STDOUT
, ':raw';
8456 print $leader if defined $leader;
8458 while (read($fd, $buf, 32768)) {
8461 binmode STDOUT
, ':utf8'; # as set at the beginning of gitweb.cgi
8469 if (!defined $hash) {
8470 if (defined $file_name) {
8471 my $base = $hash_base || git_get_head_hash
($project);
8472 $hash = git_get_hash_by_path
($base, $file_name, "blob")
8473 or die_error
(404, "Cannot find file");
8475 die_error
(400, "No file name defined");
8477 } elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) {
8478 # blobs defined by non-textual hash id's can be cached
8481 my $fullhash = git_get_full_hash
($project, "$hash^{blob}");
8482 die_error
(404, "No such blob") unless defined($fullhash);
8484 my $have_blame = gitweb_check_feature
('blame');
8485 defined(my $fd = git_cmd_pipe
"cat-file", "blob", $fullhash)
8486 or die_error
(500, "Couldn't cat $file_name, $hash");
8488 my $mimetype = blob_mimetype
($fd, $file_name);
8489 # use 'blob_plain' (aka 'raw') view for files that cannot be displayed
8490 if ($mimetype !~ m!^(?:text/|image/(?:gif|png|jpeg)$)! && -B
$fd) {
8492 return git_blob_plain
($mimetype);
8494 # we can have blame only for text/* mimetype
8495 $have_blame &&= ($mimetype =~ m!^text/!);
8497 my $highlight = gitweb_check_feature
('highlight') && defined $highlight_bin;
8498 my $syntax = guess_file_syntax
($fd, $mimetype, $file_name) if $highlight;
8499 my $highlight_mode_active;
8500 ($fd, $highlight_mode_active) = run_highlighter
($fd, $syntax) if $syntax;
8502 git_header_html
(undef, $expires);
8503 my $formats_nav = '';
8504 if (defined $hash_base && (my %co = parse_commit
($hash_base))) {
8505 if (defined $file_name) {
8508 $cgi->a({-href
=> href
(action
=>"blame", -replay
=>1),
8509 -class => "blamelink"},
8514 $cgi->a({-href
=> href
(action
=>"history", -replay
=>1)},
8517 $cgi->a({-href
=> href
(action
=>"blob_plain", -replay
=>1)},
8520 $cgi->a({-href
=> href
(action
=>"blob",
8521 hash_base
=>"HEAD", file_name
=>$file_name)},
8525 $cgi->a({-href
=> href
(action
=>"blob_plain", -replay
=>1)},
8528 git_print_page_nav
('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
8529 git_print_header_div
('commit', esc_html
($co{'title'}), $hash_base);
8531 git_print_page_nav
('',['commit','commitdiff','tree'], undef,undef,undef);
8532 print "<div class=\"title\">".esc_html
($hash)."</div>\n";
8534 git_print_page_path
($file_name, "blob", $hash_base);
8535 print "<div class=\"title_text\">\n" .
8536 "<table class=\"object_header\">\n";
8537 print "<tr><td>blob</td><td class=\"sha1\">$fullhash</td></tr>\n";
8540 print "<div class=\"page_body\">\n";
8541 if ($mimetype =~ m!^image/!) {
8542 print qq!<img
class="blob" type
="!.esc_attr($mimetype).qq!"!;
8544 print qq! alt
="!.esc_attr($file_name).qq!" title
="!.esc_attr($file_name).qq!"!;
8547 href(action=>"blob_plain
", hash=>$hash,
8548 hash_base=>$hash_base, file_name=>$file_name) .
8550 close $fd; # ignore likely EPIPE error from child
8553 while (my $line = to_utf8
(scalar <$fd>)) {
8556 $line = untabify
($line);
8557 printf qq!<div
class="pre"><a id
="l%i" href
="%s#l%i" class="linenr">%4i </a>%s</div
>\n!,
8558 $nr, esc_attr
(href
(-replay
=> 1)), $nr, $nr,
8559 $highlight_mode_active ? sanitize
($line) : esc_html
($line, -nbsp
=>1);
8562 or print "Reading blob failed.\n";
8569 if (!defined $hash_base) {
8570 $hash_base = "HEAD";
8572 if (!defined $hash) {
8573 if (defined $file_name) {
8574 $hash = git_get_hash_by_path
($hash_base, $file_name, "tree");
8579 die_error
(404, "No such tree") unless defined($hash);
8580 my $fullhash = git_get_full_hash
($project, "$hash^{tree}");
8581 die_error
(404, "No such tree") unless defined($fullhash);
8583 my $show_sizes = gitweb_check_feature
('show-sizes');
8584 my $have_blame = gitweb_check_feature
('blame');
8589 defined(my $fd = git_cmd_pipe
"ls-tree", '-z',
8590 ($show_sizes ?
'-l' : ()), @extra_options, $fullhash)
8591 or die_error
(500, "Open git-ls-tree failed");
8592 @entries = map { chomp; to_utf8
($_) } <$fd>;
8594 or die_error
(404, "Reading tree failed");
8599 if (defined $hash_base && (my %co = parse_commit
($hash_base))) {
8600 my $refs = git_get_references
();
8601 my $ref = format_ref_marker
($refs, $co{'id'});
8603 if (defined $file_name) {
8605 $cgi->a({-href
=> href
(action
=>"history", -replay
=>1)},
8607 $cgi->a({-href
=> href
(action
=>"tree",
8608 hash_base
=>"HEAD", file_name
=>$file_name)},
8611 my $snapshot_links = format_snapshot_links
($hash);
8612 if (defined $snapshot_links) {
8613 # FIXME: Should be available when we have no hash base as well.
8614 push @views_nav, $snapshot_links;
8616 git_print_page_nav
('tree','', $hash_base, undef, undef,
8617 join(' | ', @views_nav));
8618 git_print_header_div
('commit', esc_html
($co{'title'}), $hash_base, undef, $ref);
8620 git_print_page_nav
('tree',['commit','commitdiff'], undef,undef,$hash_base);
8622 print "<div class=\"title\">".esc_html
($hash)."</div>\n";
8624 if (defined $file_name) {
8625 $basedir = $file_name;
8626 if ($basedir ne '' && substr($basedir, -1) ne '/') {
8629 git_print_page_path
($file_name, 'tree', $hash_base);
8631 print "<div class=\"title_text\">\n" .
8632 "<table class=\"object_header\">\n";
8633 print "<tr><td>tree</td><td class=\"sha1\">$fullhash</td></tr>\n";
8636 print "<div class=\"page_body\">\n";
8637 print "<table class=\"tree\">\n";
8639 # '..' (top directory) link if possible
8640 if (defined $hash_base &&
8641 defined $file_name && $file_name =~ m![^/]+$!) {
8643 print "<tr class=\"dark\">\n";
8645 print "<tr class=\"light\">\n";
8649 my $up = $file_name;
8650 $up =~ s!/?[^/]+$!!;
8651 undef $up unless $up;
8652 # based on git_print_tree_entry
8653 print '<td class="mode">' . mode_str
('040000') . "</td>\n";
8654 print '<td class="size"> </td>'."\n" if $show_sizes;
8655 print '<td class="list">';
8656 print $cgi->a({-href
=> href
(action
=>"tree",
8657 hash_base
=>$hash_base,
8661 print "<td class=\"link\"></td>\n";
8665 foreach my $line (@entries) {
8666 my %t = parse_ls_tree_line
($line, -z
=> 1, -l
=> $show_sizes);
8669 print "<tr class=\"dark\">\n";
8671 print "<tr class=\"light\">\n";
8675 git_print_tree_entry
(\
%t, $basedir, $hash_base, $have_blame);
8679 print "</table>\n" .
8684 sub sanitize_for_filename
{
8688 $name =~ s/[^[:alnum:]_.-]//g;
8694 my ($project, $hash) = @_;
8696 # path/to/project.git -> project
8697 # path/to/project/.git -> project
8698 my $name = to_utf8
($project);
8699 $name =~ s
,([^/])/*\
.git
$,$1,;
8700 $name = sanitize_for_filename
(basename
($name));
8703 if ($hash =~ /^[0-9a-fA-F]+$/) {
8704 # shorten SHA-1 hash
8705 my $full_hash = git_get_full_hash
($project, $hash);
8706 if ($full_hash =~ /^$hash/ && length($hash) > 7) {
8707 $ver = git_get_short_hash
($project, $hash);
8709 } elsif ($hash =~ m!^refs/tags/(.*)$!) {
8710 # tags don't need shortened SHA-1 hash
8713 # branches and other need shortened SHA-1 hash
8714 my $strip_refs = join '|', map { quotemeta } get_branch_refs
();
8715 if ($hash =~ m!^refs/($strip_refs|remotes)/(.*)$!) {
8716 my $ref_dir = (defined $1) ?
$1 : '';
8719 $ref_dir = sanitize_for_filename
($ref_dir);
8720 # for refs neither in heads nor remotes we want to
8721 # add a ref dir to archive name
8722 if ($ref_dir ne '' and $ref_dir ne 'heads' and $ref_dir ne 'remotes') {
8723 $ver = $ref_dir . '-' . $ver;
8726 $ver .= '-' . git_get_short_hash
($project, $hash);
8728 # special case of sanitization for filename - we change
8729 # slashes to dots instead of dashes
8730 # in case of hierarchical branch names
8732 $ver =~ s/[^[:alnum:]_.-]//g;
8734 # name = project-version_string
8735 $name = "$name-$ver";
8737 return wantarray ?
($name, $name) : $name;
8740 sub exit_if_unmodified_since
{
8741 my ($latest_epoch) = @_;
8744 my $if_modified = $cgi->http('IF_MODIFIED_SINCE');
8745 if (defined $if_modified) {
8747 if (eval { require HTTP
::Date
; 1; }) {
8748 $since = HTTP
::Date
::str2time
($if_modified);
8749 } elsif (eval { require Time
::ParseDate
; 1; }) {
8750 $since = Time
::ParseDate
::parsedate
($if_modified, GMT
=> 1);
8752 if (defined $since && $latest_epoch <= $since) {
8753 my %latest_date = parse_date
($latest_epoch);
8755 -last_modified
=> $latest_date{'rfc2822'},
8756 -status
=> '304 Not Modified');
8763 my $format = $input_params{'snapshot_format'};
8764 if (!@snapshot_fmts) {
8765 die_error
(403, "Snapshots not allowed");
8767 # default to first supported snapshot format
8768 $format ||= $snapshot_fmts[0];
8769 if ($format !~ m/^[a-z0-9]+$/) {
8770 die_error
(400, "Invalid snapshot format parameter");
8771 } elsif (!exists($known_snapshot_formats{$format})) {
8772 die_error
(400, "Unknown snapshot format");
8773 } elsif ($known_snapshot_formats{$format}{'disabled'}) {
8774 die_error
(403, "Snapshot format not allowed");
8775 } elsif (!grep($_ eq $format, @snapshot_fmts)) {
8776 die_error
(403, "Unsupported snapshot format");
8779 my $type = git_get_type
("$hash^{}");
8781 die_error
(404, 'Object does not exist');
8782 } elsif ($type eq 'blob') {
8783 die_error
(400, 'Object is not a tree-ish');
8786 my ($name, $prefix) = snapshot_name
($project, $hash);
8787 my $filename = "$name$known_snapshot_formats{$format}{'suffix'}";
8789 my %co = parse_commit
($hash);
8790 exit_if_unmodified_since
($co{'committer_epoch'}) if %co;
8793 git_cmd
(), 'archive',
8794 "--format=$known_snapshot_formats{$format}{'format'}",
8795 "--prefix=$prefix/", $hash);
8796 if (exists $known_snapshot_formats{$format}{'compressor'}) {
8797 @cmd = ($posix_shell_bin, '-c', quote_command
(@cmd) .
8798 ' | ' . quote_command
(@
{$known_snapshot_formats{$format}{'compressor'}}));
8801 $filename =~ s/(["\\])/\\$1/g;
8804 %latest_date = parse_date
($co{'committer_epoch'}, $co{'committer_tz'});
8808 -type
=> $known_snapshot_formats{$format}{'type'},
8809 -content_disposition
=> 'inline; filename="' . $filename . '"',
8810 %co ?
(-last_modified
=> $latest_date{'rfc2822'}) : (),
8811 -status
=> '200 OK');
8813 defined(my $fd = cmd_pipe
@cmd)
8814 or die_error
(500, "Execute git-archive failed");
8816 binmode STDOUT
, ':raw';
8819 while (read($fd, $buf, 32768)) {
8822 binmode STDOUT
, ':utf8'; # as set at the beginning of gitweb.cgi
8827 sub git_log_generic
{
8828 my ($fmt_name, $body_subr, $base, $parent, $file_name, $file_hash) = @_;
8830 my $head = git_get_head_hash
($project);
8831 if (!defined $base) {
8834 if (!defined $page) {
8837 my $refs = git_get_references
();
8839 my $commit_hash = $base;
8840 if (defined $parent) {
8841 $commit_hash = "$parent..$base";
8844 parse_commits
($commit_hash, 101, (100 * $page),
8845 defined $file_name ?
($file_name, "--full-history") : ());
8848 if (!defined $file_hash && defined $file_name) {
8849 # some commits could have deleted file in question,
8850 # and not have it in tree, but one of them has to have it
8851 for (my $i = 0; $i < @commitlist; $i++) {
8852 $file_hash = git_get_hash_by_path
($commitlist[$i]{'id'}, $file_name);
8853 last if defined $file_hash;
8856 if (defined $file_hash) {
8857 $ftype = git_get_type
($file_hash);
8859 if (defined $file_name && !defined $ftype) {
8860 die_error
(500, "Unknown type of object");
8863 if (defined $file_name) {
8864 %co = parse_commit
($base)
8865 or die_error
(404, "Unknown commit object");
8869 my $paging_nav = format_log_nav
($fmt_name, $page, $#commitlist >= 100);
8871 if ($#commitlist >= 100) {
8873 $cgi->a({-href
=> href
(-replay
=>1, page
=>$page+1),
8874 -accesskey
=> "n", -title
=> "Alt-n"}, "next");
8876 my ($patch_max) = gitweb_get_feature
('patches');
8877 if ($patch_max && !defined $file_name) {
8878 if ($patch_max < 0 || @commitlist <= $patch_max) {
8879 $paging_nav .= " · " .
8880 $cgi->a({-href
=> href
(action
=>"patches", -replay
=>1)},
8886 local $action = 'log';
8889 git_print_page_nav
($fmt_name,'', $hash,$hash,$hash, $paging_nav);
8890 if (defined $file_name) {
8891 git_print_header_div
('commit', esc_html
($co{'title'}), $base);
8893 git_print_header_div
('summary', $project)
8895 git_print_page_path
($file_name, $ftype, $hash_base)
8896 if (defined $file_name);
8898 $body_subr->(\
@commitlist, 0, 99, $refs, $next_link,
8899 $file_name, $file_hash, $ftype);
8905 git_log_generic
('log', \
&git_log_body
,
8906 $hash, $hash_parent);
8910 $hash ||= $hash_base || "HEAD";
8911 my %co = parse_commit
($hash)
8912 or die_error
(404, "Unknown commit object");
8914 my $parent = $co{'parent'};
8915 my $parents = $co{'parents'}; # listref
8917 # we need to prepare $formats_nav before any parameter munging
8919 if (!defined $parent) {
8921 $formats_nav .= '(initial)';
8922 } elsif (@
$parents == 1) {
8923 # single parent commit
8926 $cgi->a({-href
=> href
(action
=>"commit",
8928 esc_html
(substr($parent, 0, 7))) .
8935 $cgi->a({-href
=> href
(action
=>"commit",
8937 esc_html
(substr($_, 0, 7)));
8941 if (gitweb_check_feature
('patches') && @
$parents <= 1) {
8942 $formats_nav .= " | " .
8943 $cgi->a({-href
=> href
(action
=>"patch", -replay
=>1)},
8947 if (!defined $parent) {
8951 defined(my $fd = git_cmd_pipe
"diff-tree", '-r', "--no-commit-id",
8953 (@
$parents <= 1 ?
$parent : '-c'),
8955 or die_error
(500, "Open git-diff-tree failed");
8956 @difftree = map { chomp; to_utf8
($_) } <$fd>;
8957 close $fd or die_error
(404, "Reading git-diff-tree failed");
8959 # non-textual hash id's can be cached
8961 if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
8964 my $refs = git_get_references
();
8965 my $ref = format_ref_marker
($refs, $co{'id'});
8967 git_header_html
(undef, $expires);
8968 git_print_page_nav
('commit', '',
8969 $hash, $co{'tree'}, $hash,
8972 if (defined $co{'parent'}) {
8973 git_print_header_div
('commitdiff', esc_html
($co{'title'}), $hash, undef, $ref);
8975 git_print_header_div
('tree', esc_html
($co{'title'}), $co{'tree'}, $hash, $ref);
8977 print "<div class=\"title_text\">\n" .
8978 "<table class=\"object_header\">\n";
8979 print "<tr><td>commit</td><td class=\"sha1\">$co{'id'}</td></tr>\n";
8980 git_print_authorship_rows
(\
%co);
8983 "<td class=\"sha1\">" .
8984 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$co{'tree'}, hash_base
=>$hash),
8985 class => "list"}, $co{'tree'}) .
8987 "<td class=\"link\">" .
8988 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$co{'tree'}, hash_base
=>$hash)},
8990 my $snapshot_links = format_snapshot_links
($hash);
8991 if (defined $snapshot_links) {
8992 print " | " . $snapshot_links;
8997 foreach my $par (@
$parents) {
9000 "<td class=\"sha1\">" .
9001 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$par),
9002 class => "list"}, $par) .
9004 "<td class=\"link\">" .
9005 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$par)}, "commit") .
9007 $cgi->a({-href
=> href
(action
=>"commitdiff", hash
=>$hash, hash_parent
=>$par)}, "diff") .
9014 print "<div class=\"page_body\">\n";
9015 git_print_log
($co{'comment'});
9018 git_difftree_body
(\
@difftree, $hash, @
$parents);
9024 # object is defined by:
9025 # - hash or hash_base alone
9026 # - hash_base and file_name
9029 # - hash or hash_base alone
9030 if ($hash || ($hash_base && !defined $file_name)) {
9031 my $object_id = $hash || $hash_base;
9033 defined(my $fd = git_cmd_pipe
'cat-file', '-t', $object_id)
9034 or die_error
(404, "Object does not exist");
9036 defined $type && chomp $type;
9038 or die_error
(404, "Object does not exist");
9040 # - hash_base and file_name
9041 } elsif ($hash_base && defined $file_name) {
9042 $file_name =~ s
,/+$,,;
9044 system(git_cmd
(), "cat-file", '-e', $hash_base) == 0
9045 or die_error
(404, "Base object does not exist");
9047 # here errors should not happen
9048 defined(my $fd = git_cmd_pipe
"ls-tree", $hash_base, "--", $file_name)
9049 or die_error
(500, "Open git-ls-tree failed");
9050 my $line = to_utf8
(scalar <$fd>);
9053 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'
9054 unless ($line && $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t/) {
9055 die_error
(404, "File or directory for given base does not exist");
9060 die_error
(400, "Not enough information to find object");
9063 print $cgi->redirect(-uri
=> href
(action
=>$type, -full
=>1,
9064 hash
=>$hash, hash_base
=>$hash_base,
9065 file_name
=>$file_name),
9066 -status
=> '302 Found');
9070 my $format = shift || 'html';
9071 my $diff_style = $input_params{'diff_style'} || 'inline';
9078 # preparing $fd and %diffinfo for git_patchset_body
9080 if (defined $hash_base && defined $hash_parent_base) {
9081 if (defined $file_name) {
9083 defined($fd = git_cmd_pipe
"diff-tree", '-r', @diff_opts,
9084 $hash_parent_base, $hash_base,
9085 "--", (defined $file_parent ?
$file_parent : ()), $file_name)
9086 or die_error
(500, "Open git-diff-tree failed");
9087 @difftree = map { chomp; to_utf8
($_) } <$fd>;
9089 or die_error
(404, "Reading git-diff-tree failed");
9091 or die_error
(404, "Blob diff not found");
9093 } elsif (defined $hash &&
9094 $hash =~ /[0-9a-fA-F]{40}/) {
9095 # try to find filename from $hash
9097 # read filtered raw output
9098 defined($fd = git_cmd_pipe
"diff-tree", '-r', @diff_opts,
9099 $hash_parent_base, $hash_base, "--")
9100 or die_error
(500, "Open git-diff-tree failed");
9102 # ':100644 100644 03b21826... 3b93d5e7... M ls-files.c'
9104 grep { /^:[0-7]{6} [0-7]{6} [0-9a-fA-F]{40} $hash/ }
9105 map { chomp; to_utf8
($_) } <$fd>;
9107 or die_error
(404, "Reading git-diff-tree failed");
9109 or die_error
(404, "Blob diff not found");
9112 die_error
(400, "Missing one of the blob diff parameters");
9115 if (@difftree > 1) {
9116 die_error
(400, "Ambiguous blob diff specification");
9119 %diffinfo = parse_difftree_raw_line
($difftree[0]);
9120 $file_parent ||= $diffinfo{'from_file'} || $file_name;
9121 $file_name ||= $diffinfo{'to_file'};
9123 $hash_parent ||= $diffinfo{'from_id'};
9124 $hash ||= $diffinfo{'to_id'};
9126 # non-textual hash id's can be cached
9127 if ($hash_base =~ m/^[0-9a-fA-F]{40}$/ &&
9128 $hash_parent_base =~ m/^[0-9a-fA-F]{40}$/) {
9133 defined($fd = git_cmd_pipe
"diff-tree", '-r', @diff_opts,
9134 '-p', ($format eq 'html' ?
"--full-index" : ()),
9135 $hash_parent_base, $hash_base,
9136 "--", (defined $file_parent ?
$file_parent : ()), $file_name)
9137 or die_error
(500, "Open git-diff-tree failed");
9140 # old/legacy style URI -- not generated anymore since 1.4.3.
9142 die_error
('404 Not Found', "Missing one of the blob diff parameters")
9146 if ($format eq 'html') {
9148 $cgi->a({-href
=> href
(action
=>"blobdiff_plain", -replay
=>1)},
9150 $formats_nav .= diff_style_nav
($diff_style);
9151 git_header_html
(undef, $expires);
9152 if (defined $hash_base && (my %co = parse_commit
($hash_base))) {
9153 git_print_page_nav
('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
9154 git_print_header_div
('commit', esc_html
($co{'title'}), $hash_base);
9156 print "<div class=\"page_nav\"><br/>$formats_nav<br/></div>\n";
9157 print "<div class=\"title\">".esc_html
("$hash vs $hash_parent")."</div>\n";
9159 if (defined $file_name) {
9160 git_print_page_path
($file_name, "blob", $hash_base);
9162 print "<div class=\"page_path\"></div>\n";
9165 } elsif ($format eq 'plain') {
9167 -type
=> 'text/plain',
9168 -charset
=> 'utf-8',
9169 -expires
=> $expires,
9170 -content_disposition
=> 'inline; filename="' . "$file_name" . '.patch"');
9172 print "X-Git-Url: " . $cgi->self_url() . "\n\n";
9175 die_error
(400, "Unknown blobdiff format");
9179 if ($format eq 'html') {
9180 print "<div class=\"page_body\">\n";
9182 git_patchset_body
($fd, $diff_style,
9183 [ \
%diffinfo ], $hash_base, $hash_parent_base);
9186 print "</div>\n"; # class="page_body"
9190 while (my $line = to_utf8
(scalar <$fd>)) {
9191 $line =~ s!a/($hash|$hash_parent)!'a/'.esc_path($diffinfo{'from_file'})!eg;
9192 $line =~ s!b/($hash|$hash_parent)!'b/'.esc_path($diffinfo{'to_file'})!eg;
9196 last if $line =~ m!^\+\+\+!;
9205 sub git_blobdiff_plain
{
9206 git_blobdiff
('plain');
9209 # assumes that it is added as later part of already existing navigation,
9210 # so it returns "| foo | bar" rather than just "foo | bar"
9211 sub diff_style_nav
{
9212 my ($diff_style, $is_combined) = @_;
9213 $diff_style ||= 'inline';
9215 return "" if ($is_combined);
9217 my @styles = (inline
=> 'inline', 'sidebyside' => 'side by side');
9218 my %styles = @styles;
9220 @styles[ map { $_ * 2 } 0..$#styles/2 ];
9225 $_ eq $diff_style ?
$styles{$_} :
9226 $cgi->a({-href
=> href
(-replay
=>1, diff_style
=> $_)}, $styles{$_})
9230 sub git_commitdiff
{
9232 my $format = $params{-format
} || 'html';
9233 my $diff_style = $input_params{'diff_style'} || 'inline';
9235 my ($patch_max) = gitweb_get_feature
('patches');
9236 if ($format eq 'patch') {
9237 die_error
(403, "Patch view not allowed") unless $patch_max;
9240 $hash ||= $hash_base || "HEAD";
9241 my %co = parse_commit
($hash)
9242 or die_error
(404, "Unknown commit object");
9244 # choose format for commitdiff for merge
9245 if (! defined $hash_parent && @
{$co{'parents'}} > 1) {
9246 $hash_parent = '--cc';
9248 # we need to prepare $formats_nav before almost any parameter munging
9250 if ($format eq 'html') {
9252 $cgi->a({-href
=> href
(action
=>"commitdiff_plain", -replay
=>1)},
9254 if ($patch_max && @
{$co{'parents'}} <= 1) {
9255 $formats_nav .= " | " .
9256 $cgi->a({-href
=> href
(action
=>"patch", -replay
=>1)},
9259 $formats_nav .= diff_style_nav
($diff_style, @
{$co{'parents'}} > 1);
9261 if (defined $hash_parent &&
9262 $hash_parent ne '-c' && $hash_parent ne '--cc') {
9263 # commitdiff with two commits given
9264 my $hash_parent_short = $hash_parent;
9265 if ($hash_parent =~ m/^[0-9a-fA-F]{40}$/) {
9266 $hash_parent_short = substr($hash_parent, 0, 7);
9270 for (my $i = 0; $i < @
{$co{'parents'}}; $i++) {
9271 if ($co{'parents'}[$i] eq $hash_parent) {
9272 $formats_nav .= ' parent ' . ($i+1);
9276 $formats_nav .= ': ' .
9277 $cgi->a({-href
=> href
(-replay
=>1,
9278 hash
=>$hash_parent, hash_base
=>undef)},
9279 esc_html
($hash_parent_short)) .
9281 } elsif (!$co{'parent'}) {
9283 $formats_nav .= ' (initial)';
9284 } elsif (scalar @
{$co{'parents'}} == 1) {
9285 # single parent commit
9288 $cgi->a({-href
=> href
(-replay
=>1,
9289 hash
=>$co{'parent'}, hash_base
=>undef)},
9290 esc_html
(substr($co{'parent'}, 0, 7))) .
9294 if ($hash_parent eq '--cc') {
9295 $formats_nav .= ' | ' .
9296 $cgi->a({-href
=> href
(-replay
=>1,
9297 hash
=>$hash, hash_parent
=>'-c')},
9299 } else { # $hash_parent eq '-c'
9300 $formats_nav .= ' | ' .
9301 $cgi->a({-href
=> href
(-replay
=>1,
9302 hash
=>$hash, hash_parent
=>'--cc')},
9308 $cgi->a({-href
=> href
(-replay
=>1,
9309 hash
=>$_, hash_base
=>undef)},
9310 esc_html
(substr($_, 0, 7)));
9311 } @
{$co{'parents'}} ) .
9316 my $hash_parent_param = $hash_parent;
9317 if (!defined $hash_parent_param) {
9318 # --cc for multiple parents, --root for parentless
9319 $hash_parent_param =
9320 @
{$co{'parents'}} > 1 ?
'--cc' : $co{'parent'} || '--root';
9326 if ($format eq 'html') {
9327 defined($fd = git_cmd_pipe
"diff-tree", '-r', @diff_opts,
9328 "--no-commit-id", "--patch-with-raw", "--full-index",
9329 $hash_parent_param, $hash, "--")
9330 or die_error
(500, "Open git-diff-tree failed");
9332 while (my $line = to_utf8
(scalar <$fd>)) {
9334 # empty line ends raw part of diff-tree output
9336 push @difftree, scalar parse_difftree_raw_line
($line);
9339 } elsif ($format eq 'plain') {
9340 defined($fd = git_cmd_pipe
"diff-tree", '-r', @diff_opts,
9341 '-p', $hash_parent_param, $hash, "--")
9342 or die_error
(500, "Open git-diff-tree failed");
9343 } elsif ($format eq 'patch') {
9344 # For commit ranges, we limit the output to the number of
9345 # patches specified in the 'patches' feature.
9346 # For single commits, we limit the output to a single patch,
9347 # diverging from the git-format-patch default.
9348 my @commit_spec = ();
9350 if ($patch_max > 0) {
9351 push @commit_spec, "-$patch_max";
9353 push @commit_spec, '-n', "$hash_parent..$hash";
9355 if ($params{-single
}) {
9356 push @commit_spec, '-1';
9358 if ($patch_max > 0) {
9359 push @commit_spec, "-$patch_max";
9361 push @commit_spec, "-n";
9363 push @commit_spec, '--root', $hash;
9365 defined($fd = git_cmd_pipe
"format-patch", @diff_opts,
9366 '--encoding=utf8', '--stdout', @commit_spec)
9367 or die_error
(500, "Open git-format-patch failed");
9369 die_error
(400, "Unknown commitdiff format");
9372 # non-textual hash id's can be cached
9374 if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
9378 # write commit message
9379 if ($format eq 'html') {
9380 my $refs = git_get_references
();
9381 my $ref = format_ref_marker
($refs, $co{'id'});
9383 git_header_html
(undef, $expires);
9384 git_print_page_nav
('commitdiff','', $hash,$co{'tree'},$hash, $formats_nav);
9385 git_print_header_div
('commit', esc_html
($co{'title'}), $hash, undef, $ref);
9386 print "<div class=\"title_text\">\n" .
9387 "<table class=\"object_header\">\n";
9388 git_print_authorship_rows
(\
%co);
9391 print "<div class=\"page_body\">\n";
9392 if (@
{$co{'comment'}} > 1) {
9393 print "<div class=\"log\">\n";
9394 git_print_log
($co{'comment'}, -final_empty_line
=> 1, -remove_title
=> 1);
9395 print "</div>\n"; # class="log"
9398 } elsif ($format eq 'plain') {
9399 my $refs = git_get_references
("tags");
9400 my $tagname = git_get_rev_name_tags
($hash);
9401 my $filename = basename
($project) . "-$hash.patch";
9404 -type
=> 'text/plain',
9405 -charset
=> 'utf-8',
9406 -expires
=> $expires,
9407 -content_disposition
=> 'inline; filename="' . "$filename" . '"');
9408 my %ad = parse_date
($co{'author_epoch'}, $co{'author_tz'});
9409 print "From: " . to_utf8
($co{'author'}) . "\n";
9410 print "Date: $ad{'rfc2822'} ($ad{'tz_local'})\n";
9411 print "Subject: " . to_utf8
($co{'title'}) . "\n";
9413 print "X-Git-Tag: $tagname\n" if $tagname;
9414 print "X-Git-Url: " . $cgi->self_url() . "\n\n";
9416 foreach my $line (@
{$co{'comment'}}) {
9417 print to_utf8
($line) . "\n";
9420 } elsif ($format eq 'patch') {
9421 my $filename = basename
($project) . "-$hash.patch";
9424 -type
=> 'text/plain',
9425 -charset
=> 'utf-8',
9426 -expires
=> $expires,
9427 -content_disposition
=> 'inline; filename="' . "$filename" . '"');
9431 if ($format eq 'html') {
9432 my $use_parents = !defined $hash_parent ||
9433 $hash_parent eq '-c' || $hash_parent eq '--cc';
9434 git_difftree_body
(\
@difftree, $hash,
9435 $use_parents ? @
{$co{'parents'}} : $hash_parent);
9438 git_patchset_body
($fd, $diff_style,
9440 $use_parents ? @
{$co{'parents'}} : $hash_parent);
9442 print "</div>\n"; # class="page_body"
9445 } elsif ($format eq 'plain') {
9450 or print "Reading git-diff-tree failed\n";
9451 } elsif ($format eq 'patch') {
9456 or print "Reading git-format-patch failed\n";
9460 sub git_commitdiff_plain
{
9461 git_commitdiff
(-format
=> 'plain');
9464 # format-patch-style patches
9466 git_commitdiff
(-format
=> 'patch', -single
=> 1);
9470 git_commitdiff
(-format
=> 'patch');
9474 git_log_generic
('history', \
&git_history_body
,
9475 $hash_base, $hash_parent_base,
9480 $searchtype ||= 'commit';
9482 # check if appropriate features are enabled
9483 gitweb_check_feature
('search')
9484 or die_error
(403, "Search is disabled");
9485 if ($searchtype eq 'pickaxe') {
9486 # pickaxe may take all resources of your box and run for several minutes
9487 # with every query - so decide by yourself how public you make this feature
9488 gitweb_check_feature
('pickaxe')
9489 or die_error
(403, "Pickaxe search is disabled");
9491 if ($searchtype eq 'grep') {
9492 # grep search might be potentially CPU-intensive, too
9493 gitweb_check_feature
('grep')
9494 or die_error
(403, "Grep search is disabled");
9496 if ($search_use_regexp) {
9497 # regular expression search can be disabled to avoid potentially
9498 # malicious regular expressions
9499 gitweb_check_feature
('regexp')
9500 or die_error
(403, "Regular expression search is disabled");
9503 if (!defined $searchtext) {
9504 die_error
(400, "Text field is empty");
9506 if (!defined $hash) {
9507 $hash = git_get_head_hash
($project);
9509 my %co = parse_commit
($hash);
9511 die_error
(404, "Unknown commit object");
9513 if (!defined $page) {
9517 if ($searchtype eq 'commit' ||
9518 $searchtype eq 'author' ||
9519 $searchtype eq 'committer') {
9520 git_search_message
(%co);
9521 } elsif ($searchtype eq 'pickaxe') {
9522 git_search_changes
(%co);
9523 } elsif ($searchtype eq 'grep') {
9524 git_search_files
(%co);
9526 die_error
(400, "Unknown search type");
9530 sub git_search_help
{
9532 git_print_page_nav
('','', $hash,$hash,$hash);
9534 <p><strong>Pattern</strong> is by default a normal string that is matched precisely (but without
9535 regard to case, except in the case of pickaxe). However, when you check the <em>re</em> checkbox,
9536 the pattern entered is recognized as the POSIX extended
9537 <a href="http://en.wikipedia.org/wiki/Regular_expression">regular expression</a> (also case
9540 <dt><b>commit</b></dt>
9541 <dd>The commit messages and authorship information will be scanned for the given pattern.</dd>
9543 my $have_grep = gitweb_check_feature
('grep');
9546 <dt><b>grep</b></dt>
9547 <dd>All files in the currently selected tree (HEAD unless you are explicitly browsing
9548 a different one) are searched for the given pattern. On large trees, this search can take
9549 a while and put some strain on the server, so please use it with some consideration. Note that
9550 due to git-grep peculiarity, currently if regexp mode is turned off, the matches are
9551 case-sensitive.</dd>
9555 <dt><b>author</b></dt>
9556 <dd>Name and e-mail of the change author and date of birth of the patch will be scanned for the given pattern.</dd>
9557 <dt><b>committer</b></dt>
9558 <dd>Name and e-mail of the committer and date of commit will be scanned for the given pattern.</dd>
9560 my $have_pickaxe = gitweb_check_feature
('pickaxe');
9561 if ($have_pickaxe) {
9563 <dt><b>pickaxe</b></dt>
9564 <dd>All commits that caused the string to appear or disappear from any file (changes that
9565 added, removed or "modified" the string) will be listed. This search can take a while and
9566 takes a lot of strain on the server, so please use it wisely. Note that since you may be
9567 interested even in changes just changing the case as well, this search is case sensitive.</dd>
9575 git_log_generic
('shortlog', \
&git_shortlog_body
,
9576 $hash, $hash_parent);
9579 ## ......................................................................
9580 ## feeds (RSS, Atom; OPML)
9583 my $format = shift || 'atom';
9584 my $have_blame = gitweb_check_feature
('blame');
9586 # Atom: http://www.atomenabled.org/developers/syndication/
9587 # RSS: http://www.notestips.com/80256B3A007F2692/1/NAMO5P9UPQ
9588 if ($format ne 'rss' && $format ne 'atom') {
9589 die_error
(400, "Unknown web feed format");
9592 # log/feed of current (HEAD) branch, log of given branch, history of file/directory
9593 my $head = $hash || 'HEAD';
9594 my @commitlist = parse_commits
($head, 150, 0, $file_name);
9598 my $content_type = "application/$format+xml";
9599 if (defined $cgi->http('HTTP_ACCEPT') &&
9600 $cgi->Accept('text/xml') > $cgi->Accept($content_type)) {
9601 # browser (feed reader) prefers text/xml
9602 $content_type = 'text/xml';
9604 if (defined($commitlist[0])) {
9605 %latest_commit = %{$commitlist[0]};
9606 my $latest_epoch = $latest_commit{'committer_epoch'};
9607 exit_if_unmodified_since
($latest_epoch);
9608 %latest_date = parse_date
($latest_epoch, $latest_commit{'committer_tz'});
9611 -type
=> $content_type,
9612 -charset
=> 'utf-8',
9613 %latest_date ?
(-last_modified
=> $latest_date{'rfc2822'}) : (),
9614 -status
=> '200 OK');
9616 # Optimization: skip generating the body if client asks only
9617 # for Last-Modified date.
9618 return if ($cgi->request_method() eq 'HEAD');
9621 my $title = "$site_name - $project/$action";
9622 my $feed_type = 'log';
9623 if (defined $hash) {
9624 $title .= " - '$hash'";
9625 $feed_type = 'branch log';
9626 if (defined $file_name) {
9627 $title .= " :: $file_name";
9628 $feed_type = 'history';
9630 } elsif (defined $file_name) {
9631 $title .= " - $file_name";
9632 $feed_type = 'history';
9634 $title .= " $feed_type";
9635 $title = esc_html
($title);
9636 my $descr = git_get_project_description
($project);
9637 if (defined $descr) {
9638 $descr = esc_html
($descr);
9640 $descr = "$project " .
9641 ($format eq 'rss' ?
'RSS' : 'Atom') .
9644 my $owner = git_get_project_owner
($project);
9645 $owner = esc_html
($owner);
9649 if (defined $file_name) {
9650 $alt_url = href
(-full
=>1, action
=>"history", hash
=>$hash, file_name
=>$file_name);
9651 } elsif (defined $hash) {
9652 $alt_url = href
(-full
=>1, action
=>"log", hash
=>$hash);
9654 $alt_url = href
(-full
=>1, action
=>"summary");
9656 print qq!<?xml version
="1.0" encoding
="utf-8"?
>\n!;
9657 if ($format eq 'rss') {
9659 <rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
9662 print "<title>$title</title>\n" .
9663 "<link>$alt_url</link>\n" .
9664 "<description>$descr</description>\n" .
9665 "<language>en</language>\n" .
9666 # project owner is responsible for 'editorial' content
9667 "<managingEditor>$owner</managingEditor>\n";
9668 if (defined $logo || defined $favicon) {
9669 # prefer the logo to the favicon, since RSS
9670 # doesn't allow both
9671 my $img = esc_url
($logo || $favicon);
9673 "<url>$img</url>\n" .
9674 "<title>$title</title>\n" .
9675 "<link>$alt_url</link>\n" .
9679 print "<pubDate>$latest_date{'rfc2822'}</pubDate>\n";
9680 print "<lastBuildDate>$latest_date{'rfc2822'}</lastBuildDate>\n";
9682 print "<generator>gitweb v.$version/$git_version</generator>\n";
9683 } elsif ($format eq 'atom') {
9685 <feed xmlns="http://www.w3.org/2005/Atom">
9687 print "<title>$title</title>\n" .
9688 "<subtitle>$descr</subtitle>\n" .
9689 '<link rel="alternate" type="text/html" href="' .
9690 $alt_url . '" />' . "\n" .
9691 '<link rel="self" type="' . $content_type . '" href="' .
9692 $cgi->self_url() . '" />' . "\n" .
9693 "<id>" . href
(-full
=>1) . "</id>\n" .
9694 # use project owner for feed author
9695 '<author><name>'. email_obfuscate
($owner) . '</name></author>\n';
9696 if (defined $favicon) {
9697 print "<icon>" . esc_url
($favicon) . "</icon>\n";
9699 if (defined $logo) {
9700 # not twice as wide as tall: 72 x 27 pixels
9701 print "<logo>" . esc_url
($logo) . "</logo>\n";
9703 if (! %latest_date) {
9704 # dummy date to keep the feed valid until commits trickle in:
9705 print "<updated>1970-01-01T00:00:00Z</updated>\n";
9707 print "<updated>$latest_date{'iso-8601'}</updated>\n";
9709 print "<generator version='$version/$git_version'>gitweb</generator>\n";
9713 for (my $i = 0; $i <= $#commitlist; $i++) {
9714 my %co = %{$commitlist[$i]};
9715 my $commit = $co{'id'};
9716 # we read 150, we always show 30 and the ones more recent than 48 hours
9717 if (($i >= 20) && ((time - $co{'author_epoch'}) > 48*60*60)) {
9720 my %cd = parse_date
($co{'author_epoch'}, $co{'author_tz'});
9722 # get list of changed files
9723 defined(my $fd = git_cmd_pipe
"diff-tree", '-r', @diff_opts,
9724 $co{'parent'} || "--root",
9725 $co{'id'}, "--", (defined $file_name ?
$file_name : ()))
9727 my @difftree = map { chomp; to_utf8
($_) } <$fd>;
9731 # print element (entry, item)
9732 my $co_url = href
(-full
=>1, action
=>"commitdiff", hash
=>$commit);
9733 if ($format eq 'rss') {
9735 "<title>" . esc_html
($co{'title'}) . "</title>\n" .
9736 "<author>" . esc_html
($co{'author'}) . "</author>\n" .
9737 "<pubDate>$cd{'rfc2822'}</pubDate>\n" .
9738 "<guid isPermaLink=\"true\">$co_url</guid>\n" .
9739 "<link>$co_url</link>\n" .
9740 "<description>" . esc_html
($co{'title'}) . "</description>\n" .
9741 "<content:encoded>" .
9743 } elsif ($format eq 'atom') {
9745 "<title type=\"html\">" . esc_html
($co{'title'}) . "</title>\n" .
9746 "<updated>$cd{'iso-8601'}</updated>\n" .
9748 " <name>" . esc_html
($co{'author_name'}) . "</name>\n";
9749 if ($co{'author_email'}) {
9750 print " <email>" . esc_html
($co{'author_email'}) . "</email>\n";
9752 print "</author>\n" .
9753 # use committer for contributor
9755 " <name>" . esc_html
($co{'committer_name'}) . "</name>\n";
9756 if ($co{'committer_email'}) {
9757 print " <email>" . esc_html
($co{'committer_email'}) . "</email>\n";
9759 print "</contributor>\n" .
9760 "<published>$cd{'iso-8601'}</published>\n" .
9761 "<link rel=\"alternate\" type=\"text/html\" href=\"$co_url\" />\n" .
9762 "<id>$co_url</id>\n" .
9763 "<content type=\"xhtml\" xml:base=\"" . esc_url
($my_url) . "\">\n" .
9764 "<div xmlns=\"http://www.w3.org/1999/xhtml\">\n";
9766 my $comment = $co{'comment'};
9768 foreach my $line (@
$comment) {
9769 $line = esc_html
($line);
9772 print "</pre><ul>\n";
9773 foreach my $difftree_line (@difftree) {
9774 my %difftree = parse_difftree_raw_line
($difftree_line);
9775 next if !$difftree{'from_id'};
9777 my $file = $difftree{'file'} || $difftree{'to_file'};
9781 $cgi->a({-href
=> href
(-full
=>1, action
=>"blobdiff",
9782 hash
=>$difftree{'to_id'}, hash_parent
=>$difftree{'from_id'},
9783 hash_base
=>$co{'id'}, hash_parent_base
=>$co{'parent'},
9784 file_name
=>$file, file_parent
=>$difftree{'from_file'}),
9785 -title
=> "diff"}, 'D');
9787 print $cgi->a({-href
=> href
(-full
=>1, action
=>"blame",
9788 file_name
=>$file, hash_base
=>$commit),
9789 -class => "blamelink",
9790 -title
=> "blame"}, 'B');
9792 # if this is not a feed of a file history
9793 if (!defined $file_name || $file_name ne $file) {
9794 print $cgi->a({-href
=> href
(-full
=>1, action
=>"history",
9795 file_name
=>$file, hash
=>$commit),
9796 -title
=> "history"}, 'H');
9798 $file = esc_path
($file);
9802 if ($format eq 'rss') {
9803 print "</ul>]]>\n" .
9804 "</content:encoded>\n" .
9806 } elsif ($format eq 'atom') {
9807 print "</ul>\n</div>\n" .
9814 if ($format eq 'rss') {
9815 print "</channel>\n</rss>\n";
9816 } elsif ($format eq 'atom') {
9830 my @list = git_get_projects_list
($project_filter, $strict_export);
9832 die_error
(404, "No projects found");
9836 -type
=> 'text/xml',
9837 -charset
=> 'utf-8',
9838 -content_disposition
=> 'inline; filename="opml.xml"');
9840 my $title = esc_html
($site_name);
9841 my $filter = " within subdirectory ";
9842 if (defined $project_filter) {
9843 $filter .= esc_html
($project_filter);
9848 <?xml version="1.0" encoding="utf-8"?>
9849 <opml version="1.0">
9851 <title>$title OPML Export$filter</title>
9854 <outline text="git RSS feeds">
9857 foreach my $pr (@list) {
9859 my $head = git_get_head_hash
($proj{'path'});
9860 if (!defined $head) {
9863 $git_dir = "$projectroot/$proj{'path'}";
9864 my %co = parse_commit
($head);
9869 my $path = esc_html
(chop_str
($proj{'path'}, 25, 5));
9870 my $rss = href
('project' => $proj{'path'}, 'action' => 'rss', -full
=> 1);
9871 my $html = href
('project' => $proj{'path'}, 'action' => 'summary', -full
=> 1);
9872 print "<outline type=\"rss\" text=\"$path\" title=\"$path\" xmlUrl=\"$rss\" htmlUrl=\"$html\"/>\n";