3 # gitweb - simple web interface to track changes in git repositories
5 # (C) 2005-2006, Kay Sievers <kay.sievers@vrfy.org>
6 # (C) 2005, Christian Gierke
8 # This program is licensed under the GPLv2
13 use CGI
qw(:standard :escapeHTML -nosticky);
14 use CGI
::Util
qw(unescape);
15 use CGI
::Carp
qw(fatalsToBrowser set_message);
19 use File
::Basename
qw(basename);
21 use Time
::HiRes
qw(gettimeofday tv_interval);
23 use constant GITWEB_CACHE_FORMAT
=> "Gitweb Cache Format 3";
24 binmode STDOUT
, ':utf8';
26 if (!defined($CGI::VERSION
) || $CGI::VERSION
< 4.08) {
27 eval 'sub CGI::multi_param { CGI::param(@_) }'
30 our $t0 = [ gettimeofday
() ];
31 our $number_of_git_cmds = 0;
32 our ($mdotsep, $barsep, $spcsep);
35 *mdotsep
= \'<span
class="mdotsep"> · </span>';
36 *barsep
= \'<span
class="barsep"> | </span>';
37 *spcsep
= \'<span
class="spcsep"> </span>';
38 CGI
->compile() if $ENV{'MOD_PERL'};
41 our $version = "++GIT_VERSION++";
43 our ($my_url, $my_uri, $base_url, $path_info, $home_link);
47 our $my_url = $cgi->url();
48 our $my_uri = $cgi->url(-absolute
=> 1);
50 # Base URL for relative URLs in gitweb ($logo, $favicon, ...),
51 # needed and used only for URLs with nonempty PATH_INFO
52 # This must be an absolute URL (i.e. no scheme, host or port), NOT a full one
53 our $base_url = $my_uri || '/';
55 # When the script is used as DirectoryIndex, the URL does not contain the name
56 # of the script file itself, and $cgi->url() fails to strip PATH_INFO, so we
57 # have to do it ourselves. We make $path_info global because it's also used
60 # Another issue with the script being the DirectoryIndex is that the resulting
61 # $my_url data is not the full script URL: this is good, because we want
62 # generated links to keep implying the script name if it wasn't explicitly
63 # indicated in the URL we're handling, but it means that $my_url cannot be used
65 # Therefore, if we needed to strip PATH_INFO, then we know that we have
66 # to build the base URL ourselves:
67 our $path_info = decode_utf8
($ENV{"PATH_INFO"});
69 # $path_info has already been URL-decoded by the web server, but
70 # $my_url and $my_uri have not. URL-decode them so we can properly
72 $my_url = unescape
($my_url);
73 $my_uri = unescape
($my_uri);
74 if ($my_url =~ s
,\Q
$path_info\E
$,, &&
75 $my_uri =~ s
,\Q
$path_info\E
$,, &&
76 defined $ENV{'SCRIPT_NAME'}) {
77 $base_url = $ENV{'SCRIPT_NAME'} || '/';
81 # target of the home link on top of all pages
82 our $home_link = $my_uri || "/";
85 # core git executable to use
86 # this can just be "git" if your webserver has a sensible PATH
87 our $GIT = "++GIT_BINDIR++/git";
89 # absolute fs-path which will be prepended to the project path
90 #our $projectroot = "/pub/scm";
91 our $projectroot = "++GITWEB_PROJECTROOT++";
93 # fs traversing limit for getting project list
94 # the number is relative to the projectroot
95 our $project_maxdepth = "++GITWEB_PROJECT_MAXDEPTH++";
97 # string of the home link on top of all pages
98 our $home_link_str = "++GITWEB_HOME_LINK_STR++";
100 # extra breadcrumbs preceding the home link
101 our @extra_breadcrumbs = ();
103 # name of your site or organization to appear in page titles
104 # replace this with something more descriptive for clearer bookmarks
105 our $site_name = "++GITWEB_SITENAME++"
106 || ($ENV{'SERVER_NAME'} || "Untitled") . " Git";
108 # html snippet to include in the <head> section of each page
109 our $site_html_head_string = "++GITWEB_SITE_HTML_HEAD_STRING++";
110 # filename of html text to include at top of each page
111 our $site_header = "++GITWEB_SITE_HEADER++";
112 # html text to include at home page
113 our $home_text = "++GITWEB_HOMETEXT++";
114 # filename of html text to include at bottom of each page
115 our $site_footer = "++GITWEB_SITE_FOOTER++";
118 our @stylesheets = ("++GITWEB_CSS++");
119 # URI of a single stylesheet, which can be overridden in GITWEB_CONFIG.
120 our $stylesheet = undef;
121 # URI of GIT logo (72x27 size)
122 our $logo = "++GITWEB_LOGO++";
123 # URI of GIT favicon, assumed to be image/png type
124 our $favicon = "++GITWEB_FAVICON++";
125 # URI of gitweb.js (JavaScript code for gitweb)
126 our $javascript = "++GITWEB_JS++";
128 # URI and label (title) of GIT logo link
129 #our $logo_url = "http://www.kernel.org/pub/software/scm/git/docs/";
130 #our $logo_label = "git documentation";
131 our $logo_url = "http://git-scm.com/";
132 our $logo_label = "git homepage";
134 # source of projects list
135 our $projects_list = "++GITWEB_LIST++";
137 # the width (in characters) of the projects list "Description" column
138 our $projects_list_description_width = 25;
140 # group projects by category on the projects list
141 # (enabled if this variable evaluates to true)
142 our $projects_list_group_categories = 0;
144 # default category if none specified
145 # (leave the empty string for no category)
146 our $project_list_default_category = "";
148 # default order of projects list
149 # valid values are none, project, descr, owner, and age
150 our $default_projects_order = "project";
152 # default order of refs list
153 # valid values are age and name
154 our $default_refs_order = "age";
156 # show repository only if this file exists
157 # (only effective if this variable evaluates to true)
158 our $export_ok = "++GITWEB_EXPORT_OK++";
160 # don't generate age column on the projects list page
161 our $omit_age_column = 0;
163 # use contents of this file (in iso, iso-strict or raw format) as
164 # the last activity data if it exists and is a valid date
165 our $lastactivity_file = undef;
167 # don't generate information about owners of repositories
170 # owner link hook given owner name (full and NOT obfuscated)
171 # should return full URL-escaped link to attach to owner, for example:
172 # sub { return "/showowner.cgi?owner=".CGI::Util::escape($_[0]); }
173 our $owner_link_hook = undef;
175 # show repository only if this subroutine returns true
176 # when given the path to the project, for example:
177 # sub { return -e "$_[0]/git-daemon-export-ok"; }
178 our $export_auth_hook = undef;
180 # only allow viewing of repositories also shown on the overview page
181 our $strict_export = "++GITWEB_STRICT_EXPORT++";
183 # base URL for bundle info link shown on summary page, but only if
184 # this config item is defined AND a 'bundles' subdirectory exists
185 # in the project's repository.
186 # i.e. full URL is "git_base_bundles_url/$project/bundles"
187 our $git_base_bundles_url = undef;
189 # list of git base URLs used for URL to where fetch project from,
190 # i.e. full URL is "$git_base_url/$project"
191 our @git_base_url_list = grep { $_ ne '' } ("++GITWEB_BASE_URL++");
193 # URLs designated for pushing new changes, extended by the
194 # project name (i.e. "$git_base_push_url[0]/$project")
195 our @git_base_push_urls = ();
197 # https hint html inserted right after any https push URL (undef for none)
198 our $https_hint_html = undef;
200 # default blob_plain mimetype and default charset for text/plain blob
201 our $default_blob_plain_mimetype = 'application/octet-stream';
202 our $default_text_plain_charset = undef;
204 # file to use for guessing MIME types before trying /etc/mime.types
205 # (relative to the current git repository)
206 our $mimetypes_file = undef;
208 # assume this charset if line contains non-UTF-8 characters;
209 # it should be valid encoding (see Encoding::Supported(3pm) for list),
210 # for which encoding all byte sequences are valid, for example
211 # 'iso-8859-1' aka 'latin1' (it is decoded without checking, so it
212 # could be even 'utf-8' for the old behavior)
213 our $fallback_encoding = 'latin1';
215 # rename detection options for git-diff and git-diff-tree
216 # - default is '-M', with the cost proportional to
217 # (number of removed files) * (number of new files).
218 # - more costly is '-C' (which implies '-M'), with the cost proportional to
219 # (number of changed files + number of removed files) * (number of new files)
220 # - even more costly is '-C', '--find-copies-harder' with cost
221 # (number of files in the original tree) * (number of new files)
222 # - one might want to include '-B' option, e.g. '-B', '-M'
223 our @diff_opts = ('-M'); # taken from git_commit
225 # cache directory (relative to $GIT_DIR) for project-specific html page caches.
226 # the directory must exist and be writable by the process running gitweb.
227 # additionally some actions must be selected for caching in %html_cache_actions
228 # - default is 'htmlcache'
229 our $html_cache_dir = 'htmlcache';
231 # which actions to cache in $html_cache_dir
232 # if $html_cache_dir exists (relative to $GIT_DIR) and is writable by the
233 # process running gitweb, then any actions selected here will have their output
234 # cached and the cache file will be returned instead of regenerating the page
235 # if it exists. For this to be useful, an external process must create the
236 # 'changed' file (if it does not already exist) in the $html_cache_dir whenever
237 # the project information has been changed. Alternatively it may create a
238 # "$action.changed" file (if it does not exist) instead to limit the changes
239 # to just "$action" instead of any action. If 'changed' or "$action.changed"
240 # exist, then the cached version will never be used for "$action" and a new
241 # cache page will be regenerated (and the "changed" files removed as appropriate).
243 # Additionally if $projlist_cache_lifetime is > 0 AND the forks feature has been
244 # enabled ('$feature{'forks'}{'default'} = [1];') then additionally an external
245 # process must create the 'forkchange' file or update its timestamp if it already
246 # exists whenever a fork is added to or removed from the project (as well as
247 # create the 'changed' or "$action.changed" file). Otherwise the "forks"
248 # section on the summary page may remain out-of-date indefinately.
251 # currently only caching of the summary page is supported
252 # - to enable caching of the summary page use:
253 # $html_cache_actions{'summary'} = 1;
254 our %html_cache_actions = ();
256 # utility to automatically produce a default README.html if README.html is
257 # enabled and it does not exist or is 0 bytes in length. If this is set to an
258 # executable utility that takes an absolute path to a .git directory as its
259 # first argument and outputs an HTML fragment to use for README.html, then
260 # it will be called when README.html is enabled but empty or missing.
261 our $git_automatic_readme_html = undef;
263 # Disables features that would allow repository owners to inject script into
265 our $prevent_xss = 0;
267 # Path to a POSIX shell. Needed to run $highlight_bin and a snapshot compressor.
268 # Only used when highlight is enabled or snapshots with compressors are enabled.
269 our $posix_shell_bin = "++POSIX_SHELL_BIN++";
271 # Path to the highlight executable to use (must be the one from
272 # http://www.andre-simon.de due to assumptions about parameters and output).
273 # Useful if highlight is not installed on your webserver's PATH.
274 # [Default: highlight]
275 our $highlight_bin = "++HIGHLIGHT_BIN++";
277 # Whether to include project list on the gitweb front page; 0 means yes,
278 # 1 means no list but show tag cloud if enabled (all projects still need
279 # to be scanned, unless the info is cached), 2 means no list and no tag cloud
281 our $frontpage_no_project_list = 0;
283 # projects list cache for busy sites with many projects;
284 # if you set this to non-zero, it will be used as the cached
285 # index lifetime in minutes
287 # the cached list version is stored in $cache_dir/$cache_name and can
288 # be tweaked by other scripts running with the same uid as gitweb -
289 # use this ONLY at secure installations; only single gitweb project
290 # root per system is supported, unless you tweak configuration!
291 our $projlist_cache_lifetime = 0; # in minutes
292 # FHS compliant $cache_dir would be "/var/cache/gitweb"
294 (defined $ENV{'TMPDIR'} ?
$ENV{'TMPDIR'} : '/tmp').'/gitweb';
295 our $projlist_cache_name = 'gitweb.index.cache';
296 our $cache_grpshared = 0;
298 # information about snapshot formats that gitweb is capable of serving
299 our %known_snapshot_formats = (
301 # 'display' => display name,
302 # 'type' => mime type,
303 # 'suffix' => filename suffix,
304 # 'format' => --format for git-archive,
305 # 'compressor' => [compressor command and arguments]
306 # (array reference, optional)
307 # 'disabled' => boolean (optional)}
310 'display' => 'tar.gz',
311 'type' => 'application/x-gzip',
312 'suffix' => '.tar.gz',
314 'compressor' => ['gzip', '-n']},
317 'display' => 'tar.bz2',
318 'type' => 'application/x-bzip2',
319 'suffix' => '.tar.bz2',
321 'compressor' => ['bzip2']},
324 'display' => 'tar.xz',
325 'type' => 'application/x-xz',
326 'suffix' => '.tar.xz',
328 'compressor' => ['xz'],
333 'type' => 'application/x-zip',
338 # Aliases so we understand old gitweb.snapshot values in repository
340 our %known_snapshot_format_aliases = (
345 # backward compatibility: legacy gitweb config support
346 'x-gzip' => undef, 'gz' => undef,
347 'x-bzip2' => undef, 'bz2' => undef,
348 'x-zip' => undef, '' => undef,
351 # Pixel sizes for icons and avatars. If the default font sizes or lineheights
352 # are changed, it may be appropriate to change these values too via
359 # Used to set the maximum load that we will still respond to gitweb queries.
360 # If server load exceed this value then return "503 server busy" error.
361 # If gitweb cannot determined server load, it is taken to be 0.
362 # Leave it undefined (or set to 'undef') to turn off load checking.
365 # configuration for 'highlight' (http://www.andre-simon.de/)
367 our %highlight_basename = (
370 'SConstruct' => 'py', # SCons equivalent of Makefile
371 'Makefile' => 'make',
372 'makefile' => 'make',
373 'GNUmakefile' => 'make',
374 'BSDmakefile' => 'make',
376 # match by shebang regex
377 our %highlight_shebang = (
378 # Each entry has a key which is the syntax to use and
379 # a value which is either a qr regex or an array of qr regexs to match
380 # against the first 128 (less if the blob is shorter) BYTES of the blob.
381 # We match /usr/bin/env items separately to require "/usr/bin/env" and
382 # allow a limited subset of NAME=value items to appear.
383 'awk' => [ qr
,^#!\s*/(?:\w+/)*(?:[gnm]?awk)(?:\s|$),mo,
384 qr
,^#!\s*/usr/bin/env\s+(?:\w+=\w*\s+)*(?:[gnm]?awk)(?:\s|$),mo ],
385 'make' => [ qr
,^#!\s*/(?:\w+/)*(?:g?make)(?:\s|$),mo,
386 qr
,^#!\s*/usr/bin/env\s+(?:\w+=\w*\s+)*(?:g?make)(?:\s|$),mo ],
387 'php' => [ qr
,^#!\s*/(?:\w+/)*(?:php)(?:\s|$),mo,
388 qr
,^#!\s*/usr/bin/env\s+(?:\w+=\w*\s+)*(?:php)(?:\s|$),mo ],
389 'pl' => [ qr
,^#!\s*/(?:\w+/)*(?:perl)(?:\s|$),mo,
390 qr
,^#!\s*/usr/bin/env\s+(?:\w+=\w*\s+)*(?:perl)(?:\s|$),mo ],
391 'py' => [ qr
,^#!\s*/(?:\w+/)*(?:python)(?:\s|$),mo,
392 qr
,^#!\s*/usr/bin/env\s+(?:\w+=\w*\s+)*(?:python)(?:\s|$),mo ],
393 'sh' => [ qr
,^#!\s*/(?:\w+/)*(?:[bd]ash|t?csh|[akz]?sh)(?:\s|$),mo,
394 qr
,^#!\s*/usr/bin/env\s+(?:\w+=\w*\s+)*(?:[bd]ash|t?csh|[akz]?sh)(?:\s|$),mo ],
395 'rb' => [ qr
,^#!\s*/(?:\w+/)*(?:perl)(?:\s|$),mo,
396 qr
,^#!\s*/usr/bin/env\s+(?:\w+=\w*\s+)*(?:perl)(?:\s|$),mo ],
399 our %highlight_ext = (
400 # main extensions, defining name of syntax;
401 # see files in /usr/share/highlight/langDefs/ directory
402 (map { $_ => $_ } qw(
403 4gl a4c abnf abp ada agda ahk ampl amtrix applescript arc
404 arm as asm asp aspect ats au3 avenue awk bat bb bbcode bib
405 bms bnf boo c cb cfc chl clipper clojure clp cob cs css d
406 diff dot dylan e ebnf erl euphoria exp f90 flx for frink fs
407 go haskell hcl html httpd hx icl icn idl idlang ili
408 inc_luatex ini inp io iss j java js jsp lbn ldif lgt lhs
409 lisp lotos ls lsl lua ly make mel mercury mib miranda ml mo
410 mod2 mod3 mpl ms mssql n nas nbc nice nrx nsi nut nxc oberon
411 objc octave oorexx os oz pas php pike pl pl1 pov pro
412 progress ps ps1 psl pure py pyx q qmake qu r rb rebol rexx
413 rnc s sas sc scala scilab sh sma smalltalk sml sno spec spn
414 sql sybase tcl tcsh tex ttcn3 vala vb verilog vhd xml xpp y
416 # alternate extensions, see /etc/highlight/filetypes.conf
417 (map { $_ => '4gl' } qw(informix)),
418 (map { $_ => 'a4c' } qw(ascend)),
419 (map { $_ => 'abp' } qw(abp4)),
420 (map { $_ => 'ada' } qw(a adb ads gnad)),
421 (map { $_ => 'ahk' } qw(autohotkey)),
422 (map { $_ => 'ampl' } qw(dat run)),
423 (map { $_ => 'amtrix' } qw(hnd s4 s4h s4t t4)),
424 (map { $_ => 'as' } qw(actionscript)),
425 (map { $_ => 'asm' } qw(29k 68s 68x a51 assembler x68 x86)),
426 (map { $_ => 'asp' } qw(asa)),
427 (map { $_ => 'aspect' } qw(was wud)),
428 (map { $_ => 'ats' } qw(dats)),
429 (map { $_ => 'au3' } qw(autoit)),
430 (map { $_ => 'bat' } qw(cmd)),
431 (map { $_ => 'bb' } qw(blitzbasic)),
432 (map { $_ => 'bib' } qw(bibtex)),
433 (map { $_ => 'c' } qw(c++ cc cpp cu cxx h hh hpp hxx)),
434 (map { $_ => 'cb' } qw(clearbasic)),
435 (map { $_ => 'cfc' } qw(cfm coldfusion)),
436 (map { $_ => 'chl' } qw(chill)),
437 (map { $_ => 'cob' } qw(cbl cobol)),
438 (map { $_ => 'cs' } qw(csharp)),
439 (map { $_ => 'diff' } qw(patch)),
440 (map { $_ => 'dot' } qw(graphviz)),
441 (map { $_ => 'e' } qw(eiffel se)),
442 (map { $_ => 'erl' } qw(erlang hrl)),
443 (map { $_ => 'euphoria' } qw(eu ew ex exu exw wxu)),
444 (map { $_ => 'exp' } qw(express)),
445 (map { $_ => 'f90' } qw(f95)),
446 (map { $_ => 'flx' } qw(felix)),
447 (map { $_ => 'for' } qw(f f77 ftn)),
448 (map { $_ => 'fs' } qw(fsharp fsx)),
449 (map { $_ => 'haskell' } qw(hs)),
450 (map { $_ => 'html' } qw(htm xhtml)),
451 (map { $_ => 'hx' } qw(haxe)),
452 (map { $_ => 'icl' } qw(clean)),
453 (map { $_ => 'icn' } qw(icon)),
454 (map { $_ => 'ili' } qw(interlis)),
455 (map { $_ => 'inp' } qw(fame)),
456 (map { $_ => 'iss' } qw(innosetup)),
457 (map { $_ => 'j' } qw(jasmin)),
458 (map { $_ => 'java' } qw(groovy grv)),
459 (map { $_ => 'lbn' } qw(luban)),
460 (map { $_ => 'lgt' } qw(logtalk)),
461 (map { $_ => 'lisp' } qw(cl clisp el lsp sbcl scom)),
462 (map { $_ => 'ls' } qw(lotus)),
463 (map { $_ => 'lsl' } qw(lindenscript)),
464 (map { $_ => 'ly' } qw(lilypond)),
465 (map { $_ => 'make' } qw(mak mk kmk)),
466 (map { $_ => 'mel' } qw(maya)),
467 (map { $_ => 'mib' } qw(smi snmp)),
468 (map { $_ => 'ml' } qw(mli ocaml)),
469 (map { $_ => 'mo' } qw(modelica)),
470 (map { $_ => 'mod2' } qw(def mod)),
471 (map { $_ => 'mod3' } qw(i3 m3)),
472 (map { $_ => 'mpl' } qw(maple)),
473 (map { $_ => 'n' } qw(nemerle)),
474 (map { $_ => 'nas' } qw(nasal)),
475 (map { $_ => 'nrx' } qw(netrexx)),
476 (map { $_ => 'nsi' } qw(nsis)),
477 (map { $_ => 'nut' } qw(squirrel)),
478 (map { $_ => 'oberon' } qw(ooc)),
479 (map { $_ => 'objc' } qw(M m mm)),
480 (map { $_ => 'php' } qw(php3 php4 php5 php6)),
481 (map { $_ => 'pike' } qw(pmod)),
482 (map { $_ => 'pl' } qw(perl plex plx pm)),
483 (map { $_ => 'pl1' } qw(bdy ff fp fpp rpp sf sp spb spe spp sps wf wp wpb wpp wps)),
484 (map { $_ => 'progress' } qw(i p w)),
485 (map { $_ => 'py' } qw(python)),
486 (map { $_ => 'pyx' } qw(pyrex)),
487 (map { $_ => 'rb' } qw(pp rjs ruby)),
488 (map { $_ => 'rexx' } qw(rex rx the)),
489 (map { $_ => 'sc' } qw(paradox)),
490 (map { $_ => 'scilab' } qw(sce sci)),
491 (map { $_ => 'sh' } qw(bash ebuild eclass ksh zsh)),
492 (map { $_ => 'sma' } qw(small)),
493 (map { $_ => 'smalltalk' } qw(gst sq st)),
494 (map { $_ => 'sno' } qw(snobal)),
495 (map { $_ => 'sybase' } qw(sp)),
496 (map { $_ => 'tcl' } qw(itcl wish)),
497 (map { $_ => 'tex' } qw(cls sty)),
498 (map { $_ => 'vb' } qw(bas basic bi vbs)),
499 (map { $_ => 'verilog' } qw(v)),
500 (map { $_ => 'xml' } qw(dtd ecf ent hdr hub jnlp nrm plist resx sgm sgml svg tld vxml wml xsd xsl)),
501 (map { $_ => 'y' } qw(bison)),
504 # You define site-wide feature defaults here; override them with
505 # $GITWEB_CONFIG as necessary.
508 # 'sub' => feature-sub (subroutine),
509 # 'override' => allow-override (boolean),
510 # 'default' => [ default options...] (array reference)}
512 # if feature is overridable (it means that allow-override has true value),
513 # then feature-sub will be called with default options as parameters;
514 # return value of feature-sub indicates if to enable specified feature
516 # if there is no 'sub' key (no feature-sub), then feature cannot be
519 # use gitweb_get_feature(<feature>) to retrieve the <feature> value
520 # (an array) or gitweb_check_feature(<feature>) to check if <feature>
523 # Enable the 'blame' blob view, showing the last commit that modified
524 # each line in the file. This can be very CPU-intensive.
526 # To enable system wide have in $GITWEB_CONFIG
527 # $feature{'blame'}{'default'} = [1];
528 # To have project specific config enable override in $GITWEB_CONFIG
529 # $feature{'blame'}{'override'} = 1;
530 # and in project config gitweb.blame = 0|1;
532 'sub' => sub { feature_bool
('blame', @_) },
536 # Enable the 'incremental blame' blob view, which uses javascript to
537 # incrementally show the revisions of lines as they are discovered
538 # in the history. It is better for large histories, files and slow
539 # servers, but requires javascript in the client and can slow down the
540 # browser on large files.
542 # To enable system wide have in $GITWEB_CONFIG
543 # $feature{'blame_incremental'}{'default'} = [1];
544 # To have project specific config enable override in $GITWEB_CONFIG
545 # $feature{'blame_incremental'}{'override'} = 1;
546 # and in project config gitweb.blame_incremental = 0|1;
547 'blame_incremental' => {
548 'sub' => sub { feature_bool
('blame_incremental', @_) },
552 # Enable the 'snapshot' link, providing a compressed archive of any
553 # tree. This can potentially generate high traffic if you have large
556 # Value is a list of formats defined in %known_snapshot_formats that
558 # To disable system wide have in $GITWEB_CONFIG
559 # $feature{'snapshot'}{'default'} = [];
560 # To have project specific config enable override in $GITWEB_CONFIG
561 # $feature{'snapshot'}{'override'} = 1;
562 # and in project config, a comma-separated list of formats or "none"
563 # to disable. Example: gitweb.snapshot = tbz2,zip;
565 'sub' => \
&feature_snapshot
,
567 'default' => ['tgz']},
569 # Enable text search, which will list the commits which match author,
570 # committer or commit text to a given string. Enabled by default.
571 # Project specific override is not supported.
573 # Note that this controls all search features, which means that if
574 # it is disabled, then 'grep' and 'pickaxe' search would also be
580 # Enable grep search, which will list the files in currently selected
581 # tree containing the given string. Enabled by default. This can be
582 # potentially CPU-intensive, of course.
583 # Note that you need to have 'search' feature enabled too.
585 # To enable system wide have in $GITWEB_CONFIG
586 # $feature{'grep'}{'default'} = [1];
587 # To have project specific config enable override in $GITWEB_CONFIG
588 # $feature{'grep'}{'override'} = 1;
589 # and in project config gitweb.grep = 0|1;
591 'sub' => sub { feature_bool
('grep', @_) },
595 # Enable the pickaxe search, which will list the commits that modified
596 # a given string in a file. This can be practical and quite faster
597 # alternative to 'blame', but still potentially CPU-intensive.
598 # Note that you need to have 'search' feature enabled too.
600 # To enable system wide have in $GITWEB_CONFIG
601 # $feature{'pickaxe'}{'default'} = [1];
602 # To have project specific config enable override in $GITWEB_CONFIG
603 # $feature{'pickaxe'}{'override'} = 1;
604 # and in project config gitweb.pickaxe = 0|1;
606 'sub' => sub { feature_bool
('pickaxe', @_) },
610 # Enable showing size of blobs in a 'tree' view, in a separate
611 # column, similar to what 'ls -l' does. This cost a bit of IO.
613 # To disable system wide have in $GITWEB_CONFIG
614 # $feature{'show-sizes'}{'default'} = [0];
615 # To have project specific config enable override in $GITWEB_CONFIG
616 # $feature{'show-sizes'}{'override'} = 1;
617 # and in project config gitweb.showsizes = 0|1;
619 'sub' => sub { feature_bool
('showsizes', @_) },
623 # Make gitweb use an alternative format of the URLs which can be
624 # more readable and natural-looking: project name is embedded
625 # directly in the path and the query string contains other
626 # auxiliary information. All gitweb installations recognize
627 # URL in either format; this configures in which formats gitweb
630 # To enable system wide have in $GITWEB_CONFIG
631 # $feature{'pathinfo'}{'default'} = [1];
632 # Project specific override is not supported.
634 # Note that you will need to change the default location of CSS,
635 # favicon, logo and possibly other files to an absolute URL. Also,
636 # if gitweb.cgi serves as your indexfile, you will need to force
637 # $my_uri to contain the script name in your $GITWEB_CONFIG (and you
638 # will also likely want to set $home_link if you're setting $my_uri).
643 # Make gitweb consider projects in project root subdirectories
644 # to be forks of existing projects. Given project $projname.git,
645 # projects matching $projname/*.git will not be shown in the main
646 # projects list, instead a '+' mark will be added to $projname
647 # there and a 'forks' view will be enabled for the project, listing
648 # all the forks. If project list is taken from a file, forks have
649 # to be listed after the main project.
651 # To enable system wide have in $GITWEB_CONFIG
652 # $feature{'forks'}{'default'} = [1];
653 # Project specific override is not supported.
658 # Insert custom links to the action bar of all project pages.
659 # This enables you mainly to link to third-party scripts integrating
660 # into gitweb; e.g. git-browser for graphical history representation
661 # or custom web-based repository administration interface.
663 # The 'default' value consists of a list of triplets in the form
664 # (label, link, position) where position is the label after which
665 # to insert the link and link is a format string where %n expands
666 # to the project name, %f to the project path within the filesystem,
667 # %h to the current hash (h gitweb parameter) and %b to the current
668 # hash base (hb gitweb parameter); %% expands to %. %e expands to the
669 # project name where all '+' characters have been replaced with '%2B'.
671 # To enable system wide have in $GITWEB_CONFIG e.g.
672 # $feature{'actions'}{'default'} = [('graphiclog',
673 # '/git-browser/by-commit.html?r=%n', 'summary')];
674 # Project specific override is not supported.
679 # Allow gitweb scan project content tags of project repository,
680 # and display the popular Web 2.0-ish "tag cloud" near the projects
681 # list. Note that this is something COMPLETELY different from the
684 # gitweb by itself can show existing tags, but it does not handle
685 # tagging itself; you need to do it externally, outside gitweb.
686 # The format is described in git_get_project_ctags() subroutine.
687 # You may want to install the HTML::TagCloud Perl module to get
688 # a pretty tag cloud instead of just a list of tags.
690 # To enable system wide have in $GITWEB_CONFIG
691 # $feature{'ctags'}{'default'} = [1];
692 # Project specific override is not supported.
694 # A value of 0 means no ctags display or editing. A value of
695 # 1 enables ctags display but never editing. A non-empty value
696 # that is not a string of digits enables ctags display AND the
697 # ability to add tags using a form that uses method POST and
698 # an action value set to the configured 'ctags' value.
703 # The maximum number of patches in a patchset generated in patch
704 # view. Set this to 0 or undef to disable patch view, or to a
705 # negative number to remove any limit.
707 # To disable system wide have in $GITWEB_CONFIG
708 # $feature{'patches'}{'default'} = [0];
709 # To have project specific config enable override in $GITWEB_CONFIG
710 # $feature{'patches'}{'override'} = 1;
711 # and in project config gitweb.patches = 0|n;
712 # where n is the maximum number of patches allowed in a patchset.
714 'sub' => \
&feature_patches
,
718 # Avatar support. When this feature is enabled, views such as
719 # shortlog or commit will display an avatar associated with
720 # the email of the committer(s) and/or author(s).
722 # Currently available providers are gravatar and picon.
723 # If an unknown provider is specified, the feature is disabled.
725 # Gravatar depends on Digest::MD5.
726 # Picon currently relies on the indiana.edu database.
728 # To enable system wide have in $GITWEB_CONFIG
729 # $feature{'avatar'}{'default'} = ['<provider>'];
730 # where <provider> is either gravatar or picon.
731 # To have project specific config enable override in $GITWEB_CONFIG
732 # $feature{'avatar'}{'override'} = 1;
733 # and in project config gitweb.avatar = <provider>;
735 'sub' => \
&feature_avatar
,
739 # Enable displaying how much time and how many git commands
740 # it took to generate and display page. Disabled by default.
741 # Project specific override is not supported.
746 # Enable turning some links into links to actions which require
747 # JavaScript to run (like 'blame_incremental'). Not enabled by
748 # default. Project specific override is currently not supported.
749 'javascript-actions' => {
753 # Enable and configure ability to change common timezone for dates
754 # in gitweb output via JavaScript. Enabled by default.
755 # Project specific override is not supported.
756 'javascript-timezone' => {
759 'local', # default timezone: 'utc', 'local', or '(-|+)HHMM' format,
760 # or undef to turn off this feature
761 'gitweb_tz', # name of cookie where to store selected timezone
762 'datetime', # CSS class used to mark up dates for manipulation
765 # Syntax highlighting support. This is based on Daniel Svensson's
766 # and Sham Chukoury's work in gitweb-xmms2.git.
767 # It requires the 'highlight' program present in $PATH,
768 # and therefore is disabled by default.
770 # To enable system wide have in $GITWEB_CONFIG
771 # $feature{'highlight'}{'default'} = [1];
774 'sub' => sub { feature_bool
('highlight', @_) },
778 # Enable displaying of remote heads in the heads list
780 # To enable system wide have in $GITWEB_CONFIG
781 # $feature{'remote_heads'}{'default'} = [1];
782 # To have project specific config enable override in $GITWEB_CONFIG
783 # $feature{'remote_heads'}{'override'} = 1;
784 # and in project config gitweb.remoteheads = 0|1;
786 'sub' => sub { feature_bool
('remote_heads', @_) },
790 # Enable showing branches under other refs in addition to heads
792 # To set system wide extra branch refs have in $GITWEB_CONFIG
793 # $feature{'extra-branch-refs'}{'default'} = ['dirs', 'of', 'choice'];
794 # To have project specific config enable override in $GITWEB_CONFIG
795 # $feature{'extra-branch-refs'}{'override'} = 1;
796 # and in project config gitweb.extrabranchrefs = dirs of choice
797 # Every directory is separated with whitespace.
799 'extra-branch-refs' => {
800 'sub' => \
&feature_extra_branch_refs
,
805 sub gitweb_get_feature
{
807 return unless exists $feature{$name};
808 my ($sub, $override, @defaults) = (
809 $feature{$name}{'sub'},
810 $feature{$name}{'override'},
811 @
{$feature{$name}{'default'}});
812 # project specific override is possible only if we have project
813 our $git_dir; # global variable, declared later
814 if (!$override || !defined $git_dir) {
818 warn "feature $name is not overridable";
821 return $sub->(@defaults);
824 # A wrapper to check if a given feature is enabled.
825 # With this, you can say
827 # my $bool_feat = gitweb_check_feature('bool_feat');
828 # gitweb_check_feature('bool_feat') or somecode;
832 # my ($bool_feat) = gitweb_get_feature('bool_feat');
833 # (gitweb_get_feature('bool_feat'))[0] or somecode;
835 sub gitweb_check_feature
{
836 return (gitweb_get_feature
(@_))[0];
842 my ($val) = git_get_project_config
($key, '--bool');
846 } elsif ($val eq 'true') {
848 } elsif ($val eq 'false') {
853 sub feature_snapshot
{
856 my ($val) = git_get_project_config
('snapshot');
859 @fmts = ($val eq 'none' ?
() : split /\s*[,\s]\s*/, $val);
865 sub feature_patches
{
866 my @val = (git_get_project_config
('patches', '--int'));
876 my @val = (git_get_project_config
('avatar'));
878 return @val ?
@val : @_;
881 sub feature_extra_branch_refs
{
882 my (@branch_refs) = @_;
883 my $values = git_get_project_config
('extrabranchrefs');
886 $values = config_to_multi
($values);
888 foreach my $value (@
{$values}) {
889 push @branch_refs, split /\s+/, $value;
896 # checking HEAD file with -e is fragile if the repository was
897 # initialized long time ago (i.e. symlink HEAD) and was pack-ref'ed
899 sub check_head_link
{
901 return 0 unless -d
"$dir/objects" && -x _
;
902 return 0 unless -d
"$dir/refs" && -x _
;
903 my $headfile = "$dir/HEAD";
904 return -l
$headfile ?
905 readlink($headfile) =~ /^refs\/heads\
// : -f
$headfile;
908 sub check_export_ok
{
910 return (check_head_link
($dir) &&
911 (!$export_ok || -e
"$dir/$export_ok") &&
912 (!$export_auth_hook || $export_auth_hook->($dir)));
915 # process alternate names for backward compatibility
916 # filter out unsupported (unknown) snapshot formats
917 sub filter_snapshot_fmts
{
921 exists $known_snapshot_format_aliases{$_} ?
922 $known_snapshot_format_aliases{$_} : $_} @fmts;
924 exists $known_snapshot_formats{$_} &&
925 !$known_snapshot_formats{$_}{'disabled'}} @fmts;
928 sub filter_and_validate_refs
{
930 my %unique_refs = ();
932 foreach my $ref (@refs) {
933 die_error
(500, "Invalid ref '$ref' in 'extra-branch-refs' feature") unless (is_valid_ref_format
($ref));
934 # 'heads' are added implicitly in get_branch_refs().
935 $unique_refs{$ref} = 1 if ($ref ne 'heads');
937 return sort keys %unique_refs;
940 # If it is set to code reference, it is code that it is to be run once per
941 # request, allowing updating configurations that change with each request,
942 # while running other code in config file only once.
944 # Otherwise, if it is false then gitweb would process config file only once;
945 # if it is true then gitweb config would be run for each request.
946 our $per_request_config = 1;
948 # If true and fileno STDIN is 0 and getsockname succeeds and getpeername fails
949 # with ENOTCONN, then FCGI mode will be activated automatically in just the
950 # same way as though the --fcgi option had been given instead.
953 # read and parse gitweb config file given by its parameter.
954 # returns true on success, false on recoverable error, allowing
955 # to chain this subroutine, using first file that exists.
956 # dies on errors during parsing config file, as it is unrecoverable.
957 sub read_config_file
{
958 my $filename = shift;
959 return unless defined $filename;
960 # die if there are errors parsing config file
969 our ($GITWEB_CONFIG, $GITWEB_CONFIG_SYSTEM, $GITWEB_CONFIG_COMMON);
970 sub evaluate_gitweb_config
{
971 our $GITWEB_CONFIG = $ENV{'GITWEB_CONFIG'} || "++GITWEB_CONFIG++";
972 our $GITWEB_CONFIG_SYSTEM = $ENV{'GITWEB_CONFIG_SYSTEM'} || "++GITWEB_CONFIG_SYSTEM++";
973 our $GITWEB_CONFIG_COMMON = $ENV{'GITWEB_CONFIG_COMMON'} || "++GITWEB_CONFIG_COMMON++";
975 # Protect against duplications of file names, to not read config twice.
976 # Only one of $GITWEB_CONFIG and $GITWEB_CONFIG_SYSTEM is used, so
977 # there possibility of duplication of filename there doesn't matter.
978 $GITWEB_CONFIG = "" if ($GITWEB_CONFIG eq $GITWEB_CONFIG_COMMON);
979 $GITWEB_CONFIG_SYSTEM = "" if ($GITWEB_CONFIG_SYSTEM eq $GITWEB_CONFIG_COMMON);
981 # Common system-wide settings for convenience.
982 # Those settings can be ovverriden by GITWEB_CONFIG or GITWEB_CONFIG_SYSTEM.
983 read_config_file
($GITWEB_CONFIG_COMMON);
985 # Use first config file that exists. This means use the per-instance
986 # GITWEB_CONFIG if exists, otherwise use GITWEB_SYSTEM_CONFIG.
987 read_config_file
($GITWEB_CONFIG) and return;
988 read_config_file
($GITWEB_CONFIG_SYSTEM);
993 sub evaluate_encoding
{
994 my $requested = $fallback_encoding || 'ISO-8859-1';
995 my $obj = Encode
::find_encoding
($requested) or
996 die_error
(400, "Requested fallback encoding not found");
997 if ($obj->name eq 'iso-8859-1') {
998 # Use Windows-1252 instead as required by the HTML 5 standard
999 my $altobj = Encode
::find_encoding
('Windows-1252');
1000 $obj = $altobj if $altobj;
1002 $encode_object = $obj;
1005 sub evaluate_email_obfuscate
{
1008 if (!$email && eval { require HTML
::Email
::Obfuscate
; 1 }) {
1009 $email = HTML
::Email
::Obfuscate
->new(lite
=> 1);
1013 # Get loadavg of system, to compare against $maxload.
1014 # Currently it requires '/proc/loadavg' present to get loadavg;
1015 # if it is not present it returns 0, which means no load checking.
1017 if( -e
'/proc/loadavg' ){
1018 open my $fd, '<', '/proc/loadavg'
1020 my @load = split(/\s+/, scalar <$fd>);
1023 # The first three columns measure CPU and IO utilization of the last one,
1024 # five, and 10 minute periods. The fourth column shows the number of
1025 # currently running processes and the total number of processes in the m/n
1026 # format. The last column displays the last process ID used.
1027 return $load[0] || 0;
1029 # additional checks for load average should go here for things that don't export
1035 # version of the core git binary
1037 sub evaluate_git_version
{
1038 our $git_version = $version;
1042 if (defined $maxload && get_loadavg
() > $maxload) {
1043 die_error
(503, "The load average on the server is too high");
1047 # ======================================================================
1048 # input validation and dispatch
1050 # input parameters can be collected from a variety of sources (presently, CGI
1051 # and PATH_INFO), so we define an %input_params hash that collects them all
1052 # together during validation: this allows subsequent uses (e.g. href()) to be
1053 # agnostic of the parameter origin
1055 our %input_params = ();
1057 # input parameters are stored with the long parameter name as key. This will
1058 # also be used in the href subroutine to convert parameters to their CGI
1059 # equivalent, and since the href() usage is the most frequent one, we store
1060 # the name -> CGI key mapping here, instead of the reverse.
1062 # XXX: Warning: If you touch this, check the search form for updating,
1065 our @cgi_param_mapping = (
1069 file_parent
=> "fp",
1071 hash_parent
=> "hp",
1073 hash_parent_base
=> "hpb",
1078 snapshot_format
=> "sf",
1080 extra_options
=> "opt",
1081 search_use_regexp
=> "sr",
1084 project_filter
=> "pf",
1085 # this must be last entry (for manipulation from JavaScript)
1088 our %cgi_param_mapping = @cgi_param_mapping;
1090 # we will also need to know the possible actions, for validation
1092 "blame" => \
&git_blame
,
1093 "blame_incremental" => \
&git_blame_incremental
,
1094 "blame_data" => \
&git_blame_data
,
1095 "blobdiff" => \
&git_blobdiff
,
1096 "blobdiff_plain" => \
&git_blobdiff_plain
,
1097 "blob" => \
&git_blob
,
1098 "blob_plain" => \
&git_blob_plain
,
1099 "commitdiff" => \
&git_commitdiff
,
1100 "commitdiff_plain" => \
&git_commitdiff_plain
,
1101 "commit" => \
&git_commit
,
1102 "forks" => \
&git_forks
,
1103 "heads" => \
&git_heads
,
1104 "history" => \
&git_history
,
1106 "patch" => \
&git_patch
,
1107 "patches" => \
&git_patches
,
1108 "refs" => \
&git_refs
,
1109 "remotes" => \
&git_remotes
,
1111 "atom" => \
&git_atom
,
1112 "search" => \
&git_search
,
1113 "search_help" => \
&git_search_help
,
1114 "shortlog" => \
&git_shortlog
,
1115 "summary" => \
&git_summary
,
1117 "tags" => \
&git_tags
,
1118 "tree" => \
&git_tree
,
1119 "snapshot" => \
&git_snapshot
,
1120 "object" => \
&git_object
,
1121 # those below don't need $project
1122 "opml" => \
&git_opml
,
1123 "frontpage" => \
&git_frontpage
,
1124 "project_list" => \
&git_project_list
,
1125 "project_index" => \
&git_project_index
,
1128 # the only actions we will allow to be cached
1129 my %supported_cache_actions;
1130 BEGIN {%supported_cache_actions = map {( $_ => 1 )} qw(summary)}
1132 # finally, we have the hash of allowed extra_options for the commands that
1134 our %allowed_options = (
1135 "--no-merges" => [ qw(rss atom log shortlog history) ],
1138 # fill %input_params with the CGI parameters. All values except for 'opt'
1139 # should be single values, but opt can be an array. We should probably
1140 # build an array of parameters that can be multi-valued, but since for the time
1141 # being it's only this one, we just single it out
1142 sub evaluate_query_params
{
1145 while (my ($name, $symbol) = each %cgi_param_mapping) {
1146 if ($symbol eq 'opt') {
1147 $input_params{$name} = [ map { decode_utf8
($_) } $cgi->multi_param($symbol) ];
1149 $input_params{$name} = decode_utf8
($cgi->param($symbol));
1153 # Backwards compatibility - by_tag= <=> t=
1154 if ($input_params{'ctag'}) {
1155 $input_params{'ctag_filter'} = $input_params{'ctag'};
1159 # now read PATH_INFO and update the parameter list for missing parameters
1160 sub evaluate_path_info
{
1161 return if defined $input_params{'project'};
1162 return if !$path_info;
1163 $path_info =~ s
,^/+,,;
1164 return if !$path_info;
1166 # find which part of PATH_INFO is project
1167 my $project = $path_info;
1168 $project =~ s
,/+$,,;
1169 while ($project && !check_head_link
("$projectroot/$project")) {
1170 $project =~ s
,/*[^/]*$,,;
1172 return unless $project;
1173 $input_params{'project'} = $project;
1175 # do not change any parameters if an action is given using the query string
1176 return if $input_params{'action'};
1177 $path_info =~ s
,^\Q
$project\E
/*,,;
1179 # next, check if we have an action
1180 my $action = $path_info;
1181 $action =~ s
,/.*$,,;
1182 if (exists $actions{$action}) {
1183 $path_info =~ s
,^$action/*,,;
1184 $input_params{'action'} = $action;
1187 # list of actions that want hash_base instead of hash, but can have no
1188 # pathname (f) parameter
1194 # we want to catch, among others
1195 # [$hash_parent_base[:$file_parent]..]$hash_parent[:$file_name]
1196 my ($parentrefname, $parentpathname, $refname, $pathname) =
1197 ($path_info =~ /^(?:(.+?)(?::(.+))?\.\.)?([^:]+?)?(?::(.+))?$/);
1199 # first, analyze the 'current' part
1200 if (defined $pathname) {
1201 # we got "branch:filename" or "branch:dir/"
1202 # we could use git_get_type(branch:pathname), but:
1203 # - it needs $git_dir
1204 # - it does a git() call
1205 # - the convention of terminating directories with a slash
1206 # makes it superfluous
1207 # - embedding the action in the PATH_INFO would make it even
1209 $pathname =~ s
,^/+,,;
1210 if (!$pathname || substr($pathname, -1) eq "/") {
1211 $input_params{'action'} ||= "tree";
1212 $pathname =~ s
,/$,,;
1214 # the default action depends on whether we had parent info
1216 if ($parentrefname) {
1217 $input_params{'action'} ||= "blobdiff_plain";
1219 $input_params{'action'} ||= "blob_plain";
1222 $input_params{'hash_base'} ||= $refname;
1223 $input_params{'file_name'} ||= $pathname;
1224 } elsif (defined $refname) {
1225 # we got "branch". In this case we have to choose if we have to
1226 # set hash or hash_base.
1228 # Most of the actions without a pathname only want hash to be
1229 # set, except for the ones specified in @wants_base that want
1230 # hash_base instead. It should also be noted that hand-crafted
1231 # links having 'history' as an action and no pathname or hash
1232 # set will fail, but that happens regardless of PATH_INFO.
1233 if (defined $parentrefname) {
1234 # if there is parent let the default be 'shortlog' action
1235 # (for http://git.example.com/repo.git/A..B links); if there
1236 # is no parent, dispatch will detect type of object and set
1237 # action appropriately if required (if action is not set)
1238 $input_params{'action'} ||= "shortlog";
1240 if ($input_params{'action'} &&
1241 grep { $_ eq $input_params{'action'} } @wants_base) {
1242 $input_params{'hash_base'} ||= $refname;
1244 $input_params{'hash'} ||= $refname;
1248 # next, handle the 'parent' part, if present
1249 if (defined $parentrefname) {
1250 # a missing pathspec defaults to the 'current' filename, allowing e.g.
1251 # someproject/blobdiff/oldrev..newrev:/filename
1252 if ($parentpathname) {
1253 $parentpathname =~ s
,^/+,,;
1254 $parentpathname =~ s
,/$,,;
1255 $input_params{'file_parent'} ||= $parentpathname;
1257 $input_params{'file_parent'} ||= $input_params{'file_name'};
1259 # we assume that hash_parent_base is wanted if a path was specified,
1260 # or if the action wants hash_base instead of hash
1261 if (defined $input_params{'file_parent'} ||
1262 grep { $_ eq $input_params{'action'} } @wants_base) {
1263 $input_params{'hash_parent_base'} ||= $parentrefname;
1265 $input_params{'hash_parent'} ||= $parentrefname;
1269 # for the snapshot action, we allow URLs in the form
1270 # $project/snapshot/$hash.ext
1271 # where .ext determines the snapshot and gets removed from the
1272 # passed $refname to provide the $hash.
1274 # To be able to tell that $refname includes the format extension, we
1275 # require the following two conditions to be satisfied:
1276 # - the hash input parameter MUST have been set from the $refname part
1277 # of the URL (i.e. they must be equal)
1278 # - the snapshot format MUST NOT have been defined already (e.g. from
1280 # It's also useless to try any matching unless $refname has a dot,
1281 # so we check for that too
1282 if (defined $input_params{'action'} &&
1283 $input_params{'action'} eq 'snapshot' &&
1284 defined $refname && index($refname, '.') != -1 &&
1285 $refname eq $input_params{'hash'} &&
1286 !defined $input_params{'snapshot_format'}) {
1287 # We loop over the known snapshot formats, checking for
1288 # extensions. Allowed extensions are both the defined suffix
1289 # (which includes the initial dot already) and the snapshot
1290 # format key itself, with a prepended dot
1291 while (my ($fmt, $opt) = each %known_snapshot_formats) {
1292 my $hash = $refname;
1293 unless ($hash =~ s/(\Q$opt->{'suffix'}\E|\Q.$fmt\E)$//) {
1297 # a valid suffix was found, so set the snapshot format
1298 # and reset the hash parameter
1299 $input_params{'snapshot_format'} = $fmt;
1300 $input_params{'hash'} = $hash;
1301 # we also set the format suffix to the one requested
1302 # in the URL: this way a request for e.g. .tgz returns
1303 # a .tgz instead of a .tar.gz
1304 $known_snapshot_formats{$fmt}{'suffix'} = $sfx;
1310 our ($action, $project, $file_name, $file_parent, $hash, $hash_parent, $hash_base,
1311 $hash_parent_base, @extra_options, $page, $searchtype, $search_use_regexp,
1312 $searchtext, $search_regexp, $project_filter);
1313 sub evaluate_and_validate_params
{
1314 our $action = $input_params{'action'};
1315 if (defined $action) {
1316 if (!is_valid_action
($action)) {
1317 die_error
(400, "Invalid action parameter");
1321 # parameters which are pathnames
1322 our $project = $input_params{'project'};
1323 if (defined $project) {
1324 if (!is_valid_project
($project)) {
1326 die_error
(404, "No such project");
1330 our $project_filter = $input_params{'project_filter'};
1331 if (defined $project_filter) {
1332 if (!is_valid_pathname
($project_filter)) {
1333 die_error
(404, "Invalid project_filter parameter");
1337 our $file_name = $input_params{'file_name'};
1338 if (defined $file_name) {
1339 if (!is_valid_pathname
($file_name)) {
1340 die_error
(400, "Invalid file parameter");
1344 our $file_parent = $input_params{'file_parent'};
1345 if (defined $file_parent) {
1346 if (!is_valid_pathname
($file_parent)) {
1347 die_error
(400, "Invalid file parent parameter");
1351 # parameters which are refnames
1352 our $hash = $input_params{'hash'};
1353 if (defined $hash) {
1354 if (!is_valid_refname
($hash)) {
1355 die_error
(400, "Invalid hash parameter");
1359 our $hash_parent = $input_params{'hash_parent'};
1360 if (defined $hash_parent) {
1361 if (!is_valid_refname
($hash_parent)) {
1362 die_error
(400, "Invalid hash parent parameter");
1366 our $hash_base = $input_params{'hash_base'};
1367 if (defined $hash_base) {
1368 if (!is_valid_refname
($hash_base)) {
1369 die_error
(400, "Invalid hash base parameter");
1373 our @extra_options = @
{$input_params{'extra_options'}};
1374 # @extra_options is always defined, since it can only be (currently) set from
1375 # CGI, and $cgi->param() returns the empty array in array context if the param
1377 foreach my $opt (@extra_options) {
1378 if (not exists $allowed_options{$opt}) {
1379 die_error
(400, "Invalid option parameter");
1381 if (not grep(/^$action$/, @
{$allowed_options{$opt}})) {
1382 die_error
(400, "Invalid option parameter for this action");
1386 our $hash_parent_base = $input_params{'hash_parent_base'};
1387 if (defined $hash_parent_base) {
1388 if (!is_valid_refname
($hash_parent_base)) {
1389 die_error
(400, "Invalid hash parent base parameter");
1394 our $page = $input_params{'page'};
1395 if (defined $page) {
1396 if ($page =~ m/[^0-9]/) {
1397 die_error
(400, "Invalid page parameter");
1401 our $searchtype = $input_params{'searchtype'};
1402 if (defined $searchtype) {
1403 if ($searchtype =~ m/[^a-z]/) {
1404 die_error
(400, "Invalid searchtype parameter");
1408 our $search_use_regexp = $input_params{'search_use_regexp'};
1410 our $searchtext = $input_params{'searchtext'};
1411 our $search_regexp = undef;
1412 if (defined $searchtext) {
1413 if (length($searchtext) < 2) {
1414 die_error
(403, "At least two characters are required for search parameter");
1416 if ($search_use_regexp) {
1417 $search_regexp = $searchtext;
1418 if (!eval { qr/$search_regexp/; 1; }) {
1419 (my $error = $@
) =~ s/ at \S+ line \d+.*\n?//;
1420 die_error
(400, "Invalid search regexp '$search_regexp'",
1424 $search_regexp = quotemeta $searchtext;
1429 # path to the current git repository
1431 sub evaluate_git_dir
{
1432 our $git_dir = $project ?
"$projectroot/$project" : undef;
1435 our (@snapshot_fmts, $git_avatar, @extra_branch_refs);
1436 sub configure_gitweb_features
{
1437 # list of supported snapshot formats
1438 our @snapshot_fmts = gitweb_get_feature
('snapshot');
1439 @snapshot_fmts = filter_snapshot_fmts
(@snapshot_fmts);
1441 # check that the avatar feature is set to a known provider name,
1442 # and for each provider check if the dependencies are satisfied.
1443 # if the provider name is invalid or the dependencies are not met,
1444 # reset $git_avatar to the empty string.
1445 our ($git_avatar) = gitweb_get_feature
('avatar');
1446 if ($git_avatar eq 'gravatar') {
1447 $git_avatar = '' unless (eval { require Digest
::MD5
; 1; });
1448 } elsif ($git_avatar eq 'picon') {
1454 our @extra_branch_refs = gitweb_get_feature
('extra-branch-refs');
1455 @extra_branch_refs = filter_and_validate_refs
(@extra_branch_refs);
1458 sub get_branch_refs
{
1459 return ('heads', @extra_branch_refs);
1462 # custom error handler: 'die <message>' is Internal Server Error
1463 sub handle_errors_html
{
1464 my $msg = shift; # it is already HTML escaped
1466 # to avoid infinite loop where error occurs in die_error,
1467 # change handler to default handler, disabling handle_errors_html
1468 set_message
("Error occurred when inside die_error:\n$msg");
1470 # you cannot jump out of die_error when called as error handler;
1471 # the subroutine set via CGI::Carp::set_message is called _after_
1472 # HTTP headers are already written, so it cannot write them itself
1473 die_error
(undef, undef, $msg, -error_handler
=> 1, -no_http_header
=> 1);
1475 set_message
(\
&handle_errors_html
);
1477 our $shown_stale_message = 0;
1478 our $cache_dump = undef;
1479 our $cache_dump_mtime = undef;
1482 my $cache_mode_active;
1484 $shown_stale_message = 0;
1485 if (!defined $action) {
1486 if (defined $hash) {
1487 $action = git_get_type
($hash);
1488 $action or die_error
(404, "Object does not exist");
1489 } elsif (defined $hash_base && defined $file_name) {
1490 $action = git_get_type
("$hash_base:$file_name");
1491 $action or die_error
(404, "File or directory does not exist");
1492 } elsif (defined $project) {
1493 $action = 'summary';
1495 $action = 'frontpage';
1498 if (!defined($actions{$action})) {
1499 die_error
(400, "Unknown action");
1501 if ($action !~ m/^(?:opml|frontpage|project_list|project_index)$/ &&
1503 die_error
(400, "Project needed");
1506 my $defstyle = $stylesheet;
1507 local $stylesheet = $defstyle;
1508 if ($ENV{'HTTP_COOKIE'} && ";$ENV{'HTTP_COOKIE'};" =~ /;\s*style\s*=\s*([a-z][a-z0-9_-]*)\s*;/) {{
1510 last unless $ENV{'DOCUMENT_ROOT'} && -r
"$ENV{'DOCUMENT_ROOT'}/style/$stylename.css";
1511 $stylesheet = "/style/$stylename.css";
1514 my $cached_page = $supported_cache_actions{$action}
1515 ? cached_action_page
($action)
1517 goto DUMPCACHE
if $cached_page;
1518 local *SAVEOUT
= *STDOUT
;
1519 $cache_mode_active = $supported_cache_actions{$action}
1520 ? cached_action_start
($action)
1523 configure_gitweb_features
();
1524 $actions{$action}->();
1526 return unless $cache_mode_active;
1528 $cached_page = cached_action_finish
($action);
1533 $cache_mode_active = 0;
1534 # Avoid any extra unwanted encoding steps as $cached_page is raw bytes
1535 binmode STDOUT
, ':raw';
1536 our $fcgi_raw_mode = 1;
1537 print expand_gitweb_pi
($cached_page, time);
1538 binmode STDOUT
, ':utf8'; # as set at the beginning of gitweb.cgi
1543 our $t0 = [ gettimeofday
() ]
1545 our $number_of_git_cmds = 0;
1548 our $first_request = 1;
1549 our $evaluate_uri_force = undef;
1553 # do not reuse stale config or project list from prior FCGI request
1554 our $config_file = '';
1555 our $gitweb_project_owner = undef;
1557 # Only allow GET and HEAD methods
1558 if (!$ENV{'REQUEST_METHOD'} || ($ENV{'REQUEST_METHOD'} ne 'GET' && $ENV{'REQUEST_METHOD'} ne 'HEAD')) {
1560 Status: 405 Method Not Allowed
1561 Content-Type: text/plain
1564 405 Method Not Allowed
1570 &$evaluate_uri_force() if $evaluate_uri_force;
1571 if ($per_request_config) {
1572 if (ref($per_request_config) eq 'CODE') {
1573 $per_request_config->();
1574 } elsif (!$first_request) {
1575 evaluate_gitweb_config
();
1576 evaluate_email_obfuscate
();
1581 # $projectroot and $projects_list might be set in gitweb config file
1582 $projects_list ||= $projectroot;
1584 evaluate_query_params
();
1585 evaluate_path_info
();
1586 evaluate_and_validate_params
();
1592 our $is_last_request = sub { 1 };
1593 our ($pre_dispatch_hook, $post_dispatch_hook, $pre_listen_hook);
1597 our $fcgi_nproc_active = 0;
1598 our $fcgi_raw_mode = 0;
1601 my $stdinfno = fileno STDIN
;
1602 return 0 unless defined $stdinfno && $stdinfno == 0;
1603 return 0 unless getsockname STDIN
;
1604 return 0 if getpeername STDIN
;
1605 return $!{ENOTCONN
}?
1:0;
1607 sub configure_as_fcgi
{
1608 return if $fcgi_mode;
1613 # We have gone to great effort to make sure that all incoming data has
1614 # been converted from whatever format it was in into UTF-8. We have
1615 # even taken care to make sure the output handle is in ':utf8' mode.
1616 # Now along comes FCGI and blows it with:
1618 # Use of wide characters in FCGI::Stream::PRINT is deprecated
1619 # and will stop wprking[sic] in a future version of FCGI
1621 # To fix this we replace FCGI::Stream::PRINT with our own routine that
1622 # first encodes everything and then calls the original routine, but
1623 # not if $fcgi_raw_mode is true (then we just call the original routine).
1625 # Note that we could do this by using utf8::is_utf8 to check instead
1626 # of having a $fcgi_raw_mode global, but that would be slower to run
1627 # the test on each element and much slower than skipping the conversion
1628 # entirely when we know we're outputting raw bytes.
1629 my $orig = \
&FCGI
::Stream
::PRINT
;
1630 undef *FCGI
::Stream
::PRINT
;
1631 *FCGI
::Stream
::PRINT
= sub {
1632 @_ = (shift, map {my $x=$_; utf8
::encode
($x); $x} @_)
1633 unless $fcgi_raw_mode;
1637 our $CGI = 'CGI::Fast';
1641 my $request_number = 0;
1642 # let each child service 100 requests
1643 our $is_last_request = sub { ++$request_number > 100 };
1646 my $script_name = $ENV{'SCRIPT_NAME'} || $ENV{'SCRIPT_FILENAME'} || __FILE__
;
1648 if $script_name =~ /\.fcgi$/ || ($auto_fcgi && is_fcgi
());
1650 my $nproc_sub = sub {
1651 my ($arg, $val) = @_;
1652 return unless eval { require FCGI
::ProcManager
; 1; };
1653 $fcgi_nproc_active = 1;
1654 my $proc_manager = FCGI
::ProcManager
->new({
1655 n_processes
=> $val,
1657 our $pre_listen_hook = sub { $proc_manager->pm_manage() };
1658 our $pre_dispatch_hook = sub { $proc_manager->pm_pre_dispatch() };
1659 our $post_dispatch_hook = sub { $proc_manager->pm_post_dispatch() };
1662 require Getopt
::Long
;
1663 Getopt
::Long
::GetOptions
(
1664 'fastcgi|fcgi|f' => \
&configure_as_fcgi
,
1665 'nproc|n=i' => $nproc_sub,
1668 if (!$fcgi_nproc_active && defined $ENV{'GITWEB_FCGI_NPROC'} && $ENV{'GITWEB_FCGI_NPROC'} =~ /^\d+$/) {
1669 &$nproc_sub('nproc', $ENV{'GITWEB_FCGI_NPROC'});
1674 evaluate_gitweb_config
();
1675 evaluate_encoding
();
1676 evaluate_email_obfuscate
();
1677 evaluate_git_version
();
1678 my ($ml, $mi, $bu, $hl, $subroutine) = ($my_url, $my_uri, $base_url, $home_link, '');
1679 $subroutine .= '$my_url = $ml;' if defined $my_url && $my_url ne '';
1680 $subroutine .= '$my_uri = $mi;' if defined $my_uri; # this one can be ""
1681 $subroutine .= '$base_url = $bu;' if defined $base_url && $base_url ne '';
1682 $subroutine .= '$home_link = $hl;' if defined $home_link && $home_link ne '';
1683 $evaluate_uri_force = eval "sub {$subroutine}" if $subroutine;
1687 $pre_listen_hook->()
1688 if $pre_listen_hook;
1691 while ($cgi = $CGI->new()) {
1692 $pre_dispatch_hook->()
1693 if $pre_dispatch_hook;
1695 eval {run_request
()};
1697 $post_dispatch_hook->()
1698 if $post_dispatch_hook;
1701 last REQUEST
if ($is_last_request->());
1709 if (defined caller) {
1710 # wrapped in a subroutine processing requests,
1711 # e.g. mod_perl with ModPerl::Registry, or PSGI with Plack::App::WrapCGI
1714 # pure CGI script, serving single request
1718 ## ======================================================================
1721 # possible values of extra options
1722 # -full => 0|1 - use absolute/full URL ($my_uri/$my_url as base)
1723 # -replay => 1 - start from a current view (replay with modifications)
1724 # -path_info => 0|1 - don't use/use path_info URL (if possible)
1725 # -anchor => ANCHOR - add #ANCHOR to end of URL, implies -replay if used alone
1728 # default is to use -absolute url() i.e. $my_uri
1729 my $href = $params{-full
} ?
$my_url : $my_uri;
1731 # implicit -replay, must be first of implicit params
1732 $params{-replay
} = 1 if (keys %params == 1 && $params{-anchor
});
1734 $params{'project'} = $project unless exists $params{'project'};
1736 if ($params{-replay
}) {
1737 while (my ($name, $symbol) = each %cgi_param_mapping) {
1738 if (!exists $params{$name}) {
1739 $params{$name} = $input_params{$name};
1744 my $use_pathinfo = gitweb_check_feature
('pathinfo');
1745 if (defined $params{'project'} &&
1746 (exists $params{-path_info
} ?
$params{-path_info
} : $use_pathinfo)) {
1747 # try to put as many parameters as possible in PATH_INFO:
1750 # - hash_parent or hash_parent_base:/file_parent
1751 # - hash or hash_base:/filename
1752 # - the snapshot_format as an appropriate suffix
1754 # When the script is the root DirectoryIndex for the domain,
1755 # $href here would be something like http://gitweb.example.com/
1756 # Thus, we strip any trailing / from $href, to spare us double
1757 # slashes in the final URL
1760 # Then add the project name, if present
1761 $href .= "/".esc_path_info
($params{'project'});
1762 delete $params{'project'};
1764 # since we destructively absorb parameters, we keep this
1765 # boolean that remembers if we're handling a snapshot
1766 my $is_snapshot = $params{'action'} eq 'snapshot';
1768 # Summary just uses the project path URL, any other action is
1770 if (defined $params{'action'}) {
1771 $href .= "/".esc_path_info
($params{'action'})
1772 unless $params{'action'} eq 'summary';
1773 delete $params{'action'};
1776 # Next, we put hash_parent_base:/file_parent..hash_base:/file_name,
1777 # stripping nonexistent or useless pieces
1778 $href .= "/" if ($params{'hash_base'} || $params{'hash_parent_base'}
1779 || $params{'hash_parent'} || $params{'hash'});
1780 if (defined $params{'hash_base'}) {
1781 if (defined $params{'hash_parent_base'}) {
1782 $href .= esc_path_info
($params{'hash_parent_base'});
1783 # skip the file_parent if it's the same as the file_name
1784 if (defined $params{'file_parent'}) {
1785 if (defined $params{'file_name'} && $params{'file_parent'} eq $params{'file_name'}) {
1786 delete $params{'file_parent'};
1787 } elsif ($params{'file_parent'} !~ /\.\./) {
1788 $href .= ":/".esc_path_info
($params{'file_parent'});
1789 delete $params{'file_parent'};
1793 delete $params{'hash_parent'};
1794 delete $params{'hash_parent_base'};
1795 } elsif (defined $params{'hash_parent'}) {
1796 $href .= esc_path_info
($params{'hash_parent'}). "..";
1797 delete $params{'hash_parent'};
1800 $href .= esc_path_info
($params{'hash_base'});
1801 if (defined $params{'file_name'} && $params{'file_name'} !~ /\.\./) {
1802 $href .= ":/".esc_path_info
($params{'file_name'});
1803 delete $params{'file_name'};
1805 delete $params{'hash'};
1806 delete $params{'hash_base'};
1807 } elsif (defined $params{'hash'}) {
1808 $href .= esc_path_info
($params{'hash'});
1809 delete $params{'hash'};
1812 # If the action was a snapshot, we can absorb the
1813 # snapshot_format parameter too
1815 my $fmt = $params{'snapshot_format'};
1816 # snapshot_format should always be defined when href()
1817 # is called, but just in case some code forgets, we
1818 # fall back to the default
1819 $fmt ||= $snapshot_fmts[0];
1820 $href .= $known_snapshot_formats{$fmt}{'suffix'};
1821 delete $params{'snapshot_format'};
1825 # now encode the parameters explicitly
1827 for (my $i = 0; $i < @cgi_param_mapping; $i += 2) {
1828 my ($name, $symbol) = ($cgi_param_mapping[$i], $cgi_param_mapping[$i+1]);
1829 if (defined $params{$name}) {
1830 if (ref($params{$name}) eq "ARRAY") {
1831 foreach my $par (@
{$params{$name}}) {
1832 push @result, $symbol . "=" . esc_param
($par);
1835 push @result, $symbol . "=" . esc_param
($params{$name});
1839 $href .= "?" . join(';', @result) if scalar @result;
1841 # final transformation: trailing spaces must be escaped (URI-encoded)
1842 $href =~ s/(\s+)$/CGI::escape($1)/e;
1844 if ($params{-anchor
}) {
1845 $href .= "#".esc_param
($params{-anchor
});
1852 ## ======================================================================
1853 ## validation, quoting/unquoting and escaping
1855 sub is_valid_action
{
1857 return undef unless exists $actions{$input};
1861 sub is_valid_project
{
1864 return unless defined $input;
1865 if (!is_valid_pathname
($input) ||
1866 !(-d
"$projectroot/$input") ||
1867 !check_export_ok
("$projectroot/$input") ||
1868 ($strict_export && !project_in_list
($input))) {
1875 sub is_valid_pathname
{
1878 return undef unless defined $input;
1879 # no '.' or '..' as elements of path, i.e. no '.' or '..'
1880 # at the beginning, at the end, and between slashes.
1881 # also this catches doubled slashes
1882 if ($input =~ m!(^|/)(|\.|\.\.)(/|$)!) {
1885 # no null characters
1886 if ($input =~ m!\0!) {
1892 sub is_valid_ref_format
{
1895 return undef unless defined $input;
1896 # restrictions on ref name according to git-check-ref-format
1897 if ($input =~ m!(/\.|\.\.|[\000-\040\177 ~^:?*\[]|/$)!) {
1903 sub is_valid_refname
{
1906 return undef unless defined $input;
1907 # textual hashes are O.K.
1908 if ($input =~ m/^[0-9a-fA-F]{40}$/) {
1911 # it must be correct pathname
1912 is_valid_pathname
($input) or return undef;
1913 # check git-check-ref-format restrictions
1914 is_valid_ref_format
($input) or return undef;
1918 # decode sequences of octets in utf8 into Perl's internal form,
1919 # which is utf-8 with utf8 flag set if needed. gitweb writes out
1920 # in utf-8 thanks to "binmode STDOUT, ':utf8'" at beginning
1923 return undef unless defined $str;
1925 if (utf8
::is_utf8
($str) || utf8
::decode
($str)) {
1928 return $encode_object->decode($str, Encode
::FB_DEFAULT
);
1932 # quote unsafe chars, but keep the slash, even when it's not
1933 # correct, but quoted slashes look too horrible in bookmarks
1936 return undef unless defined $str;
1937 $str =~ s/([^A-Za-z0-9\-_.~()\/:@ ]+)/CGI
::escape
($1)/eg
;
1942 # the quoting rules for path_info fragment are slightly different
1945 return undef unless defined $str;
1947 # path_info doesn't treat '+' as space (specially), but '?' must be escaped
1948 $str =~ s/([^A-Za-z0-9\-_.~();\/;:@&= +]+)/CGI
::escape
($1)/eg
;
1953 # quote unsafe chars in whole URL, so some characters cannot be quoted
1956 return undef unless defined $str;
1957 $str =~ s/([^A-Za-z0-9\-_.~();\/;?:@&= ]+)/CGI
::escape
($1)/eg
;
1962 # quote unsafe characters in HTML attributes
1965 # for XHTML conformance escaping '"' to '"' is not enough
1966 return esc_html
(@_);
1969 # replace invalid utf8 character with SUBSTITUTION sequence
1974 return undef unless defined $str;
1976 $str = to_utf8
($str);
1977 $str = $cgi->escapeHTML($str);
1978 if ($opts{'-nbsp'}) {
1979 $str =~ s/ / /g;
1982 $str =~ s
|([[:cntrl
:]])|(($1 ne "\t") ? quot_cec
($1) : $1)|eg
;
1986 # quote control characters and escape filename to HTML
1991 return undef unless defined $str;
1993 $str = to_utf8
($str);
1994 $str = $cgi->escapeHTML($str);
1995 if ($opts{'-nbsp'}) {
1996 $str =~ s/ / /g;
1999 $str =~ s
|([[:cntrl
:]])|quot_cec
($1)|eg
;
2003 # Sanitize for use in XHTML + application/xml+xhtm (valid XML 1.0)
2007 return undef unless defined $str;
2009 $str = to_utf8
($str);
2011 $str =~ s
|([[:cntrl
:]])|(index("\t\n\r", $1) != -1 ?
$1 : quot_cec
($1))|eg
;
2015 # Make control characters "printable", using character escape codes (CEC)
2019 my %es = ( # character escape codes, aka escape sequences
2020 "\t" => '\t', # tab (HT)
2021 "\n" => '\n', # line feed (LF)
2022 "\r" => '\r', # carrige return (CR)
2023 "\f" => '\f', # form feed (FF)
2024 "\b" => '\b', # backspace (BS)
2025 "\a" => '\a', # alarm (bell) (BEL)
2026 "\e" => '\e', # escape (ESC)
2027 "\013" => '\v', # vertical tab (VT)
2028 "\000" => '\0', # nul character (NUL)
2030 my $chr = ( (exists $es{$cntrl})
2032 : sprintf('\x%02x', ord($cntrl)) );
2033 if ($opts{-nohtml
}) {
2036 return "<span class=\"cntrl\">$chr</span>";
2040 # Alternatively use unicode control pictures codepoints,
2041 # Unicode "printable representation" (PR)
2046 my $chr = sprintf('&#%04d;', 0x2400+ord($cntrl));
2047 if ($opts{-nohtml
}) {
2050 return "<span class=\"cntrl\">$chr</span>";
2054 # git may return quoted and escaped filenames
2060 my %es = ( # character escape codes, aka escape sequences
2061 't' => "\t", # tab (HT, TAB)
2062 'n' => "\n", # newline (NL)
2063 'r' => "\r", # return (CR)
2064 'f' => "\f", # form feed (FF)
2065 'b' => "\b", # backspace (BS)
2066 'a' => "\a", # alarm (bell) (BEL)
2067 'e' => "\e", # escape (ESC)
2068 'v' => "\013", # vertical tab (VT)
2071 if ($seq =~ m/^[0-7]{1,3}$/) {
2072 # octal char sequence
2073 return chr(oct($seq));
2074 } elsif (exists $es{$seq}) {
2075 # C escape sequence, aka character escape code
2078 # quoted ordinary character
2082 if ($str =~ m/^"(.*)"$/) {
2085 $str =~ s/\\([^0-7]|[0-7]{1,3})/unq($1)/eg;
2090 # escape tabs (convert tabs to spaces)
2094 while ((my $pos = index($line, "\t")) != -1) {
2095 if (my $count = (8 - ($pos % 8))) {
2096 my $spaces = ' ' x
$count;
2097 $line =~ s/\t/$spaces/;
2104 sub project_in_list
{
2105 my $project = shift;
2106 my @list = git_get_projects_list
();
2107 return @list && scalar(grep { $_->{'path'} eq $project } @list);
2110 sub cached_page_precondition_check
{
2113 $action eq 'summary' &&
2114 $projlist_cache_lifetime > 0 &&
2115 gitweb_check_feature
('forks');
2117 # Note that ALL the 'forkchange' logic is in this function.
2118 # It does NOT belong in cached_action_page NOR in cached_action_start
2119 # NOR in cached_action_finish. None of those functions should know anything
2120 # about nor do anything to any of the 'forkchange'/"$action.forkchange" files.
2122 # besides the basic 'changed' "$action.changed" check, we may only use
2123 # a summary cache if:
2125 # 1) we are not using a project list cache file
2127 # 2) we are not using the 'forks' feature
2129 # 3) there is no 'forkchange' nor "$action.forkchange" file in $html_cache_dir
2131 # 4) there is no cache file ("$cache_dir/$projlist_cache_name")
2133 # 5) the OLDER of 'forkchange'/"$action.forkchange" is NEWER than the cache file
2135 # Otherwise we must re-generate the cache because we've had a fork change
2136 # (either a fork was added or a fork was removed) AND the change has been
2137 # picked up in the cache file AND we've not got that in our cached copy
2139 # For (5) regenerating the cached page wouldn't get us anything if the project
2140 # cache file is older than the 'forkchange'/"$action.forkchange" because the
2141 # forks information comes from the project cache file and it's clearly not
2142 # picked up the changes yet so we may continue to use a cached page until it does.
2144 my $htmlcd = "$projectroot/$project/$html_cache_dir";
2145 my $fc_mt = (stat("$htmlcd/forkchange"))[9];
2146 my $afc_mt = (stat("$htmlcd/$action.forkchange"))[9];
2147 return 1 unless defined($fc_mt) || defined($afc_mt);
2148 my $prj_mt = (stat("$cache_dir/$projlist_cache_name"))[9];
2149 return 1 unless $prj_mt;
2150 my $old_mt = $fc_mt;
2151 $old_mt = $afc_mt if !defined($old_mt) || (defined($afc_mt) && $afc_mt < $old_mt);
2152 return 1 if $old_mt > $prj_mt;
2154 # We're going to regenerate the cached page because we know the project cache
2155 # has new fork information that we cannot possibly have in our cached copy.
2157 # However, if both 'forkchange' and "$action.forkchange" exist and one of
2158 # them is older than the project cache and one of them is newer, we still
2159 # need to regenerate the page cache, but we will also need to do it again
2160 # in the future because there's yet another fork update not yet in the cache.
2162 # So we make sure to touch "$action.changed" to force a cache regeneration
2163 # and then we remove either or both of 'forkchange'/"$action.forkchange" if
2164 # they're older than the project cache (they've served their purpose, we're
2165 # forcing a page regeneration by touching "$action.changed" but the project
2166 # cache was rebuilt since then so there are no more pending fork updates to
2167 # pick up in the future and they need to go).
2169 # For best results, the external code that touches 'forkchange' should always
2170 # touch 'forkchange' and additionally touch 'summary.forkchange' but only
2171 # if it does not already exist. That way the cached page will be regenerated
2172 # each time it's requested and ANY fork updates are available in the proj
2173 # cache rather than waiting until they all are before updating.
2175 # Note that we take a shortcut here and will zap 'forkchange' since we know
2176 # that it only affects the 'summary' cache. If, in the future, it affects
2177 # other cache types, it will first need to be propogated down to
2178 # "$action.forkchange" for those types before we zap it.
2181 open $fd, '>', "$htmlcd/$action.changed" and close $fd;
2182 $fc_mt=undef, unlink "$htmlcd/forkchange" if defined $fc_mt && $fc_mt < $prj_mt;
2183 $afc_mt=undef, unlink "$htmlcd/$action.forkchange" if defined $afc_mt && $afc_mt < $prj_mt;
2185 # Now we propagate 'forkchange' to "$action.forkchange" if we have the
2186 # one and not the other.
2188 if (defined $fc_mt && ! defined $afc_mt) {
2189 open $fd, '>', "$htmlcd/$action.forkchange" and close $fd;
2190 -e
"$htmlcd/$action.forkchange" and
2191 utime($fc_mt, $fc_mt, "$htmlcd/$action.forkchange") and
2192 unlink "$htmlcd/forkchange";
2198 sub cached_action_page
{
2201 return undef unless $action && $html_cache_actions{$action} && $html_cache_dir;
2202 my $htmlcd = "$projectroot/$project/$html_cache_dir";
2203 return undef if -e
"$htmlcd/changed" || -e
"$htmlcd/$action.changed";
2204 return undef unless cached_page_precondition_check
($action);
2205 open my $fd, '<', "$htmlcd/$action" or return undef;
2208 my $cached_page = <$fd>;
2209 close $fd or return undef;
2210 return $cached_page;
2213 package Git
::Gitweb
::CacheFile
;
2216 use POSIX
qw(:fcntl_h);
2218 my $cachefile = shift;
2220 sysopen(my $self, $cachefile, O_WRONLY
|O_CREAT
|O_EXCL
, 0664)
2222 $$self->{'cachefile'} = $cachefile;
2223 $$self->{'opened'} = 1;
2224 $$self->{'contents'} = '';
2225 return bless $self, $class;
2230 if ($$self->{'opened'}) {
2231 $$self->{'opened'} = 0;
2232 my $result = close $self;
2233 unlink $$self->{'cachefile'} unless $result;
2241 if ($$self->{'opened'}) {
2242 $self->CLOSE() and unlink $$self->{'cachefile'};
2248 @_ = (map {my $x=$_; utf8
::encode
($x); $x} @_) unless $fcgi_raw_mode;
2249 print $self @_ if $$self->{'opened'};
2250 $$self->{'contents'} .= join('', @_);
2256 my $template = shift;
2257 return $self->PRINT(sprintf $template, @_);
2262 return $$self->{'contents'};
2267 # Caller is responsible for preserving STDOUT beforehand if needed
2268 sub cached_action_start
{
2271 return undef unless $html_cache_actions{$action} && $html_cache_dir;
2272 my $htmlcd = "$projectroot/$project/$html_cache_dir";
2273 return undef unless -d
$htmlcd;
2274 if (-e
"$htmlcd/changed") {
2275 foreach my $cacheable (keys(%html_cache_actions)) {
2276 next unless $supported_cache_actions{$cacheable} &&
2277 $html_cache_actions{$cacheable};
2279 open $fd, '>', "$htmlcd/$cacheable.changed"
2282 unlink "$htmlcd/changed";
2285 tie
*CACHEFILE
, 'Git::Gitweb::CacheFile', "$htmlcd/$action.lock" or return undef;
2286 *STDOUT
= *CACHEFILE
;
2287 unlink "$htmlcd/$action", "$htmlcd/$action.changed";
2291 # Caller is responsible for restoring STDOUT afterward if needed
2292 sub cached_action_finish
{
2297 my $obj = tied *STDOUT
;
2298 return undef unless ref($obj) eq 'Git::Gitweb::CacheFile';
2299 my $cached_page = $obj->contents;
2300 (my $result = close(STDOUT
)) or warn "couldn't close cache file on STDOUT: $!";
2301 # Do not leave STDOUT file descriptor invalid!
2303 open(NULL
, '>', File
::Spec
->devnull) or die "couldn't open NULL to devnull: $!";
2305 return $cached_page unless $result;
2306 my $htmlcd = "$projectroot/$project/$html_cache_dir";
2307 return $cached_page unless -d
$htmlcd;
2308 unlink "$htmlcd/$action.lock" unless rename "$htmlcd/$action.lock", "$htmlcd/$action";
2309 return $cached_page;
2313 BEGIN {%expand_pi_subs = (
2314 'age_string' => \
&age_string
,
2315 'age_string_date' => \
&age_string_date
,
2316 'age_string_age' => \
&age_string_age
,
2317 'compute_timed_interval' => \
&compute_timed_interval
,
2318 'compute_commands_count' => \
&compute_commands_count
,
2319 'format_lastrefresh_row' => \
&format_lastrefresh_row
,
2320 'compute_stylesheet_links' => \
&compute_stylesheet_links
,
2323 # Expands any <?gitweb...> processing instructions and returns the result
2324 sub expand_gitweb_pi
{
2327 my @time_now = gettimeofday
();
2328 $page =~ s
{<\?gitweb
(?
:\s
+([^\s
>]+)([^>]*))?\s
*\?>}
2330 (ref($expand_pi_subs{$1}) eq 'CODE' ?
2331 $expand_pi_subs{$1}->(split(' ',$2), @time_now) :
2337 ## ----------------------------------------------------------------------
2338 ## HTML aware string manipulation
2340 # Try to chop given string on a word boundary between position
2341 # $len and $len+$add_len. If there is no word boundary there,
2342 # chop at $len+$add_len. Do not chop if chopped part plus ellipsis
2343 # (marking chopped part) would be longer than given string.
2347 my $add_len = shift || 10;
2348 my $where = shift || 'right'; # 'left' | 'center' | 'right'
2350 # Make sure perl knows it is utf8 encoded so we don't
2351 # cut in the middle of a utf8 multibyte char.
2352 $str = to_utf8
($str);
2354 # allow only $len chars, but don't cut a word if it would fit in $add_len
2355 # if it doesn't fit, cut it if it's still longer than the dots we would add
2356 # remove chopped character entities entirely
2358 # when chopping in the middle, distribute $len into left and right part
2359 # return early if chopping wouldn't make string shorter
2360 if ($where eq 'center') {
2361 return $str if ($len + 5 >= length($str)); # filler is length 5
2364 return $str if ($len + 4 >= length($str)); # filler is length 4
2367 # regexps: ending and beginning with word part up to $add_len
2368 my $endre = qr/.{$len}\w{0,$add_len}/;
2369 my $begre = qr/\w{0,$add_len}.{$len}/;
2371 if ($where eq 'left') {
2372 $str =~ m/^(.*?)($begre)$/;
2373 my ($lead, $body) = ($1, $2);
2374 if (length($lead) > 4) {
2377 return "$lead$body";
2379 } elsif ($where eq 'center') {
2380 $str =~ m/^($endre)(.*)$/;
2381 my ($left, $str) = ($1, $2);
2382 $str =~ m/^(.*?)($begre)$/;
2383 my ($mid, $right) = ($1, $2);
2384 if (length($mid) > 5) {
2387 return "$left$mid$right";
2390 $str =~ m/^($endre)(.*)$/;
2393 if (length($tail) > 4) {
2396 return "$body$tail";
2400 # pass-through email filter, obfuscating it when possible
2401 sub email_obfuscate
{
2405 $str = $email->escape_html($str);
2406 # Stock HTML::Email::Obfuscate version likes to produce
2408 $str =~ s
#<(/?)B>#<$1b>#g;
2411 $str = esc_html
($str);
2412 $str =~ s/@/@/;
2417 # takes the same arguments as chop_str, but also wraps a <span> around the
2418 # result with a title attribute if it does get chopped. Additionally, the
2419 # string is HTML-escaped.
2420 sub chop_and_escape_str
{
2423 my $chopped = chop_str
(@_);
2424 $str = to_utf8
($str);
2425 if ($chopped eq $str) {
2426 return email_obfuscate
($chopped);
2429 $str =~ s/[[:cntrl:]]/?/g;
2430 return $cgi->span({-title
=>$str}, email_obfuscate
($chopped));
2434 # Highlight selected fragments of string, using given CSS class,
2435 # and escape HTML. It is assumed that fragments do not overlap.
2436 # Regions are passed as list of pairs (array references).
2438 # Example: esc_html_hl_regions("foobar", "mark", [ 0, 3 ]) returns
2439 # '<span class="mark">foo</span>bar'
2440 sub esc_html_hl_regions
{
2441 my ($str, $css_class, @sel) = @_;
2442 my %opts = grep { ref($_) ne 'ARRAY' } @sel;
2443 @sel = grep { ref($_) eq 'ARRAY' } @sel;
2444 return esc_html
($str, %opts) unless @sel;
2450 my ($begin, $end) = @
$s;
2452 # Don't create empty <span> elements.
2453 next if $end <= $begin;
2455 my $escaped = esc_html
(substr($str, $begin, $end - $begin),
2458 $out .= esc_html
(substr($str, $pos, $begin - $pos), %opts)
2459 if ($begin - $pos > 0);
2460 $out .= $cgi->span({-class => $css_class}, $escaped);
2464 $out .= esc_html
(substr($str, $pos), %opts)
2465 if ($pos < length($str));
2470 # return positions of beginning and end of each match
2472 my ($str, $regexp) = @_;
2473 return unless (defined $str && defined $regexp);
2476 while ($str =~ /$regexp/g) {
2477 push @matches, [$-[0], $+[0]];
2482 # highlight match (if any), and escape HTML
2483 sub esc_html_match_hl
{
2484 my ($str, $regexp) = @_;
2485 return esc_html
($str) unless defined $regexp;
2487 my @matches = matchpos_list
($str, $regexp);
2488 return esc_html
($str) unless @matches;
2490 return esc_html_hl_regions
($str, 'match', @matches);
2494 # highlight match (if any) of shortened string, and escape HTML
2495 sub esc_html_match_hl_chopped
{
2496 my ($str, $chopped, $regexp) = @_;
2497 return esc_html_match_hl
($str, $regexp) unless defined $chopped;
2499 my @matches = matchpos_list
($str, $regexp);
2500 return esc_html
($chopped) unless @matches;
2502 # filter matches so that we mark chopped string
2503 my $tail = "... "; # see chop_str
2504 unless ($chopped =~ s/\Q$tail\E$//) {
2507 my $chop_len = length($chopped);
2508 my $tail_len = length($tail);
2511 for my $m (@matches) {
2512 if ($m->[0] > $chop_len) {
2513 push @filtered, [ $chop_len, $chop_len + $tail_len ] if ($tail_len > 0);
2515 } elsif ($m->[1] > $chop_len) {
2516 push @filtered, [ $m->[0], $chop_len + $tail_len ];
2522 return esc_html_hl_regions
($chopped . $tail, 'match', @filtered);
2525 ## ----------------------------------------------------------------------
2526 ## functions returning short strings
2528 # CSS class for given age epoch value (in seconds)
2529 # and reference time (optional, defaults to now) as second value
2531 my ($age_epoch, $time_now) = @_;
2532 return "noage" unless defined $age_epoch;
2533 defined $time_now or $time_now = time;
2534 my $age = $time_now - $age_epoch;
2536 if ($age < 60*60*2) {
2538 } elsif ($age < 60*60*24*2) {
2545 # convert age epoch in seconds to "nn units ago" string
2546 # reference time used is now unless second argument passed in
2547 # to get the old behavior, pass 0 as the first argument and
2548 # the time in seconds as the second
2550 my ($age_epoch, $time_now) = @_;
2551 return "unknown" unless defined $age_epoch;
2552 return "<?gitweb age_string $age_epoch?>" if $cache_mode_active;
2553 defined $time_now or $time_now = time;
2554 my $age = $time_now - $age_epoch;
2557 if ($age > 60*60*24*365*2) {
2558 $age_str = (int $age/60/60/24/365);
2559 $age_str .= " years ago";
2560 } elsif ($age > 60*60*24*(365/12)*2) {
2561 $age_str = int $age/60/60/24/(365/12);
2562 $age_str .= " months ago";
2563 } elsif ($age > 60*60*24*7*2) {
2564 $age_str = int $age/60/60/24/7;
2565 $age_str .= " weeks ago";
2566 } elsif ($age > 60*60*24*2) {
2567 $age_str = int $age/60/60/24;
2568 $age_str .= " days ago";
2569 } elsif ($age > 60*60*2) {
2570 $age_str = int $age/60/60;
2571 $age_str .= " hours ago";
2572 } elsif ($age > 60*2) {
2573 $age_str = int $age/60;
2574 $age_str .= " min ago";
2575 } elsif ($age > 2) {
2576 $age_str = int $age;
2577 $age_str .= " sec ago";
2579 $age_str .= " right now";
2584 # returns age_string if the age is <= 2 weeks otherwise an absolute date
2585 # this is typically shown to the user directly with the age_string_age as a title
2586 sub age_string_date
{
2587 my ($age_epoch, $time_now) = @_;
2588 return "unknown" unless defined $age_epoch;
2589 return "<?gitweb age_string_date $age_epoch?>" if $cache_mode_active;
2590 defined $time_now or $time_now = time;
2591 my $age = $time_now - $age_epoch;
2593 if ($age > 60*60*24*7*2) {
2594 my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($age_epoch);
2595 return sprintf "%4i-%02u-%02i", 1900 + $year, $mon+1, $mday;
2597 return age_string
($age_epoch, $time_now);
2601 # returns an absolute date if the age is <= 2 weeks otherwise age_string
2602 # this is typically used for the 'title' attribute so it will show as a tooltip
2603 sub age_string_age
{
2604 my ($age_epoch, $time_now) = @_;
2605 return "unknown" unless defined $age_epoch;
2606 return "<?gitweb age_string_age $age_epoch?>" if $cache_mode_active;
2607 defined $time_now or $time_now = time;
2608 my $age = $time_now - $age_epoch;
2610 if ($age > 60*60*24*7*2) {
2611 return age_string
($age_epoch, $time_now);
2613 my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($age_epoch);
2614 return sprintf "%4i-%02u-%02i", 1900 + $year, $mon+1, $mday;
2619 S_IFINVALID
=> 0030000,
2620 S_IFGITLINK
=> 0160000,
2623 # submodule/subproject, a commit object reference
2627 return (($mode & S_IFMT
) == S_IFGITLINK
)
2630 # convert file mode in octal to symbolic file mode string
2632 my $mode = oct shift;
2634 if (S_ISGITLINK
($mode)) {
2635 return 'm---------';
2636 } elsif (S_ISDIR
($mode & S_IFMT
)) {
2637 return 'drwxr-xr-x';
2638 } elsif (S_ISLNK
($mode)) {
2639 return 'lrwxrwxrwx';
2640 } elsif (S_ISREG
($mode)) {
2641 # git cares only about the executable bit
2642 if ($mode & S_IXUSR
) {
2643 return '-rwxr-xr-x';
2645 return '-rw-r--r--';
2648 return '----------';
2652 # convert file mode in octal to file type string
2656 if ($mode !~ m/^[0-7]+$/) {
2662 if (S_ISGITLINK
($mode)) {
2664 } elsif (S_ISDIR
($mode & S_IFMT
)) {
2666 } elsif (S_ISLNK
($mode)) {
2668 } elsif (S_ISREG
($mode)) {
2675 # convert file mode in octal to file type description string
2676 sub file_type_long
{
2679 if ($mode !~ m/^[0-7]+$/) {
2685 if (S_ISGITLINK
($mode)) {
2687 } elsif (S_ISDIR
($mode & S_IFMT
)) {
2689 } elsif (S_ISLNK
($mode)) {
2691 } elsif (S_ISREG
($mode)) {
2692 if ($mode & S_IXUSR
) {
2693 return "executable";
2703 ## ----------------------------------------------------------------------
2704 ## functions returning short HTML fragments, or transforming HTML fragments
2705 ## which don't belong to other sections
2707 # format line of commit message.
2708 sub format_log_line_html
{
2711 $line = esc_html
($line, -nbsp
=>1);
2712 $line =~ s
{\b([0-9a
-fA
-F
]{8,40})\b}{
2713 $cgi->a({-href
=> href
(action
=>"object", hash
=>$1),
2714 -class => "text"}, $1);
2715 }eg
unless $line =~ /^\s*git-svn-id:/;
2720 # format marker of refs pointing to given object
2722 # the destination action is chosen based on object type and current context:
2723 # - for annotated tags, we choose the tag view unless it's the current view
2724 # already, in which case we go to shortlog view
2725 # - for other refs, we keep the current view if we're in history, shortlog or
2726 # log view, and select shortlog otherwise
2727 sub format_ref_marker
{
2728 my ($refs, $id) = @_;
2731 if (defined $refs->{$id}) {
2732 foreach my $ref (@
{$refs->{$id}}) {
2733 # this code exploits the fact that non-lightweight tags are the
2734 # only indirect objects, and that they are the only objects for which
2735 # we want to use tag instead of shortlog as action
2736 my ($type, $name) = qw();
2737 my $indirect = ($ref =~ s/\^\{\}$//);
2738 # e.g. tags/v2.6.11 or heads/next
2739 if ($ref =~ m!^(.*?)s?/(.*)$!) {
2748 $class .= " indirect" if $indirect;
2750 my $dest_action = "shortlog";
2753 $dest_action = "tag" unless $action eq "tag";
2754 } elsif ($action =~ /^(history|(short)?log)$/) {
2755 $dest_action = $action;
2759 $dest .= "refs/" unless $ref =~ m
!^refs
/!;
2762 my $link = $cgi->a({
2764 action
=>$dest_action,
2768 $markers .= "<span class=\"".esc_attr
($class)."\" title=\"".esc_attr
($ref)."\">" .
2774 return '<span class="refs">'. $markers . '</span>';
2780 # format, perhaps shortened and with markers, title line
2781 sub format_subject_html
{
2782 my ($long, $short, $href, $extra) = @_;
2783 $extra = '' unless defined($extra);
2785 if (length($short) < length($long)) {
2787 $long =~ s/[[:cntrl:]]/?/g;
2788 return $cgi->a({-href
=> $href, -class => "list subject",
2789 -title
=> to_utf8
($long)},
2790 esc_html
($short)) . $extra;
2792 return $cgi->a({-href
=> $href, -class => "list subject"},
2793 esc_html
($long)) . $extra;
2797 # Rather than recomputing the url for an email multiple times, we cache it
2798 # after the first hit. This gives a visible benefit in views where the avatar
2799 # for the same email is used repeatedly (e.g. shortlog).
2800 # The cache is shared by all avatar engines (currently gravatar only), which
2801 # are free to use it as preferred. Since only one avatar engine is used for any
2802 # given page, there's no risk for cache conflicts.
2803 our %avatar_cache = ();
2805 # Compute the picon url for a given email, by using the picon search service over at
2806 # http://www.cs.indiana.edu/picons/search.html
2808 my $email = lc shift;
2809 if (!$avatar_cache{$email}) {
2810 my ($user, $domain) = split('@', $email);
2811 $avatar_cache{$email} =
2812 "//www.cs.indiana.edu/cgi-pub/kinzler/piconsearch.cgi/" .
2814 "users+domains+unknown/up/single";
2816 return $avatar_cache{$email};
2819 # Compute the gravatar url for a given email, if it's not in the cache already.
2820 # Gravatar stores only the part of the URL before the size, since that's the
2821 # one computationally more expensive. This also allows reuse of the cache for
2822 # different sizes (for this particular engine).
2824 my $email = lc shift;
2826 $avatar_cache{$email} ||=
2827 "//www.gravatar.com/avatar/" .
2828 Digest
::MD5
::md5_hex
($email) . "?s=";
2829 return $avatar_cache{$email} . $size;
2832 # Insert an avatar for the given $email at the given $size if the feature
2834 sub git_get_avatar
{
2835 my ($email, %opts) = @_;
2836 my $pre_white = ($opts{-pad_before
} ?
" " : "");
2837 my $post_white = ($opts{-pad_after
} ?
" " : "");
2838 $opts{-size
} ||= 'default';
2839 my $size = $avatar_size{$opts{-size
}} || $avatar_size{'default'};
2841 if ($git_avatar eq 'gravatar') {
2842 $url = gravatar_url
($email, $size);
2843 } elsif ($git_avatar eq 'picon') {
2844 $url = picon_url
($email);
2846 # Other providers can be added by extending the if chain, defining $url
2847 # as needed. If no variant puts something in $url, we assume avatars
2848 # are completely disabled/unavailable.
2851 "<img width=\"$size\" " .
2852 "class=\"avatar\" " .
2853 "src=\"".esc_url
($url)."\" " .
2861 sub format_search_author
{
2862 my ($author, $searchtype, $displaytext) = @_;
2863 my $have_search = gitweb_check_feature
('search');
2867 if ($searchtype eq 'author') {
2868 $performed = "authored";
2869 } elsif ($searchtype eq 'committer') {
2870 $performed = "committed";
2873 return $cgi->a({-href
=> href
(action
=>"search", hash
=>$hash,
2874 searchtext
=>$author,
2875 searchtype
=>$searchtype), class=>"list",
2876 title
=>"Search for commits $performed by $author"},
2880 return $displaytext;
2884 # format the author name of the given commit with the given tag
2885 # the author name is chopped and escaped according to the other
2886 # optional parameters (see chop_str).
2887 sub format_author_html
{
2890 my $author = chop_and_escape_str
($co->{'author_name'}, @_);
2891 return "<$tag class=\"author\">" .
2892 format_search_author
($co->{'author_name'}, "author",
2893 git_get_avatar
($co->{'author_email'}, -pad_after
=> 1) .
2898 # format git diff header line, i.e. "diff --(git|combined|cc) ..."
2899 sub format_git_diff_header_line
{
2901 my $diffinfo = shift;
2902 my ($from, $to) = @_;
2904 if ($diffinfo->{'nparents'}) {
2906 $line =~ s!^(diff (.*?) )"?.*$!$1!;
2907 if ($to->{'href'}) {
2908 $line .= $cgi->a({-href
=> $to->{'href'}, -class => "path"},
2909 esc_path
($to->{'file'}));
2910 } else { # file was deleted (no href)
2911 $line .= esc_path
($to->{'file'});
2915 $line =~ s!^(diff (.*?) )"?a/.*$!$1!;
2916 if ($from->{'href'}) {
2917 $line .= $cgi->a({-href
=> $from->{'href'}, -class => "path"},
2918 'a/' . esc_path
($from->{'file'}));
2919 } else { # file was added (no href)
2920 $line .= 'a/' . esc_path
($from->{'file'});
2923 if ($to->{'href'}) {
2924 $line .= $cgi->a({-href
=> $to->{'href'}, -class => "path"},
2925 'b/' . esc_path
($to->{'file'}));
2926 } else { # file was deleted
2927 $line .= 'b/' . esc_path
($to->{'file'});
2931 return "<div class=\"diff header\">$line</div>\n";
2934 # format extended diff header line, before patch itself
2935 sub format_extended_diff_header_line
{
2937 my $diffinfo = shift;
2938 my ($from, $to) = @_;
2941 if ($line =~ s!^((copy|rename) from ).*$!$1! && $from->{'href'}) {
2942 $line .= $cgi->a({-href
=>$from->{'href'}, -class=>"path"},
2943 esc_path
($from->{'file'}));
2945 if ($line =~ s!^((copy|rename) to ).*$!$1! && $to->{'href'}) {
2946 $line .= $cgi->a({-href
=>$to->{'href'}, -class=>"path"},
2947 esc_path
($to->{'file'}));
2949 # match single <mode>
2950 if ($line =~ m/\s(\d{6})$/) {
2951 $line .= '<span class="info"> (' .
2952 file_type_long
($1) .
2956 if ($line =~ m/^index [0-9a-fA-F]{40},[0-9a-fA-F]{40}/) {
2957 # can match only for combined diff
2959 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
2960 if ($from->{'href'}[$i]) {
2961 $line .= $cgi->a({-href
=>$from->{'href'}[$i],
2963 substr($diffinfo->{'from_id'}[$i],0,7));
2968 $line .= ',' if ($i < $diffinfo->{'nparents'} - 1);
2971 if ($to->{'href'}) {
2972 $line .= $cgi->a({-href
=>$to->{'href'}, -class=>"hash"},
2973 substr($diffinfo->{'to_id'},0,7));
2978 } elsif ($line =~ m/^index [0-9a-fA-F]{40}..[0-9a-fA-F]{40}/) {
2979 # can match only for ordinary diff
2980 my ($from_link, $to_link);
2981 if ($from->{'href'}) {
2982 $from_link = $cgi->a({-href
=>$from->{'href'}, -class=>"hash"},
2983 substr($diffinfo->{'from_id'},0,7));
2985 $from_link = '0' x
7;
2987 if ($to->{'href'}) {
2988 $to_link = $cgi->a({-href
=>$to->{'href'}, -class=>"hash"},
2989 substr($diffinfo->{'to_id'},0,7));
2993 my ($from_id, $to_id) = ($diffinfo->{'from_id'}, $diffinfo->{'to_id'});
2994 $line =~ s!$from_id\.\.$to_id!$from_link..$to_link!;
2997 return $line . "<br/>\n";
3000 # format from-file/to-file diff header
3001 sub format_diff_from_to_header
{
3002 my ($from_line, $to_line, $diffinfo, $from, $to, @parents) = @_;
3007 #assert($line =~ m/^---/) if DEBUG;
3008 # no extra formatting for "^--- /dev/null"
3009 if (! $diffinfo->{'nparents'}) {
3010 # ordinary (single parent) diff
3011 if ($line =~ m!^--- "?a/!) {
3012 if ($from->{'href'}) {
3014 $cgi->a({-href
=>$from->{'href'}, -class=>"path"},
3015 esc_path
($from->{'file'}));
3018 esc_path
($from->{'file'});
3021 $result .= qq!<div
class="diff from_file">$line</div
>\n!;
3024 # combined diff (merge commit)
3025 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
3026 if ($from->{'href'}[$i]) {
3028 $cgi->a({-href
=>href
(action
=>"blobdiff",
3029 hash_parent
=>$diffinfo->{'from_id'}[$i],
3030 hash_parent_base
=>$parents[$i],
3031 file_parent
=>$from->{'file'}[$i],
3032 hash
=>$diffinfo->{'to_id'},
3034 file_name
=>$to->{'file'}),
3036 -title
=>"diff" . ($i+1)},
3039 $cgi->a({-href
=>$from->{'href'}[$i], -class=>"path"},
3040 esc_path
($from->{'file'}[$i]));
3042 $line = '--- /dev/null';
3044 $result .= qq!<div
class="diff from_file">$line</div
>\n!;
3049 #assert($line =~ m/^\+\+\+/) if DEBUG;
3050 # no extra formatting for "^+++ /dev/null"
3051 if ($line =~ m!^\+\+\+ "?b/!) {
3052 if ($to->{'href'}) {
3054 $cgi->a({-href
=>$to->{'href'}, -class=>"path"},
3055 esc_path
($to->{'file'}));
3058 esc_path
($to->{'file'});
3061 $result .= qq!<div
class="diff to_file">$line</div
>\n!;
3066 # create note for patch simplified by combined diff
3067 sub format_diff_cc_simplified
{
3068 my ($diffinfo, @parents) = @_;
3071 $result .= "<div class=\"diff header\">" .
3073 if (!is_deleted
($diffinfo)) {
3074 $result .= $cgi->a({-href
=> href
(action
=>"blob",
3076 hash
=>$diffinfo->{'to_id'},
3077 file_name
=>$diffinfo->{'to_file'}),
3079 esc_path
($diffinfo->{'to_file'}));
3081 $result .= esc_path
($diffinfo->{'to_file'});
3083 $result .= "</div>\n" . # class="diff header"
3084 "<div class=\"diff nodifferences\">" .
3086 "</div>\n"; # class="diff nodifferences"
3091 sub diff_line_class
{
3092 my ($line, $from, $to) = @_;
3097 if ($from && $to && ref($from->{'href'}) eq "ARRAY") {
3098 $num_sign = scalar @
{$from->{'href'}};
3101 my @diff_line_classifier = (
3102 { regexp
=> qr/^\@\@{$num_sign} /, class => "chunk_header"},
3103 { regexp
=> qr/^\\/, class => "incomplete" },
3104 { regexp
=> qr/^ {$num_sign}/, class => "ctx" },
3105 # classifier for context must come before classifier add/rem,
3106 # or we would have to use more complicated regexp, for example
3107 # qr/(?= {0,$m}\+)[+ ]{$num_sign}/, where $m = $num_sign - 1;
3108 { regexp
=> qr/^[+ ]{$num_sign}/, class => "add" },
3109 { regexp
=> qr/^[- ]{$num_sign}/, class => "rem" },
3111 for my $clsfy (@diff_line_classifier) {
3112 return $clsfy->{'class'}
3113 if ($line =~ $clsfy->{'regexp'});
3120 # assumes that $from and $to are defined and correctly filled,
3121 # and that $line holds a line of chunk header for unified diff
3122 sub format_unidiff_chunk_header
{
3123 my ($line, $from, $to) = @_;
3125 my ($from_text, $from_start, $from_lines, $to_text, $to_start, $to_lines, $section) =
3126 $line =~ m/^\@{2} (-(\d+)(?:,(\d+))?) (\+(\d+)(?:,(\d+))?) \@{2}(.*)$/;
3128 $from_lines = 0 unless defined $from_lines;
3129 $to_lines = 0 unless defined $to_lines;
3131 if ($from->{'href'}) {
3132 $from_text = $cgi->a({-href
=>"$from->{'href'}#l$from_start",
3133 -class=>"list"}, $from_text);
3135 if ($to->{'href'}) {
3136 $to_text = $cgi->a({-href
=>"$to->{'href'}#l$to_start",
3137 -class=>"list"}, $to_text);
3139 $line = "<span class=\"chunk_info\">@@ $from_text $to_text @@</span>" .
3140 "<span class=\"section\">" . esc_html
($section, -nbsp
=>1) . "</span>";
3144 # assumes that $from and $to are defined and correctly filled,
3145 # and that $line holds a line of chunk header for combined diff
3146 sub format_cc_diff_chunk_header
{
3147 my ($line, $from, $to) = @_;
3149 my ($prefix, $ranges, $section) = $line =~ m/^(\@+) (.*?) \@+(.*)$/;
3150 my (@from_text, @from_start, @from_nlines, $to_text, $to_start, $to_nlines);
3152 @from_text = split(' ', $ranges);
3153 for (my $i = 0; $i < @from_text; ++$i) {
3154 ($from_start[$i], $from_nlines[$i]) =
3155 (split(',', substr($from_text[$i], 1)), 0);
3158 $to_text = pop @from_text;
3159 $to_start = pop @from_start;
3160 $to_nlines = pop @from_nlines;
3162 $line = "<span class=\"chunk_info\">$prefix ";
3163 for (my $i = 0; $i < @from_text; ++$i) {
3164 if ($from->{'href'}[$i]) {
3165 $line .= $cgi->a({-href
=>"$from->{'href'}[$i]#l$from_start[$i]",
3166 -class=>"list"}, $from_text[$i]);
3168 $line .= $from_text[$i];
3172 if ($to->{'href'}) {
3173 $line .= $cgi->a({-href
=>"$to->{'href'}#l$to_start",
3174 -class=>"list"}, $to_text);
3178 $line .= " $prefix</span>" .
3179 "<span class=\"section\">" . esc_html
($section, -nbsp
=>1) . "</span>";
3183 # process patch (diff) line (not to be used for diff headers),
3184 # returning HTML-formatted (but not wrapped) line.
3185 # If the line is passed as a reference, it is treated as HTML and not
3187 sub format_diff_line
{
3188 my ($line, $diff_class, $from, $to) = @_;
3194 $line = untabify
($line);
3196 if ($from && $to && $line =~ m/^\@{2} /) {
3197 $line = format_unidiff_chunk_header
($line, $from, $to);
3198 } elsif ($from && $to && $line =~ m/^\@{3}/) {
3199 $line = format_cc_diff_chunk_header
($line, $from, $to);
3201 $line = esc_html
($line, -nbsp
=>1);
3205 my $diff_classes = "diff diff_body";
3206 $diff_classes .= " $diff_class" if ($diff_class);
3207 $line = "<div class=\"$diff_classes\">$line</div>\n";
3212 # Generates undef or something like "_snapshot_" or "snapshot (_tbz2_ _zip_)",
3213 # linked. Pass the hash of the tree/commit to snapshot.
3214 sub format_snapshot_links
{
3216 my $num_fmts = @snapshot_fmts;
3217 if ($num_fmts > 1) {
3218 # A parenthesized list of links bearing format names.
3219 # e.g. "snapshot (_tar.gz_ _zip_)"
3220 return "<span class=\"snapshots\">snapshot (" . join(' ', map
3227 }, $known_snapshot_formats{$_}{'display'})
3228 , @snapshot_fmts) . ")</span>";
3229 } elsif ($num_fmts == 1) {
3230 # A single "snapshot" link whose tooltip bears the format name.
3232 my ($fmt) = @snapshot_fmts;
3233 return "<span class=\"snapshots\">" .
3238 snapshot_format
=>$fmt
3240 -title
=> "in format: $known_snapshot_formats{$fmt}{'display'}"
3241 }, "snapshot") . "</span>";
3242 } else { # $num_fmts == 0
3247 ## ......................................................................
3248 ## functions returning values to be passed, perhaps after some
3249 ## transformation, to other functions; e.g. returning arguments to href()
3251 # returns hash to be passed to href to generate gitweb URL
3252 # in -title key it returns description of link
3254 my $format = shift || 'Atom';
3255 my %res = (action
=> lc($format));
3256 my $matched_ref = 0;
3258 # feed links are possible only for project views
3259 return unless (defined $project);
3260 # some views should link to OPML, or to generic project feed,
3261 # or don't have specific feed yet (so they should use generic)
3262 return if (!$action || $action =~ /^(?:tags|heads|forks|tag|search)$/x);
3265 # branches refs uses 'refs/' + $get_branch_refs()[x] + '/' prefix
3266 # (fullname) to differentiate from tag links; this also makes
3267 # possible to detect branch links
3268 for my $ref (get_branch_refs
()) {
3269 if ((defined $hash_base && $hash_base =~ m!^refs/\Q$ref\E/(.*)$!) ||
3270 (defined $hash && $hash =~ m!^refs/\Q$ref\E/(.*)$!)) {
3272 $matched_ref = $ref;
3276 # find log type for feed description (title)
3278 if (defined $file_name) {
3279 $type = "history of $file_name";
3280 $type .= "/" if ($action eq 'tree');
3281 $type .= " on '$branch'" if (defined $branch);
3283 $type = "log of $branch" if (defined $branch);
3286 $res{-title
} = $type;
3287 $res{'hash'} = (defined $branch ?
"refs/$matched_ref/$branch" : undef);
3288 $res{'file_name'} = $file_name;
3293 ## ----------------------------------------------------------------------
3294 ## git utility subroutines, invoking git commands
3296 # returns path to the core git executable and the --git-dir parameter as list
3298 $number_of_git_cmds++;
3299 return $GIT, '--git-dir='.$git_dir;
3302 # opens a "-|" cmd pipe handle with 2>/dev/null and returns it
3305 # In order to be compatible with FCGI mode we must use POSIX
3306 # and access the STDERR_FILENO file descriptor directly
3308 use POSIX
qw(STDERR_FILENO dup dup2);
3310 open(my $null, '>', File
::Spec
->devnull) or die "couldn't open devnull: $!";
3311 (my $saveerr = dup
(STDERR_FILENO
)) or die "couldn't dup STDERR: $!";
3312 my $dup2ok = dup2
(fileno($null), STDERR_FILENO
);
3313 close($null) or !$dup2ok or die "couldn't close NULL: $!";
3314 $dup2ok or POSIX
::close($saveerr), die "couldn't dup NULL to STDERR: $!";
3315 my $result = open(my $fd, "-|", @_);
3316 $dup2ok = dup2
($saveerr, STDERR_FILENO
);
3317 POSIX
::close($saveerr) or !$dup2ok or die "couldn't close SAVEERR: $!";
3318 $dup2ok or die "couldn't dup SAVERR to STDERR: $!";
3320 return $result ?
$fd : undef;
3323 # opens a "-|" git_cmd pipe handle with 2>/dev/null and returns it
3325 return cmd_pipe git_cmd
(), @_;
3328 # quote the given arguments for passing them to the shell
3329 # quote_command("command", "arg 1", "arg with ' and ! characters")
3330 # => "'command' 'arg 1' 'arg with '\'' and '\!' characters'"
3331 # Try to avoid using this function wherever possible.
3334 map { my $a = $_; $a =~ s/(['!])/'\\$1'/g; "'$a'" } @_ );
3337 # get HEAD ref of given project as hash
3338 sub git_get_head_hash
{
3339 return git_get_full_hash
(shift, 'HEAD');
3342 sub git_get_full_hash
{
3343 return git_get_hash
(@_);
3346 sub git_get_short_hash
{
3347 return git_get_hash
(@_, '--short=7');
3351 my ($project, $hash, @options) = @_;
3352 my $o_git_dir = $git_dir;
3354 $git_dir = "$projectroot/$project";
3355 if (defined(my $fd = git_cmd_pipe
'rev-parse',
3356 '--verify', '-q', @options, $hash)) {
3358 chomp $retval if defined $retval;
3361 if (defined $o_git_dir) {
3362 $git_dir = $o_git_dir;
3367 # get type of given object
3371 defined(my $fd = git_cmd_pipe
"cat-file", '-t', $hash) or return;
3373 close $fd or return;
3378 # repository configuration
3379 our $config_file = '';
3382 # store multiple values for single key as anonymous array reference
3383 # single values stored directly in the hash, not as [ <value> ]
3384 sub hash_set_multi
{
3385 my ($hash, $key, $value) = @_;
3387 if (!exists $hash->{$key}) {
3388 $hash->{$key} = $value;
3389 } elsif (!ref $hash->{$key}) {
3390 $hash->{$key} = [ $hash->{$key}, $value ];
3392 push @
{$hash->{$key}}, $value;
3396 # return hash of git project configuration
3397 # optionally limited to some section, e.g. 'gitweb'
3398 sub git_parse_project_config
{
3399 my $section_regexp = shift;
3404 defined(my $fh = git_cmd_pipe
"config", '-z', '-l')
3407 while (my $keyval = to_utf8
(scalar <$fh>)) {
3409 my ($key, $value) = split(/\n/, $keyval, 2);
3411 hash_set_multi
(\
%config, $key, $value)
3412 if (!defined $section_regexp || $key =~ /^(?:$section_regexp)\./o);
3419 # convert config value to boolean: 'true' or 'false'
3420 # no value, number > 0, 'true' and 'yes' values are true
3421 # rest of values are treated as false (never as error)
3422 sub config_to_bool
{
3425 return 1 if !defined $val; # section.key
3427 # strip leading and trailing whitespace
3431 return (($val =~ /^\d+$/ && $val) || # section.key = 1
3432 ($val =~ /^(?:true|yes)$/i)); # section.key = true
3435 # convert config value to simple decimal number
3436 # an optional value suffix of 'k', 'm', or 'g' will cause the value
3437 # to be multiplied by 1024, 1048576, or 1073741824
3441 # strip leading and trailing whitespace
3445 if (my ($num, $unit) = ($val =~ /^([0-9]*)([kmg])$/i)) {
3447 # unknown unit is treated as 1
3448 return $num * ($unit eq 'g' ?
1073741824 :
3449 $unit eq 'm' ?
1048576 :
3450 $unit eq 'k' ?
1024 : 1);
3455 # convert config value to array reference, if needed
3456 sub config_to_multi
{
3459 return ref($val) ?
$val : (defined($val) ?
[ $val ] : []);
3462 sub git_get_project_config
{
3463 my ($key, $type) = @_;
3465 return unless defined $git_dir;
3468 return unless ($key);
3469 # only subsection, if exists, is case sensitive,
3470 # and not lowercased by 'git config -z -l'
3471 if (my ($hi, $mi, $lo) = ($key =~ /^([^.]*)\.(.*)\.([^.]*)$/)) {
3473 $key = join(".", lc($hi), $mi, lc($lo));
3474 return if ($lo =~ /\W/ || $hi =~ /\W/);
3478 return if ($key =~ /\W/);
3480 $key =~ s/^gitweb\.//;
3483 if (defined $type) {
3486 unless ($type eq 'bool' || $type eq 'int');
3490 if (!defined $config_file ||
3491 $config_file ne "$git_dir/config") {
3492 %config = git_parse_project_config
('gitweb');
3493 $config_file = "$git_dir/config";
3496 # check if config variable (key) exists
3497 return unless exists $config{"gitweb.$key"};
3500 if (!defined $type) {
3501 return $config{"gitweb.$key"};
3502 } elsif ($type eq 'bool') {
3503 # backward compatibility: 'git config --bool' returns true/false
3504 return config_to_bool
($config{"gitweb.$key"}) ?
'true' : 'false';
3505 } elsif ($type eq 'int') {
3506 return config_to_int
($config{"gitweb.$key"});
3508 return $config{"gitweb.$key"};
3511 # get hash of given path at given ref
3512 sub git_get_hash_by_path
{
3514 my $path = shift || return undef;
3519 defined(my $fd = git_cmd_pipe
"ls-tree", $base, "--", $path)
3520 or die_error
(500, "Open git-ls-tree failed");
3521 my $line = to_utf8
(scalar <$fd>);
3522 close $fd or return undef;
3524 if (!defined $line) {
3525 # there is no tree or hash given by $path at $base
3529 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'
3530 $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t/;
3531 if (defined $type && $type ne $2) {
3532 # type doesn't match
3538 # get path of entry with given hash at given tree-ish (ref)
3539 # used to get 'from' filename for combined diff (merge commit) for renames
3540 sub git_get_path_by_hash
{
3541 my $base = shift || return;
3542 my $hash = shift || return;
3546 defined(my $fd = git_cmd_pipe
"ls-tree", '-r', '-t', '-z', $base)
3548 while (my $line = to_utf8
(scalar <$fd>)) {
3551 #'040000 tree 595596a6a9117ddba9fe379b6b012b558bac8423 gitweb'
3552 #'100644 blob e02e90f0429be0d2a69b76571101f20b8f75530f gitweb/README'
3553 if ($line =~ m/(?:[0-9]+) (?:.+) $hash\t(.+)$/) {
3562 ## ......................................................................
3563 ## git utility functions, directly accessing git repository
3565 # get the value of config variable either from file named as the variable
3566 # itself in the repository ($GIT_DIR/$name file), or from gitweb.$name
3567 # configuration variable in the repository config file.
3568 sub git_get_file_or_project_config
{
3569 my ($path, $name) = @_;
3571 $git_dir = "$projectroot/$path";
3572 open my $fd, '<', "$git_dir/$name"
3573 or return git_get_project_config
($name);
3574 my $conf = to_utf8
(scalar <$fd>);
3576 if (defined $conf) {
3582 sub git_get_project_description
{
3584 return git_get_file_or_project_config
($path, 'description');
3587 sub git_get_project_category
{
3589 return git_get_file_or_project_config
($path, 'category');
3593 # supported formats:
3594 # * $GIT_DIR/ctags/<tagname> file (in 'ctags' subdirectory)
3595 # - if its contents is a number, use it as tag weight,
3596 # - otherwise add a tag with weight 1
3597 # * $GIT_DIR/ctags file, each line is a tag (with weight 1)
3598 # the same value multiple times increases tag weight
3599 # * `gitweb.ctag' multi-valued repo config variable
3600 sub git_get_project_ctags
{
3601 my $project = shift;
3604 $git_dir = "$projectroot/$project";
3605 if (opendir my $dh, "$git_dir/ctags") {
3606 my @files = grep { -f
$_ } map { "$git_dir/ctags/$_" } readdir($dh);
3607 foreach my $tagfile (@files) {
3608 open my $ct, '<', $tagfile
3614 (my $ctag = $tagfile) =~ s
#.*/##;
3615 $ctag = to_utf8
($ctag);
3616 if ($val =~ /^\d+$/) {
3617 $ctags->{$ctag} = $val;
3619 $ctags->{$ctag} = 1;
3624 } elsif (open my $fh, '<', "$git_dir/ctags") {
3625 while (my $line = to_utf8
(scalar <$fh>)) {
3627 $ctags->{$line}++ if $line;
3632 my $taglist = config_to_multi
(git_get_project_config
('ctag'));
3633 foreach my $tag (@
$taglist) {
3641 # return hash, where keys are content tags ('ctags'),
3642 # and values are sum of weights of given tag in every project
3643 sub git_gather_all_ctags
{
3644 my $projects = shift;
3647 foreach my $p (@
$projects) {
3648 foreach my $ct (keys %{$p->{'ctags'}}) {
3649 $ctags->{$ct} += $p->{'ctags'}->{$ct};
3656 sub git_populate_project_tagcloud
{
3657 my ($ctags, $action) = @_;
3659 # First, merge different-cased tags; tags vote on casing
3661 foreach (keys %$ctags) {
3662 $ctags_lc{lc $_}->{count
} += $ctags->{$_};
3663 if (not $ctags_lc{lc $_}->{topcount
}
3664 or $ctags_lc{lc $_}->{topcount
} < $ctags->{$_}) {
3665 $ctags_lc{lc $_}->{topcount
} = $ctags->{$_};
3666 $ctags_lc{lc $_}->{topname
} = $_;
3671 my $matched = $input_params{'ctag_filter'};
3672 if (eval { require HTML
::TagCloud
; 1; }) {
3673 $cloud = HTML
::TagCloud
->new;
3674 foreach my $ctag (sort keys %ctags_lc) {
3675 # Pad the title with spaces so that the cloud looks
3677 my $title = esc_html
($ctags_lc{$ctag}->{topname
});
3678 $title =~ s/ / /g;
3679 $title =~ s/^/ /g;
3680 $title =~ s/$/ /g;
3681 if (defined $matched && $matched eq $ctag) {
3682 $title = qq(<span
class="match">$title</span
>);
3684 $cloud->add($title, href
(-replay
=>1, action
=>$action, ctag_filter
=>$ctag),
3685 $ctags_lc{$ctag}->{count
});
3689 foreach my $ctag (keys %ctags_lc) {
3690 my $title = esc_html
($ctags_lc{$ctag}->{topname
}, -nbsp
=>1);
3691 if (defined $matched && $matched eq $ctag) {
3692 $title = qq(<span
class="match">$title</span
>);
3694 $cloud->{$ctag}{count
} = $ctags_lc{$ctag}->{count
};
3695 $cloud->{$ctag}{ctag
} =
3696 $cgi->a({-href
=>href
(-replay
=>1, action
=>$action, ctag_filter
=>$ctag)}, $title);
3702 sub git_show_project_tagcloud
{
3703 my ($cloud, $count) = @_;
3704 if (ref $cloud eq 'HTML::TagCloud') {
3705 return $cloud->html_and_css($count);
3707 my @tags = sort { $cloud->{$a}->{'count'} <=> $cloud->{$b}->{'count'} } keys %$cloud;
3709 '<div id="htmltagcloud"'.($project ?
'' : ' align="center"').'>' .
3711 $cloud->{$_}->{'ctag'}
3712 } splice(@tags, 0, $count)) .
3717 sub git_get_project_url_list
{
3720 $git_dir = "$projectroot/$path";
3721 open my $fd, '<', "$git_dir/cloneurl"
3722 or return wantarray ?
3723 @
{ config_to_multi
(git_get_project_config
('url')) } :
3724 config_to_multi
(git_get_project_config
('url'));
3725 my @git_project_url_list = map { chomp; to_utf8
($_) } <$fd>;
3728 return wantarray ?
@git_project_url_list : \
@git_project_url_list;
3731 sub git_get_projects_list
{
3732 my $filter = shift || '';
3733 my $paranoid = shift;
3736 if (-d
$projects_list) {
3737 # search in directory
3738 my $dir = $projects_list;
3739 # remove the trailing "/"
3741 my $pfxlen = length("$dir");
3742 my $pfxdepth = ($dir =~ tr!/!!);
3743 # when filtering, search only given subdirectory
3744 if ($filter && !$paranoid) {
3750 follow_fast
=> 1, # follow symbolic links
3751 follow_skip
=> 2, # ignore duplicates
3752 dangling_symlinks
=> 0, # ignore dangling symlinks, silently
3755 our $project_maxdepth;
3757 # skip project-list toplevel, if we get it.
3758 return if (m!^[/.]$!);
3759 # only directories can be git repositories
3760 return unless (-d
$_);
3761 # don't traverse too deep (Find is super slow on os x)
3762 # $project_maxdepth excludes depth of $projectroot
3763 if (($File::Find
::name
=~ tr!/!!) - $pfxdepth > $project_maxdepth) {
3764 $File::Find
::prune
= 1;
3768 my $path = substr($File::Find
::name
, $pfxlen + 1);
3769 # paranoidly only filter here
3770 if ($paranoid && $filter && $path !~ m!^\Q$filter\E/!) {
3773 # we check related file in $projectroot
3774 if (check_export_ok
("$projectroot/$path")) {
3775 push @list, { path
=> $path };
3776 $File::Find
::prune
= 1;
3781 } elsif (-f
$projects_list) {
3782 # read from file(url-encoded):
3783 # 'git%2Fgit.git Linus+Torvalds'
3784 # 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin'
3785 # 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman'
3786 open my $fd, '<', $projects_list or return;
3788 while (my $line = <$fd>) {
3790 my ($path, $owner) = split ' ', $line;
3791 $path = unescape
($path);
3792 $owner = unescape
($owner);
3793 if (!defined $path) {
3796 # if $filter is rpovided, check if $path begins with $filter
3797 if ($filter && $path !~ m!^\Q$filter\E/!) {
3800 if (check_export_ok
("$projectroot/$path")) {
3805 $pr->{'owner'} = to_utf8
($owner);
3815 # written with help of Tree::Trie module (Perl Artistic License, GPL compatibile)
3816 # as side effects it sets 'forks' field to list of forks for forked projects
3817 sub filter_forks_from_projects_list
{
3818 my $projects = shift;
3820 my %trie; # prefix tree of directories (path components)
3821 # generate trie out of those directories that might contain forks
3822 foreach my $pr (@
$projects) {
3823 my $path = $pr->{'path'};
3824 $path =~ s/\.git$//; # forks of 'repo.git' are in 'repo/' directory
3825 next if ($path =~ m!/$!); # skip non-bare repositories, e.g. 'repo/.git'
3826 next unless ($path); # skip '.git' repository: tests, git-instaweb
3827 next unless (-d
"$projectroot/$path"); # containing directory exists
3828 $pr->{'forks'} = []; # there can be 0 or more forks of project
3831 my @dirs = split('/', $path);
3832 # walk the trie, until either runs out of components or out of trie
3834 while (scalar @dirs &&
3835 exists($ref->{$dirs[0]})) {
3836 $ref = $ref->{shift @dirs};
3838 # create rest of trie structure from rest of components
3839 foreach my $dir (@dirs) {
3840 $ref = $ref->{$dir} = {};
3842 # create end marker, store $pr as a data
3843 $ref->{''} = $pr if (!exists $ref->{''});
3846 # filter out forks, by finding shortest prefix match for paths
3849 foreach my $pr (@
$projects) {
3853 foreach my $dir (split('/', $pr->{'path'})) {
3854 if (exists $ref->{''}) {
3855 # found [shortest] prefix, is a fork - skip it
3856 push @
{$ref->{''}{'forks'}}, $pr;
3859 if (!exists $ref->{$dir}) {
3860 # not in trie, cannot have prefix, not a fork
3861 push @filtered, $pr;
3864 # If the dir is there, we just walk one step down the trie.
3865 $ref = $ref->{$dir};
3867 # we ran out of trie
3868 # (shouldn't happen: it's either no match, or end marker)
3869 push @filtered, $pr;
3875 # note: fill_project_list_info must be run first,
3876 # for 'descr_long' and 'ctags' to be filled
3877 sub search_projects_list
{
3878 my ($projlist, %opts) = @_;
3879 my $tagfilter = $opts{'tagfilter'};
3880 my $search_re = $opts{'search_regexp'};
3883 unless ($tagfilter || $search_re);
3885 # searching projects require filling to be run before it;
3886 fill_project_list_info
($projlist,
3887 $tagfilter ?
'ctags' : (),
3888 $search_re ?
('path', 'descr') : ());
3891 foreach my $pr (@
$projlist) {
3894 next unless ref($pr->{'ctags'}) eq 'HASH';
3896 grep { lc($_) eq lc($tagfilter) } keys %{$pr->{'ctags'}};
3900 my $path = $pr->{'path'};
3901 $path =~ s/\.git$//; # should not be included in search
3903 $path =~ /$search_re/ ||
3904 $pr->{'descr_long'} =~ /$search_re/;
3907 push @projects, $pr;
3913 our $gitweb_project_owner = undef;
3914 sub git_get_project_list_from_file
{
3916 return if (defined $gitweb_project_owner);
3918 $gitweb_project_owner = {};
3919 # read from file (url-encoded):
3920 # 'git%2Fgit.git Linus+Torvalds'
3921 # 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin'
3922 # 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman'
3923 if (-f
$projects_list) {
3924 open(my $fd, '<', $projects_list);
3925 while (my $line = <$fd>) {
3927 my ($pr, $ow) = split ' ', $line;
3928 $pr = unescape
($pr);
3929 $ow = unescape
($ow);
3930 $gitweb_project_owner->{$pr} = to_utf8
($ow);
3936 sub git_get_project_owner
{
3940 return undef unless $proj;
3941 $git_dir = "$projectroot/$proj";
3943 if (defined $project && $proj eq $project) {
3944 $owner = git_get_project_config
('owner');
3946 if (!defined $owner && !defined $gitweb_project_owner) {
3947 git_get_project_list_from_file
();
3949 if (!defined $owner && exists $gitweb_project_owner->{$proj}) {
3950 $owner = $gitweb_project_owner->{$proj};
3952 if (!defined $owner && (!defined $project || $proj ne $project)) {
3953 $owner = git_get_project_config
('owner');
3955 if (!defined $owner) {
3956 $owner = get_file_owner
("$git_dir");
3962 sub parse_activity_date
{
3965 if ($dstr =~ /^\s*([-+]?\d+)(?:\s+([-+]\d{4}))?\s*$/) {
3969 if ($dstr =~ /^\s*(\d{4})-(\d{2})-(\d{2})[Tt _](\d{1,2}):(\d{2}):(\d{2})(?:[ _]?([Zz]|(?:[-+]\d{1,2}:?\d{2})))?\s*$/) {
3970 my ($Y,$m,$d,$H,$M,$S,$z) = ($1,$2,$3,$4,$5,$6,$7||'');
3971 my $seconds = timegm
(0+$S, 0+$M, 0+$H, 0+$d, $m-1, $Y-1900);
3972 defined($z) && $z ne '' or $z = 'Z';
3974 substr($z,1,0) = '0' if length($z) == 4;
3976 if (uc($z) ne 'Z') {
3977 $off = 60 * (60 * (0+substr($z,1,2)) + (0+substr($z,3,2)));
3978 $off = -$off if substr($z,0,1) eq '-';
3980 return $seconds - $off;
3985 # If $quick is true only look at $lastactivity_file
3986 sub git_get_last_activity
{
3987 my ($path, $quick) = @_;
3990 $git_dir = "$projectroot/$path";
3991 if ($lastactivity_file && open($fd, "<", "$git_dir/$lastactivity_file")) {
3992 my $activity = <$fd>;
3994 return (undef) unless defined $activity;
3996 return (undef) if $activity eq '';
3997 if (my $timestamp = parse_activity_date
($activity)) {
3998 return ($timestamp);
4001 return (undef) if $quick;
4002 defined($fd = git_cmd_pipe
'for-each-ref',
4003 '--format=%(committer)',
4004 '--sort=-committerdate',
4006 map { "refs/$_" } get_branch_refs
()) or return;
4007 my $most_recent = <$fd>;
4008 close $fd or return (undef);
4009 if (defined $most_recent &&
4010 $most_recent =~ / (\d+) [-+][01]\d\d\d$/) {
4012 return ($timestamp);
4017 # Implementation note: when a single remote is wanted, we cannot use 'git
4018 # remote show -n' because that command always work (assuming it's a remote URL
4019 # if it's not defined), and we cannot use 'git remote show' because that would
4020 # try to make a network roundtrip. So the only way to find if that particular
4021 # remote is defined is to walk the list provided by 'git remote -v' and stop if
4022 # and when we find what we want.
4023 sub git_get_remotes_list
{
4027 my $fd = git_cmd_pipe
'remote', '-v';
4029 while (my $remote = to_utf8
(scalar <$fd>)) {
4031 $remote =~ s!\t(.*?)\s+\((\w+)\)$!!;
4032 next if $wanted and not $remote eq $wanted;
4033 my ($url, $key) = ($1, $2);
4035 $remotes{$remote} ||= { 'heads' => [] };
4036 $remotes{$remote}{$key} = $url;
4038 close $fd or return;
4039 return wantarray ?
%remotes : \
%remotes;
4042 # Takes a hash of remotes as first parameter and fills it by adding the
4043 # available remote heads for each of the indicated remotes.
4044 sub fill_remote_heads
{
4045 my $remotes = shift;
4046 my @heads = map { "remotes/$_" } keys %$remotes;
4047 my @remoteheads = git_get_heads_list
(undef, @heads);
4048 foreach my $remote (keys %$remotes) {
4049 $remotes->{$remote}{'heads'} = [ grep {
4050 $_->{'name'} =~ s!^$remote/!!
4055 sub git_get_references
{
4056 my $type = shift || "";
4058 # 5dc01c595e6c6ec9ccda4f6f69c131c0dd945f8c refs/tags/v2.6.11
4059 # c39ae07f393806ccf406ef966e9a15afc43cc36a refs/tags/v2.6.11^{}
4060 defined(my $fd = git_cmd_pipe
"show-ref", "--dereference",
4061 ($type ?
("--", "refs/$type") : ())) # use -- <pattern> if $type
4064 while (my $line = to_utf8
(scalar <$fd>)) {
4066 if ($line =~ m!^([0-9a-fA-F]{40})\srefs/($type.*)$!) {
4067 if (defined $refs{$1}) {
4068 push @
{$refs{$1}}, $2;
4074 close $fd or return;
4078 sub git_get_rev_name_tags
{
4079 my $hash = shift || return undef;
4081 defined(my $fd = git_cmd_pipe
"name-rev", "--tags", $hash)
4083 my $name_rev = to_utf8
(scalar <$fd>);
4086 if ($name_rev =~ m
|^$hash tags
/(.*)$|) {
4089 # catches also '$hash undefined' output
4094 ## ----------------------------------------------------------------------
4095 ## parse to hash functions
4099 my $tz = shift || "-0000";
4102 my @months = ("Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec");
4103 my @days = ("Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat");
4104 my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($epoch);
4105 $date{'hour'} = $hour;
4106 $date{'minute'} = $min;
4107 $date{'mday'} = $mday;
4108 $date{'day'} = $days[$wday];
4109 $date{'month'} = $months[$mon];
4110 $date{'rfc2822'} = sprintf "%s, %d %s %4d %02d:%02d:%02d +0000",
4111 $days[$wday], $mday, $months[$mon], 1900+$year, $hour ,$min, $sec;
4112 $date{'mday-time'} = sprintf "%d %s %02d:%02d",
4113 $mday, $months[$mon], $hour ,$min;
4114 $date{'iso-8601'} = sprintf "%04d-%02d-%02dT%02d:%02d:%02dZ",
4115 1900+$year, 1+$mon, $mday, $hour ,$min, $sec;
4117 my ($tz_sign, $tz_hour, $tz_min) =
4118 ($tz =~ m/^([-+])(\d\d)(\d\d)$/);
4119 $tz_sign = ($tz_sign eq '-' ?
-1 : +1);
4120 my $local = $epoch + $tz_sign*((($tz_hour*60) + $tz_min)*60);
4121 ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($local);
4122 $date{'hour_local'} = $hour;
4123 $date{'minute_local'} = $min;
4124 $date{'mday_local'} = $mday;
4125 $date{'tz_local'} = $tz;
4126 $date{'iso-tz'} = sprintf("%04d-%02d-%02d %02d:%02d:%02d %s",
4127 1900+$year, $mon+1, $mday,
4128 $hour, $min, $sec, $tz);
4132 sub parse_file_date
{
4134 my $mtime = (stat("$projectroot/$project/$file"))[9];
4135 return () unless defined $mtime;
4136 my $tzoffset = timegm
((localtime($mtime))[0..5]) - $mtime;
4138 if ($tzoffset <= 0) {
4142 $tzoffset = int($tzoffset/60);
4143 $tzstring .= sprintf("%02d%02d", int($tzoffset/60), $tzoffset%60);
4144 return parse_date
($mtime, $tzstring);
4152 defined(my $fd = git_cmd_pipe
"cat-file", "tag", $tag_id) or return;
4153 $tag{'id'} = $tag_id;
4154 while (my $line = to_utf8
(scalar <$fd>)) {
4156 if ($line =~ m/^object ([0-9a-fA-F]{40})$/) {
4157 $tag{'object'} = $1;
4158 } elsif ($line =~ m/^type (.+)$/) {
4160 } elsif ($line =~ m/^tag (.+)$/) {
4162 } elsif ($line =~ m/^tagger (.*) ([0-9]+) (.*)$/) {
4163 $tag{'author'} = $1;
4164 $tag{'author_epoch'} = $2;
4165 $tag{'author_tz'} = $3;
4166 if ($tag{'author'} =~ m/^([^<]+) <([^>]*)>/) {
4167 $tag{'author_name'} = $1;
4168 $tag{'author_email'} = $2;
4170 $tag{'author_name'} = $tag{'author'};
4172 } elsif ($line =~ m/--BEGIN/) {
4173 push @comment, $line;
4175 } elsif ($line eq "") {
4179 push @comment, map(to_utf8
($_), <$fd>);
4180 $tag{'comment'} = \
@comment;
4181 close $fd or return;
4182 if (!defined $tag{'name'}) {
4188 sub parse_commit_text
{
4189 my ($commit_text, $withparents) = @_;
4190 my @commit_lines = split '\n', $commit_text;
4193 pop @commit_lines; # Remove '\0'
4195 if (! @commit_lines) {
4199 my $header = shift @commit_lines;
4200 if ($header !~ m/^[0-9a-fA-F]{40}/) {
4203 ($co{'id'}, my @parents) = split ' ', $header;
4204 while (my $line = shift @commit_lines) {
4205 last if $line eq "\n";
4206 if ($line =~ m/^tree ([0-9a-fA-F]{40})$/) {
4208 } elsif ((!defined $withparents) && ($line =~ m/^parent ([0-9a-fA-F]{40})$/)) {
4210 } elsif ($line =~ m/^author (.*) ([0-9]+) (.*)$/) {
4211 $co{'author'} = to_utf8
($1);
4212 $co{'author_epoch'} = $2;
4213 $co{'author_tz'} = $3;
4214 if ($co{'author'} =~ m/^([^<]+) <([^>]*)>/) {
4215 $co{'author_name'} = $1;
4216 $co{'author_email'} = $2;
4218 $co{'author_name'} = $co{'author'};
4220 } elsif ($line =~ m/^committer (.*) ([0-9]+) (.*)$/) {
4221 $co{'committer'} = to_utf8
($1);
4222 $co{'committer_epoch'} = $2;
4223 $co{'committer_tz'} = $3;
4224 if ($co{'committer'} =~ m/^([^<]+) <([^>]*)>/) {
4225 $co{'committer_name'} = $1;
4226 $co{'committer_email'} = $2;
4228 $co{'committer_name'} = $co{'committer'};
4232 if (!defined $co{'tree'}) {
4235 $co{'parents'} = \
@parents;
4236 $co{'parent'} = $parents[0];
4238 @commit_lines = map to_utf8
($_), @commit_lines;
4239 foreach my $title (@commit_lines) {
4242 $co{'title'} = chop_str
($title, 80, 5);
4243 # remove leading stuff of merges to make the interesting part visible
4244 if (length($title) > 50) {
4245 $title =~ s/^Automatic //;
4246 $title =~ s/^merge (of|with) /Merge ... /i;
4247 if (length($title) > 50) {
4248 $title =~ s/(http|rsync):\/\///;
4250 if (length($title) > 50) {
4251 $title =~ s/(master|www|rsync)\.//;
4253 if (length($title) > 50) {
4254 $title =~ s/kernel.org:?//;
4256 if (length($title) > 50) {
4257 $title =~ s/\/pub\/scm//;
4260 $co{'title_short'} = chop_str
($title, 50, 5);
4264 if (! defined $co{'title'} || $co{'title'} eq "") {
4265 $co{'title'} = $co{'title_short'} = '(no commit message)';
4267 # remove added spaces
4268 foreach my $line (@commit_lines) {
4271 $co{'comment'} = \
@commit_lines;
4273 my $age_epoch = $co{'committer_epoch'};
4274 $co{'age_epoch'} = $age_epoch;
4275 my $time_now = time;
4276 $co{'age_string'} = age_string
($age_epoch, $time_now);
4277 $co{'age_string_date'} = age_string_date
($age_epoch, $time_now);
4278 $co{'age_string_age'} = age_string_age
($age_epoch, $time_now);
4283 my ($commit_id) = @_;
4288 defined(my $fd = git_cmd_pipe
"rev-list",
4294 or die_error
(500, "Open git-rev-list failed");
4295 %co = parse_commit_text
(<$fd>, 1);
4302 my ($commit_id, $maxcount, $skip, $filename, @args) = @_;
4310 defined(my $fd = git_cmd_pipe
"rev-list",
4313 ("--max-count=" . $maxcount),
4314 ("--skip=" . $skip),
4318 ($filename ?
($filename) : ()))
4319 or die_error
(500, "Open git-rev-list failed");
4320 while (my $line = <$fd>) {
4321 my %co = parse_commit_text
($line);
4326 return wantarray ?
@cos : \
@cos;
4329 # parse line of git-diff-tree "raw" output
4330 sub parse_difftree_raw_line
{
4334 # ':100644 100644 03b218260e99b78c6df0ed378e59ed9205ccc96d 3b93d5e7cc7f7dd4ebed13a5cc1a4ad976fc94d8 M ls-files.c'
4335 # ':100644 100644 7f9281985086971d3877aca27704f2aaf9c448ce bc190ebc71bbd923f2b728e505408f5e54bd073a M rev-tree.c'
4336 if ($line =~ m/^:([0-7]{6}) ([0-7]{6}) ([0-9a-fA-F]{40}) ([0-9a-fA-F]{40}) (.)([0-9]{0,3})\t(.*)$/) {
4337 $res{'from_mode'} = $1;
4338 $res{'to_mode'} = $2;
4339 $res{'from_id'} = $3;
4341 $res{'status'} = $5;
4342 $res{'similarity'} = $6;
4343 if ($res{'status'} eq 'R' || $res{'status'} eq 'C') { # renamed or copied
4344 ($res{'from_file'}, $res{'to_file'}) = map { unquote
($_) } split("\t", $7);
4346 $res{'from_file'} = $res{'to_file'} = $res{'file'} = unquote
($7);
4349 # '::100755 100755 100755 60e79ca1b01bc8b057abe17ddab484699a7f5fdb 94067cc5f73388f33722d52ae02f44692bc07490 94067cc5f73388f33722d52ae02f44692bc07490 MR git-gui/git-gui.sh'
4350 # combined diff (for merge commit)
4351 elsif ($line =~ s/^(::+)((?:[0-7]{6} )+)((?:[0-9a-fA-F]{40} )+)([a-zA-Z]+)\t(.*)$//) {
4352 $res{'nparents'} = length($1);
4353 $res{'from_mode'} = [ split(' ', $2) ];
4354 $res{'to_mode'} = pop @
{$res{'from_mode'}};
4355 $res{'from_id'} = [ split(' ', $3) ];
4356 $res{'to_id'} = pop @
{$res{'from_id'}};
4357 $res{'status'} = [ split('', $4) ];
4358 $res{'to_file'} = unquote
($5);
4360 # 'c512b523472485aef4fff9e57b229d9d243c967f'
4361 elsif ($line =~ m/^([0-9a-fA-F]{40})$/) {
4362 $res{'commit'} = $1;
4365 return wantarray ?
%res : \
%res;
4368 # wrapper: return parsed line of git-diff-tree "raw" output
4369 # (the argument might be raw line, or parsed info)
4370 sub parsed_difftree_line
{
4371 my $line_or_ref = shift;
4373 if (ref($line_or_ref) eq "HASH") {
4374 # pre-parsed (or generated by hand)
4375 return $line_or_ref;
4377 return parse_difftree_raw_line
($line_or_ref);
4381 # parse line of git-ls-tree output
4382 sub parse_ls_tree_line
{
4388 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa 16717 panic.c'
4389 $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40}) +(-|[0-9]+)\t(.+)$/s;
4398 $res{'name'} = unquote
($5);
4401 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'
4402 $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t(.+)$/s;
4410 $res{'name'} = unquote
($4);
4414 return wantarray ?
%res : \
%res;
4417 # generates _two_ hashes, references to which are passed as 2 and 3 argument
4418 sub parse_from_to_diffinfo
{
4419 my ($diffinfo, $from, $to, @parents) = @_;
4421 if ($diffinfo->{'nparents'}) {
4423 $from->{'file'} = [];
4424 $from->{'href'} = [];
4425 fill_from_file_info
($diffinfo, @parents)
4426 unless exists $diffinfo->{'from_file'};
4427 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
4428 $from->{'file'}[$i] =
4429 defined $diffinfo->{'from_file'}[$i] ?
4430 $diffinfo->{'from_file'}[$i] :
4431 $diffinfo->{'to_file'};
4432 if ($diffinfo->{'status'}[$i] ne "A") { # not new (added) file
4433 $from->{'href'}[$i] = href
(action
=>"blob",
4434 hash_base
=>$parents[$i],
4435 hash
=>$diffinfo->{'from_id'}[$i],
4436 file_name
=>$from->{'file'}[$i]);
4438 $from->{'href'}[$i] = undef;
4442 # ordinary (not combined) diff
4443 $from->{'file'} = $diffinfo->{'from_file'};
4444 if ($diffinfo->{'status'} ne "A") { # not new (added) file
4445 $from->{'href'} = href
(action
=>"blob", hash_base
=>$hash_parent,
4446 hash
=>$diffinfo->{'from_id'},
4447 file_name
=>$from->{'file'});
4449 delete $from->{'href'};
4453 $to->{'file'} = $diffinfo->{'to_file'};
4454 if (!is_deleted
($diffinfo)) { # file exists in result
4455 $to->{'href'} = href
(action
=>"blob", hash_base
=>$hash,
4456 hash
=>$diffinfo->{'to_id'},
4457 file_name
=>$to->{'file'});
4459 delete $to->{'href'};
4463 ## ......................................................................
4464 ## parse to array of hashes functions
4466 sub git_get_heads_list
{
4467 my ($limit, @classes) = @_;
4468 @classes = get_branch_refs
() unless @classes;
4469 my @patterns = map { "refs/$_" } @classes;
4472 defined(my $fd = git_cmd_pipe
'for-each-ref',
4473 ($limit ?
'--count='.($limit+1) : ()), '--sort=-committerdate',
4474 '--format=%(objectname) %(refname) %(subject)%00%(committer)',
4477 while (my $line = to_utf8
(scalar <$fd>)) {
4481 my ($refinfo, $committerinfo) = split(/\0/, $line);
4482 my ($hash, $name, $title) = split(' ', $refinfo, 3);
4483 my ($committer, $epoch, $tz) =
4484 ($committerinfo =~ /^(.*) ([0-9]+) (.*)$/);
4485 $ref_item{'fullname'} = $name;
4486 my $strip_refs = join '|', map { quotemeta } get_branch_refs
();
4487 $name =~ s!^refs/($strip_refs|remotes)/!!;
4488 $ref_item{'name'} = $name;
4489 # for refs neither in 'heads' nor 'remotes' we want to
4490 # show their ref dir
4491 my $ref_dir = (defined $1) ?
$1 : '';
4492 if ($ref_dir ne '' and $ref_dir ne 'heads' and $ref_dir ne 'remotes') {
4493 $ref_item{'name'} .= ' (' . $ref_dir . ')';
4496 $ref_item{'id'} = $hash;
4497 $ref_item{'title'} = $title || '(no commit message)';
4498 $ref_item{'epoch'} = $epoch;
4500 $ref_item{'age'} = age_string
($ref_item{'epoch'});
4502 $ref_item{'age'} = "unknown";
4505 push @headslist, \
%ref_item;
4509 return wantarray ?
@headslist : \
@headslist;
4512 sub git_get_tags_list
{
4515 my $all = shift || 0;
4516 my $order = shift || $default_refs_order;
4517 my $sortkey = $all && $order eq 'name' ?
'refname' : '-creatordate';
4519 defined(my $fd = git_cmd_pipe
'for-each-ref',
4520 ($limit ?
'--count='.($limit+1) : ()), "--sort=$sortkey",
4521 '--format=%(objectname) %(objecttype) %(refname) '.
4522 '%(*objectname) %(*objecttype) %(subject)%00%(creator)',
4523 ($all ?
'refs' : 'refs/tags'))
4525 while (my $line = to_utf8
(scalar <$fd>)) {
4529 my ($refinfo, $creatorinfo) = split(/\0/, $line);
4530 my ($id, $type, $name, $refid, $reftype, $title) = split(' ', $refinfo, 6);
4531 my ($creator, $epoch, $tz) =
4532 ($creatorinfo =~ /^(.*) ([0-9]+) (.*)$/);
4533 $ref_item{'fullname'} = $name;
4534 $name =~ s!^refs/!! if $all;
4535 $name =~ s!^refs/tags/!! unless $all;
4537 $ref_item{'type'} = $type;
4538 $ref_item{'id'} = $id;
4539 $ref_item{'name'} = $name;
4540 if ($type eq "tag") {
4541 $ref_item{'subject'} = $title;
4542 $ref_item{'reftype'} = $reftype;
4543 $ref_item{'refid'} = $refid;
4545 $ref_item{'reftype'} = $type;
4546 $ref_item{'refid'} = $id;
4549 if ($type eq "tag" || $type eq "commit") {
4550 $ref_item{'epoch'} = $epoch;
4552 $ref_item{'age'} = age_string
($ref_item{'epoch'});
4554 $ref_item{'age'} = "unknown";
4558 push @tagslist, \
%ref_item;
4562 return wantarray ?
@tagslist : \
@tagslist;
4565 ## ----------------------------------------------------------------------
4566 ## filesystem-related functions
4568 sub get_file_owner
{
4571 my ($dev, $ino, $mode, $nlink, $st_uid, $st_gid, $rdev, $size) = stat($path);
4572 my ($name, $passwd, $uid, $gid, $quota, $comment, $gcos, $dir, $shell) = getpwuid($st_uid);
4573 if (!defined $gcos) {
4577 $owner =~ s/[,;].*$//;
4578 return to_utf8
($owner);
4581 # assume that file exists
4583 my $filename = shift;
4585 open my $fd, '<', $filename;
4592 # return undef on failure
4593 sub collect_output
{
4594 defined(my $fd = cmd_pipe
@_) or return undef;
4599 my $result = join('', map({ to_utf8
($_) } <$fd>));
4600 close $fd or return undef;
4604 # return undef on failure
4605 # return '' if only comments
4606 sub collect_html_file
{
4607 my $filename = shift;
4609 open my $fd, '<', $filename or return undef;
4610 my $result = join('', map({ to_utf8
($_) } <$fd>));
4611 close $fd or return undef;
4612 return undef unless defined($result);
4614 $test =~ s/<!--(?:[^-]|(?:-(?!-)))*-->//gs;
4616 return $test eq '' ?
'' : $result;
4619 ## ......................................................................
4620 ## mimetype related functions
4622 sub mimetype_guess_file
{
4623 my $filename = shift;
4624 my $mimemap = shift;
4625 my $rawmode = shift;
4626 -r
$mimemap or return undef;
4629 open(my $mh, '<', $mimemap) or return undef;
4631 next if m/^#/; # skip comments
4632 my ($mimetype, @exts) = split(/\s+/);
4633 foreach my $ext (@exts) {
4634 $mimemap{$ext} = $mimetype;
4640 $ext = $1 if $filename =~ /\.([^.]*)$/;
4641 $ans = $mimemap{$ext} if $ext;
4644 $ans = 'text/html' if $l eq 'application/xhtml+xml';
4646 $ans = 'text/xml' if $l =~ m
!^application
/[^\s
:;,=]+\
+xml
$! ||
4647 $l eq 'image/svg+xml' ||
4648 $l eq 'application/xml-dtd' ||
4649 $l eq 'application/xml-external-parsed-entity';
4655 sub mimetype_guess
{
4656 my $filename = shift;
4657 my $rawmode = shift;
4659 $filename =~ /\./ or return undef;
4661 if ($mimetypes_file) {
4662 my $file = $mimetypes_file;
4663 if ($file !~ m!^/!) { # if it is relative path
4664 # it is relative to project
4665 $file = "$projectroot/$project/$file";
4667 $mime = mimetype_guess_file
($filename, $file, $rawmode);
4669 $mime ||= mimetype_guess_file
($filename, '/etc/mime.types', $rawmode);
4675 my $filename = shift;
4676 my $rawmode = shift;
4679 # The -T/-B file operators produce the wrong result unless a perlio
4680 # layer is present when the file handle is a pipe that delivers less
4681 # than 512 bytes of data before reaching EOF.
4683 # If we are running in a Perl that uses the stdio layer rather than the
4684 # unix+perlio layers we will end up adding a perlio layer on top of the
4685 # stdio layer and get a second level of buffering. This is harmless
4686 # and it makes the -T/-B file operators work properly in all cases.
4688 binmode $fd, ":perlio" or die_error
(500, "Adding perlio layer failed")
4689 unless grep /^perlio$/, PerlIO
::get_layers
($fd);
4691 $mime = mimetype_guess
($filename, $rawmode) if defined $filename;
4693 if (!$mime && $filename) {
4694 if ($filename =~ m/\.html?$/i) {
4695 $mime = 'text/html';
4696 } elsif ($filename =~ m/\.xht(?:ml)?$/i) {
4697 $mime = 'text/html';
4698 } elsif ($filename =~ m/\.te?xt?$/i) {
4699 $mime = 'text/plain';
4700 } elsif ($filename =~ m/\.(?:markdown|md)$/i) {
4701 $mime = 'text/plain';
4702 } elsif ($filename =~ m/\.png$/i) {
4703 $mime = 'image/png';
4704 } elsif ($filename =~ m/\.gif$/i) {
4705 $mime = 'image/gif';
4706 } elsif ($filename =~ m/\.jpe?g$/i) {
4707 $mime = 'image/jpeg';
4708 } elsif ($filename =~ m/\.svgz?$/i) {
4709 $mime = 'image/svg+xml';
4714 return $default_blob_plain_mimetype || 'application/octet-stream' unless $fd || $mime;
4716 $mime = -T
$fd ?
'text/plain' : 'application/octet-stream' unless $mime;
4724 return scalar($data =~ /^[\x00-\x7f]*$/);
4729 return utf8
::decode
($data);
4732 sub extract_html_charset
{
4733 return undef unless $_[0] && "$_[0]</head>" =~ m
#<head(?:\s+[^>]*)?(?<!/)>(.*?)</head\s*>#is;
4735 return $2 if $head =~ m
#<meta\s+charset\s*=\s*(['"])\s*([a-z0-9(:)_.+-]+)\s*\1\s*/?>#is;
4736 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) {
4737 my %kv = (lc($1) => $3, lc($4) => $6);
4738 my ($he, $c) = (lc($kv{'http-equiv'}), $kv{'content'});
4739 return $1 if $he && $c && $he eq 'content-type' &&
4740 $c =~ m!\s*text/html\s*;\s*charset\s*=\s*([a-z0-9(:)_.+-]+)\s*$!is;
4745 sub blob_contenttype
{
4746 my ($fd, $file_name, $type) = @_;
4748 $type ||= blob_mimetype
($fd, $file_name, 1);
4749 return $type unless $type =~ m!^text/.+!i;
4750 my ($leader, $charset, $htmlcharset);
4751 if ($fd && read($fd, $leader, 32768)) {{
4752 $charset='US-ASCII' if is_ascii
($leader);
4753 return ("$type; charset=UTF-8", $leader) if !$charset && is_valid_utf8
($leader);
4754 $charset='ISO-8859-1' unless $charset;
4755 $htmlcharset = extract_html_charset
($leader) if $type eq 'text/html';
4756 if ($htmlcharset && $charset ne 'US-ASCII') {
4757 $htmlcharset = undef if $htmlcharset =~ /^(?:utf-8|us-ascii)$/i
4760 return ("$type; charset=$htmlcharset", $leader) if $htmlcharset;
4761 my $defcharset = $default_text_plain_charset || '';
4762 $defcharset =~ s/^\s+//;
4763 $defcharset =~ s/\s+$//;
4764 $defcharset = '' if $charset && $charset ne 'US-ASCII' && $defcharset =~ /^(?:utf-8|us-ascii)$/i;
4765 return ("$type; charset=" . ($defcharset || 'ISO-8859-1'), $leader);
4768 # peek the first upto 128 bytes off a file handle
4776 return '' unless $fd && read($fd, $prefix128, 128);
4778 # In the general case, we're guaranteed only to be able to ungetc one
4779 # character (provided, of course, we actually got a character first).
4783 # 1) we are dealing with a :perlio layer since blob_mimetype will have
4784 # already been called at least once on the file handle before us
4786 # 2) we have an $fd positioned at the start of the input stream and
4787 # therefore know we were positioned at a buffer boundary before
4788 # reading the initial upto 128 bytes
4790 # 3) the buffer size is at least 512 bytes
4792 # 4) we are careful to only unget raw bytes
4794 # 5) we are attempting to unget exactly the same number of bytes we got
4796 # Given the above conditions we will ALWAYS be able to safely unget
4797 # the $prefix128 value we just got.
4799 # In fact, we could read up to 511 bytes and still be sure.
4800 # (Reading 512 might pop us into the next internal buffer, but probably
4801 # not since that could break the always able to unget at least the one
4802 # you just got guarantee.)
4804 map {$fd->ungetc(ord($_))} reverse(split //, $prefix128);
4809 # guess file syntax for syntax highlighting; return undef if no highlighting
4810 # the name of syntax can (in the future) depend on syntax highlighter used
4811 sub guess_file_syntax
{
4812 my ($fd, $mimetype, $file_name) = @_;
4813 return undef unless $fd && defined $file_name &&
4814 defined $mimetype && $mimetype =~ m!^text/.+!i;
4815 my $basename = basename
($file_name, '.in');
4816 return $highlight_basename{$basename}
4817 if exists $highlight_basename{$basename};
4819 # Peek to see if there's a shebang or xml line.
4820 # We always operate on bytes when testing this.
4823 my $shebang = peek128bytes
($fd);
4824 if (length($shebang) >= 4 && $shebang =~ /^#!/) { # 4 would be '#!/x'
4825 foreach my $key (keys %highlight_shebang) {
4826 my $ar = ref($highlight_shebang{$key}) ?
4827 $highlight_shebang{$key} :
4828 [$highlight_shebang{key
}];
4829 map {return $key if $shebang =~ /$_/} @
$ar;
4832 return 'xml' if $shebang =~ m!^\s*<\?xml\s!; # "xml" must be lowercase
4835 $basename =~ /\.([^.]*)$/;
4836 my $ext = $1 or return undef;
4837 return $highlight_ext{$ext}
4838 if exists $highlight_ext{$ext};
4843 # run highlighter and return FD of its output,
4844 # or return original FD if no highlighting
4845 sub run_highlighter
{
4846 my ($fd, $syntax) = @_;
4847 return $fd unless $fd && !eof($fd) && defined $highlight_bin && defined $syntax;
4849 defined(my $hifd = cmd_pipe
$posix_shell_bin, '-c',
4850 quote_command
(git_cmd
(), "cat-file", "blob", $hash)." | ".
4851 quote_command
($highlight_bin).
4852 " --replace-tabs=8 --fragment --syntax $syntax")
4853 or die_error
(500, "Couldn't open file or run syntax highlighter");
4855 # just in case, should not happen as we tested !eof($fd) above
4856 return $fd if close($hifd);
4859 !$! or die_error
(500, "Couldn't close syntax highighter pipe");
4861 # leaving us with the only possibility a non-zero exit status (possibly a signal);
4862 # instead of dying horribly on this, just skip the highlighting
4863 # but do output a message about it to STDERR that will end up in the log
4864 print STDERR
"warning: skipping failed highlight for --syntax $syntax: ".
4865 sprintf("child exit status 0x%x\n", $?
);
4872 ## ======================================================================
4873 ## functions printing HTML: header, footer, error page
4875 sub get_page_title
{
4876 my $title = to_utf8
($site_name);
4878 unless (defined $project) {
4879 if (defined $project_filter) {
4880 $title .= " - projects in '" . esc_path
($project_filter) . "'";
4884 $title .= " - " . to_utf8
($project);
4886 return $title unless (defined $action);
4887 my $action_print = $action eq 'blame_incremental' ?
'blame' : $action;
4888 $title .= "/$action_print"; # $action is US-ASCII (7bit ASCII)
4890 return $title unless (defined $file_name);
4891 $title .= " - " . esc_path
($file_name);
4892 if ($action eq "tree" && $file_name !~ m
|/$|) {
4899 sub get_content_type_html
{
4900 # We do not ever emit application/xhtml+xml since that gives us
4901 # no benefits and it makes many browsers (e.g. Firefox) exceedingly
4902 # strict, which is troublesome for example when showing user-supplied
4903 # README.html files.
4907 sub print_feed_meta
{
4908 if (defined $project) {
4909 my %href_params = get_feed_info
();
4910 if (!exists $href_params{'-title'}) {
4911 $href_params{'-title'} = 'log';
4914 foreach my $format (qw(RSS Atom)) {
4915 my $type = lc($format);
4917 '-rel' => 'alternate',
4918 '-title' => esc_attr
("$project - $href_params{'-title'} - $format feed"),
4919 '-type' => "application/$type+xml"
4922 $href_params{'extra_options'} = undef;
4923 $href_params{'action'} = $type;
4924 $link_attr{'-href'} = href
(%href_params);
4926 "rel=\"$link_attr{'-rel'}\" ".
4927 "title=\"$link_attr{'-title'}\" ".
4928 "href=\"$link_attr{'-href'}\" ".
4929 "type=\"$link_attr{'-type'}\" ".
4932 $href_params{'extra_options'} = '--no-merges';
4933 $link_attr{'-href'} = href
(%href_params);
4934 $link_attr{'-title'} .= ' (no merges)';
4936 "rel=\"$link_attr{'-rel'}\" ".
4937 "title=\"$link_attr{'-title'}\" ".
4938 "href=\"$link_attr{'-href'}\" ".
4939 "type=\"$link_attr{'-type'}\" ".
4944 printf('<link rel="alternate" title="%s projects list" '.
4945 'href="%s" type="text/plain; charset=utf-8" />'."\n",
4946 esc_attr
($site_name), href
(project
=>undef, action
=>"project_index"));
4947 printf('<link rel="alternate" title="%s projects feeds" '.
4948 'href="%s" type="text/x-opml" />'."\n",
4949 esc_attr
($site_name), href
(project
=>undef, action
=>"opml"));
4953 sub compute_stylesheet_links
{
4954 return "<?gitweb compute_stylesheet_links?>" if $cache_mode_active;
4956 # include each stylesheet that exists, providing backwards capability
4957 # for those people who defined $stylesheet in a config file
4958 if (defined $stylesheet) {
4959 return '<link rel="stylesheet" type="text/css" href="'.esc_url
($stylesheet).'"/>'."\n";
4962 foreach my $stylesheet (@stylesheets) {
4963 next unless $stylesheet;
4964 $sheets .= '<link rel="stylesheet" type="text/css" href="'.esc_url
($stylesheet).'"/>'."\n";
4970 sub print_header_links
{
4973 print compute_stylesheet_links
();
4975 if ($status eq '200 OK');
4976 if (defined $favicon) {
4977 print qq(<link rel
="shortcut icon" href
=").esc_url($favicon).qq(" type
="image/png" />\n);
4981 sub print_nav_breadcrumbs_path
{
4982 my $dirprefix = undef;
4983 while (my $part = shift) {
4984 $dirprefix .= "/" if defined $dirprefix;
4985 $dirprefix .= $part;
4986 print $cgi->a({-href
=> href
(project
=> undef,
4987 project_filter
=> $dirprefix,
4988 action
=> "project_list")},
4989 esc_html
($part)) . " / ";
4993 sub print_nav_breadcrumbs
{
4996 for my $crumb (@extra_breadcrumbs, [ $home_link_str => $home_link ]) {
4997 print $cgi->a({-href
=> esc_url
($crumb->[1])}, $crumb->[0]) . " / ";
4999 if (defined $project) {
5000 my @dirname = split '/', $project;
5001 my $projectbasename = pop @dirname;
5002 print_nav_breadcrumbs_path
(@dirname);
5003 print $cgi->a({-href
=> href
(action
=>"summary")}, esc_html
($projectbasename));
5004 if (defined $action) {
5005 my $action_print = $action ;
5006 $action_print = 'blame' if $action_print eq 'blame_incremental';
5007 if (defined $opts{-action_extra
}) {
5008 $action_print = $cgi->a({-href
=> href
(action
=>$action)},
5011 print " / $action_print";
5013 if (defined $opts{-action_extra
}) {
5014 print " / $opts{-action_extra}";
5017 } elsif (defined $project_filter) {
5018 print_nav_breadcrumbs_path
(split '/', $project_filter);
5022 sub print_search_form
{
5023 if (!defined $searchtext) {
5027 if (defined $hash_base) {
5028 $search_hash = $hash_base;
5029 } elsif (defined $hash) {
5030 $search_hash = $hash;
5032 $search_hash = "HEAD";
5034 # We can't use href() here because we need to encode the
5035 # URL parameters into the form, not into the action link.
5036 my $action = $my_uri;
5037 my $use_pathinfo = gitweb_check_feature
('pathinfo');
5038 if ($use_pathinfo) {
5039 # See notes about doubled / in href()
5041 $action .= "/".esc_path_info
($project);
5043 print $cgi->start_form(-method
=> "get", -action
=> $action) .
5044 "<div class=\"search\">\n" .
5046 $cgi->input({-name
=>"p", -value
=>$project, -type
=>"hidden"}) . "\n") .
5047 $cgi->input({-name
=>"a", -value
=>"search", -type
=>"hidden"}) . "\n" .
5048 $cgi->input({-name
=>"h", -value
=>$search_hash, -type
=>"hidden"}) . "\n" .
5049 $cgi->popup_menu(-name
=> 'st', -default => 'commit',
5050 -values => ['commit', 'grep', 'author', 'committer', 'pickaxe']) .
5051 $cgi->sup($cgi->a({-href
=> href
(action
=>"search_help"),
5052 -title
=> "search help" },
5053 "<span style=\"padding-bottom:1em\">? </span>")) . " search:\n",
5054 $cgi->textfield(-name
=> "s", -value
=> $searchtext, -override
=> 1) . "\n" .
5055 "<span title=\"Extended regular expression\">" .
5056 $cgi->checkbox(-name
=> 'sr', -value
=> 1, -label
=> 're',
5057 -checked
=> $search_use_regexp) .
5060 $cgi->end_form() . "\n";
5063 sub git_header_html
{
5064 my $status = shift || "200 OK";
5065 my $expires = shift;
5068 my $title = get_page_title
();
5069 my $content_type = get_content_type_html
();
5070 print $cgi->header(-type
=>$content_type, -charset
=> 'utf-8',
5071 -status
=> $status, -expires
=> $expires)
5072 unless ($opts{'-no_http_header'});
5073 my $mod_perl_version = $ENV{'MOD_PERL'} ?
" $ENV{'MOD_PERL'}" : '';
5075 <?xml version="1.0" encoding="utf-8"?>
5076 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
5077 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US" lang="en-US">
5078 <!-- git web interface version $version, (C) 2005-2006, Kay Sievers <kay.sievers\@vrfy.org>, Christian Gierke -->
5079 <!-- git core binaries version $git_version -->
5081 <meta http-equiv="content-type" content="$content_type; charset=utf-8"/>
5082 <meta name="generator" content="gitweb/$version git/$git_version$mod_perl_version"/>
5083 <meta name="robots" content="index, nofollow"/>
5084 <title>$title</title>
5085 <script type="text/javascript">/* <![CDATA[ */
5086 function fixBlameLinks() {
5087 var allLinks = document.getElementsByTagName("a");
5088 for (var i = 0; i < allLinks.length; i++) {
5089 var link = allLinks.item(i);
5090 if (link.className == 'blamelink')
5091 link.href = link.href.replace("/blame/", "/blame_incremental/");
5096 # the stylesheet, favicon etc urls won't work correctly with path_info
5097 # unless we set the appropriate base URL
5098 if ($ENV{'PATH_INFO'}) {
5099 print "<base href=\"".esc_url
($base_url)."\" />\n";
5101 print_header_links
($status);
5103 if (defined $site_html_head_string) {
5104 print to_utf8
($site_html_head_string);
5108 "<body><span class=\"body\">\n";
5110 if (defined $site_header && -f
$site_header) {
5111 insert_file
($site_header);
5114 print "<div class=\"page_header\">\n";
5115 print "<span class=\"logo-container\"><span class=\"logo-default\">";
5116 if (defined $logo) {
5117 print $cgi->a({-href
=> esc_url
($logo_url),
5118 -title
=> $logo_label,
5119 -class => "logo-link"},
5120 $cgi->img({-src
=> esc_url
($logo),
5121 -width
=> 72, -height
=> 27,
5123 -class => "logo"}));
5125 print "</span></span><span class=\"banner-container\">";
5126 print_nav_breadcrumbs
(%opts);
5127 print "</span></div>\n";
5129 my $have_search = gitweb_check_feature
('search');
5130 if (defined $project && $have_search) {
5131 print_search_form
();
5135 sub compute_timed_interval
{
5136 return "<?gitweb compute_timed_interval?>" if $cache_mode_active;
5137 return tv_interval
($t0, [ gettimeofday
() ]);
5140 sub compute_commands_count
{
5141 return "<?gitweb compute_commands_count?>" if $cache_mode_active;
5142 my $s = $number_of_git_cmds == 1 ?
'' : 's';
5143 return '<span id="generating_cmd">'.
5144 $number_of_git_cmds.
5145 "</span> git command$s";
5148 sub git_footer_html
{
5149 my $feed_class = 'rss_logo';
5151 print "<div class=\"page_footer\">\n";
5152 if (defined $project) {
5153 my $descr = git_get_project_description
($project);
5154 if (defined $descr) {
5155 print "<div class=\"page_footer_text\">" . esc_html
($descr) . "</div>\n";
5158 my %href_params = get_feed_info
();
5159 if (!%href_params) {
5160 $feed_class .= ' generic';
5162 $href_params{'-title'} ||= 'log';
5164 foreach my $format (qw(RSS Atom)) {
5165 $href_params{'action'} = lc($format);
5166 print $cgi->a({-href
=> href
(%href_params),
5167 -title
=> "$href_params{'-title'} $format feed",
5168 -class => $feed_class}, $format)."\n";
5172 print $cgi->a({-href
=> href
(project
=>undef, action
=>"opml",
5173 project_filter
=> $project_filter),
5174 -class => $feed_class}, "OPML") . " ";
5175 print $cgi->a({-href
=> href
(project
=>undef, action
=>"project_index",
5176 project_filter
=> $project_filter),
5177 -class => $feed_class}, "TXT") . "\n";
5179 print "</div>\n"; # class="page_footer"
5181 if (defined $t0 && gitweb_check_feature
('timed')) {
5182 print "<div id=\"generating_info\">\n";
5183 print 'This page took '.
5184 '<span id="generating_time" class="time_span">'.
5185 compute_timed_interval
().
5188 compute_commands_count
().
5190 print "</div>\n"; # class="page_footer"
5193 if (defined $site_footer && -f
$site_footer) {
5194 insert_file
($site_footer);
5197 print qq!<script type
="text/javascript" src
="!.esc_url($javascript).qq!"></script
>\n!;
5198 if (defined $action &&
5199 $action eq 'blame_incremental') {
5200 print qq!<script type
="text/javascript">\n!.
5201 qq!startBlame
("!. href(action=>"blame_data
", -replay=>1) .qq!",\n!.
5202 qq! "!. href() .qq!");\n!.
5205 my ($jstimezone, $tz_cookie, $datetime_class) =
5206 gitweb_get_feature
('javascript-timezone');
5208 print qq!<script type
="text/javascript">\n!.
5209 qq!window
.onload
= function
() {\n!;
5210 if (gitweb_check_feature
('blame_incremental')) {
5211 print qq! fixBlameLinks
();\n!;
5213 if (gitweb_check_feature
('javascript-actions')) {
5214 print qq! fixLinks
();\n!;
5216 if ($jstimezone && $tz_cookie && $datetime_class) {
5217 print qq! var tz_cookie
= { name
: '$tz_cookie', expires
: 14, path
: '/' };\n!. # in days
5218 qq! onloadTZSetup
('$jstimezone', tz_cookie
, '$datetime_class');\n!;
5224 print "</span></body>\n" .
5228 # die_error(<http_status_code>, <error_message>[, <detailed_html_description>])
5229 # Example: die_error(404, 'Hash not found')
5230 # By convention, use the following status codes (as defined in RFC 2616):
5231 # 400: Invalid or missing CGI parameters, or
5232 # requested object exists but has wrong type.
5233 # 403: Requested feature (like "pickaxe" or "snapshot") not enabled on
5234 # this server or project.
5235 # 404: Requested object/revision/project doesn't exist.
5236 # 500: The server isn't configured properly, or
5237 # an internal error occurred (e.g. failed assertions caused by bugs), or
5238 # an unknown error occurred (e.g. the git binary died unexpectedly).
5239 # 503: The server is currently unavailable (because it is overloaded,
5240 # or down for maintenance). Generally, this is a temporary state.
5242 my $status = shift || 500;
5243 my $error = esc_html
(shift) || "Internal Server Error";
5247 my %http_responses = (
5248 400 => '400 Bad Request',
5249 403 => '403 Forbidden',
5250 404 => '404 Not Found',
5251 500 => '500 Internal Server Error',
5252 503 => '503 Service Unavailable',
5254 git_header_html
($http_responses{$status}, undef, %opts);
5256 <div class="page_body">
5261 if (defined $extra) {
5269 unless ($opts{'-error_handler'});
5272 ## ----------------------------------------------------------------------
5273 ## functions printing or outputting HTML: navigation
5275 # $content is wrapped in a span with class 'tab'
5276 # If $selected is true it also has class 'selected'
5277 # If $disabled is true it also has class 'disabled'
5278 # Whether or not a tab can be disabled and selected at the same time
5279 # is up to the caller
5280 # If $extra_classes is non-empty, it is a whitespace-separated list of
5281 # additional class names to include
5282 # Note that $content MUST already be html-escaped as needed because
5283 # it is included verbatim. And so are any extra class names.
5285 my ($content, $selected, $disabled, $extra_classes) = @_;
5286 my @classes = ("tab");
5287 push(@classes, "selected") if $selected;
5288 push(@classes, "disabled") if $disabled;
5289 push(@classes, split(' ', $extra_classes)) if $extra_classes;
5290 return "<span class=\"" . join(" ", @classes) . "\">". $content . "</span>";
5293 sub git_print_page_nav
{
5294 my ($current, $suppress, $head, $treehead, $treebase, $extra) = @_;
5295 $extra = '' if !defined $extra; # pager or formats
5296 $extra = "<span class=\"page_nav_sub\">".$extra."</span>" if $extra ne '';
5298 my @navs = qw(summary log commit commitdiff tree refs);
5300 @navs = grep { $_ ne $suppress } @navs;
5303 my %arg = map { $_ => {action
=>$_} } @navs;
5304 if (defined $head) {
5305 for (qw(commit commitdiff)) {
5306 $arg{$_}{'hash'} = $head;
5308 if ($current =~ m/^(tree | log | shortlog | commit | commitdiff | search)$/x) {
5309 $arg{'log'}{'hash'} = $head;
5313 $arg{'log'}{'action'} = 'shortlog';
5314 if ($current eq 'log') {
5315 $current = 'shortlog';
5316 } elsif ($current eq 'shortlog') {
5319 $arg{'tree'}{'hash'} = $treehead if defined $treehead;
5320 $arg{'tree'}{'hash_base'} = $treebase if defined $treebase;
5322 my @actions = gitweb_get_feature
('actions');
5323 my $escname = $project;
5324 $escname =~ s/[+]/%2B/g;
5327 'n' => $project, # project name
5328 'f' => $git_dir, # project path within filesystem
5329 'h' => $treehead || '', # current hash ('h' parameter)
5330 'b' => $treebase || '', # hash base ('hb' parameter)
5331 'e' => $escname, # project name with '+' escaped
5334 my ($label, $link, $pos) = splice(@actions,0,3);
5336 @navs = map { $_ eq $pos ?
($_, $label) : $_ } @navs;
5338 $link =~ s/%([%nfhbe])/$repl{$1}/g;
5339 $arg{$label}{'_href'} = $link;
5342 print "<div class=\"page_nav\">\n" .
5344 map { $_ eq $current ?
5346 tabspan
($cgi->a({-href
=> ($arg{$_}{_href
} ?
$arg{$_}{_href
} : href
(%{$arg{$_}}))}, "$_"))
5348 print "<br/>\n$extra<br/>\n" .
5352 # returns a submenu for the nagivation of the refs views (tags, heads,
5353 # remotes) with the current view disabled and the remotes view only
5354 # available if the feature is enabled
5355 sub format_ref_views
{
5357 my @ref_views = qw{tags heads
};
5358 push @ref_views, 'remotes' if gitweb_check_feature
('remote_heads');
5359 return join $barsep, map {
5360 $_ eq $current ? tabspan
($_, 1) :
5361 tabspan
($cgi->a({-href
=> href
(action
=>$_)}, $_))
5365 sub format_paging_nav
{
5366 my ($action, $page, $has_next_link) = @_;
5367 my $paging_nav = "<span class=\"paging_nav\">";
5370 $paging_nav .= tabspan
(
5371 $cgi->a({-href
=> href
(-replay
=>1, page
=>undef)}, "first")) .
5373 $cgi->a({-href
=> href
(-replay
=>1, page
=>$page-1),
5374 -accesskey
=> "p", -title
=> "Alt-p"}, "prev"));
5376 $paging_nav .= tabspan
("first", 1).${mdotsep
}.tabspan
("prev", 0, 1);
5379 if ($has_next_link) {
5380 $paging_nav .= $mdotsep . tabspan
(
5381 $cgi->a({-href
=> href
(-replay
=>1, page
=>$page+1),
5382 -accesskey
=> "n", -title
=> "Alt-n"}, "next"));
5384 $paging_nav .= ${mdotsep
}.tabspan
("next", 0, 1);
5387 return $paging_nav."</span>";
5390 sub format_log_nav
{
5391 my ($action, $page, $has_next_link, $extra) = @_;
5393 defined $extra or $extra = '';
5394 $extra eq '' or $extra .= $barsep;
5396 if ($action eq 'shortlog') {
5397 $paging_nav .= tabspan
('shortlog', 1);
5399 $paging_nav .= tabspan
($cgi->a({-href
=> href
(action
=>'shortlog', -replay
=>1)}, 'shortlog'));
5401 $paging_nav .= $barsep;
5402 if ($action eq 'log') {
5403 $paging_nav .= tabspan
('fulllog', 1);
5405 $paging_nav .= tabspan
($cgi->a({-href
=> href
(action
=>'log', -replay
=>1)}, 'fulllog'));
5408 $paging_nav .= $barsep . $extra . format_paging_nav
($action, $page, $has_next_link);
5412 ## ......................................................................
5413 ## functions printing or outputting HTML: div
5415 sub git_print_header_div
{
5416 my ($action, $title, $hash, $hash_base, $extra) = @_;
5418 defined $extra or $extra = '';
5420 $args{'action'} = $action;
5421 $args{'hash'} = $hash if $hash;
5422 $args{'hash_base'} = $hash_base if $hash_base;
5424 my $link1 = $cgi->a({-href
=> href
(%args), -class => "title"},
5425 $title ?
$title : $action);
5426 my $link2 = $cgi->a({-href
=> href
(%args), -class => "cover"}, "");
5427 print "<div class=\"header\">\n" . '<span class="title">' .
5428 $link1 . $extra . $link2 . '</span>' . "\n</div>\n";
5431 sub format_repo_url
{
5432 my ($name, $url) = @_;
5433 return "<tr class=\"metadata_url\"><td>$name</td><td>$url</td></tr>\n";
5436 # Group output by placing it in a DIV element and adding a header.
5437 # Options for start_div() can be provided by passing a hash reference as the
5438 # first parameter to the function.
5439 # Options to git_print_header_div() can be provided by passing an array
5440 # reference. This must follow the options to start_div if they are present.
5441 # The content can be a scalar, which is output as-is, a scalar reference, which
5442 # is output after html escaping, an IO handle passed either as *handle or
5443 # *handle{IO}, or a function reference. In the latter case all following
5444 # parameters will be taken as argument to the content function call.
5445 sub git_print_section
{
5446 my ($div_args, $header_args, $content);
5448 if (ref($arg) eq 'HASH') {
5452 if (ref($arg) eq 'ARRAY') {
5453 $header_args = $arg;
5458 print $cgi->start_div($div_args);
5459 git_print_header_div
(@
$header_args);
5461 if (ref($content) eq 'CODE') {
5463 } elsif (ref($content) eq 'SCALAR') {
5464 print esc_html
($$content);
5465 } elsif (ref($content) eq 'GLOB' or ref($content) eq 'IO::Handle') {
5466 while (<$content>) {
5469 } elsif (!ref($content) && defined($content)) {
5473 print $cgi->end_div;
5476 sub format_timestamp_html
{
5478 my $useatnight = shift;
5479 defined($useatnight) or $useatnight = 1;
5480 my $strtime = $date->{'rfc2822'};
5482 my (undef, undef, $datetime_class) =
5483 gitweb_get_feature
('javascript-timezone');
5484 if ($datetime_class) {
5485 $strtime = qq!<span
class="$datetime_class">$strtime</span
>!;
5488 my $localtime_format = '(%d %02d:%02d %s)';
5489 if ($useatnight && $date->{'hour_local'} < 6) {
5490 $localtime_format = '(%d <span class="atnight">%02d:%02d</span> %s)';
5493 sprintf($localtime_format, $date->{'mday_local'},
5494 $date->{'hour_local'}, $date->{'minute_local'}, $date->{'tz_local'});
5499 sub format_lastrefresh_row
{
5500 return "<?gitweb format_lastrefresh_row?>" if $cache_mode_active;
5501 my %rd = parse_file_date
('.last_refresh');
5502 if (defined $rd{'rfc2822'}) {
5503 return "<tr id=\"metadata_lrefresh\"><td>last refresh</td>" .
5504 "<td>".format_timestamp_html
(\
%rd,0)."</td></tr>";
5509 # Outputs the author name and date in long form
5510 sub git_print_authorship
{
5513 my $tag = $opts{-tag
} || 'div';
5514 my $author = $co->{'author_name'};
5516 my %ad = parse_date
($co->{'author_epoch'}, $co->{'author_tz'});
5517 print "<$tag class=\"author_date\">" .
5518 format_search_author
($author, "author", esc_html
($author)) .
5519 " [".format_timestamp_html
(\
%ad)."]".
5520 git_get_avatar
($co->{'author_email'}, -pad_before
=> 1) .
5524 # Outputs table rows containing the full author or committer information,
5525 # in the format expected for 'commit' view (& similar).
5526 # Parameters are a commit hash reference, followed by the list of people
5527 # to output information for. If the list is empty it defaults to both
5528 # author and committer.
5529 sub git_print_authorship_rows
{
5531 # too bad we can't use @people = @_ || ('author', 'committer')
5533 @people = ('author', 'committer') unless @people;
5534 foreach my $who (@people) {
5535 my %wd = parse_date
($co->{"${who}_epoch"}, $co->{"${who}_tz"});
5536 print "<tr><td>$who</td><td>" .
5537 format_search_author
($co->{"${who}_name"}, $who,
5538 esc_html
($co->{"${who}_name"})) . " " .
5539 format_search_author
($co->{"${who}_email"}, $who,
5540 esc_html
("<" . $co->{"${who}_email"} . ">")) .
5541 "</td><td rowspan=\"2\">" .
5542 git_get_avatar
($co->{"${who}_email"}, -size
=> 'double') .
5546 format_timestamp_html
(\
%wd) .
5552 sub git_print_page_path
{
5558 print "<div class=\"page_path\">";
5559 print $cgi->a({-href
=> href
(action
=>"tree", hash_base
=>$hb),
5560 -title
=> 'tree root'}, to_utf8
("[$project]"));
5562 if (defined $name) {
5563 my @dirname = split '/', $name;
5564 my $basename = pop @dirname;
5567 foreach my $dir (@dirname) {
5568 $fullname .= ($fullname ?
'/' : '') . $dir;
5569 print $cgi->a({-href
=> href
(action
=>"tree", file_name
=>$fullname,
5571 -title
=> $fullname}, esc_path
($dir));
5574 if (defined $type && $type eq 'blob') {
5575 print $cgi->a({-href
=> href
(action
=>"blob_plain", file_name
=>$file_name,
5577 -title
=> $name}, esc_path
($basename));
5578 } elsif (defined $type && $type eq 'tree') {
5579 print $cgi->a({-href
=> href
(action
=>"tree", file_name
=>$file_name,
5581 -title
=> $name}, esc_path
($basename));
5584 print esc_path
($basename);
5587 print "<br/></div>\n";
5594 if ($opts{'-remove_title'}) {
5595 # remove title, i.e. first line of log
5598 # remove leading empty lines
5599 while (defined $log->[0] && $log->[0] eq "") {
5604 my $skip_blank_line = 0;
5605 foreach my $line (@
$log) {
5606 if ($line =~ m/^\s*([A-Z][-A-Za-z]*-[Bb]y|C[Cc]): /) {
5607 if (! $opts{'-remove_signoff'}) {
5608 print "<span class=\"signoff\">" . esc_html
($line) . "</span><br/>\n";
5609 $skip_blank_line = 1;
5614 if ($line =~ m
,\s
*([a
-z
]*link): (https?
://\S
+),i
) {
5615 if (! $opts{'-remove_signoff'}) {
5616 print "<span class=\"signoff\">" . esc_html
($1) . ": " .
5617 "<a href=\"" . esc_html
($2) . "\">" . esc_html
($2) . "</a>" .
5619 $skip_blank_line = 1;
5624 # print only one empty line
5625 # do not print empty line after signoff
5627 next if ($skip_blank_line);
5628 $skip_blank_line = 1;
5630 $skip_blank_line = 0;
5633 print format_log_line_html
($line) . "<br/>\n";
5636 if ($opts{'-final_empty_line'}) {
5637 # end with single empty line
5638 print "<br/>\n" unless $skip_blank_line;
5642 # return link target (what link points to)
5643 sub git_get_link_target
{
5648 defined(my $fd = git_cmd_pipe
"cat-file", "blob", $hash)
5652 $link_target = to_utf8
(scalar <$fd>);
5657 return $link_target;
5660 # given link target, and the directory (basedir) the link is in,
5661 # return target of link relative to top directory (top tree);
5662 # return undef if it is not possible (including absolute links).
5663 sub normalize_link_target
{
5664 my ($link_target, $basedir) = @_;
5666 # absolute symlinks (beginning with '/') cannot be normalized
5667 return if (substr($link_target, 0, 1) eq '/');
5669 # normalize link target to path from top (root) tree (dir)
5672 $path = $basedir . '/' . $link_target;
5674 # we are in top (root) tree (dir)
5675 $path = $link_target;
5678 # remove //, /./, and /../
5680 foreach my $part (split('/', $path)) {
5681 # discard '.' and ''
5682 next if (!$part || $part eq '.');
5684 if ($part eq '..') {
5688 # link leads outside repository (outside top dir)
5692 push @path_parts, $part;
5695 $path = join('/', @path_parts);
5700 # print tree entry (row of git_tree), but without encompassing <tr> element
5701 sub git_print_tree_entry
{
5702 my ($t, $basedir, $hash_base, $have_blame) = @_;
5705 $base_key{'hash_base'} = $hash_base if defined $hash_base;
5707 # The format of a table row is: mode list link. Where mode is
5708 # the mode of the entry, list is the name of the entry, an href,
5709 # and link is the action links of the entry.
5711 print "<td class=\"mode\">" . mode_str
($t->{'mode'}) . "</td>\n";
5712 if (exists $t->{'size'}) {
5713 print "<td class=\"size\">$t->{'size'}</td>\n";
5715 if ($t->{'type'} eq "blob") {
5716 print "<td class=\"list\">" .
5717 $cgi->a({-href
=> href
(action
=>"blob", hash
=>$t->{'hash'},
5718 file_name
=>"$basedir$t->{'name'}", %base_key),
5719 -class => "list"}, esc_path
($t->{'name'}));
5720 if (S_ISLNK
(oct $t->{'mode'})) {
5721 my $link_target = git_get_link_target
($t->{'hash'});
5723 my $norm_target = normalize_link_target
($link_target, $basedir);
5724 if (defined $norm_target) {
5726 $cgi->a({-href
=> href
(action
=>"object", hash_base
=>$hash_base,
5727 file_name
=>$norm_target),
5728 -title
=> $norm_target}, esc_path
($link_target));
5730 print " -> " . esc_path
($link_target);
5735 print "<td class=\"link\">";
5736 print $cgi->a({-href
=> href
(action
=>"blob", hash
=>$t->{'hash'},
5737 file_name
=>"$basedir$t->{'name'}", %base_key)},
5741 $cgi->a({-href
=> href
(action
=>"blame", hash
=>$t->{'hash'},
5742 file_name
=>"$basedir$t->{'name'}", %base_key),
5743 -class => "blamelink"},
5746 if (defined $hash_base) {
5748 $cgi->a({-href
=> href
(action
=>"history", hash_base
=>$hash_base,
5749 hash
=>$t->{'hash'}, file_name
=>"$basedir$t->{'name'}")},
5753 $cgi->a({-href
=> href
(action
=>"blob_plain", hash_base
=>$hash_base,
5754 file_name
=>"$basedir$t->{'name'}")},
5758 } elsif ($t->{'type'} eq "tree") {
5759 print "<td class=\"list\">";
5760 print $cgi->a({-href
=> href
(action
=>"tree", hash
=>$t->{'hash'},
5761 file_name
=>"$basedir$t->{'name'}",
5763 esc_path
($t->{'name'}));
5765 print "<td class=\"link\">";
5766 print $cgi->a({-href
=> href
(action
=>"tree", hash
=>$t->{'hash'},
5767 file_name
=>"$basedir$t->{'name'}",
5770 if (defined $hash_base) {
5772 $cgi->a({-href
=> href
(action
=>"history", hash_base
=>$hash_base,
5773 file_name
=>"$basedir$t->{'name'}")},
5778 # unknown object: we can only present history for it
5779 # (this includes 'commit' object, i.e. submodule support)
5780 print "<td class=\"list\">" .
5781 esc_path
($t->{'name'}) .
5783 print "<td class=\"link\">";
5784 if (defined $hash_base) {
5785 print $cgi->a({-href
=> href
(action
=>"history",
5786 hash_base
=>$hash_base,
5787 file_name
=>"$basedir$t->{'name'}")},
5794 ## ......................................................................
5795 ## functions printing large fragments of HTML
5797 # get pre-image filenames for merge (combined) diff
5798 sub fill_from_file_info
{
5799 my ($diff, @parents) = @_;
5801 $diff->{'from_file'} = [ ];
5802 $diff->{'from_file'}[$diff->{'nparents'} - 1] = undef;
5803 for (my $i = 0; $i < $diff->{'nparents'}; $i++) {
5804 if ($diff->{'status'}[$i] eq 'R' ||
5805 $diff->{'status'}[$i] eq 'C') {
5806 $diff->{'from_file'}[$i] =
5807 git_get_path_by_hash
($parents[$i], $diff->{'from_id'}[$i]);
5814 # is current raw difftree line of file deletion
5816 my $diffinfo = shift;
5818 return $diffinfo->{'to_id'} eq ('0' x
40);
5821 # does patch correspond to [previous] difftree raw line
5822 # $diffinfo - hashref of parsed raw diff format
5823 # $patchinfo - hashref of parsed patch diff format
5824 # (the same keys as in $diffinfo)
5825 sub is_patch_split
{
5826 my ($diffinfo, $patchinfo) = @_;
5828 return defined $diffinfo && defined $patchinfo
5829 && $diffinfo->{'to_file'} eq $patchinfo->{'to_file'};
5833 sub git_difftree_body
{
5834 my ($difftree, $hash, @parents) = @_;
5835 my ($parent) = $parents[0];
5836 my $have_blame = gitweb_check_feature
('blame');
5837 print "<div class=\"list_head\">\n";
5838 if ($#{$difftree} > 10) {
5839 print(($#{$difftree} + 1) . " files changed:\n");
5843 print "<table class=\"" .
5844 (@parents > 1 ?
"combined " : "") .
5847 # header only for combined diff in 'commitdiff' view
5848 my $has_header = @
$difftree && @parents > 1 && $action eq 'commitdiff';
5851 print "<thead><tr>\n" .
5852 "<th></th><th></th>\n"; # filename, patchN link
5853 for (my $i = 0; $i < @parents; $i++) {
5854 my $par = $parents[$i];
5856 $cgi->a({-href
=> href
(action
=>"commitdiff",
5857 hash
=>$hash, hash_parent
=>$par),
5858 -title
=> 'commitdiff to parent number ' .
5859 ($i+1) . ': ' . substr($par,0,7)},
5863 print "</tr></thead>\n<tbody>\n";
5868 foreach my $line (@
{$difftree}) {
5869 my $diff = parsed_difftree_line
($line);
5872 print "<tr class=\"dark\">\n";
5874 print "<tr class=\"light\">\n";
5878 if (exists $diff->{'nparents'}) { # combined diff
5880 fill_from_file_info
($diff, @parents)
5881 unless exists $diff->{'from_file'};
5883 if (!is_deleted
($diff)) {
5884 # file exists in the result (child) commit
5886 $cgi->a({-href
=> href
(action
=>"blob", hash
=>$diff->{'to_id'},
5887 file_name
=>$diff->{'to_file'},
5889 -class => "list"}, esc_path
($diff->{'to_file'})) .
5893 esc_path
($diff->{'to_file'}) .
5897 if ($action eq 'commitdiff') {
5900 print "<td class=\"link\">" .
5901 $cgi->a({-href
=> href
(-anchor
=>"patch$patchno")},
5907 my $has_history = 0;
5908 my $not_deleted = 0;
5909 for (my $i = 0; $i < $diff->{'nparents'}; $i++) {
5910 my $hash_parent = $parents[$i];
5911 my $from_hash = $diff->{'from_id'}[$i];
5912 my $from_path = $diff->{'from_file'}[$i];
5913 my $status = $diff->{'status'}[$i];
5915 $has_history ||= ($status ne 'A');
5916 $not_deleted ||= ($status ne 'D');
5918 if ($status eq 'A') {
5919 print "<td class=\"link\" align=\"right\">$barsep</td>\n";
5920 } elsif ($status eq 'D') {
5921 print "<td class=\"link\">" .
5922 $cgi->a({-href
=> href
(action
=>"blob",
5925 file_name
=>$from_path)},
5929 if ($diff->{'to_id'} eq $from_hash) {
5930 print "<td class=\"link nochange\">";
5932 print "<td class=\"link\">";
5934 print $cgi->a({-href
=> href
(action
=>"blobdiff",
5935 hash
=>$diff->{'to_id'},
5936 hash_parent
=>$from_hash,
5938 hash_parent_base
=>$hash_parent,
5939 file_name
=>$diff->{'to_file'},
5940 file_parent
=>$from_path)},
5946 print "<td class=\"link\">";
5948 print $cgi->a({-href
=> href
(action
=>"blob",
5949 hash
=>$diff->{'to_id'},
5950 file_name
=>$diff->{'to_file'},
5953 print $barsep if ($has_history);
5956 print $cgi->a({-href
=> href
(action
=>"history",
5957 file_name
=>$diff->{'to_file'},
5964 next; # instead of 'else' clause, to avoid extra indent
5966 # else ordinary diff
5968 my ($to_mode_oct, $to_mode_str, $to_file_type);
5969 my ($from_mode_oct, $from_mode_str, $from_file_type);
5970 if ($diff->{'to_mode'} ne ('0' x
6)) {
5971 $to_mode_oct = oct $diff->{'to_mode'};
5972 if (S_ISREG
($to_mode_oct)) { # only for regular file
5973 $to_mode_str = sprintf("%04o", $to_mode_oct & 0777); # permission bits
5975 $to_file_type = file_type
($diff->{'to_mode'});
5977 if ($diff->{'from_mode'} ne ('0' x
6)) {
5978 $from_mode_oct = oct $diff->{'from_mode'};
5979 if (S_ISREG
($from_mode_oct)) { # only for regular file
5980 $from_mode_str = sprintf("%04o", $from_mode_oct & 0777); # permission bits
5982 $from_file_type = file_type
($diff->{'from_mode'});
5985 if ($diff->{'status'} eq "A") { # created
5986 my $mode_chng = "<span class=\"file_status new\">[new $to_file_type";
5987 $mode_chng .= " with mode: $to_mode_str" if $to_mode_str;
5988 $mode_chng .= "]</span>";
5990 print $cgi->a({-href
=> href
(action
=>"blob", hash
=>$diff->{'to_id'},
5991 hash_base
=>$hash, file_name
=>$diff->{'file'}),
5992 -class => "list"}, esc_path
($diff->{'file'}));
5994 print "<td>$mode_chng</td>\n";
5995 print "<td class=\"link\">";
5996 if ($action eq 'commitdiff') {
5999 print $cgi->a({-href
=> href
(-anchor
=>"patch$patchno")},
6003 print $cgi->a({-href
=> href
(action
=>"blob", hash
=>$diff->{'to_id'},
6004 hash_base
=>$hash, file_name
=>$diff->{'file'})},
6008 } elsif ($diff->{'status'} eq "D") { # deleted
6009 my $mode_chng = "<span class=\"file_status deleted\">[deleted $from_file_type]</span>";
6011 print $cgi->a({-href
=> href
(action
=>"blob", hash
=>$diff->{'from_id'},
6012 hash_base
=>$parent, file_name
=>$diff->{'file'}),
6013 -class => "list"}, esc_path
($diff->{'file'}));
6015 print "<td>$mode_chng</td>\n";
6016 print "<td class=\"link\">";
6017 if ($action eq 'commitdiff') {
6020 print $cgi->a({-href
=> href
(-anchor
=>"patch$patchno")},
6024 print $cgi->a({-href
=> href
(action
=>"blob", hash
=>$diff->{'from_id'},
6025 hash_base
=>$parent, file_name
=>$diff->{'file'})},
6028 print $cgi->a({-href
=> href
(action
=>"blame", hash_base
=>$parent,
6029 file_name
=>$diff->{'file'}),
6030 -class => "blamelink"},
6033 print $cgi->a({-href
=> href
(action
=>"history", hash_base
=>$parent,
6034 file_name
=>$diff->{'file'})},
6038 } elsif ($diff->{'status'} eq "M" || $diff->{'status'} eq "T") { # modified, or type changed
6039 my $mode_chnge = "";
6040 if ($diff->{'from_mode'} != $diff->{'to_mode'}) {
6041 $mode_chnge = "<span class=\"file_status mode_chnge\">[changed";
6042 if ($from_file_type ne $to_file_type) {
6043 $mode_chnge .= " from $from_file_type to $to_file_type";
6045 if (($from_mode_oct & 0777) != ($to_mode_oct & 0777)) {
6046 if ($from_mode_str && $to_mode_str) {
6047 $mode_chnge .= " mode: $from_mode_str->$to_mode_str";
6048 } elsif ($to_mode_str) {
6049 $mode_chnge .= " mode: $to_mode_str";
6052 $mode_chnge .= "]</span>\n";
6055 print $cgi->a({-href
=> href
(action
=>"blob", hash
=>$diff->{'to_id'},
6056 hash_base
=>$hash, file_name
=>$diff->{'file'}),
6057 -class => "list"}, esc_path
($diff->{'file'}));
6059 print "<td>$mode_chnge</td>\n";
6060 print "<td class=\"link\">";
6061 if ($action eq 'commitdiff') {
6064 print $cgi->a({-href
=> href
(-anchor
=>"patch$patchno")},
6067 } elsif ($diff->{'to_id'} ne $diff->{'from_id'}) {
6068 # "commit" view and modified file (not onlu mode changed)
6069 print $cgi->a({-href
=> href
(action
=>"blobdiff",
6070 hash
=>$diff->{'to_id'}, hash_parent
=>$diff->{'from_id'},
6071 hash_base
=>$hash, hash_parent_base
=>$parent,
6072 file_name
=>$diff->{'file'})},
6076 print $cgi->a({-href
=> href
(action
=>"blob", hash
=>$diff->{'to_id'},
6077 hash_base
=>$hash, file_name
=>$diff->{'file'})},
6080 print $cgi->a({-href
=> href
(action
=>"blame", hash_base
=>$hash,
6081 file_name
=>$diff->{'file'}),
6082 -class => "blamelink"},
6085 print $cgi->a({-href
=> href
(action
=>"history", hash_base
=>$hash,
6086 file_name
=>$diff->{'file'})},
6090 } elsif ($diff->{'status'} eq "R" || $diff->{'status'} eq "C") { # renamed or copied
6091 my %status_name = ('R' => 'moved', 'C' => 'copied');
6092 my $nstatus = $status_name{$diff->{'status'}};
6094 if ($diff->{'from_mode'} != $diff->{'to_mode'}) {
6095 # mode also for directories, so we cannot use $to_mode_str
6096 $mode_chng = sprintf(", mode: %04o", $to_mode_oct & 0777);
6099 $cgi->a({-href
=> href
(action
=>"blob", hash_base
=>$hash,
6100 hash
=>$diff->{'to_id'}, file_name
=>$diff->{'to_file'}),
6101 -class => "list"}, esc_path
($diff->{'to_file'})) . "</td>\n" .
6102 "<td><span class=\"file_status $nstatus\">[$nstatus from " .
6103 $cgi->a({-href
=> href
(action
=>"blob", hash_base
=>$parent,
6104 hash
=>$diff->{'from_id'}, file_name
=>$diff->{'from_file'}),
6105 -class => "list"}, esc_path
($diff->{'from_file'})) .
6106 " with " . (int $diff->{'similarity'}) . "% similarity$mode_chng]</span></td>\n" .
6107 "<td class=\"link\">";
6108 if ($action eq 'commitdiff') {
6111 print $cgi->a({-href
=> href
(-anchor
=>"patch$patchno")},
6114 } elsif ($diff->{'to_id'} ne $diff->{'from_id'}) {
6115 # "commit" view and modified file (not only pure rename or copy)
6116 print $cgi->a({-href
=> href
(action
=>"blobdiff",
6117 hash
=>$diff->{'to_id'}, hash_parent
=>$diff->{'from_id'},
6118 hash_base
=>$hash, hash_parent_base
=>$parent,
6119 file_name
=>$diff->{'to_file'}, file_parent
=>$diff->{'from_file'})},
6123 print $cgi->a({-href
=> href
(action
=>"blob", hash
=>$diff->{'to_id'},
6124 hash_base
=>$parent, file_name
=>$diff->{'to_file'})},
6127 print $cgi->a({-href
=> href
(action
=>"blame", hash_base
=>$hash,
6128 file_name
=>$diff->{'to_file'}),
6129 -class => "blamelink"},
6132 print $cgi->a({-href
=> href
(action
=>"history", hash_base
=>$hash,
6133 file_name
=>$diff->{'to_file'})},
6137 } # we should not encounter Unmerged (U) or Unknown (X) status
6140 print "</tbody>" if $has_header;
6144 # Print context lines and then rem/add lines in a side-by-side manner.
6145 sub print_sidebyside_diff_lines
{
6146 my ($ctx, $rem, $add) = @_;
6148 # print context block before add/rem block
6151 '<div class="chunk_block ctx">',
6152 '<div class="old">',
6155 '<div class="new">',
6164 '<div class="chunk_block rem">',
6165 '<div class="old">',
6172 '<div class="chunk_block add">',
6173 '<div class="new">',
6179 '<div class="chunk_block chg">',
6180 '<div class="old">',
6183 '<div class="new">',
6190 # Print context lines and then rem/add lines in inline manner.
6191 sub print_inline_diff_lines
{
6192 my ($ctx, $rem, $add) = @_;
6194 print @
$ctx, @
$rem, @
$add;
6197 # Format removed and added line, mark changed part and HTML-format them.
6198 # Implementation is based on contrib/diff-highlight
6199 sub format_rem_add_lines_pair
{
6200 my ($rem, $add, $num_parents) = @_;
6202 # We need to untabify lines before split()'ing them;
6203 # otherwise offsets would be invalid.
6206 $rem = untabify
($rem);
6207 $add = untabify
($add);
6209 my @rem = split(//, $rem);
6210 my @add = split(//, $add);
6211 my ($esc_rem, $esc_add);
6212 # Ignore leading +/- characters for each parent.
6213 my ($prefix_len, $suffix_len) = ($num_parents, 0);
6214 my ($prefix_has_nonspace, $suffix_has_nonspace);
6216 my $shorter = (@rem < @add) ?
@rem : @add;
6217 while ($prefix_len < $shorter) {
6218 last if ($rem[$prefix_len] ne $add[$prefix_len]);
6220 $prefix_has_nonspace = 1 if ($rem[$prefix_len] !~ /\s/);
6224 while ($prefix_len + $suffix_len < $shorter) {
6225 last if ($rem[-1 - $suffix_len] ne $add[-1 - $suffix_len]);
6227 $suffix_has_nonspace = 1 if ($rem[-1 - $suffix_len] !~ /\s/);
6231 # Mark lines that are different from each other, but have some common
6232 # part that isn't whitespace. If lines are completely different, don't
6233 # mark them because that would make output unreadable, especially if
6234 # diff consists of multiple lines.
6235 if ($prefix_has_nonspace || $suffix_has_nonspace) {
6236 $esc_rem = esc_html_hl_regions
($rem, 'marked',
6237 [$prefix_len, @rem - $suffix_len], -nbsp
=>1);
6238 $esc_add = esc_html_hl_regions
($add, 'marked',
6239 [$prefix_len, @add - $suffix_len], -nbsp
=>1);
6241 $esc_rem = esc_html
($rem, -nbsp
=>1);
6242 $esc_add = esc_html
($add, -nbsp
=>1);
6245 return format_diff_line
(\
$esc_rem, 'rem'),
6246 format_diff_line
(\
$esc_add, 'add');
6249 # HTML-format diff context, removed and added lines.
6250 sub format_ctx_rem_add_lines
{
6251 my ($ctx, $rem, $add, $num_parents) = @_;
6252 my (@new_ctx, @new_rem, @new_add);
6253 my $can_highlight = 0;
6254 my $is_combined = ($num_parents > 1);
6256 # Highlight if every removed line has a corresponding added line.
6257 if (@
$add > 0 && @
$add == @
$rem) {
6260 # Highlight lines in combined diff only if the chunk contains
6261 # diff between the same version, e.g.
6268 # Otherwise the highlightling would be confusing.
6270 for (my $i = 0; $i < @
$add; $i++) {
6271 my $prefix_rem = substr($rem->[$i], 0, $num_parents);
6272 my $prefix_add = substr($add->[$i], 0, $num_parents);
6274 $prefix_rem =~ s/-/+/g;
6276 if ($prefix_rem ne $prefix_add) {
6284 if ($can_highlight) {
6285 for (my $i = 0; $i < @
$add; $i++) {
6286 my ($line_rem, $line_add) = format_rem_add_lines_pair
(
6287 $rem->[$i], $add->[$i], $num_parents);
6288 push @new_rem, $line_rem;
6289 push @new_add, $line_add;
6292 @new_rem = map { format_diff_line
($_, 'rem') } @
$rem;
6293 @new_add = map { format_diff_line
($_, 'add') } @
$add;
6296 @new_ctx = map { format_diff_line
($_, 'ctx') } @
$ctx;
6298 return (\
@new_ctx, \
@new_rem, \
@new_add);
6301 # Print context lines and then rem/add lines.
6302 sub print_diff_lines
{
6303 my ($ctx, $rem, $add, $diff_style, $num_parents) = @_;
6304 my $is_combined = $num_parents > 1;
6306 ($ctx, $rem, $add) = format_ctx_rem_add_lines
($ctx, $rem, $add,
6309 if ($diff_style eq 'sidebyside' && !$is_combined) {
6310 print_sidebyside_diff_lines
($ctx, $rem, $add);
6312 # default 'inline' style and unknown styles
6313 print_inline_diff_lines
($ctx, $rem, $add);
6317 sub print_diff_chunk
{
6318 my ($diff_style, $num_parents, $from, $to, @chunk) = @_;
6319 my (@ctx, @rem, @add);
6321 # The class of the previous line.
6322 my $prev_class = '';
6324 return unless @chunk;
6326 # incomplete last line might be among removed or added lines,
6327 # or both, or among context lines: find which
6328 for (my $i = 1; $i < @chunk; $i++) {
6329 if ($chunk[$i][0] eq 'incomplete') {
6330 $chunk[$i][0] = $chunk[$i-1][0];
6335 push @chunk, ["", ""];
6337 foreach my $line_info (@chunk) {
6338 my ($class, $line) = @
$line_info;
6340 # print chunk headers
6341 if ($class && $class eq 'chunk_header') {
6342 print format_diff_line
($line, $class, $from, $to);
6346 ## print from accumulator when have some add/rem lines or end
6347 # of chunk (flush context lines), or when have add and rem
6348 # lines and new block is reached (otherwise add/rem lines could
6350 if (!$class || ((@rem || @add) && $class eq 'ctx') ||
6351 (@rem && @add && $class ne $prev_class)) {
6352 print_diff_lines
(\
@ctx, \
@rem, \
@add,
6353 $diff_style, $num_parents);
6354 @ctx = @rem = @add = ();
6357 ## adding lines to accumulator
6360 # rem, add or change
6361 if ($class eq 'rem') {
6363 } elsif ($class eq 'add') {
6367 if ($class eq 'ctx') {
6371 $prev_class = $class;
6375 sub git_patchset_body
{
6376 my ($fd, $diff_style, $difftree, $hash, @hash_parents) = @_;
6377 my ($hash_parent) = $hash_parents[0];
6379 my $is_combined = (@hash_parents > 1);
6381 my $patch_number = 0;
6386 my @chunk; # for side-by-side diff
6388 print "<div class=\"patchset\">\n";
6390 # skip to first patch
6391 while ($patch_line = to_utf8
(scalar <$fd>)) {
6394 last if ($patch_line =~ m/^diff /);
6398 while ($patch_line) {
6400 # parse "git diff" header line
6401 if ($patch_line =~ m/^diff --git (\"(?:[^\\\"]*(?:\\.[^\\\"]*)*)\"|[^ "]*) (.*)$/) {
6402 # $1 is from_name, which we do not use
6403 $to_name = unquote
($2);
6404 $to_name =~ s!^b/!!;
6405 } elsif ($patch_line =~ m/^diff --(cc|combined) ("?.*"?)$/) {
6406 # $1 is 'cc' or 'combined', which we do not use
6407 $to_name = unquote
($2);
6412 # check if current patch belong to current raw line
6413 # and parse raw git-diff line if needed
6414 if (is_patch_split
($diffinfo, { 'to_file' => $to_name })) {
6415 # this is continuation of a split patch
6416 print "<div class=\"patch cont\">\n";
6418 # advance raw git-diff output if needed
6419 $patch_idx++ if defined $diffinfo;
6421 # read and prepare patch information
6422 $diffinfo = parsed_difftree_line
($difftree->[$patch_idx]);
6424 # compact combined diff output can have some patches skipped
6425 # find which patch (using pathname of result) we are at now;
6427 while ($to_name ne $diffinfo->{'to_file'}) {
6428 print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n" .
6429 format_diff_cc_simplified
($diffinfo, @hash_parents) .
6430 "</div>\n"; # class="patch"
6435 last if $patch_idx > $#$difftree;
6436 $diffinfo = parsed_difftree_line
($difftree->[$patch_idx]);
6440 # modifies %from, %to hashes
6441 parse_from_to_diffinfo
($diffinfo, \
%from, \
%to, @hash_parents);
6443 # this is first patch for raw difftree line with $patch_idx index
6444 # we index @$difftree array from 0, but number patches from 1
6445 print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n";
6449 #assert($patch_line =~ m/^diff /) if DEBUG;
6450 #assert($patch_line !~ m!$/$!) if DEBUG; # is chomp-ed
6452 # print "git diff" header
6453 print format_git_diff_header_line
($patch_line, $diffinfo,
6456 # print extended diff header
6457 print "<div class=\"diff extended_header\">\n";
6459 while ($patch_line = to_utf8
(scalar<$fd>)) {
6462 last EXTENDED_HEADER
if ($patch_line =~ m/^--- |^diff /);
6464 print format_extended_diff_header_line
($patch_line, $diffinfo,
6467 print "</div>\n"; # class="diff extended_header"
6469 # from-file/to-file diff header
6470 if (! $patch_line) {
6471 print "</div>\n"; # class="patch"
6474 next PATCH
if ($patch_line =~ m/^diff /);
6475 #assert($patch_line =~ m/^---/) if DEBUG;
6477 my $last_patch_line = $patch_line;
6478 $patch_line = to_utf8
(scalar <$fd>);
6480 #assert($patch_line =~ m/^\+\+\+/) if DEBUG;
6482 print format_diff_from_to_header
($last_patch_line, $patch_line,
6483 $diffinfo, \
%from, \
%to,
6488 while ($patch_line = to_utf8
(scalar <$fd>)) {
6491 next PATCH
if ($patch_line =~ m/^diff /);
6493 my $class = diff_line_class
($patch_line, \
%from, \
%to);
6495 if ($class eq 'chunk_header') {
6496 print_diff_chunk
($diff_style, scalar @hash_parents, \
%from, \
%to, @chunk);
6500 push @chunk, [ $class, $patch_line ];
6505 print_diff_chunk
($diff_style, scalar @hash_parents, \
%from, \
%to, @chunk);
6508 print "</div>\n"; # class="patch"
6511 # for compact combined (--cc) format, with chunk and patch simplification
6512 # the patchset might be empty, but there might be unprocessed raw lines
6513 for (++$patch_idx if $patch_number > 0;
6514 $patch_idx < @
$difftree;
6516 # read and prepare patch information
6517 $diffinfo = parsed_difftree_line
($difftree->[$patch_idx]);
6519 # generate anchor for "patch" links in difftree / whatchanged part
6520 print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n" .
6521 format_diff_cc_simplified
($diffinfo, @hash_parents) .
6522 "</div>\n"; # class="patch"
6527 if ($patch_number == 0) {
6528 if (@hash_parents > 1) {
6529 print "<div class=\"diff nodifferences\">Trivial merge</div>\n";
6531 print "<div class=\"diff nodifferences\">No differences found</div>\n";
6535 print "</div>\n"; # class="patchset"
6538 # . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
6540 sub git_project_search_form
{
6541 my ($searchtext, $search_use_regexp) = @_;
6544 if ($project_filter) {
6545 $limit = " in '$project_filter'";
6548 print "<div class=\"projsearch\">\n";
6549 print $cgi->start_form(-method
=> 'get', -action
=> $my_uri) .
6550 $cgi->hidden(-name
=> 'a', -value
=> 'project_list') . "\n";
6551 print $cgi->hidden(-name
=> 'pf', -value
=> $project_filter). "\n"
6552 if (defined $project_filter);
6553 print $cgi->textfield(-name
=> 's', -value
=> $searchtext,
6554 -title
=> "Search project by name and description$limit",
6555 -size
=> 60) . "\n" .
6556 "<span title=\"Extended regular expression\">" .
6557 $cgi->checkbox(-name
=> 'sr', -value
=> 1, -label
=> 're',
6558 -checked
=> $search_use_regexp) .
6560 $cgi->submit(-name
=> 'btnS', -value
=> 'Search') .
6561 $cgi->end_form() . "\n" .
6562 "<span class=\"projectlist_link\">" .
6563 $cgi->a({-href
=> href
(project
=> undef, searchtext
=> undef,
6564 action
=> 'project_list',
6565 project_filter
=> $project_filter)},
6566 esc_html
("List all projects$limit")) . "</span><br />\n";
6567 print "<span class=\"projectlist_link\">" .
6568 $cgi->a({-href
=> href
(project
=> undef, searchtext
=> undef,
6569 action
=> 'project_list',
6570 project_filter
=> undef)},
6571 esc_html
("List all projects")) . "</span>\n" if $project_filter;
6575 # entry for given @keys needs filling if at least one of keys in list
6576 # is not present in %$project_info
6577 sub project_info_needs_filling
{
6578 my ($project_info, @keys) = @_;
6580 # return List::MoreUtils::any { !exists $project_info->{$_} } @keys;
6581 foreach my $key (@keys) {
6582 if (!exists $project_info->{$key}) {
6589 sub git_cache_file_format
{
6590 return GITWEB_CACHE_FORMAT
.
6591 (gitweb_check_feature
('forks') ?
" (forks)" : "");
6594 sub git_retrieve_cache_file
{
6595 my $cache_file = shift;
6597 use Storable
qw(retrieve);
6599 if ((my $dump = eval { retrieve
($cache_file) })) {
6601 ref($dump) eq 'ARRAY' &&
6603 ref($$dump[1]) eq 'ARRAY' &&
6604 @
{$$dump[1]} == 2 &&
6605 ref(${$$dump[1]}[0]) eq 'ARRAY' &&
6606 ref(${$$dump[1]}[1]) eq 'HASH' &&
6607 $$dump[0] eq git_cache_file_format
();
6613 sub git_store_cache_file
{
6614 my ($cache_file, $cachedata) = @_;
6616 use File
::Basename
qw(dirname);
6618 use POSIX
qw(:fcntl_h);
6619 use Storable
qw(store_fd);
6622 my $cache_d = dirname
($cache_file);
6624 umask($mask & ~0070) if $cache_grpshared;
6625 if ((-d
$cache_d || mkdir($cache_d, $cache_grpshared ?
0770 : 0700)) &&
6626 sysopen(my $fd, "$cache_file.lock", O_WRONLY
|O_CREAT
|O_EXCL
, $cache_grpshared ?
0660 : 0600)) {
6627 store_fd
([git_cache_file_format
(), $cachedata], $fd);
6629 rename "$cache_file.lock", $cache_file;
6630 $result = stat($cache_file)->mtime;
6632 umask($mask) if $cache_grpshared;
6636 sub verify_cached_project
{
6637 my ($hashref, $path) = @_;
6638 return undef unless $path;
6639 delete $$hashref{$path}, return undef unless is_valid_project
($path);
6640 return $$hashref{$path} if exists $$hashref{$path};
6642 # A valid project was requested but it's not yet in the cache
6643 # Manufacture a minimal project entry (path, name, description)
6644 # Also provide age, but only if it's available via $lastactivity_file
6646 my %proj = ('path' => $path);
6647 my $val = git_get_project_description
($path);
6648 defined $val or $val = '';
6649 $proj{'descr_long'} = $val;
6650 $proj{'descr'} = chop_str
($val, $projects_list_description_width, 5);
6651 unless ($omit_owner) {
6652 $val = git_get_project_owner
($path);
6653 defined $val or $val = '';
6654 $proj{'owner'} = $val;
6656 unless ($omit_age_column) {
6657 ($val) = git_get_last_activity
($path, 1);
6658 $proj{'age_epoch'} = $val if defined $val;
6660 $$hashref{$path} = \
%proj;
6664 sub git_filter_cached_projects
{
6665 my ($cache, $projlist, $verify) = @_;
6666 my $hashref = $$cache[1];
6668 sub {verify_cached_project
($hashref, $_[0])} :
6669 sub {$$hashref{$_[0]}};
6671 my $c = &$sub($_->{'path'});
6672 defined $c ?
($_ = $c) : ()
6676 # fills project list info (age, description, owner, category, forks, etc.)
6677 # for each project in the list, removing invalid projects from
6678 # returned list, or fill only specified info.
6680 # Invalid projects are removed from the returned list if and only if you
6681 # ask 'age_epoch' to be filled, because they are the only fields
6682 # that run unconditionally git command that requires repository, and
6683 # therefore do always check if project repository is invalid.
6686 # * fill_project_list_info(\@project_list, 'descr_long', 'ctags')
6687 # ensures that 'descr_long' and 'ctags' fields are filled
6688 # * @project_list = fill_project_list_info(\@project_list)
6689 # ensures that all fields are filled (and invalid projects removed)
6691 # NOTE: modifies $projlist, but does not remove entries from it
6692 sub fill_project_list_info
{
6693 my ($projlist, @wanted_keys) = @_;
6695 my $rebuild = @wanted_keys && $wanted_keys[0] eq 'rebuild-cache' && shift @wanted_keys;
6696 return fill_project_list_info_uncached
($projlist, @wanted_keys)
6697 unless $projlist_cache_lifetime && $projlist_cache_lifetime > 0;
6701 my $cache_lifetime = $rebuild ?
0 : $projlist_cache_lifetime;
6702 my $cache_file = "$cache_dir/$projlist_cache_name";
6708 if ($cache_lifetime && -f
$cache_file) {
6709 $cache_mtime = stat($cache_file)->mtime;
6710 $cache_dump = undef if $cache_mtime &&
6711 (!$cache_dump_mtime || $cache_dump_mtime != $cache_mtime);
6713 if (defined $cache_mtime && # caching is on and $cache_file exists
6714 $cache_mtime + $cache_lifetime*60 > $now &&
6715 ($cache_dump || ($cache_dump = git_retrieve_cache_file
($cache_file)))) {
6717 $cache_dump_mtime = $cache_mtime;
6718 $stale = $now - $cache_mtime;
6719 my $verify = ($action eq 'summary' || $action eq 'forks') &&
6720 gitweb_check_feature
('forks');
6721 @projects = git_filter_cached_projects
($cache_dump, $projlist, $verify);
6723 } else { # Cache miss.
6724 if (defined $cache_mtime) {
6725 # Postpone timeout by two minutes so that we get
6726 # enough time to do our job, or to be more exact
6727 # make cache expire after two minutes from now.
6728 my $time = $now - $cache_lifetime*60 + 120;
6729 utime $time, $time, $cache_file;
6731 my @all_projects = git_get_projects_list
();
6732 my %all_projects_filled = map { ( $_->{'path'} => $_ ) }
6733 fill_project_list_info_uncached
(\
@all_projects);
6734 map { $all_projects_filled{$_->{'path'}} = $_ }
6735 filter_forks_from_projects_list
([values(%all_projects_filled)])
6736 if gitweb_check_feature
('forks');
6737 $cache_dump = [[sort {$a->{'path'} cmp $b->{'path'}} values(%all_projects_filled)],
6738 \
%all_projects_filled];
6739 $cache_dump_mtime = git_store_cache_file
($cache_file, $cache_dump);
6740 @projects = git_filter_cached_projects
($cache_dump, $projlist);
6743 if ($cache_lifetime && $stale > 0) {
6744 print "<div class=\"stale_info\">Cached version (${stale}s old)</div>\n"
6745 unless $shown_stale_message;
6746 $shown_stale_message = 1;
6752 sub fill_project_list_info_uncached
{
6753 my ($projlist, @wanted_keys) = @_;
6755 my $filter_set = sub { return @_; };
6757 my %wanted_keys = map { $_ => 1 } @wanted_keys;
6758 $filter_set = sub { return grep { $wanted_keys{$_} } @_; };
6761 my $show_ctags = gitweb_check_feature
('ctags');
6763 foreach my $pr (@
$projlist) {
6764 if (project_info_needs_filling
($pr, $filter_set->('age_epoch'))) {
6765 my (@activity) = git_get_last_activity
($pr->{'path'});
6766 unless (@activity) {
6769 ($pr->{'age_epoch'}) = @activity;
6771 if (project_info_needs_filling
($pr, $filter_set->('descr', 'descr_long'))) {
6772 my $descr = git_get_project_description
($pr->{'path'}) || "";
6773 $descr = to_utf8
($descr);
6774 $pr->{'descr_long'} = $descr;
6775 $pr->{'descr'} = chop_str
($descr, $projects_list_description_width, 5);
6777 if (project_info_needs_filling
($pr, $filter_set->('owner'))) {
6778 $pr->{'owner'} = git_get_project_owner
("$pr->{'path'}") || "";
6781 project_info_needs_filling
($pr, $filter_set->('ctags'))) {
6782 $pr->{'ctags'} = git_get_project_ctags
($pr->{'path'});
6784 if ($projects_list_group_categories &&
6785 project_info_needs_filling
($pr, $filter_set->('category'))) {
6786 my $cat = git_get_project_category
($pr->{'path'}) ||
6787 $project_list_default_category;
6788 $pr->{'category'} = to_utf8
($cat);
6791 push @projects, $pr;
6797 sub sort_projects_list
{
6798 my ($projlist, $order) = @_;
6802 return sub { lc($a->{$key}) cmp lc($b->{$key}) };
6805 sub order_reverse_num_then_undef
{
6808 defined $a->{$key} ?
6809 (defined $b->{$key} ?
$b->{$key} <=> $a->{$key} : -1) :
6810 (defined $b->{$key} ?
1 : 0)
6815 project
=> order_str
('path'),
6816 descr
=> order_str
('descr_long'),
6817 owner
=> order_str
('owner'),
6818 age
=> order_reverse_num_then_undef
('age_epoch'),
6821 my $ordering = $orderings{$order};
6822 return defined $ordering ?
sort $ordering @
$projlist : @
$projlist;
6825 # returns a hash of categories, containing the list of project
6826 # belonging to each category
6827 sub build_projlist_by_category
{
6828 my ($projlist, $from, $to) = @_;
6831 $from = 0 unless defined $from;
6832 $to = $#$projlist if (!defined $to || $#$projlist < $to);
6834 for (my $i = $from; $i <= $to; $i++) {
6835 my $pr = $projlist->[$i];
6836 push @
{$categories{ $pr->{'category'} }}, $pr;
6839 return wantarray ?
%categories : \
%categories;
6842 # print 'sort by' <th> element, generating 'sort by $name' replay link
6843 # if that order is not selected
6845 print format_sort_th
(@_);
6848 sub format_sort_th
{
6849 my ($name, $order, $header) = @_;
6851 $header ||= ucfirst($name);
6853 if ($order eq $name) {
6854 $sort_th .= "<th>$header</th>\n";
6856 $sort_th .= "<th>" .
6857 $cgi->a({-href
=> href
(-replay
=>1, order
=>$name),
6858 -class => "header"}, $header) .
6865 sub git_project_list_rows
{
6866 my ($projlist, $from, $to, $check_forks) = @_;
6868 $from = 0 unless defined $from;
6869 $to = $#$projlist if (!defined $to || $#$projlist < $to);
6873 for (my $i = $from; $i <= $to; $i++) {
6874 my $pr = $projlist->[$i];
6877 print "<tr class=\"dark\">\n";
6879 print "<tr class=\"light\">\n";
6885 if ($pr->{'forks'}) {
6886 my $nforks = scalar @
{$pr->{'forks'}};
6887 my $s = $nforks == 1 ?
'' : 's';
6889 print $cgi->a({-href
=> href
(project
=>$pr->{'path'}, action
=>"forks"),
6890 -title
=> "$nforks fork$s"}, "+");
6892 print $cgi->span({-title
=> "$nforks fork$s"}, "+");
6897 my $path = $pr->{'path'};
6898 my $dotgit = $path =~ s/\.git$// ?
'.git' : '';
6899 print "<td>" . $cgi->a({-href
=> href
(project
=>$pr->{'path'}, action
=>"summary"),
6901 esc_html_match_hl
($path, $search_regexp).$dotgit) .
6903 "<td>" . $cgi->a({-href
=> href
(project
=>$pr->{'path'}, action
=>"summary"),
6905 -title
=> $pr->{'descr_long'}},
6907 ? esc_html_match_hl_chopped
($pr->{'descr_long'},
6908 $pr->{'descr'}, $search_regexp)
6909 : esc_html
($pr->{'descr'})) .
6911 unless ($omit_owner) {
6912 print "<td><i>" . ($owner_link_hook
6913 ?
$cgi->a({-href
=> $owner_link_hook->($pr->{'owner'}), -class => "list"},
6914 chop_and_escape_str
($pr->{'owner'}, 15))
6915 : chop_and_escape_str
($pr->{'owner'}, 15)) . "</i></td>\n";
6917 unless ($omit_age_column) {
6918 my ($age_epoch, $age_string) = ($pr->{'age_epoch'});
6919 $age_string = defined $age_epoch ? age_string
($age_epoch, $now) : "No commits";
6920 print "<td class=\"". age_class
($age_epoch, $now) . "\">" . $age_string . "</td>\n";
6922 print"<td class=\"link\">" .
6923 $cgi->a({-href
=> href
(project
=>$pr->{'path'}, action
=>"summary")}, "summary") . $barsep .
6924 $cgi->a({-href
=> href
(project
=>$pr->{'path'}, action
=>"shortlog")}, "log") . $barsep .
6925 $cgi->a({-href
=> href
(project
=>$pr->{'path'}, action
=>"tree")}, "tree") .
6926 ($pr->{'forks'} ?
$barsep . $cgi->a({-href
=> href
(project
=>$pr->{'path'}, action
=>"forks")}, "forks") : '') .
6932 sub git_project_list_body
{
6933 # actually uses global variable $project
6934 my ($projlist, $order, $from, $to, $extra, $no_header, $ctags_action, $keep_top) = @_;
6935 my @projects = @
$projlist;
6937 my $check_forks = gitweb_check_feature
('forks');
6938 my $show_ctags = gitweb_check_feature
('ctags');
6939 my $tagfilter = $show_ctags ?
$input_params{'ctag_filter'} : undef;
6940 $check_forks = undef
6941 if ($tagfilter || $search_regexp);
6943 # filtering out forks before filling info allows us to do less work
6945 @projects = filter_forks_from_projects_list
(\
@projects);
6946 push @projects, { 'path' => "$project_filter.git" }
6947 if $project_filter && $keep_top && is_valid_project
("$project_filter.git");
6949 # search_projects_list pre-fills required info
6950 @projects = search_projects_list
(\
@projects,
6951 'search_regexp' => $search_regexp,
6952 'tagfilter' => $tagfilter)
6953 if ($tagfilter || $search_regexp);
6955 my @all_fields = ('descr', 'descr_long', 'ctags', 'category');
6956 push @all_fields, 'age_epoch' unless($omit_age_column);
6957 push @all_fields, 'owner' unless($omit_owner);
6958 @projects = fill_project_list_info
(\
@projects, @all_fields);
6960 $order ||= $default_projects_order;
6961 $from = 0 unless defined $from;
6962 $to = $#projects if (!defined $to || $#projects < $to);
6967 "<b>No such projects found</b><br />\n".
6968 "Click ".$cgi->a({-href
=>href
(project
=>undef,action
=>'project_list')},"here")." to view all projects<br />\n".
6969 "</center>\n<br />\n";
6973 @projects = sort_projects_list
(\
@projects, $order);
6976 my $ctags = git_gather_all_ctags
(\
@projects);
6977 my $cloud = git_populate_project_tagcloud
($ctags, $ctags_action||'project_list');
6978 print git_show_project_tagcloud
($cloud, 64);
6981 print "<table class=\"project_list\">\n";
6982 unless ($no_header) {
6985 print "<th></th>\n";
6987 print_sort_th
('project', $order, 'Project');
6988 print_sort_th
('descr', $order, 'Description');
6989 print_sort_th
('owner', $order, 'Owner') unless $omit_owner;
6990 print_sort_th
('age', $order, 'Last Change') unless $omit_age_column;
6991 print "<th></th>\n" . # for links
6995 if ($projects_list_group_categories) {
6996 # only display categories with projects in the $from-$to window
6997 @projects = sort {$a->{'category'} cmp $b->{'category'}} @projects[$from..$to];
6998 my %categories = build_projlist_by_category
(\
@projects, $from, $to);
6999 foreach my $cat (sort keys %categories) {
7000 unless ($cat eq "") {
7003 print "<td></td>\n";
7005 print "<td class=\"category\" colspan=\"5\">".esc_html
($cat)."</td>\n";
7009 git_project_list_rows
($categories{$cat}, undef, undef, $check_forks);
7012 git_project_list_rows
(\
@projects, $from, $to, $check_forks);
7015 if (defined $extra) {
7016 print "<tr class=\"extra\">\n";
7018 print "<td></td>\n";
7020 print "<td colspan=\"5\" class=\"extra\">$extra</td>\n" .
7027 # uses global variable $project
7028 my ($commitlist, $from, $to, $refs, $extra) = @_;
7030 $from = 0 unless defined $from;
7031 $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
7033 for (my $i = 0; $i <= $to; $i++) {
7034 my %co = %{$commitlist->[$i]};
7036 my $commit = $co{'id'};
7037 my $ref = format_ref_marker
($refs, $commit);
7038 git_print_header_div
('commit',
7039 "<span class=\"age\">$co{'age_string'}</span>" .
7040 esc_html
($co{'title'}),
7041 $commit, undef, $ref);
7042 print "<div class=\"title_text\">\n" .
7043 "<div class=\"log_link\">\n" .
7044 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$commit)}, "commit") .
7046 $cgi->a({-href
=> href
(action
=>"commitdiff", hash
=>$commit)}, "commitdiff") .
7048 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$commit, hash_base
=>$commit)}, "tree") .
7051 git_print_authorship
(\
%co, -tag
=> 'span');
7052 print "<br/>\n</div>\n";
7054 print "<div class=\"log_body\">\n";
7055 git_print_log
($co{'comment'}, -final_empty_line
=> 1);
7059 print "<div class=\"page_nav_trailer\">\n";
7065 sub git_shortlog_body
{
7066 # uses global variable $project
7067 my ($commitlist, $from, $to, $refs, $extra) = @_;
7069 $from = 0 unless defined $from;
7070 $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
7072 print "<table class=\"shortlog\">\n";
7074 for (my $i = $from; $i <= $to; $i++) {
7075 my %co = %{$commitlist->[$i]};
7076 my $commit = $co{'id'};
7077 my $ref = format_ref_marker
($refs, $commit);
7079 print "<tr class=\"dark\">\n";
7081 print "<tr class=\"light\">\n";
7084 # git_summary() used print "<td><i>$co{'age_string'}</i></td>\n" .
7085 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
7086 format_author_html
('td', \
%co, 10) . "<td>";
7087 print format_subject_html
($co{'title'}, $co{'title_short'},
7088 href
(action
=>"commit", hash
=>$commit), $ref);
7090 "<td class=\"link\">" .
7091 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$commit)}, "commit") . $barsep .
7092 $cgi->a({-href
=> href
(action
=>"commitdiff", hash
=>$commit)}, "commitdiff") . $barsep .
7093 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$commit, hash_base
=>$commit)}, "tree");
7094 my $snapshot_links = format_snapshot_links
($commit);
7095 if (defined $snapshot_links) {
7096 print $barsep . $snapshot_links;
7101 if (defined $extra) {
7102 print "<tr class=\"extra\">\n" .
7103 "<td colspan=\"4\" class=\"extra\">$extra</td>\n" .
7109 sub git_history_body
{
7110 # Warning: assumes constant type (blob or tree) during history
7111 my ($commitlist, $from, $to, $refs, $extra,
7112 $file_name, $file_hash, $ftype) = @_;
7114 $from = 0 unless defined $from;
7115 $to = $#{$commitlist} unless (defined $to && $to <= $#{$commitlist});
7117 print "<table class=\"history\">\n";
7119 for (my $i = $from; $i <= $to; $i++) {
7120 my %co = %{$commitlist->[$i]};
7124 my $commit = $co{'id'};
7126 my $ref = format_ref_marker
($refs, $commit);
7129 print "<tr class=\"dark\">\n";
7131 print "<tr class=\"light\">\n";
7134 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
7135 # shortlog: format_author_html('td', \%co, 10)
7136 format_author_html
('td', \
%co, 15, 3) . "<td>";
7137 # originally git_history used chop_str($co{'title'}, 50)
7138 print format_subject_html
($co{'title'}, $co{'title_short'},
7139 href
(action
=>"commit", hash
=>$commit), $ref);
7141 "<td class=\"link\">" .
7142 $cgi->a({-href
=> href
(action
=>$ftype, hash_base
=>$commit, file_name
=>$file_name)}, $ftype) . $barsep .
7143 $cgi->a({-href
=> href
(action
=>"commitdiff", hash
=>$commit)}, "commitdiff");
7145 if ($ftype eq 'blob') {
7146 my $blob_current = $file_hash;
7147 my $blob_parent = git_get_hash_by_path
($commit, $file_name);
7148 if (defined $blob_current && defined $blob_parent &&
7149 $blob_current ne $blob_parent) {
7151 $cgi->a({-href
=> href
(action
=>"blobdiff",
7152 hash
=>$blob_current, hash_parent
=>$blob_parent,
7153 hash_base
=>$hash_base, hash_parent_base
=>$commit,
7154 file_name
=>$file_name)},
7161 if (defined $extra) {
7162 print "<tr class=\"extra\">\n" .
7163 "<td colspan=\"4\" class=\"extra\">$extra</td>\n" .
7170 # uses global variable $project
7171 my ($taglist, $from, $to, $extra, $head_at, $full, $order) = @_;
7172 $from = 0 unless defined $from;
7173 $to = $#{$taglist} if (!defined $to || $#{$taglist} < $to);
7174 $order ||= $default_refs_order;
7176 print "<table class=\"tags\">\n";
7178 print "<tr class=\"tags_header\">\n";
7179 print_sort_th
('age', $order, 'Last Change');
7180 print_sort_th
('name', $order, 'Name');
7181 print "<th></th>\n" . # for comment
7182 "<th></th>\n" . # for tag
7183 "<th></th>\n" . # for links
7187 for (my $i = $from; $i <= $to; $i++) {
7188 my $entry = $taglist->[$i];
7190 my $comment = $tag{'subject'};
7192 if (defined $comment) {
7193 $comment_short = chop_str
($comment, 30, 5);
7195 my $curr = defined $head_at && $tag{'id'} eq $head_at;
7197 print "<tr class=\"dark\">\n";
7199 print "<tr class=\"light\">\n";
7202 if (defined $tag{'age'}) {
7203 print "<td><i>$tag{'age'}</i></td>\n";
7205 print "<td></td>\n";
7207 print(($curr ?
"<td class=\"current_head\">" : "<td>") .
7208 $cgi->a({-href
=> href
(action
=>$tag{'reftype'}, hash
=>$tag{'refid'}),
7209 -class => "list name"}, esc_html
($tag{'name'})) .
7212 if (defined $comment) {
7213 print format_subject_html
($comment, $comment_short,
7214 href
(action
=>"tag", hash
=>$tag{'id'}));
7217 "<td class=\"selflink\">";
7218 if ($tag{'type'} eq "tag") {
7219 print $cgi->a({-href
=> href
(action
=>"tag", hash
=>$tag{'id'})}, "tag");
7224 "<td class=\"link\">" . $barsep .
7225 $cgi->a({-href
=> href
(action
=>$tag{'reftype'}, hash
=>$tag{'refid'})}, $tag{'reftype'});
7226 if ($tag{'reftype'} eq "commit") {
7227 print $barsep . $cgi->a({-href
=> href
(action
=>"shortlog", hash
=>$tag{'fullname'})}, "log");
7228 print $barsep . $cgi->a({-href
=> href
(action
=>"tree", hash
=>$tag{'fullname'})}, "tree") if $full;
7229 } elsif ($tag{'reftype'} eq "blob") {
7230 print $barsep . $cgi->a({-href
=> href
(action
=>"blob_plain", hash
=>$tag{'refid'})}, "raw");
7235 if (defined $extra) {
7236 print "<tr class=\"extra\">\n" .
7237 "<td colspan=\"5\" class=\"extra\">$extra</td>\n" .
7243 sub git_heads_body
{
7244 # uses global variable $project
7245 my ($headlist, $head_at, $from, $to, $extra) = @_;
7246 $from = 0 unless defined $from;
7247 $to = $#{$headlist} if (!defined $to || $#{$headlist} < $to);
7249 print "<table class=\"heads\">\n";
7251 for (my $i = $from; $i <= $to; $i++) {
7252 my $entry = $headlist->[$i];
7254 my $curr = defined $head_at && $ref{'id'} eq $head_at;
7256 print "<tr class=\"dark\">\n";
7258 print "<tr class=\"light\">\n";
7261 print "<td><i>$ref{'age'}</i></td>\n" .
7262 ($curr ?
"<td class=\"current_head\">" : "<td>") .
7263 $cgi->a({-href
=> href
(action
=>"shortlog", hash
=>$ref{'fullname'}),
7264 -class => "list name"},esc_html
($ref{'name'})) .
7266 "<td class=\"link\">" .
7267 $cgi->a({-href
=> href
(action
=>"shortlog", hash
=>$ref{'fullname'})}, "log") . $barsep .
7268 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$ref{'fullname'}, hash_base
=>$ref{'fullname'})}, "tree") .
7272 if (defined $extra) {
7273 print "<tr class=\"extra\">\n" .
7274 "<td colspan=\"3\" class=\"extra\">$extra</td>\n" .
7280 # Display a single remote block
7281 sub git_remote_block
{
7282 my ($remote, $rdata, $limit, $head) = @_;
7284 my $heads = $rdata->{'heads'};
7285 my $fetch = $rdata->{'fetch'};
7286 my $push = $rdata->{'push'};
7288 my $urls_table = "<table class=\"projects_list\">\n" ;
7290 if (defined $fetch) {
7291 if ($fetch eq $push) {
7292 $urls_table .= format_repo_url
("URL", $fetch);
7294 $urls_table .= format_repo_url
("Fetch URL", $fetch);
7295 $urls_table .= format_repo_url
("Push URL", $push) if defined $push;
7297 } elsif (defined $push) {
7298 $urls_table .= format_repo_url
("Push URL", $push);
7300 $urls_table .= format_repo_url
("", "No remote URL");
7303 $urls_table .= "</table>\n";
7306 if (defined $limit && $limit < @
$heads) {
7307 $dots = $cgi->a({-href
=> href
(action
=>"remotes", hash
=>$remote)}, "...");
7311 git_heads_body
($heads, $head, 0, $limit, $dots);
7314 # Display a list of remote names with the respective fetch and push URLs
7315 sub git_remotes_list
{
7316 my ($remotedata, $limit) = @_;
7317 print "<table class=\"heads\">\n";
7319 my @remotes = sort keys %$remotedata;
7321 my $limited = $limit && $limit < @remotes;
7323 $#remotes = $limit - 1 if $limited;
7325 while (my $remote = shift @remotes) {
7326 my $rdata = $remotedata->{$remote};
7327 my $fetch = $rdata->{'fetch'};
7328 my $push = $rdata->{'push'};
7330 print "<tr class=\"dark\">\n";
7332 print "<tr class=\"light\">\n";
7336 $cgi->a({-href
=> href
(action
=>'remotes', hash
=>$remote),
7337 -class=> "list name"},esc_html
($remote)) .
7339 print "<td class=\"link\">" .
7340 (defined $fetch ?
$cgi->a({-href
=> $fetch}, "fetch") : "fetch") .
7342 (defined $push ?
$cgi->a({-href
=> $push}, "push") : "push") .
7350 "<td colspan=\"3\">" .
7351 $cgi->a({-href
=> href
(action
=>"remotes")}, "...") .
7352 "</td>\n" . "</tr>\n";
7358 # Display remote heads grouped by remote, unless there are too many
7359 # remotes, in which case we only display the remote names
7360 sub git_remotes_body
{
7361 my ($remotedata, $limit, $head) = @_;
7362 if ($limit and $limit < keys %$remotedata) {
7363 git_remotes_list
($remotedata, $limit);
7365 fill_remote_heads
($remotedata);
7366 while (my ($remote, $rdata) = each %$remotedata) {
7367 git_print_section
({-class=>"remote", -id
=>$remote},
7368 ["remotes", $remote, $remote], sub {
7369 git_remote_block
($remote, $rdata, $limit, $head);
7375 sub git_search_message
{
7379 if ($searchtype eq 'commit') {
7380 $greptype = "--grep=";
7381 } elsif ($searchtype eq 'author') {
7382 $greptype = "--author=";
7383 } elsif ($searchtype eq 'committer') {
7384 $greptype = "--committer=";
7386 $greptype .= $searchtext;
7387 my @commitlist = parse_commits
($hash, 101, (100 * $page), undef,
7388 $greptype, '--regexp-ignore-case',
7389 $search_use_regexp ?
'--extended-regexp' : '--fixed-strings');
7391 my $paging_nav = "<span class=\"paging_nav\">";
7393 $paging_nav .= tabspan
(
7394 $cgi->a({-href
=> href
(-replay
=>1, page
=>undef)},
7397 $cgi->a({-href
=> href
(-replay
=>1, page
=>$page-1),
7398 -accesskey
=> "p", -title
=> "Alt-p"}, "prev"));
7400 $paging_nav .= tabspan
("first", 1, 0).${mdotsep
}.tabspan
("prev", 0, 1);
7403 if ($#commitlist >= 100) {
7404 $next_link = tabspan
(
7405 $cgi->a({-href
=> href
(-replay
=>1, page
=>$page+1),
7406 -accesskey
=> "n", -title
=> "Alt-n"}, "next"));
7407 $paging_nav .= "${mdotsep}$next_link";
7409 $paging_nav .= ${mdotsep
}.tabspan
("next", 0, 1);
7414 git_print_page_nav
('','', $hash,$co{'tree'},$hash, $paging_nav."</span>");
7415 git_print_header_div
('commit', esc_html
($co{'title'}), $hash);
7416 if ($page == 0 && !@commitlist) {
7417 print "<p>No match.</p>\n";
7419 git_search_grep_body
(\
@commitlist, 0, 99, $next_link);
7425 sub git_search_changes
{
7429 defined(my $fd = git_cmd_pipe
'--no-pager', 'log', @diff_opts,
7430 '--pretty=format:%H', '--no-abbrev', '--raw', "-S$searchtext",
7431 ($search_use_regexp ?
'--pickaxe-regex' : ()))
7432 or die_error
(500, "Open git-log failed");
7436 git_print_page_nav
('','', $hash,$co{'tree'},$hash);
7437 git_print_header_div
('commit', esc_html
($co{'title'}), $hash);
7439 print "<table class=\"pickaxe search\">\n";
7443 while (my $line = to_utf8
(scalar <$fd>)) {
7447 my %set = parse_difftree_raw_line
($line);
7448 if (defined $set{'commit'}) {
7449 # finish previous commit
7452 "<td class=\"link\">" .
7453 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$co{'id'})},
7456 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$co{'tree'},
7457 hash_base
=>$co{'id'})},
7464 print "<tr class=\"dark\">\n";
7466 print "<tr class=\"light\">\n";
7469 %co = parse_commit
($set{'commit'});
7470 my $author = chop_and_escape_str
($co{'author_name'}, 15, 5);
7471 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
7472 "<td><i>$author</i></td>\n" .
7474 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$co{'id'}),
7475 -class => "list subject"},
7476 chop_and_escape_str
($co{'title'}, 50) . "<br/>");
7477 } elsif (defined $set{'to_id'}) {
7478 next if ($set{'to_id'} =~ m/^0{40}$/);
7480 print $cgi->a({-href
=> href
(action
=>"blob", hash_base
=>$co{'id'},
7481 hash
=>$set{'to_id'}, file_name
=>$set{'to_file'}),
7483 "<span class=\"match\">" . esc_path
($set{'file'}) . "</span>") .
7489 # finish last commit (warning: repetition!)
7492 "<td class=\"link\">" .
7493 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$co{'id'})},
7496 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$co{'tree'},
7497 hash_base
=>$co{'id'})},
7508 sub git_search_files
{
7512 defined(my $fd = git_cmd_pipe
'grep', '-n', '-z',
7513 $search_use_regexp ?
('-E', '-i') : '-F',
7514 $searchtext, $co{'tree'})
7515 or die_error
(500, "Open git-grep failed");
7519 git_print_page_nav
('','', $hash,$co{'tree'},$hash);
7520 git_print_header_div
('commit', esc_html
($co{'title'}), $hash);
7522 print "<table class=\"grep_search\">\n";
7527 while (my $line = to_utf8
(scalar <$fd>)) {
7529 my ($file, $lno, $ltext, $binary);
7530 last if ($matches++ > 1000);
7531 if ($line =~ /^Binary file (.+) matches$/) {
7535 ($file, $lno, $ltext) = split(/\0/, $line, 3);
7536 $file =~ s/^$co{'tree'}://;
7538 if ($file ne $lastfile) {
7539 $lastfile and print "</td></tr>\n";
7541 print "<tr class=\"dark\">\n";
7543 print "<tr class=\"light\">\n";
7545 $file_href = href
(action
=>"blob", hash_base
=>$co{'id'},
7547 print "<td class=\"list\">".
7548 $cgi->a({-href
=> $file_href, -class => "list"}, esc_path
($file));
7549 print "</td><td>\n";
7553 print "<div class=\"binary\">Binary file</div>\n";
7555 $ltext = untabify
($ltext);
7556 if ($ltext =~ m/^(.*)($search_regexp)(.*)$/i) {
7557 $ltext = esc_html
($1, -nbsp
=>1);
7558 $ltext .= '<span class="match">';
7559 $ltext .= esc_html
($2, -nbsp
=>1);
7560 $ltext .= '</span>';
7561 $ltext .= esc_html
($3, -nbsp
=>1);
7563 $ltext = esc_html
($ltext, -nbsp
=>1);
7565 print "<div class=\"pre\">" .
7566 $cgi->a({-href
=> $file_href.'#l'.$lno,
7567 -class => "linenr"}, sprintf('%4i', $lno)) .
7568 ' ' . $ltext . "</div>\n";
7572 print "</td></tr>\n";
7573 if ($matches > 1000) {
7574 print "<div class=\"diff nodifferences\">Too many matches, listing trimmed</div>\n";
7577 print "<div class=\"diff nodifferences\">No matches found</div>\n";
7586 sub git_search_grep_body
{
7587 my ($commitlist, $from, $to, $extra) = @_;
7588 $from = 0 unless defined $from;
7589 $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
7591 print "<table class=\"commit_search\">\n";
7593 for (my $i = $from; $i <= $to; $i++) {
7594 my %co = %{$commitlist->[$i]};
7598 my $commit = $co{'id'};
7600 print "<tr class=\"dark\">\n";
7602 print "<tr class=\"light\">\n";
7605 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
7606 format_author_html
('td', \
%co, 15, 5) .
7608 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$co{'id'}),
7609 -class => "list subject"},
7610 chop_and_escape_str
($co{'title'}, 50) . "<br/>");
7611 my $comment = $co{'comment'};
7612 foreach my $line (@
$comment) {
7613 if ($line =~ m/^(.*?)($search_regexp)(.*)$/i) {
7614 my ($lead, $match, $trail) = ($1, $2, $3);
7615 $match = chop_str
($match, 70, 5, 'center');
7616 my $contextlen = int((80 - length($match))/2);
7617 $contextlen = 30 if ($contextlen > 30);
7618 $lead = chop_str
($lead, $contextlen, 10, 'left');
7619 $trail = chop_str
($trail, $contextlen, 10, 'right');
7621 $lead = esc_html
($lead);
7622 $match = esc_html
($match);
7623 $trail = esc_html
($trail);
7625 print "$lead<span class=\"match\">$match</span>$trail<br />";
7629 "<td class=\"link\">" .
7630 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$co{'id'})}, "commit") .
7632 $cgi->a({-href
=> href
(action
=>"commitdiff", hash
=>$co{'id'})}, "commitdiff") .
7634 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$co{'tree'}, hash_base
=>$co{'id'})}, "tree");
7638 if (defined $extra) {
7639 print "<tr class=\"extra\">\n" .
7640 "<td colspan=\"3\" class=\"extra\">$extra</td>\n" .
7646 ## ======================================================================
7647 ## ======================================================================
7650 sub git_project_list_load
{
7651 my $empty_list_ok = shift;
7652 my $order = $input_params{'order'};
7653 if (defined $order && $order !~ m/none|project|descr|owner|age/) {
7654 die_error
(400, "Unknown order parameter");
7657 my @list = git_get_projects_list
($project_filter, $strict_export);
7658 if ($project_filter && (!@list || !gitweb_check_feature
('forks'))) {
7659 push @list, { 'path' => "$project_filter.git" }
7660 if is_valid_project
("$project_filter.git");
7663 die_error
(404, "No projects found") unless $empty_list_ok;
7666 return (\
@list, $order);
7670 my ($projlist, $order);
7672 if ($frontpage_no_project_list) {
7674 $project_filter = undef;
7676 ($projlist, $order) = git_project_list_load
(1);
7679 if (defined $home_text && -f
$home_text) {
7680 print "<div class=\"index_include\">\n";
7681 insert_file
($home_text);
7684 git_project_search_form
($searchtext, $search_use_regexp);
7685 if ($frontpage_no_project_list) {
7686 my $show_ctags = gitweb_check_feature
('ctags');
7687 if ($frontpage_no_project_list == 1 and $show_ctags) {
7688 my @projects = git_get_projects_list
($project_filter, $strict_export);
7689 @projects = filter_forks_from_projects_list
(\
@projects) if gitweb_check_feature
('forks');
7690 @projects = fill_project_list_info
(\
@projects, 'ctags');
7691 my $ctags = git_gather_all_ctags
(\
@projects);
7692 my $cloud = git_populate_project_tagcloud
($ctags, 'project_list');
7693 print git_show_project_tagcloud
($cloud, 64);
7696 git_project_list_body
($projlist, $order, undef, undef, undef, undef, undef, 1);
7701 sub git_project_list
{
7702 my ($projlist, $order) = git_project_list_load
();
7704 if (!$frontpage_no_project_list && defined $home_text && -f
$home_text) {
7705 print "<div class=\"index_include\">\n";
7706 insert_file
($home_text);
7709 git_project_search_form
();
7710 git_project_list_body
($projlist, $order, undef, undef, undef, undef, undef, 1);
7715 my $order = $input_params{'order'};
7716 if (defined $order && $order !~ m/none|project|descr|owner|age/) {
7717 die_error
(400, "Unknown order parameter");
7720 my $filter = $project;
7721 $filter =~ s/\.git$//;
7722 my @list = git_get_projects_list
($filter);
7724 die_error
(404, "No forks found");
7728 git_print_page_nav
('','');
7729 git_print_header_div
('summary', "$project forks");
7730 git_project_list_body
(\
@list, $order, undef, undef, undef, undef, 'forks');
7734 sub git_project_index
{
7735 my @projects = git_get_projects_list
($project_filter, $strict_export);
7737 die_error
(404, "No projects found");
7741 -type
=> 'text/plain',
7742 -charset
=> 'utf-8',
7743 -content_disposition
=> 'inline; filename="index.aux"');
7745 foreach my $pr (@projects) {
7746 if (!exists $pr->{'owner'}) {
7747 $pr->{'owner'} = git_get_project_owner
("$pr->{'path'}");
7750 my ($path, $owner) = ($pr->{'path'}, $pr->{'owner'});
7751 # quote as in CGI::Util::encode, but keep the slash, and use '+' for ' '
7752 $path =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf
("%%%02X", ord($1))/eg
;
7753 $owner =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf
("%%%02X", ord($1))/eg
;
7757 print "$path $owner\n";
7762 my $descr = git_get_project_description
($project) || "none";
7763 my %co = parse_commit
("HEAD");
7764 my %cd = %co ? parse_date
($co{'committer_epoch'}, $co{'committer_tz'}) : ();
7765 my $head = $co{'id'};
7766 my $remote_heads = gitweb_check_feature
('remote_heads');
7768 my $owner = git_get_project_owner
($project);
7769 my $homepage = git_get_project_config
('homepage');
7770 my $base_url = git_get_project_config
('baseurl');
7772 my $refs = git_get_references
();
7773 # These get_*_list functions return one more to allow us to see if
7774 # there are more ...
7775 my @taglist = git_get_tags_list
(16);
7776 my @headlist = git_get_heads_list
(16);
7777 my %remotedata = $remote_heads ? git_get_remotes_list
() : ();
7779 my $check_forks = gitweb_check_feature
('forks');
7782 # find forks of a project
7783 my $filter = $project;
7784 $filter =~ s/\.git$//;
7785 @forklist = git_get_projects_list
($filter);
7786 # filter out forks of forks
7787 @forklist = filter_forks_from_projects_list
(\
@forklist)
7792 git_print_page_nav
('summary','', $head);
7794 if ($check_forks and $project =~ m
#/#) {
7795 my $xproject = $project; $xproject =~ s
#/[^/]+$#.git#; #
7796 my $r = $cgi->a({-href
=> href
(project
=> $xproject, action
=> 'summary')}, $xproject);
7798 <div class="forkinfo">
7799 This project is a fork of the $r project. If you have that one
7800 already cloned locally, you can use
7801 <pre>git clone --reference /path/to/your/$xproject/incarnation mirror_URL</pre>
7802 to save bandwidth during cloning.
7807 print "<div class=\"title\"> </div>\n";
7808 print "<table class=\"projects_list\">\n" .
7809 "<tr id=\"metadata_desc\"><td>description</td><td>" . esc_html
($descr) . "</td></tr>\n";
7811 print "<tr id=\"metadata_homepage\"><td>homepage URL</td><td>" . $cgi->a({-href
=> $homepage}, $homepage) . "</td></tr>\n";
7814 print "<tr id=\"metadata_baseurl\"><td>repository URL</td><td>" . esc_html
($base_url) . "</td></tr>\n";
7816 if ($owner and not $omit_owner) {
7817 print "<tr id=\"metadata_owner\"><td>owner</td><td>" . ($owner_link_hook
7818 ?
$cgi->a({-href
=> $owner_link_hook->($owner)}, email_obfuscate
($owner))
7819 : email_obfuscate
($owner)) . "</td></tr>\n";
7821 if (defined $cd{'rfc2822'}) {
7822 print "<tr id=\"metadata_lchange\"><td>last change</td>" .
7823 "<td>".format_timestamp_html
(\
%cd)."</td></tr>\n";
7825 print format_lastrefresh_row
(), "\n";
7827 # use per project git URL list in $projectroot/$project/cloneurl
7828 # or make project git URL from git base URL and project name
7829 my $url_tag = $base_url ?
"mirror URL" : "URL";
7830 my @url_list = git_get_project_url_list
($project);
7831 @url_list = map { "$_/$project" } @git_base_url_list unless @url_list;
7832 foreach my $git_url (@url_list) {
7833 next unless $git_url;
7834 print format_repo_url
($url_tag, $git_url);
7837 @url_list = map { "$_/$project" } @git_base_push_urls;
7838 if ((git_get_project_config
("showpush", '--bool')||'false') eq "true" ||
7839 -f
"$projectroot/$project/.nofetch") {
7840 $url_tag = "push URL";
7841 foreach my $git_push_url (@url_list) {
7842 next unless $git_push_url;
7843 my $hint = $https_hint_html && $git_push_url =~ /^https:/i ?
7844 " $https_hint_html" : '';
7845 print "<tr class=\"metadata_pushurl\"><td>$url_tag</td><td>$git_push_url$hint</td></tr>\n";
7850 if (defined($git_base_bundles_url) && -d
"$projectroot/$project/bundles") {
7851 my $projname = $project;
7852 $projname =~ s
|^.*/||;
7853 my $url = "$git_base_bundles_url/$project/bundles";
7854 print format_repo_url
(
7856 "<a rel='nofollow' href='$url'>$projname downloadable bundles</a>");
7860 my $show_ctags = gitweb_check_feature
('ctags');
7862 my $ctags = git_get_project_ctags
($project);
7863 if (%$ctags || $show_ctags !~ /^\d+$/) {
7864 # without ability to add tags, don't show if there are none
7865 my $cloud = git_populate_project_tagcloud
($ctags, 'project_list');
7866 print "<tr id=\"metadata_ctags\">" .
7867 "<td style=\"vertical-align:middle\">content tags<br />";
7868 print "</td>\n<td>" unless %$ctags;
7869 print "<form action=\"$show_ctags\" method=\"post\" style=\"white-space:nowrap\">" .
7870 "<input type=\"hidden\" name=\"p\" value=\"$project\"/>" .
7871 "add: <input type=\"text\" name=\"t\" size=\"8\" /></form>"
7872 unless $show_ctags =~ /^\d+$/;
7873 print "</td>\n<td>" if %$ctags;
7874 print git_show_project_tagcloud
($cloud, 48)."</td>" .
7881 # If XSS prevention is on, we don't include README.html.
7882 # TODO: Allow a readme in some safe format.
7883 if (!$prevent_xss) {
7884 my $readme = -s
"$projectroot/$project/README.html"
7885 ? collect_html_file
("$projectroot/$project/README.html")
7886 : collect_output
($git_automatic_readme_html, "$projectroot/$project");
7887 if (defined($readme)) {
7888 $readme =~ s/^\s+//s;
7889 $readme =~ s/\s+$//s;
7890 print "<div class=\"title\">readme</div>\n",
7891 "<div id=\"readme\" class=\"readme\">\n",
7898 # we need to request one more than 16 (0..15) to check if
7900 my @commitlist = $head ? parse_commits
($head, 17) : ();
7902 git_print_header_div
('shortlog');
7903 git_shortlog_body
(\
@commitlist, 0, 15, $refs,
7904 $#commitlist <= 15 ?
undef :
7905 $cgi->a({-href
=> href
(action
=>"shortlog")}, "..."));
7909 git_print_header_div
('tags');
7910 git_tags_body
(\
@taglist, 0, 15,
7911 $#taglist <= 15 ?
undef :
7912 $cgi->a({-href
=> href
(action
=>"tags")}, "..."));
7916 git_print_header_div
('heads');
7917 git_heads_body
(\
@headlist, $head, 0, 15,
7918 $#headlist <= 15 ?
undef :
7919 $cgi->a({-href
=> href
(action
=>"heads")}, "..."));
7923 git_print_header_div
('remotes');
7924 git_remotes_body
(\
%remotedata, 15, $head);
7928 git_print_header_div
('forks');
7929 git_project_list_body
(\
@forklist, 'age', 0, 15,
7930 $#forklist <= 15 ?
undef :
7931 $cgi->a({-href
=> href
(action
=>"forks")}, "..."),
7932 'no_header', 'forks');
7939 my %tag = parse_tag
($hash);
7942 die_error
(404, "Unknown tag object");
7946 $fullhash = $hash if $hash =~ m/^[0-9a-fA-F]{40}$/;
7947 $fullhash = git_get_full_hash
($project, $hash) unless $fullhash;
7949 my $head = git_get_head_hash
($project);
7951 git_print_page_nav
('','', $head,undef,$head);
7952 git_print_header_div
('commit', esc_html
($tag{'name'}), $hash);
7953 print "<div class=\"title_text\">\n" .
7954 "<table class=\"object_header\">\n" .
7955 "<tr><td>tag</td><td class=\"sha1\">$fullhash</td></tr>\n" .
7957 "<td>object</td>\n" .
7958 "<td>" . $cgi->a({-class => "list", -href
=> href
(action
=>$tag{'type'}, hash
=>$tag{'object'})},
7959 $tag{'object'}) . "</td>\n" .
7960 "<td class=\"link\">" . $cgi->a({-href
=> href
(action
=>$tag{'type'}, hash
=>$tag{'object'})},
7961 $tag{'type'}) . "</td>\n" .
7963 if (defined($tag{'author'})) {
7964 git_print_authorship_rows
(\
%tag, 'author');
7966 print "</table>\n\n" .
7968 print "<div class=\"page_body\">";
7969 my $comment = $tag{'comment'};
7970 foreach my $line (@
$comment) {
7972 print esc_html
($line, -nbsp
=>1) . "<br/>\n";
7978 sub git_blame_common
{
7979 my $format = shift || 'porcelain';
7980 if ($format eq 'porcelain' && $input_params{'javascript'}) {
7981 $format = 'incremental';
7982 $action = 'blame_incremental'; # for page title etc
7986 gitweb_check_feature
('blame')
7987 or die_error
(403, "Blame view not allowed");
7990 die_error
(400, "No file name given") unless $file_name;
7991 $hash_base ||= git_get_head_hash
($project);
7992 die_error
(404, "Couldn't find base commit") unless $hash_base;
7993 my %co = parse_commit
($hash_base)
7994 or die_error
(404, "Commit not found");
7996 if (!defined $hash) {
7997 $hash = git_get_hash_by_path
($hash_base, $file_name, "blob")
7998 or die_error
(404, "Error looking up file");
8000 $ftype = git_get_type
($hash);
8001 if ($ftype !~ "blob") {
8002 die_error
(400, "Object is not a blob");
8007 if ($format eq 'incremental') {
8008 # get file contents (as base)
8009 defined($fd = git_cmd_pipe
'cat-file', 'blob', $hash)
8010 or die_error
(500, "Open git-cat-file failed");
8011 } elsif ($format eq 'data') {
8012 # run git-blame --incremental
8013 defined($fd = git_cmd_pipe
"blame", "--incremental",
8014 $hash_base, "--", $file_name)
8015 or die_error
(500, "Open git-blame --incremental failed");
8017 # run git-blame --porcelain
8018 defined($fd = git_cmd_pipe
"blame", '-p',
8019 $hash_base, '--', $file_name)
8020 or die_error
(500, "Open git-blame --porcelain failed");
8023 # incremental blame data returns early
8024 if ($format eq 'data') {
8026 -type
=>"text/plain", -charset
=> "utf-8",
8027 -status
=> "200 OK");
8028 local $| = 1; # output autoflush
8033 or print "ERROR $!\n";
8036 if (defined $t0 && gitweb_check_feature
('timed')) {
8038 tv_interval
($t0, [ gettimeofday
() ]).
8039 ' '.$number_of_git_cmds;
8048 my $formats_nav = tabspan
(
8049 $cgi->a({-href
=> href
(action
=>"blob", -replay
=>1)},
8053 $cgi->a({-href
=> href
(action
=>"history", -replay
=>1)},
8056 $cgi->a({-href
=> href
(action
=>$action, file_name
=>$file_name)},
8058 git_print_page_nav
('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
8059 git_print_header_div
('commit', esc_html
($co{'title'}), $hash_base);
8060 git_print_page_path
($file_name, $ftype, $hash_base);
8063 if ($format eq 'incremental') {
8064 print "<noscript>\n<div class=\"error\"><center><b>\n".
8065 "This page requires JavaScript to run.\n Use ".
8066 $cgi->a({-href
=> href
(action
=>'blame',javascript
=>0,-replay
=>1)},
8069 "</b></center></div>\n</noscript>\n";
8071 print qq!<div id
="progress_bar" style
="width: 100%; background-color: yellow"></div
>\n!;
8074 print qq!<div
class="page_body">\n!;
8075 print qq!<div id
="progress_info">... / ...</div
>\n!
8076 if ($format eq 'incremental');
8077 print qq!<table id
="blame_table" class="blame" width
="100%">\n!.
8078 #qq!<col width="5.5em" /><col width="2.5em" /><col width="*" />\n!.
8080 qq!<tr
><th nowrap
="nowrap" style
="white-space:nowrap">!.
8081 qq!Commit
 <a href="javascript:extra_blame_columns()" id="columns_expander" !.
8082 qq!title
="toggles blame author information display">[+]</a></th
>!.
8083 qq!<th
class="extra_column">Author
</th><th class="extra_column">Date</th
>!.
8084 qq!<th
>Line
</th><th width="100%">Data</th
></tr
>\n!.
8088 my @rev_color = qw(light dark);
8089 my $num_colors = scalar(@rev_color);
8090 my $current_color = 0;
8092 if ($format eq 'incremental') {
8093 my $color_class = $rev_color[$current_color];
8098 while (my $line = to_utf8
(scalar <$fd>)) {
8102 print qq!<tr id
="l$linenr" class="$color_class">!.
8103 qq!<td
class="sha1"><a href
=""> </a></td
>!.
8104 qq!<td
class="extra_column" nowrap
="nowrap"></td
>!.
8105 qq!<td
class="extra_column" nowrap
="nowrap"></td
>!.
8106 qq!<td
class="linenr">!.
8107 qq!<a
class="linenr" href
="">$linenr</a></td
>!;
8108 print qq!<td
class="pre">! . esc_html
($line) . "</td>\n";
8112 } else { # porcelain, i.e. ordinary blame
8113 my %metainfo = (); # saves information about commits
8117 while (my $line = to_utf8
(scalar <$fd>)) {
8119 # the header: <SHA-1> <src lineno> <dst lineno> [<lines in group>]
8120 # no <lines in group> for subsequent lines in group of lines
8121 my ($full_rev, $orig_lineno, $lineno, $group_size) =
8122 ($line =~ /^([0-9a-f]{40}) (\d+) (\d+)(?: (\d+))?$/);
8123 if (!exists $metainfo{$full_rev}) {
8124 $metainfo{$full_rev} = { 'nprevious' => 0 };
8126 my $meta = $metainfo{$full_rev};
8128 while ($data = to_utf8
(scalar <$fd>)) {
8130 last if ($data =~ s/^\t//); # contents of line
8131 if ($data =~ /^(\S+)(?: (.*))?$/) {
8132 $meta->{$1} = $2 unless exists $meta->{$1};
8134 if ($data =~ /^previous /) {
8135 $meta->{'nprevious'}++;
8138 my $short_rev = substr($full_rev, 0, 8);
8139 my $author = $meta->{'author'};
8141 parse_date
($meta->{'author-time'}, $meta->{'author-tz'});
8142 my $date = $date{'iso-tz'};
8144 $current_color = ($current_color + 1) % $num_colors;
8146 my $tr_class = $rev_color[$current_color];
8147 $tr_class .= ' boundary' if (exists $meta->{'boundary'});
8148 $tr_class .= ' no-previous' if ($meta->{'nprevious'} == 0);
8149 $tr_class .= ' multiple-previous' if ($meta->{'nprevious'} > 1);
8150 print "<tr id=\"l$lineno\" class=\"$tr_class\">\n";
8152 my $rowspan = $group_size > 1 ?
" rowspan=\"$group_size\"" : "";
8153 print "<td class=\"sha1\"";
8154 print " title=\"". esc_html
($author) . ", $date\"";
8156 print $cgi->a({-href
=> href
(action
=>"commit",
8158 file_name
=>$file_name)},
8159 esc_html
($short_rev));
8160 if ($group_size >= 2) {
8161 my @author_initials = ($author =~ /\b([[:upper:]])\B/g);
8162 if (@author_initials) {
8164 esc_html
(join('', @author_initials));
8169 print "<td class=\"extra_column\" nowrap=\"nowrap\"$rowspan>". esc_html
($author) . "</td>";
8170 print "<td class=\"extra_column\" nowrap=\"nowrap\"$rowspan>". $date . "</td>";
8172 # 'previous' <sha1 of parent commit> <filename at commit>
8173 if (exists $meta->{'previous'} &&
8174 $meta->{'previous'} =~ /^([a-fA-F0-9]{40}) (.*)$/) {
8175 $meta->{'parent'} = $1;
8176 $meta->{'file_parent'} = unquote
($2);
8179 exists($meta->{'parent'}) ?
8180 $meta->{'parent'} : $full_rev;
8181 my $linenr_filename =
8182 exists($meta->{'file_parent'}) ?
8183 $meta->{'file_parent'} : unquote
($meta->{'filename'});
8184 my $blamed = href
(action
=> 'blame',
8185 file_name
=> $linenr_filename,
8186 hash_base
=> $linenr_commit);
8187 print "<td class=\"linenr\">";
8188 print $cgi->a({ -href
=> "$blamed#l$orig_lineno",
8189 -class => "linenr" },
8192 print "<td class=\"pre\">" . esc_html
($data) . "</td>\n";
8200 "</table>\n"; # class="blame"
8201 print "</div>\n"; # class="blame_body"
8203 or print "Reading blob failed\n";
8212 sub git_blame_incremental
{
8213 git_blame_common
('incremental');
8216 sub git_blame_data
{
8217 git_blame_common
('data');
8221 my $head = git_get_head_hash
($project);
8223 git_print_page_nav
('','', $head,undef,$head,format_ref_views
('tags'));
8224 git_print_header_div
('summary', $project);
8226 my @tagslist = git_get_tags_list
();
8228 git_tags_body
(\
@tagslist);
8234 my $order = $input_params{'order'};
8235 if (defined $order && $order !~ m/age|name/) {
8236 die_error
(400, "Unknown order parameter");
8239 my $head = git_get_head_hash
($project);
8241 git_print_page_nav
('','', $head,undef,$head,format_ref_views
('refs'));
8242 git_print_header_div
('summary', $project);
8244 my @refslist = git_get_tags_list
(undef, 1, $order);
8246 git_tags_body
(\
@refslist, undef, undef, undef, $head, 1, $order);
8252 my $head = git_get_head_hash
($project);
8254 git_print_page_nav
('','', $head,undef,$head,format_ref_views
('heads'));
8255 git_print_header_div
('summary', $project);
8257 my @headslist = git_get_heads_list
();
8259 git_heads_body
(\
@headslist, $head);
8264 # used both for single remote view and for list of all the remotes
8266 gitweb_check_feature
('remote_heads')
8267 or die_error
(403, "Remote heads view is disabled");
8269 my $head = git_get_head_hash
($project);
8270 my $remote = $input_params{'hash'};
8272 my $remotedata = git_get_remotes_list
($remote);
8273 die_error
(500, "Unable to get remote information") unless defined $remotedata;
8275 unless (%$remotedata) {
8276 die_error
(404, defined $remote ?
8277 "Remote $remote not found" :
8278 "No remotes found");
8281 git_header_html
(undef, undef, -action_extra
=> $remote);
8282 git_print_page_nav
('', '', $head, undef, $head,
8283 format_ref_views
($remote ?
'' : 'remotes'));
8285 fill_remote_heads
($remotedata);
8286 if (defined $remote) {
8287 git_print_header_div
('remotes', "$remote remote for $project");
8288 git_remote_block
($remote, $remotedata->{$remote}, undef, $head);
8290 git_print_header_div
('summary', "$project remotes");
8291 git_remotes_body
($remotedata, undef, $head);
8297 sub git_blob_plain
{
8301 if (!defined $hash) {
8302 if (defined $file_name) {
8303 my $base = $hash_base || git_get_head_hash
($project);
8304 $hash = git_get_hash_by_path
($base, $file_name, "blob")
8305 or die_error
(404, "Cannot find file");
8307 die_error
(400, "No file name defined");
8309 } elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) {
8310 # blobs defined by non-textual hash id's can be cached
8314 defined(my $fd = git_cmd_pipe
"cat-file", "blob", $hash)
8315 or die_error
(500, "Open git-cat-file blob '$hash' failed");
8318 # content-type (can include charset)
8320 ($type, $leader) = blob_contenttype
($fd, $file_name, $type);
8322 # "save as" filename, even when no $file_name is given
8323 my $save_as = "$hash";
8324 if (defined $file_name) {
8325 $save_as = $file_name;
8326 } elsif ($type =~ m/^text\//) {
8330 # With XSS prevention on, blobs of all types except a few known safe
8331 # ones are served with "Content-Disposition: attachment" to make sure
8332 # they don't run in our security domain. For certain image types,
8333 # blob view writes an <img> tag referring to blob_plain view, and we
8334 # want to be sure not to break that by serving the image as an
8335 # attachment (though Firefox 3 doesn't seem to care).
8336 my $sandbox = $prevent_xss &&
8337 $type !~ m!^(?:text/[a-z]+|image/(?:gif|png|jpeg))(?:[ ;]|$)!;
8339 # serve text/* as text/plain
8341 ($type =~ m!^text/[a-z]+\b(.*)$! ||
8342 ($type =~ m!^[a-z]+/[a-z]\+xml\b(.*)$! && -T
$fd))) {
8344 $rest = defined $rest ?
$rest : '';
8345 $type = "text/plain$rest";
8350 -expires
=> $expires,
8351 -content_disposition
=>
8352 ($sandbox ?
'attachment' : 'inline')
8353 . '; filename="' . $save_as . '"');
8354 binmode STDOUT
, ':raw';
8356 print $leader if defined $leader;
8358 while (read($fd, $buf, 32768)) {
8361 binmode STDOUT
, ':utf8'; # as set at the beginning of gitweb.cgi
8370 if (!defined $hash) {
8371 if (defined $file_name) {
8372 my $base = $hash_base || git_get_head_hash
($project);
8373 $hash = git_get_hash_by_path
($base, $file_name, "blob")
8374 or die_error
(404, "Cannot find file");
8377 die_error
(400, "No file name defined");
8379 } elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) {
8380 # blobs defined by non-textual hash id's can be cached
8384 $fullhash = git_get_full_hash
($project, $hash) unless $fullhash;
8386 my $have_blame = gitweb_check_feature
('blame');
8387 defined(my $fd = git_cmd_pipe
"cat-file", "blob", $hash)
8388 or die_error
(500, "Couldn't cat $file_name, $hash");
8390 my $mimetype = blob_mimetype
($fd, $file_name);
8391 # use 'blob_plain' (aka 'raw') view for files that cannot be displayed
8392 if ($mimetype !~ m!^(?:text/|image/(?:gif|png|jpeg)$)! && -B
$fd) {
8394 return git_blob_plain
($mimetype);
8396 # we can have blame only for text/* mimetype
8397 $have_blame &&= ($mimetype =~ m!^text/!);
8399 my $highlight = gitweb_check_feature
('highlight') && defined $highlight_bin;
8400 my $syntax = guess_file_syntax
($fd, $mimetype, $file_name) if $highlight;
8401 my $highlight_mode_active;
8402 ($fd, $highlight_mode_active) = run_highlighter
($fd, $syntax) if $syntax;
8404 git_header_html
(undef, $expires);
8405 my $formats_nav = '';
8406 if (defined $hash_base && (my %co = parse_commit
($hash_base))) {
8407 if (defined $file_name) {
8409 $formats_nav .= tabspan
(
8410 $cgi->a({-href
=> href
(action
=>"blame", -replay
=>1),
8411 -class => "blamelink"},
8415 $formats_nav .= tabspan
(
8416 $cgi->a({-href
=> href
(action
=>"history", -replay
=>1)},
8419 $cgi->a({-href
=> href
(action
=>"blob_plain", -replay
=>1)},
8422 $cgi->a({-href
=> href
(action
=>"blob",
8423 hash_base
=>"HEAD", file_name
=>$file_name)},
8426 $formats_nav .= tabspan
(
8427 $cgi->a({-href
=> href
(action
=>"blob_plain", -replay
=>1)},
8430 git_print_page_nav
('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
8431 git_print_header_div
('commit', esc_html
($co{'title'}), $hash_base);
8433 print "<div class=\"page_nav\">\n" .
8434 "<br/><br/></div>\n" .
8435 "<div class=\"title\">".esc_html
($hash)."</div>\n";
8437 git_print_page_path
($file_name, "blob", $hash_base);
8438 print "<div class=\"title_text\">\n" .
8439 "<table class=\"object_header\">\n";
8440 print "<tr><td>blob</td><td class=\"sha1\">$fullhash</td></tr>\n";
8443 print "<div class=\"page_body\">\n";
8444 if ($mimetype =~ m!^image/!) {
8445 print qq!<img
class="blob" type
="!.esc_attr($mimetype).qq!"!;
8447 print qq! alt
="!.esc_attr($file_name).qq!" title
="!.esc_attr($file_name).qq!"!;
8450 href(action=>"blob_plain
", hash=>$hash,
8451 hash_base=>$hash_base, file_name=>$file_name) .
8455 while (my $line = to_utf8
(scalar <$fd>)) {
8458 $line = untabify
($line);
8459 printf qq!<div
class="pre"><a id
="l%i" href
="%s#l%i" class="linenr">%4i</a> %s</div
>\n!,
8460 $nr, esc_attr
(href
(-replay
=> 1)), $nr, $nr,
8461 $highlight_mode_active ? sanitize
($line) : esc_html
($line, -nbsp
=>1);
8465 or print "Reading blob failed.\n";
8472 if (!defined $hash_base) {
8473 $hash_base = "HEAD";
8475 if (!defined $hash) {
8476 if (defined $file_name) {
8477 $hash = git_get_hash_by_path
($hash_base, $file_name, "tree");
8483 die_error
(404, "No such tree") unless defined($hash);
8484 $fullhash = $hash if !$fullhash && $hash =~ m/^[0-9a-fA-F]{40}$/;
8485 $fullhash = git_get_full_hash
($project, $hash) unless $fullhash;
8487 my $show_sizes = gitweb_check_feature
('show-sizes');
8488 my $have_blame = gitweb_check_feature
('blame');
8493 defined(my $fd = git_cmd_pipe
"ls-tree", '-z',
8494 ($show_sizes ?
'-l' : ()), @extra_options, $hash)
8495 or die_error
(500, "Open git-ls-tree failed");
8496 @entries = map { chomp; to_utf8
($_) } <$fd>;
8498 or die_error
(404, "Reading tree failed");
8503 if (defined $hash_base && (my %co = parse_commit
($hash_base))) {
8504 my $refs = git_get_references
();
8505 my $ref = format_ref_marker
($refs, $co{'id'});
8507 if (defined $file_name) {
8509 tabspan
($cgi->a({-href
=> href
(action
=>"history", -replay
=>1)},
8511 tabspan
($cgi->a({-href
=> href
(action
=>"tree",
8512 hash_base
=>"HEAD", file_name
=>$file_name)},
8515 my $snapshot_links = format_snapshot_links
($hash);
8516 if (defined $snapshot_links) {
8517 # FIXME: Should be available when we have no hash base as well.
8518 push @views_nav, $snapshot_links;
8520 git_print_page_nav
('tree','', $hash_base, undef, undef,
8521 join($barsep, @views_nav));
8522 git_print_header_div
('commit', esc_html
($co{'title'}), $hash_base, undef, $ref);
8525 print "<div class=\"page_nav\">\n";
8526 print "<br/><br/></div>\n";
8527 print "<div class=\"title\">".esc_html
($hash)."</div>\n";
8529 if (defined $file_name) {
8530 $basedir = $file_name;
8531 if ($basedir ne '' && substr($basedir, -1) ne '/') {
8534 git_print_page_path
($file_name, 'tree', $hash_base);
8536 print "<div class=\"title_text\">\n" .
8537 "<table class=\"object_header\">\n";
8538 print "<tr><td>tree</td><td class=\"sha1\">$fullhash</td></tr>\n";
8541 print "<div class=\"page_body\">\n";
8542 print "<table class=\"tree\">\n";
8544 # '..' (top directory) link if possible
8545 if (defined $hash_base &&
8546 defined $file_name && $file_name =~ m![^/]+$!) {
8548 print "<tr class=\"dark\">\n";
8550 print "<tr class=\"light\">\n";
8554 my $up = $file_name;
8555 $up =~ s!/?[^/]+$!!;
8556 undef $up unless $up;
8557 # based on git_print_tree_entry
8558 print '<td class="mode">' . mode_str
('040000') . "</td>\n";
8559 print '<td class="size"> </td>'."\n" if $show_sizes;
8560 print '<td class="list">';
8561 print $cgi->a({-href
=> href
(action
=>"tree",
8562 hash_base
=>$hash_base,
8566 print "<td class=\"link\"></td>\n";
8570 foreach my $line (@entries) {
8571 my %t = parse_ls_tree_line
($line, -z
=> 1, -l
=> $show_sizes);
8574 print "<tr class=\"dark\">\n";
8576 print "<tr class=\"light\">\n";
8580 git_print_tree_entry
(\
%t, $basedir, $hash_base, $have_blame);
8584 print "</table>\n" .
8589 sub sanitize_for_filename
{
8593 $name =~ s/[^[:alnum:]_.-]//g;
8599 my ($project, $hash) = @_;
8601 # path/to/project.git -> project
8602 # path/to/project/.git -> project
8603 my $name = to_utf8
($project);
8604 $name =~ s
,([^/])/*\
.git
$,$1,;
8605 $name = sanitize_for_filename
(basename
($name));
8608 if ($hash =~ /^[0-9a-fA-F]+$/) {
8609 # shorten SHA-1 hash
8610 my $full_hash = git_get_full_hash
($project, $hash);
8611 if ($full_hash =~ /^$hash/ && length($hash) > 7) {
8612 $ver = git_get_short_hash
($project, $hash);
8614 } elsif ($hash =~ m!^refs/tags/(.*)$!) {
8615 # tags don't need shortened SHA-1 hash
8618 # branches and other need shortened SHA-1 hash
8619 my $strip_refs = join '|', map { quotemeta } get_branch_refs
();
8620 if ($hash =~ m!^refs/($strip_refs|remotes)/(.*)$!) {
8621 my $ref_dir = (defined $1) ?
$1 : '';
8624 $ref_dir = sanitize_for_filename
($ref_dir);
8625 # for refs neither in heads nor remotes we want to
8626 # add a ref dir to archive name
8627 if ($ref_dir ne '' and $ref_dir ne 'heads' and $ref_dir ne 'remotes') {
8628 $ver = $ref_dir . '-' . $ver;
8631 $ver .= '-' . git_get_short_hash
($project, $hash);
8633 # special case of sanitization for filename - we change
8634 # slashes to dots instead of dashes
8635 # in case of hierarchical branch names
8637 $ver =~ s/[^[:alnum:]_.-]//g;
8639 # name = project-version_string
8640 $name = "$name-$ver";
8642 return wantarray ?
($name, $name) : $name;
8645 sub exit_if_unmodified_since
{
8646 my ($latest_epoch) = @_;
8649 my $if_modified = $cgi->http('IF_MODIFIED_SINCE');
8650 if (defined $if_modified) {
8652 if (eval { require HTTP
::Date
; 1; }) {
8653 $since = HTTP
::Date
::str2time
($if_modified);
8654 } elsif (eval { require Time
::ParseDate
; 1; }) {
8655 $since = Time
::ParseDate
::parsedate
($if_modified, GMT
=> 1);
8657 if (defined $since && $latest_epoch <= $since) {
8658 my %latest_date = parse_date
($latest_epoch);
8660 -last_modified
=> $latest_date{'rfc2822'},
8661 -status
=> '304 Not Modified');
8668 my $format = $input_params{'snapshot_format'};
8669 if (!@snapshot_fmts) {
8670 die_error
(403, "Snapshots not allowed");
8672 # default to first supported snapshot format
8673 $format ||= $snapshot_fmts[0];
8674 if ($format !~ m/^[a-z0-9]+$/) {
8675 die_error
(400, "Invalid snapshot format parameter");
8676 } elsif (!exists($known_snapshot_formats{$format})) {
8677 die_error
(400, "Unknown snapshot format");
8678 } elsif ($known_snapshot_formats{$format}{'disabled'}) {
8679 die_error
(403, "Snapshot format not allowed");
8680 } elsif (!grep($_ eq $format, @snapshot_fmts)) {
8681 die_error
(403, "Unsupported snapshot format");
8684 my $type = git_get_type
("$hash^{}");
8686 die_error
(404, 'Object does not exist');
8687 } elsif ($type eq 'blob') {
8688 die_error
(400, 'Object is not a tree-ish');
8691 my ($name, $prefix) = snapshot_name
($project, $hash);
8692 my $filename = "$name$known_snapshot_formats{$format}{'suffix'}";
8694 my %co = parse_commit
($hash);
8695 exit_if_unmodified_since
($co{'committer_epoch'}) if %co;
8698 git_cmd
(), 'archive',
8699 "--format=$known_snapshot_formats{$format}{'format'}",
8700 "--prefix=$prefix/", $hash);
8701 if (exists $known_snapshot_formats{$format}{'compressor'}) {
8702 @cmd = ($posix_shell_bin, '-c', quote_command
(@cmd) .
8703 ' | ' . quote_command
(@
{$known_snapshot_formats{$format}{'compressor'}}));
8706 $filename =~ s/(["\\])/\\$1/g;
8709 %latest_date = parse_date
($co{'committer_epoch'}, $co{'committer_tz'});
8713 -type
=> $known_snapshot_formats{$format}{'type'},
8714 -content_disposition
=> 'inline; filename="' . $filename . '"',
8715 %co ?
(-last_modified
=> $latest_date{'rfc2822'}) : (),
8716 -status
=> '200 OK');
8718 defined(my $fd = cmd_pipe
@cmd)
8719 or die_error
(500, "Execute git-archive failed");
8721 binmode STDOUT
, ':raw';
8724 while (read($fd, $buf, 32768)) {
8727 binmode STDOUT
, ':utf8'; # as set at the beginning of gitweb.cgi
8732 sub git_log_generic
{
8733 my ($fmt_name, $body_subr, $base, $parent, $file_name, $file_hash) = @_;
8735 my $head = git_get_head_hash
($project);
8736 if (!defined $base) {
8739 if (!defined $page) {
8742 my $refs = git_get_references
();
8744 my $commit_hash = $base;
8745 if (defined $parent) {
8746 $commit_hash = "$parent..$base";
8749 parse_commits
($commit_hash, 101, (100 * $page),
8750 defined $file_name ?
($file_name, "--full-history") : ());
8753 if (!defined $file_hash && defined $file_name) {
8754 # some commits could have deleted file in question,
8755 # and not have it in tree, but one of them has to have it
8756 for (my $i = 0; $i < @commitlist; $i++) {
8757 $file_hash = git_get_hash_by_path
($commitlist[$i]{'id'}, $file_name);
8758 last if defined $file_hash;
8761 if (defined $file_hash) {
8762 $ftype = git_get_type
($file_hash);
8764 if (defined $file_name && !defined $ftype) {
8765 die_error
(500, "Unknown type of object");
8768 if (defined $file_name) {
8769 %co = parse_commit
($base)
8770 or die_error
(404, "Unknown commit object");
8775 if ($#commitlist >= 100) {
8777 $cgi->a({-href
=> href
(-replay
=>1, page
=>$page+1),
8778 -accesskey
=> "n", -title
=> "Alt-n"}, "next");
8781 my ($patch_max) = gitweb_get_feature
('patches');
8782 if ($patch_max && !defined $file_name) {
8783 if ($patch_max < 0 || @commitlist <= $patch_max) {
8784 $extra = $cgi->a({-href
=> href
(action
=>"patches", -replay
=>1)},
8788 my $paging_nav = format_log_nav
($fmt_name, $page, $#commitlist >= 100, $extra);
8791 local $action = 'fulllog';
8794 git_print_page_nav
($fmt_name,'', $hash,$hash,$hash, $paging_nav);
8795 if (defined $file_name) {
8796 git_print_header_div
('commit', esc_html
($co{'title'}), $base);
8798 git_print_header_div
('summary', $project)
8800 git_print_page_path
($file_name, $ftype, $hash_base)
8801 if (defined $file_name);
8803 $body_subr->(\
@commitlist, 0, 99, $refs, $next_link,
8804 $file_name, $file_hash, $ftype);
8810 git_log_generic
('log', \
&git_log_body
,
8811 $hash, $hash_parent);
8815 $hash ||= $hash_base || "HEAD";
8816 my %co = parse_commit
($hash)
8817 or die_error
(404, "Unknown commit object");
8819 my $parent = $co{'parent'};
8820 my $parents = $co{'parents'}; # listref
8822 # we need to prepare $formats_nav before any parameter munging
8824 if (!defined $parent) {
8826 $formats_nav .= '<span class="parents none">(initial)</span>';
8827 } elsif (@
$parents == 1) {
8828 # single parent commit
8830 '<span class="parents single">(parent: ' .
8831 $cgi->a({-href
=> href
(action
=>"commit",
8833 esc_html
(substr($parent, 0, 7))) .
8838 '<span class="parents multiple">(merge: ' .
8840 $cgi->a({-href
=> href
(action
=>"commit",
8842 esc_html
(substr($_, 0, 7)));
8846 if (gitweb_check_feature
('patches') && @
$parents <= 1) {
8847 $formats_nav .= $barsep . tabspan
(
8848 $cgi->a({-href
=> href
(action
=>"patch", -replay
=>1)},
8852 if (!defined $parent) {
8856 defined(my $fd = git_cmd_pipe
"diff-tree", '-r', "--no-commit-id",
8858 (@
$parents <= 1 ?
$parent : '-c'),
8860 or die_error
(500, "Open git-diff-tree failed");
8861 @difftree = map { chomp; to_utf8
($_) } <$fd>;
8862 close $fd or die_error
(404, "Reading git-diff-tree failed");
8864 # non-textual hash id's can be cached
8866 if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
8869 my $refs = git_get_references
();
8870 my $ref = format_ref_marker
($refs, $co{'id'});
8872 git_header_html
(undef, $expires);
8873 git_print_page_nav
('commit', '',
8874 $hash, $co{'tree'}, $hash,
8877 if (defined $co{'parent'}) {
8878 git_print_header_div
('commitdiff', esc_html
($co{'title'}), $hash, undef, $ref);
8880 git_print_header_div
('tree', esc_html
($co{'title'}), $co{'tree'}, $hash, $ref);
8882 print "<div class=\"title_text\">\n" .
8883 "<table class=\"object_header\">\n";
8884 print "<tr><td>commit</td><td class=\"sha1\">$co{'id'}</td></tr>\n";
8885 git_print_authorship_rows
(\
%co);
8888 "<td class=\"sha1\">" .
8889 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$co{'tree'}, hash_base
=>$hash),
8890 class => "list"}, $co{'tree'}) .
8892 "<td class=\"link\">" .
8893 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$co{'tree'}, hash_base
=>$hash)},
8895 my $snapshot_links = format_snapshot_links
($hash);
8896 if (defined $snapshot_links) {
8897 print $barsep . $snapshot_links;
8902 foreach my $par (@
$parents) {
8905 "<td class=\"sha1\">" .
8906 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$par),
8907 class => "list"}, $par) .
8909 "<td class=\"link\">" .
8910 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$par)}, "commit") .
8912 $cgi->a({-href
=> href
(action
=>"commitdiff", hash
=>$hash, hash_parent
=>$par)}, "diff") .
8919 print "<div class=\"page_body\">\n";
8920 git_print_log
($co{'comment'});
8923 git_difftree_body
(\
@difftree, $hash, @
$parents);
8929 # object is defined by:
8930 # - hash or hash_base alone
8931 # - hash_base and file_name
8934 # - hash or hash_base alone
8935 if ($hash || ($hash_base && !defined $file_name)) {
8936 my $object_id = $hash || $hash_base;
8938 defined(my $fd = git_cmd_pipe
'cat-file', '-t', $object_id)
8939 or die_error
(404, "Object does not exist");
8943 or die_error
(404, "Object does not exist");
8945 # - hash_base and file_name
8946 } elsif ($hash_base && defined $file_name) {
8947 $file_name =~ s
,/+$,,;
8949 system(git_cmd
(), "cat-file", '-e', $hash_base) == 0
8950 or die_error
(404, "Base object does not exist");
8952 # here errors should not happen
8953 defined(my $fd = git_cmd_pipe
"ls-tree", $hash_base, "--", $file_name)
8954 or die_error
(500, "Open git-ls-tree failed");
8955 my $line = to_utf8
(scalar <$fd>);
8958 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'
8959 unless ($line && $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t/) {
8960 die_error
(404, "File or directory for given base does not exist");
8965 die_error
(400, "Not enough information to find object");
8968 print $cgi->redirect(-uri
=> href
(action
=>$type, -full
=>1,
8969 hash
=>$hash, hash_base
=>$hash_base,
8970 file_name
=>$file_name),
8971 -status
=> '302 Found');
8975 my $format = shift || 'html';
8976 my $diff_style = $input_params{'diff_style'} || 'inline';
8983 # preparing $fd and %diffinfo for git_patchset_body
8985 if (defined $hash_base && defined $hash_parent_base) {
8986 if (defined $file_name) {
8988 defined($fd = git_cmd_pipe
"diff-tree", '-r', @diff_opts,
8989 $hash_parent_base, $hash_base,
8990 "--", (defined $file_parent ?
$file_parent : ()), $file_name)
8991 or die_error
(500, "Open git-diff-tree failed");
8992 @difftree = map { chomp; to_utf8
($_) } <$fd>;
8994 or die_error
(404, "Reading git-diff-tree failed");
8996 or die_error
(404, "Blob diff not found");
8998 } elsif (defined $hash &&
8999 $hash =~ /[0-9a-fA-F]{40}/) {
9000 # try to find filename from $hash
9002 # read filtered raw output
9003 defined($fd = git_cmd_pipe
"diff-tree", '-r', @diff_opts,
9004 $hash_parent_base, $hash_base, "--")
9005 or die_error
(500, "Open git-diff-tree failed");
9007 # ':100644 100644 03b21826... 3b93d5e7... M ls-files.c'
9009 grep { /^:[0-7]{6} [0-7]{6} [0-9a-fA-F]{40} $hash/ }
9010 map { chomp; to_utf8
($_) } <$fd>;
9012 or die_error
(404, "Reading git-diff-tree failed");
9014 or die_error
(404, "Blob diff not found");
9017 die_error
(400, "Missing one of the blob diff parameters");
9020 if (@difftree > 1) {
9021 die_error
(400, "Ambiguous blob diff specification");
9024 %diffinfo = parse_difftree_raw_line
($difftree[0]);
9025 $file_parent ||= $diffinfo{'from_file'} || $file_name;
9026 $file_name ||= $diffinfo{'to_file'};
9028 $hash_parent ||= $diffinfo{'from_id'};
9029 $hash ||= $diffinfo{'to_id'};
9031 # non-textual hash id's can be cached
9032 if ($hash_base =~ m/^[0-9a-fA-F]{40}$/ &&
9033 $hash_parent_base =~ m/^[0-9a-fA-F]{40}$/) {
9038 defined($fd = git_cmd_pipe
"diff-tree", '-r', @diff_opts,
9039 '-p', ($format eq 'html' ?
"--full-index" : ()),
9040 $hash_parent_base, $hash_base,
9041 "--", (defined $file_parent ?
$file_parent : ()), $file_name)
9042 or die_error
(500, "Open git-diff-tree failed");
9045 # old/legacy style URI -- not generated anymore since 1.4.3.
9047 die_error
('404 Not Found', "Missing one of the blob diff parameters")
9051 if ($format eq 'html') {
9053 $cgi->a({-href
=> href
(action
=>"blobdiff_plain", -replay
=>1)},
9055 $formats_nav .= diff_style_nav
($diff_style);
9056 git_header_html
(undef, $expires);
9057 if (defined $hash_base && (my %co = parse_commit
($hash_base))) {
9058 git_print_page_nav
('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
9059 git_print_header_div
('commit', esc_html
($co{'title'}), $hash_base);
9061 print "<div class=\"page_nav\"><br/>$formats_nav<br/></div>\n";
9062 print "<div class=\"title\">".esc_html
("$hash vs $hash_parent")."</div>\n";
9064 if (defined $file_name) {
9065 git_print_page_path
($file_name, "blob", $hash_base);
9067 print "<div class=\"page_path\"></div>\n";
9070 } elsif ($format eq 'plain') {
9072 -type
=> 'text/plain',
9073 -charset
=> 'utf-8',
9074 -expires
=> $expires,
9075 -content_disposition
=> 'inline; filename="' . "$file_name" . '.patch"');
9077 print "X-Git-Url: " . $cgi->self_url() . "\n\n";
9080 die_error
(400, "Unknown blobdiff format");
9084 if ($format eq 'html') {
9085 print "<div class=\"page_body\">\n";
9087 git_patchset_body
($fd, $diff_style,
9088 [ \
%diffinfo ], $hash_base, $hash_parent_base);
9091 print "</div>\n"; # class="page_body"
9095 while (my $line = to_utf8
(scalar <$fd>)) {
9096 $line =~ s!a/($hash|$hash_parent)!'a/'.esc_path($diffinfo{'from_file'})!eg;
9097 $line =~ s!b/($hash|$hash_parent)!'b/'.esc_path($diffinfo{'to_file'})!eg;
9101 last if $line =~ m!^\+\+\+!;
9110 sub git_blobdiff_plain
{
9111 git_blobdiff
('plain');
9114 # assumes that it is added as later part of already existing navigation,
9115 # so it returns "| foo | bar" rather than just "foo | bar"
9116 sub diff_style_nav
{
9117 my ($diff_style, $is_combined) = @_;
9118 $diff_style ||= 'inline';
9120 return "" if ($is_combined);
9122 my @styles = (inline
=> 'inline', 'sidebyside' => 'side by side');
9123 my %styles = @styles;
9125 @styles[ map { $_ * 2 } 0..$#styles/2 ];
9127 return $barsep . '<span class="diffstyles">' . join($barsep,
9130 '<span class="diffstyle selected">' . $styles{$_} . '</span>' :
9131 '<span class="diffstyle">' .
9132 $cgi->a({-href
=> href
(-replay
=>1, diff_style
=> $_)}, $styles{$_}) .
9134 } @styles) . '</span>';
9137 sub git_commitdiff
{
9139 my $format = $params{-format
} || 'html';
9140 my $diff_style = $input_params{'diff_style'} || 'inline';
9142 my ($patch_max) = gitweb_get_feature
('patches');
9143 if ($format eq 'patch') {
9144 die_error
(403, "Patch view not allowed") unless $patch_max;
9147 $hash ||= $hash_base || "HEAD";
9148 my %co = parse_commit
($hash)
9149 or die_error
(404, "Unknown commit object");
9151 # choose format for commitdiff for merge
9152 if (! defined $hash_parent && @
{$co{'parents'}} > 1) {
9153 $hash_parent = '--cc';
9155 # we need to prepare $formats_nav before almost any parameter munging
9157 if ($format eq 'html') {
9158 $formats_nav = tabspan
(
9159 $cgi->a({-href
=> href
(action
=>"commitdiff_plain", -replay
=>1)},
9161 if ($patch_max && @
{$co{'parents'}} <= 1) {
9162 $formats_nav .= $barsep . tabspan
(
9163 $cgi->a({-href
=> href
(action
=>"patch", -replay
=>1)},
9166 $formats_nav .= diff_style_nav
($diff_style, @
{$co{'parents'}} > 1);
9168 if (defined $hash_parent &&
9169 $hash_parent ne '-c' && $hash_parent ne '--cc') {
9170 # commitdiff with two commits given
9171 my $hash_parent_short = $hash_parent;
9172 if ($hash_parent =~ m/^[0-9a-fA-F]{40}$/) {
9173 $hash_parent_short = substr($hash_parent, 0, 7);
9175 $formats_nav .= $spcsep . '<span class="parents multiple">' .
9177 for (my $i = 0; $i < @
{$co{'parents'}}; $i++) {
9178 if ($co{'parents'}[$i] eq $hash_parent) {
9179 $formats_nav .= ' parent ' . ($i+1);
9183 $formats_nav .= ': ' .
9184 $cgi->a({-href
=> href
(-replay
=>1,
9185 hash
=>$hash_parent, hash_base
=>undef)},
9186 esc_html
($hash_parent_short)) .
9188 } elsif (!$co{'parent'}) {
9190 $formats_nav .= $spcsep . '<span class="parents none">(initial)</span>';
9191 } elsif (scalar @
{$co{'parents'}} == 1) {
9192 # single parent commit
9193 $formats_nav .= $spcsep .
9194 '<span class="parents single">(parent: ' .
9195 $cgi->a({-href
=> href
(-replay
=>1,
9196 hash
=>$co{'parent'}, hash_base
=>undef)},
9197 esc_html
(substr($co{'parent'}, 0, 7))) .
9201 if ($hash_parent eq '--cc') {
9202 $formats_nav .= $barsep . tabspan
(
9203 $cgi->a({-href
=> href
(-replay
=>1,
9204 hash
=>$hash, hash_parent
=>'-c')},
9206 } else { # $hash_parent eq '-c'
9207 $formats_nav .= $barsep . tabspan
(
9208 $cgi->a({-href
=> href
(-replay
=>1,
9209 hash
=>$hash, hash_parent
=>'--cc')},
9212 $formats_nav .= $spcsep .
9213 '<span class="parents multiple">(merge: ' .
9215 $cgi->a({-href
=> href
(-replay
=>1,
9216 hash
=>$_, hash_base
=>undef)},
9217 esc_html
(substr($_, 0, 7)));
9218 } @
{$co{'parents'}} ) .
9223 my $hash_parent_param = $hash_parent;
9224 if (!defined $hash_parent_param) {
9225 # --cc for multiple parents, --root for parentless
9226 $hash_parent_param =
9227 @
{$co{'parents'}} > 1 ?
'--cc' : $co{'parent'} || '--root';
9233 if ($format eq 'html') {
9234 defined($fd = git_cmd_pipe
"diff-tree", '-r', @diff_opts,
9235 "--no-commit-id", "--patch-with-raw", "--full-index",
9236 $hash_parent_param, $hash, "--")
9237 or die_error
(500, "Open git-diff-tree failed");
9239 while (my $line = to_utf8
(scalar <$fd>)) {
9241 # empty line ends raw part of diff-tree output
9243 push @difftree, scalar parse_difftree_raw_line
($line);
9246 } elsif ($format eq 'plain') {
9247 defined($fd = git_cmd_pipe
"diff-tree", '-r', @diff_opts,
9248 '-p', $hash_parent_param, $hash, "--")
9249 or die_error
(500, "Open git-diff-tree failed");
9250 } elsif ($format eq 'patch') {
9251 # For commit ranges, we limit the output to the number of
9252 # patches specified in the 'patches' feature.
9253 # For single commits, we limit the output to a single patch,
9254 # diverging from the git-format-patch default.
9255 my @commit_spec = ();
9257 if ($patch_max > 0) {
9258 push @commit_spec, "-$patch_max";
9260 push @commit_spec, '-n', "$hash_parent..$hash";
9262 if ($params{-single
}) {
9263 push @commit_spec, '-1';
9265 if ($patch_max > 0) {
9266 push @commit_spec, "-$patch_max";
9268 push @commit_spec, "-n";
9270 push @commit_spec, '--root', $hash;
9272 defined($fd = git_cmd_pipe
"format-patch", @diff_opts,
9273 '--encoding=utf8', '--stdout', @commit_spec)
9274 or die_error
(500, "Open git-format-patch failed");
9276 die_error
(400, "Unknown commitdiff format");
9279 # non-textual hash id's can be cached
9281 if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
9285 # write commit message
9286 if ($format eq 'html') {
9287 my $refs = git_get_references
();
9288 my $ref = format_ref_marker
($refs, $co{'id'});
9290 git_header_html
(undef, $expires);
9291 git_print_page_nav
('commitdiff','', $hash,$co{'tree'},$hash, $formats_nav);
9292 git_print_header_div
('commit', esc_html
($co{'title'}), $hash, undef, $ref);
9293 print "<div class=\"title_text\">\n" .
9294 "<table class=\"object_header\">\n";
9295 git_print_authorship_rows
(\
%co);
9298 print "<div class=\"page_body\">\n";
9299 if (@
{$co{'comment'}} > 1) {
9300 print "<div class=\"log\">\n";
9301 git_print_log
($co{'comment'}, -final_empty_line
=> 1, -remove_title
=> 1);
9302 print "</div>\n"; # class="log"
9305 } elsif ($format eq 'plain') {
9306 my $refs = git_get_references
("tags");
9307 my $tagname = git_get_rev_name_tags
($hash);
9308 my $filename = basename
($project) . "-$hash.patch";
9311 -type
=> 'text/plain',
9312 -charset
=> 'utf-8',
9313 -expires
=> $expires,
9314 -content_disposition
=> 'inline; filename="' . "$filename" . '"');
9315 my %ad = parse_date
($co{'author_epoch'}, $co{'author_tz'});
9316 print "From: " . to_utf8
($co{'author'}) . "\n";
9317 print "Date: $ad{'rfc2822'} ($ad{'tz_local'})\n";
9318 print "Subject: " . to_utf8
($co{'title'}) . "\n";
9320 print "X-Git-Tag: $tagname\n" if $tagname;
9321 print "X-Git-Url: " . $cgi->self_url() . "\n\n";
9323 foreach my $line (@
{$co{'comment'}}) {
9324 print to_utf8
($line) . "\n";
9327 } elsif ($format eq 'patch') {
9328 my $filename = basename
($project) . "-$hash.patch";
9331 -type
=> 'text/plain',
9332 -charset
=> 'utf-8',
9333 -expires
=> $expires,
9334 -content_disposition
=> 'inline; filename="' . "$filename" . '"');
9338 if ($format eq 'html') {
9339 my $use_parents = !defined $hash_parent ||
9340 $hash_parent eq '-c' || $hash_parent eq '--cc';
9341 git_difftree_body
(\
@difftree, $hash,
9342 $use_parents ? @
{$co{'parents'}} : $hash_parent);
9345 git_patchset_body
($fd, $diff_style,
9347 $use_parents ? @
{$co{'parents'}} : $hash_parent);
9349 print "</div>\n"; # class="page_body"
9352 } elsif ($format eq 'plain') {
9357 or print "Reading git-diff-tree failed\n";
9358 } elsif ($format eq 'patch') {
9363 or print "Reading git-format-patch failed\n";
9367 sub git_commitdiff_plain
{
9368 git_commitdiff
(-format
=> 'plain');
9371 # format-patch-style patches
9373 git_commitdiff
(-format
=> 'patch', -single
=> 1);
9377 git_commitdiff
(-format
=> 'patch');
9381 git_log_generic
('history', \
&git_history_body
,
9382 $hash_base, $hash_parent_base,
9387 $searchtype ||= 'commit';
9389 # check if appropriate features are enabled
9390 gitweb_check_feature
('search')
9391 or die_error
(403, "Search is disabled");
9392 if ($searchtype eq 'pickaxe') {
9393 # pickaxe may take all resources of your box and run for several minutes
9394 # with every query - so decide by yourself how public you make this feature
9395 gitweb_check_feature
('pickaxe')
9396 or die_error
(403, "Pickaxe search is disabled");
9398 if ($searchtype eq 'grep') {
9399 # grep search might be potentially CPU-intensive, too
9400 gitweb_check_feature
('grep')
9401 or die_error
(403, "Grep search is disabled");
9404 if (!defined $searchtext) {
9405 die_error
(400, "Text field is empty");
9407 if (!defined $hash) {
9408 $hash = git_get_head_hash
($project);
9410 my %co = parse_commit
($hash);
9412 die_error
(404, "Unknown commit object");
9414 if (!defined $page) {
9418 if ($searchtype eq 'commit' ||
9419 $searchtype eq 'author' ||
9420 $searchtype eq 'committer') {
9421 git_search_message
(%co);
9422 } elsif ($searchtype eq 'pickaxe') {
9423 git_search_changes
(%co);
9424 } elsif ($searchtype eq 'grep') {
9425 git_search_files
(%co);
9427 die_error
(400, "Unknown search type");
9431 sub git_search_help
{
9433 git_print_page_nav
('','', $hash,$hash,$hash);
9435 <div class="search_help">
9436 <p><strong>Pattern</strong> is by default a normal string that is matched precisely (but without
9437 regard to case, except in the case of pickaxe). However, when you check the <em>re</em> checkbox,
9438 the pattern entered is recognized as the POSIX extended
9439 <a href="http://en.wikipedia.org/wiki/Regular_expression">regular expression</a> (also case
9442 <dt><b>commit</b></dt>
9443 <dd>The commit messages and authorship information will be scanned for the given pattern.</dd>
9445 my $have_grep = gitweb_check_feature
('grep');
9448 <dt><b>grep</b></dt>
9449 <dd>All files in the currently selected tree (HEAD unless you are explicitly browsing
9450 a different one) are searched for the given pattern. On large trees, this search can take
9451 a while and put some strain on the server, so please use it with some consideration. Note that
9452 due to git-grep peculiarity, currently if regexp mode is turned off, the matches are
9453 case-sensitive.</dd>
9457 <dt><b>author</b></dt>
9458 <dd>Name and e-mail of the change author and date of birth of the patch will be scanned for the given pattern.</dd>
9459 <dt><b>committer</b></dt>
9460 <dd>Name and e-mail of the committer and date of commit will be scanned for the given pattern.</dd>
9462 my $have_pickaxe = gitweb_check_feature
('pickaxe');
9463 if ($have_pickaxe) {
9465 <dt><b>pickaxe</b></dt>
9466 <dd>All commits that caused the string to appear or disappear from any file (changes that
9467 added, removed or "modified" the string) will be listed. This search can take a while and
9468 takes a lot of strain on the server, so please use it wisely. Note that since you may be
9469 interested even in changes just changing the case as well, this search is case sensitive.</dd>
9472 print "</dl>\n</div>\n";
9477 git_log_generic
('shortlog', \
&git_shortlog_body
,
9478 $hash, $hash_parent);
9481 ## ......................................................................
9482 ## feeds (RSS, Atom; OPML)
9485 my $format = shift || 'atom';
9486 my $have_blame = gitweb_check_feature
('blame');
9488 # Atom: http://www.atomenabled.org/developers/syndication/
9489 # RSS: http://www.notestips.com/80256B3A007F2692/1/NAMO5P9UPQ
9490 if ($format ne 'rss' && $format ne 'atom') {
9491 die_error
(400, "Unknown web feed format");
9494 # log/feed of current (HEAD) branch, log of given branch, history of file/directory
9495 my $head = $hash || 'HEAD';
9496 my @commitlist = parse_commits
($head, 150, 0, $file_name);
9500 my $content_type = "application/$format+xml";
9501 if (defined $cgi->http('HTTP_ACCEPT') &&
9502 $cgi->Accept('text/xml') > $cgi->Accept($content_type)) {
9503 # browser (feed reader) prefers text/xml
9504 $content_type = 'text/xml';
9506 if (defined($commitlist[0])) {
9507 %latest_commit = %{$commitlist[0]};
9508 my $latest_epoch = $latest_commit{'committer_epoch'};
9509 exit_if_unmodified_since
($latest_epoch);
9510 %latest_date = parse_date
($latest_epoch, $latest_commit{'committer_tz'});
9513 -type
=> $content_type,
9514 -charset
=> 'utf-8',
9515 %latest_date ?
(-last_modified
=> $latest_date{'rfc2822'}) : (),
9516 -status
=> '200 OK');
9518 # Optimization: skip generating the body if client asks only
9519 # for Last-Modified date.
9520 return if ($cgi->request_method() eq 'HEAD');
9523 my $title = "$site_name - $project/$action";
9524 my $feed_type = 'log';
9525 if (defined $hash) {
9526 $title .= " - '$hash'";
9527 $feed_type = 'branch log';
9528 if (defined $file_name) {
9529 $title .= " :: $file_name";
9530 $feed_type = 'history';
9532 } elsif (defined $file_name) {
9533 $title .= " - $file_name";
9534 $feed_type = 'history';
9536 $title .= " $feed_type";
9537 $title = esc_html
($title);
9538 my $descr = git_get_project_description
($project);
9539 if (defined $descr) {
9540 $descr = esc_html
($descr);
9542 $descr = "$project " .
9543 ($format eq 'rss' ?
'RSS' : 'Atom') .
9546 my $owner = git_get_project_owner
($project);
9547 $owner = esc_html
($owner);
9551 if (defined $file_name) {
9552 $alt_url = href
(-full
=>1, action
=>"history", hash
=>$hash, file_name
=>$file_name);
9553 } elsif (defined $hash) {
9554 $alt_url = href
(-full
=>1, action
=>"log", hash
=>$hash);
9556 $alt_url = href
(-full
=>1, action
=>"summary");
9558 print qq!<?xml version
="1.0" encoding
="utf-8"?
>\n!;
9559 if ($format eq 'rss') {
9561 <rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
9564 print "<title>$title</title>\n" .
9565 "<link>$alt_url</link>\n" .
9566 "<description>$descr</description>\n" .
9567 "<language>en</language>\n" .
9568 # project owner is responsible for 'editorial' content
9569 "<managingEditor>$owner</managingEditor>\n";
9570 if (defined $logo || defined $favicon) {
9571 # prefer the logo to the favicon, since RSS
9572 # doesn't allow both
9573 my $img = esc_url
($logo || $favicon);
9575 "<url>$img</url>\n" .
9576 "<title>$title</title>\n" .
9577 "<link>$alt_url</link>\n" .
9581 print "<pubDate>$latest_date{'rfc2822'}</pubDate>\n";
9582 print "<lastBuildDate>$latest_date{'rfc2822'}</lastBuildDate>\n";
9584 print "<generator>gitweb v.$version/$git_version</generator>\n";
9585 } elsif ($format eq 'atom') {
9587 <feed xmlns="http://www.w3.org/2005/Atom">
9589 print "<title>$title</title>\n" .
9590 "<subtitle>$descr</subtitle>\n" .
9591 '<link rel="alternate" type="text/html" href="' .
9592 $alt_url . '" />' . "\n" .
9593 '<link rel="self" type="' . $content_type . '" href="' .
9594 $cgi->self_url() . '" />' . "\n" .
9595 "<id>" . href
(-full
=>1) . "</id>\n" .
9596 # use project owner for feed author
9597 '<author><name>'. email_obfuscate
($owner) . '</name></author>\n';
9598 if (defined $favicon) {
9599 print "<icon>" . esc_url
($favicon) . "</icon>\n";
9601 if (defined $logo) {
9602 # not twice as wide as tall: 72 x 27 pixels
9603 print "<logo>" . esc_url
($logo) . "</logo>\n";
9605 if (! %latest_date) {
9606 # dummy date to keep the feed valid until commits trickle in:
9607 print "<updated>1970-01-01T00:00:00Z</updated>\n";
9609 print "<updated>$latest_date{'iso-8601'}</updated>\n";
9611 print "<generator version='$version/$git_version'>gitweb</generator>\n";
9615 for (my $i = 0; $i <= $#commitlist; $i++) {
9616 my %co = %{$commitlist[$i]};
9617 my $commit = $co{'id'};
9618 # we read 150, we always show 30 and the ones more recent than 48 hours
9619 if (($i >= 20) && ((time - $co{'author_epoch'}) > 48*60*60)) {
9622 my %cd = parse_date
($co{'author_epoch'}, $co{'author_tz'});
9624 # get list of changed files
9625 defined(my $fd = git_cmd_pipe
"diff-tree", '-r', @diff_opts,
9626 $co{'parent'} || "--root",
9627 $co{'id'}, "--", (defined $file_name ?
$file_name : ()))
9629 my @difftree = map { chomp; to_utf8
($_) } <$fd>;
9633 # print element (entry, item)
9634 my $co_url = href
(-full
=>1, action
=>"commitdiff", hash
=>$commit);
9635 if ($format eq 'rss') {
9637 "<title>" . esc_html
($co{'title'}) . "</title>\n" .
9638 "<author>" . esc_html
($co{'author'}) . "</author>\n" .
9639 "<pubDate>$cd{'rfc2822'}</pubDate>\n" .
9640 "<guid isPermaLink=\"true\">$co_url</guid>\n" .
9641 "<link>$co_url</link>\n" .
9642 "<description>" . esc_html
($co{'title'}) . "</description>\n" .
9643 "<content:encoded>" .
9645 } elsif ($format eq 'atom') {
9647 "<title type=\"html\">" . esc_html
($co{'title'}) . "</title>\n" .
9648 "<updated>$cd{'iso-8601'}</updated>\n" .
9650 " <name>" . esc_html
($co{'author_name'}) . "</name>\n";
9651 if ($co{'author_email'}) {
9652 print " <email>" . esc_html
($co{'author_email'}) . "</email>\n";
9654 print "</author>\n" .
9655 # use committer for contributor
9657 " <name>" . esc_html
($co{'committer_name'}) . "</name>\n";
9658 if ($co{'committer_email'}) {
9659 print " <email>" . esc_html
($co{'committer_email'}) . "</email>\n";
9661 print "</contributor>\n" .
9662 "<published>$cd{'iso-8601'}</published>\n" .
9663 "<link rel=\"alternate\" type=\"text/html\" href=\"$co_url\" />\n" .
9664 "<id>$co_url</id>\n" .
9665 "<content type=\"xhtml\" xml:base=\"" . esc_url
($my_url) . "\">\n" .
9666 "<div xmlns=\"http://www.w3.org/1999/xhtml\">\n";
9668 my $comment = $co{'comment'};
9670 foreach my $line (@
$comment) {
9671 $line = esc_html
($line);
9674 print "</pre><ul>\n";
9675 foreach my $difftree_line (@difftree) {
9676 my %difftree = parse_difftree_raw_line
($difftree_line);
9677 next if !$difftree{'from_id'};
9679 my $file = $difftree{'file'} || $difftree{'to_file'};
9683 $cgi->a({-href
=> href
(-full
=>1, action
=>"blobdiff",
9684 hash
=>$difftree{'to_id'}, hash_parent
=>$difftree{'from_id'},
9685 hash_base
=>$co{'id'}, hash_parent_base
=>$co{'parent'},
9686 file_name
=>$file, file_parent
=>$difftree{'from_file'}),
9687 -title
=> "diff"}, 'D');
9689 print $cgi->a({-href
=> href
(-full
=>1, action
=>"blame",
9690 file_name
=>$file, hash_base
=>$commit),
9691 -class => "blamelink",
9692 -title
=> "blame"}, 'B');
9694 # if this is not a feed of a file history
9695 if (!defined $file_name || $file_name ne $file) {
9696 print $cgi->a({-href
=> href
(-full
=>1, action
=>"history",
9697 file_name
=>$file, hash
=>$commit),
9698 -title
=> "history"}, 'H');
9700 $file = esc_path
($file);
9704 if ($format eq 'rss') {
9705 print "</ul>]]>\n" .
9706 "</content:encoded>\n" .
9708 } elsif ($format eq 'atom') {
9709 print "</ul>\n</div>\n" .
9716 if ($format eq 'rss') {
9717 print "</channel>\n</rss>\n";
9718 } elsif ($format eq 'atom') {
9732 my @list = git_get_projects_list
($project_filter, $strict_export);
9734 die_error
(404, "No projects found");
9738 -type
=> 'text/xml',
9739 -charset
=> 'utf-8',
9740 -content_disposition
=> 'inline; filename="opml.xml"');
9742 my $title = esc_html
($site_name);
9743 my $filter = " within subdirectory ";
9744 if (defined $project_filter) {
9745 $filter .= esc_html
($project_filter);
9750 <?xml version="1.0" encoding="utf-8"?>
9751 <opml version="1.0">
9753 <title>$title OPML Export$filter</title>
9756 <outline text="git RSS feeds">
9759 foreach my $pr (@list) {
9761 my $head = git_get_head_hash
($proj{'path'});
9762 if (!defined $head) {
9765 $git_dir = "$projectroot/$proj{'path'}";
9766 my %co = parse_commit
($head);
9771 my $path = esc_html
(chop_str
($proj{'path'}, 25, 5));
9772 my $rss = href
('project' => $proj{'path'}, 'action' => 'rss', -full
=> 1);
9773 my $html = href
('project' => $proj{'path'}, 'action' => 'summary', -full
=> 1);
9774 print "<outline type=\"rss\" text=\"$path\" title=\"$path\" xmlUrl=\"$rss\" htmlUrl=\"$html\"/>\n";