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 ($mu, $hl, $subroutine) = ($my_uri, $home_link, '');
1679 $subroutine .= '$my_uri = $mu;' if defined $my_uri && $my_uri ne '';
1680 $subroutine .= '$home_link = $hl;' if defined $home_link && $home_link ne '';
1681 $evaluate_uri_force = eval "sub {$subroutine}" if $subroutine;
1685 $pre_listen_hook->()
1686 if $pre_listen_hook;
1689 while ($cgi = $CGI->new()) {
1690 $pre_dispatch_hook->()
1691 if $pre_dispatch_hook;
1693 eval {run_request
()};
1695 $post_dispatch_hook->()
1696 if $post_dispatch_hook;
1699 last REQUEST
if ($is_last_request->());
1707 if (defined caller) {
1708 # wrapped in a subroutine processing requests,
1709 # e.g. mod_perl with ModPerl::Registry, or PSGI with Plack::App::WrapCGI
1712 # pure CGI script, serving single request
1716 ## ======================================================================
1719 # possible values of extra options
1720 # -full => 0|1 - use absolute/full URL ($my_uri/$my_url as base)
1721 # -replay => 1 - start from a current view (replay with modifications)
1722 # -path_info => 0|1 - don't use/use path_info URL (if possible)
1723 # -anchor => ANCHOR - add #ANCHOR to end of URL, implies -replay if used alone
1726 # default is to use -absolute url() i.e. $my_uri
1727 my $href = $params{-full
} ?
$my_url : $my_uri;
1729 # implicit -replay, must be first of implicit params
1730 $params{-replay
} = 1 if (keys %params == 1 && $params{-anchor
});
1732 $params{'project'} = $project unless exists $params{'project'};
1734 if ($params{-replay
}) {
1735 while (my ($name, $symbol) = each %cgi_param_mapping) {
1736 if (!exists $params{$name}) {
1737 $params{$name} = $input_params{$name};
1742 my $use_pathinfo = gitweb_check_feature
('pathinfo');
1743 if (defined $params{'project'} &&
1744 (exists $params{-path_info
} ?
$params{-path_info
} : $use_pathinfo)) {
1745 # try to put as many parameters as possible in PATH_INFO:
1748 # - hash_parent or hash_parent_base:/file_parent
1749 # - hash or hash_base:/filename
1750 # - the snapshot_format as an appropriate suffix
1752 # When the script is the root DirectoryIndex for the domain,
1753 # $href here would be something like http://gitweb.example.com/
1754 # Thus, we strip any trailing / from $href, to spare us double
1755 # slashes in the final URL
1758 # Then add the project name, if present
1759 $href .= "/".esc_path_info
($params{'project'});
1760 delete $params{'project'};
1762 # since we destructively absorb parameters, we keep this
1763 # boolean that remembers if we're handling a snapshot
1764 my $is_snapshot = $params{'action'} eq 'snapshot';
1766 # Summary just uses the project path URL, any other action is
1768 if (defined $params{'action'}) {
1769 $href .= "/".esc_path_info
($params{'action'})
1770 unless $params{'action'} eq 'summary';
1771 delete $params{'action'};
1774 # Next, we put hash_parent_base:/file_parent..hash_base:/file_name,
1775 # stripping nonexistent or useless pieces
1776 $href .= "/" if ($params{'hash_base'} || $params{'hash_parent_base'}
1777 || $params{'hash_parent'} || $params{'hash'});
1778 if (defined $params{'hash_base'}) {
1779 if (defined $params{'hash_parent_base'}) {
1780 $href .= esc_path_info
($params{'hash_parent_base'});
1781 # skip the file_parent if it's the same as the file_name
1782 if (defined $params{'file_parent'}) {
1783 if (defined $params{'file_name'} && $params{'file_parent'} eq $params{'file_name'}) {
1784 delete $params{'file_parent'};
1785 } elsif ($params{'file_parent'} !~ /\.\./) {
1786 $href .= ":/".esc_path_info
($params{'file_parent'});
1787 delete $params{'file_parent'};
1791 delete $params{'hash_parent'};
1792 delete $params{'hash_parent_base'};
1793 } elsif (defined $params{'hash_parent'}) {
1794 $href .= esc_path_info
($params{'hash_parent'}). "..";
1795 delete $params{'hash_parent'};
1798 $href .= esc_path_info
($params{'hash_base'});
1799 if (defined $params{'file_name'} && $params{'file_name'} !~ /\.\./) {
1800 $href .= ":/".esc_path_info
($params{'file_name'});
1801 delete $params{'file_name'};
1803 delete $params{'hash'};
1804 delete $params{'hash_base'};
1805 } elsif (defined $params{'hash'}) {
1806 $href .= esc_path_info
($params{'hash'});
1807 delete $params{'hash'};
1810 # If the action was a snapshot, we can absorb the
1811 # snapshot_format parameter too
1813 my $fmt = $params{'snapshot_format'};
1814 # snapshot_format should always be defined when href()
1815 # is called, but just in case some code forgets, we
1816 # fall back to the default
1817 $fmt ||= $snapshot_fmts[0];
1818 $href .= $known_snapshot_formats{$fmt}{'suffix'};
1819 delete $params{'snapshot_format'};
1823 # now encode the parameters explicitly
1825 for (my $i = 0; $i < @cgi_param_mapping; $i += 2) {
1826 my ($name, $symbol) = ($cgi_param_mapping[$i], $cgi_param_mapping[$i+1]);
1827 if (defined $params{$name}) {
1828 if (ref($params{$name}) eq "ARRAY") {
1829 foreach my $par (@
{$params{$name}}) {
1830 push @result, $symbol . "=" . esc_param
($par);
1833 push @result, $symbol . "=" . esc_param
($params{$name});
1837 $href .= "?" . join(';', @result) if scalar @result;
1839 # final transformation: trailing spaces must be escaped (URI-encoded)
1840 $href =~ s/(\s+)$/CGI::escape($1)/e;
1842 if ($params{-anchor
}) {
1843 $href .= "#".esc_param
($params{-anchor
});
1850 ## ======================================================================
1851 ## validation, quoting/unquoting and escaping
1853 sub is_valid_action
{
1855 return undef unless exists $actions{$input};
1859 sub is_valid_project
{
1862 return unless defined $input;
1863 if (!is_valid_pathname
($input) ||
1864 !(-d
"$projectroot/$input") ||
1865 !check_export_ok
("$projectroot/$input") ||
1866 ($strict_export && !project_in_list
($input))) {
1873 sub is_valid_pathname
{
1876 return undef unless defined $input;
1877 # no '.' or '..' as elements of path, i.e. no '.' or '..'
1878 # at the beginning, at the end, and between slashes.
1879 # also this catches doubled slashes
1880 if ($input =~ m!(^|/)(|\.|\.\.)(/|$)!) {
1883 # no null characters
1884 if ($input =~ m!\0!) {
1890 sub is_valid_ref_format
{
1893 return undef unless defined $input;
1894 # restrictions on ref name according to git-check-ref-format
1895 if ($input =~ m!(/\.|\.\.|[\000-\040\177 ~^:?*\[]|/$)!) {
1901 sub is_valid_refname
{
1904 return undef unless defined $input;
1905 # textual hashes are O.K.
1906 if ($input =~ m/^[0-9a-fA-F]{40}$/) {
1909 # it must be correct pathname
1910 is_valid_pathname
($input) or return undef;
1911 # check git-check-ref-format restrictions
1912 is_valid_ref_format
($input) or return undef;
1916 # decode sequences of octets in utf8 into Perl's internal form,
1917 # which is utf-8 with utf8 flag set if needed. gitweb writes out
1918 # in utf-8 thanks to "binmode STDOUT, ':utf8'" at beginning
1921 return undef unless defined $str;
1923 if (utf8
::is_utf8
($str) || utf8
::decode
($str)) {
1926 return $encode_object->decode($str, Encode
::FB_DEFAULT
);
1930 # quote unsafe chars, but keep the slash, even when it's not
1931 # correct, but quoted slashes look too horrible in bookmarks
1934 return undef unless defined $str;
1935 $str =~ s/([^A-Za-z0-9\-_.~()\/:@ ]+)/CGI
::escape
($1)/eg
;
1940 # the quoting rules for path_info fragment are slightly different
1943 return undef unless defined $str;
1945 # path_info doesn't treat '+' as space (specially), but '?' must be escaped
1946 $str =~ s/([^A-Za-z0-9\-_.~();\/;:@&= +]+)/CGI
::escape
($1)/eg
;
1951 # quote unsafe chars in whole URL, so some characters cannot be quoted
1954 return undef unless defined $str;
1955 $str =~ s/([^A-Za-z0-9\-_.~();\/;?:@&= ]+)/CGI
::escape
($1)/eg
;
1960 # quote unsafe characters in HTML attributes
1963 # for XHTML conformance escaping '"' to '"' is not enough
1964 return esc_html
(@_);
1967 # replace invalid utf8 character with SUBSTITUTION sequence
1972 return undef unless defined $str;
1974 $str = to_utf8
($str);
1975 $str = $cgi->escapeHTML($str);
1976 if ($opts{'-nbsp'}) {
1977 $str =~ s/ / /g;
1980 $str =~ s
|([[:cntrl
:]])|(($1 ne "\t") ? quot_cec
($1) : $1)|eg
;
1984 # quote control characters and escape filename to HTML
1989 return undef unless defined $str;
1991 $str = to_utf8
($str);
1992 $str = $cgi->escapeHTML($str);
1993 if ($opts{'-nbsp'}) {
1994 $str =~ s/ / /g;
1997 $str =~ s
|([[:cntrl
:]])|quot_cec
($1)|eg
;
2001 # Sanitize for use in XHTML + application/xml+xhtm (valid XML 1.0)
2005 return undef unless defined $str;
2007 $str = to_utf8
($str);
2009 $str =~ s
|([[:cntrl
:]])|(index("\t\n\r", $1) != -1 ?
$1 : quot_cec
($1))|eg
;
2013 # Make control characters "printable", using character escape codes (CEC)
2017 my %es = ( # character escape codes, aka escape sequences
2018 "\t" => '\t', # tab (HT)
2019 "\n" => '\n', # line feed (LF)
2020 "\r" => '\r', # carrige return (CR)
2021 "\f" => '\f', # form feed (FF)
2022 "\b" => '\b', # backspace (BS)
2023 "\a" => '\a', # alarm (bell) (BEL)
2024 "\e" => '\e', # escape (ESC)
2025 "\013" => '\v', # vertical tab (VT)
2026 "\000" => '\0', # nul character (NUL)
2028 my $chr = ( (exists $es{$cntrl})
2030 : sprintf('\x%02x', ord($cntrl)) );
2031 if ($opts{-nohtml
}) {
2034 return "<span class=\"cntrl\">$chr</span>";
2038 # Alternatively use unicode control pictures codepoints,
2039 # Unicode "printable representation" (PR)
2044 my $chr = sprintf('&#%04d;', 0x2400+ord($cntrl));
2045 if ($opts{-nohtml
}) {
2048 return "<span class=\"cntrl\">$chr</span>";
2052 # git may return quoted and escaped filenames
2058 my %es = ( # character escape codes, aka escape sequences
2059 't' => "\t", # tab (HT, TAB)
2060 'n' => "\n", # newline (NL)
2061 'r' => "\r", # return (CR)
2062 'f' => "\f", # form feed (FF)
2063 'b' => "\b", # backspace (BS)
2064 'a' => "\a", # alarm (bell) (BEL)
2065 'e' => "\e", # escape (ESC)
2066 'v' => "\013", # vertical tab (VT)
2069 if ($seq =~ m/^[0-7]{1,3}$/) {
2070 # octal char sequence
2071 return chr(oct($seq));
2072 } elsif (exists $es{$seq}) {
2073 # C escape sequence, aka character escape code
2076 # quoted ordinary character
2080 if ($str =~ m/^"(.*)"$/) {
2083 $str =~ s/\\([^0-7]|[0-7]{1,3})/unq($1)/eg;
2088 # escape tabs (convert tabs to spaces)
2092 while ((my $pos = index($line, "\t")) != -1) {
2093 if (my $count = (8 - ($pos % 8))) {
2094 my $spaces = ' ' x
$count;
2095 $line =~ s/\t/$spaces/;
2102 sub project_in_list
{
2103 my $project = shift;
2104 my @list = git_get_projects_list
();
2105 return @list && scalar(grep { $_->{'path'} eq $project } @list);
2108 sub cached_page_precondition_check
{
2111 $action eq 'summary' &&
2112 $projlist_cache_lifetime > 0 &&
2113 gitweb_check_feature
('forks');
2115 # Note that ALL the 'forkchange' logic is in this function.
2116 # It does NOT belong in cached_action_page NOR in cached_action_start
2117 # NOR in cached_action_finish. None of those functions should know anything
2118 # about nor do anything to any of the 'forkchange'/"$action.forkchange" files.
2120 # besides the basic 'changed' "$action.changed" check, we may only use
2121 # a summary cache if:
2123 # 1) we are not using a project list cache file
2125 # 2) we are not using the 'forks' feature
2127 # 3) there is no 'forkchange' nor "$action.forkchange" file in $html_cache_dir
2129 # 4) there is no cache file ("$cache_dir/$projlist_cache_name")
2131 # 5) the OLDER of 'forkchange'/"$action.forkchange" is NEWER than the cache file
2133 # Otherwise we must re-generate the cache because we've had a fork change
2134 # (either a fork was added or a fork was removed) AND the change has been
2135 # picked up in the cache file AND we've not got that in our cached copy
2137 # For (5) regenerating the cached page wouldn't get us anything if the project
2138 # cache file is older than the 'forkchange'/"$action.forkchange" because the
2139 # forks information comes from the project cache file and it's clearly not
2140 # picked up the changes yet so we may continue to use a cached page until it does.
2142 my $htmlcd = "$projectroot/$project/$html_cache_dir";
2143 my $fc_mt = (stat("$htmlcd/forkchange"))[9];
2144 my $afc_mt = (stat("$htmlcd/$action.forkchange"))[9];
2145 return 1 unless defined($fc_mt) || defined($afc_mt);
2146 my $prj_mt = (stat("$cache_dir/$projlist_cache_name"))[9];
2147 return 1 unless $prj_mt;
2148 my $old_mt = $fc_mt;
2149 $old_mt = $afc_mt if !defined($old_mt) || (defined($afc_mt) && $afc_mt < $old_mt);
2150 return 1 if $old_mt > $prj_mt;
2152 # We're going to regenerate the cached page because we know the project cache
2153 # has new fork information that we cannot possibly have in our cached copy.
2155 # However, if both 'forkchange' and "$action.forkchange" exist and one of
2156 # them is older than the project cache and one of them is newer, we still
2157 # need to regenerate the page cache, but we will also need to do it again
2158 # in the future because there's yet another fork update not yet in the cache.
2160 # So we make sure to touch "$action.changed" to force a cache regeneration
2161 # and then we remove either or both of 'forkchange'/"$action.forkchange" if
2162 # they're older than the project cache (they've served their purpose, we're
2163 # forcing a page regeneration by touching "$action.changed" but the project
2164 # cache was rebuilt since then so there are no more pending fork updates to
2165 # pick up in the future and they need to go).
2167 # For best results, the external code that touches 'forkchange' should always
2168 # touch 'forkchange' and additionally touch 'summary.forkchange' but only
2169 # if it does not already exist. That way the cached page will be regenerated
2170 # each time it's requested and ANY fork updates are available in the proj
2171 # cache rather than waiting until they all are before updating.
2173 # Note that we take a shortcut here and will zap 'forkchange' since we know
2174 # that it only affects the 'summary' cache. If, in the future, it affects
2175 # other cache types, it will first need to be propogated down to
2176 # "$action.forkchange" for those types before we zap it.
2179 open $fd, '>', "$htmlcd/$action.changed" and close $fd;
2180 $fc_mt=undef, unlink "$htmlcd/forkchange" if defined $fc_mt && $fc_mt < $prj_mt;
2181 $afc_mt=undef, unlink "$htmlcd/$action.forkchange" if defined $afc_mt && $afc_mt < $prj_mt;
2183 # Now we propagate 'forkchange' to "$action.forkchange" if we have the
2184 # one and not the other.
2186 if (defined $fc_mt && ! defined $afc_mt) {
2187 open $fd, '>', "$htmlcd/$action.forkchange" and close $fd;
2188 -e
"$htmlcd/$action.forkchange" and
2189 utime($fc_mt, $fc_mt, "$htmlcd/$action.forkchange") and
2190 unlink "$htmlcd/forkchange";
2196 sub cached_action_page
{
2199 return undef unless $action && $html_cache_actions{$action} && $html_cache_dir;
2200 my $htmlcd = "$projectroot/$project/$html_cache_dir";
2201 return undef if -e
"$htmlcd/changed" || -e
"$htmlcd/$action.changed";
2202 return undef unless cached_page_precondition_check
($action);
2203 open my $fd, '<', "$htmlcd/$action" or return undef;
2206 my $cached_page = <$fd>;
2207 close $fd or return undef;
2208 return $cached_page;
2211 package Git
::Gitweb
::CacheFile
;
2214 use POSIX
qw(:fcntl_h);
2216 my $cachefile = shift;
2218 sysopen(my $self, $cachefile, O_WRONLY
|O_CREAT
|O_EXCL
, 0664)
2220 $$self->{'cachefile'} = $cachefile;
2221 $$self->{'opened'} = 1;
2222 $$self->{'contents'} = '';
2223 return bless $self, $class;
2228 if ($$self->{'opened'}) {
2229 $$self->{'opened'} = 0;
2230 my $result = close $self;
2231 unlink $$self->{'cachefile'} unless $result;
2239 if ($$self->{'opened'}) {
2240 $self->CLOSE() and unlink $$self->{'cachefile'};
2246 @_ = (map {my $x=$_; utf8
::encode
($x); $x} @_) unless $fcgi_raw_mode;
2247 print $self @_ if $$self->{'opened'};
2248 $$self->{'contents'} .= join('', @_);
2254 my $template = shift;
2255 return $self->PRINT(sprintf $template, @_);
2260 return $$self->{'contents'};
2265 # Caller is responsible for preserving STDOUT beforehand if needed
2266 sub cached_action_start
{
2269 return undef unless $html_cache_actions{$action} && $html_cache_dir;
2270 my $htmlcd = "$projectroot/$project/$html_cache_dir";
2271 return undef unless -d
$htmlcd;
2272 if (-e
"$htmlcd/changed") {
2273 foreach my $cacheable (keys(%html_cache_actions)) {
2274 next unless $supported_cache_actions{$cacheable} &&
2275 $html_cache_actions{$cacheable};
2277 open $fd, '>', "$htmlcd/$cacheable.changed"
2280 unlink "$htmlcd/changed";
2283 tie
*CACHEFILE
, 'Git::Gitweb::CacheFile', "$htmlcd/$action.lock" or return undef;
2284 *STDOUT
= *CACHEFILE
;
2285 unlink "$htmlcd/$action", "$htmlcd/$action.changed";
2289 # Caller is responsible for restoring STDOUT afterward if needed
2290 sub cached_action_finish
{
2295 my $obj = tied *STDOUT
;
2296 return undef unless ref($obj) eq 'Git::Gitweb::CacheFile';
2297 my $cached_page = $obj->contents;
2298 (my $result = close(STDOUT
)) or warn "couldn't close cache file on STDOUT: $!";
2299 # Do not leave STDOUT file descriptor invalid!
2301 open(NULL
, '>', File
::Spec
->devnull) or die "couldn't open NULL to devnull: $!";
2303 return $cached_page unless $result;
2304 my $htmlcd = "$projectroot/$project/$html_cache_dir";
2305 return $cached_page unless -d
$htmlcd;
2306 unlink "$htmlcd/$action.lock" unless rename "$htmlcd/$action.lock", "$htmlcd/$action";
2307 return $cached_page;
2311 BEGIN {%expand_pi_subs = (
2312 'age_string' => \
&age_string
,
2313 'age_string_date' => \
&age_string_date
,
2314 'age_string_age' => \
&age_string_age
,
2315 'compute_timed_interval' => \
&compute_timed_interval
,
2316 'compute_commands_count' => \
&compute_commands_count
,
2317 'format_lastrefresh_row' => \
&format_lastrefresh_row
,
2318 'compute_stylesheet_links' => \
&compute_stylesheet_links
,
2321 # Expands any <?gitweb...> processing instructions and returns the result
2322 sub expand_gitweb_pi
{
2325 my @time_now = gettimeofday
();
2326 $page =~ s
{<\?gitweb
(?
:\s
+([^\s
>]+)([^>]*))?\s
*\?>}
2328 (ref($expand_pi_subs{$1}) eq 'CODE' ?
2329 $expand_pi_subs{$1}->(split(' ',$2), @time_now) :
2335 ## ----------------------------------------------------------------------
2336 ## HTML aware string manipulation
2338 # Try to chop given string on a word boundary between position
2339 # $len and $len+$add_len. If there is no word boundary there,
2340 # chop at $len+$add_len. Do not chop if chopped part plus ellipsis
2341 # (marking chopped part) would be longer than given string.
2345 my $add_len = shift || 10;
2346 my $where = shift || 'right'; # 'left' | 'center' | 'right'
2348 # Make sure perl knows it is utf8 encoded so we don't
2349 # cut in the middle of a utf8 multibyte char.
2350 $str = to_utf8
($str);
2352 # allow only $len chars, but don't cut a word if it would fit in $add_len
2353 # if it doesn't fit, cut it if it's still longer than the dots we would add
2354 # remove chopped character entities entirely
2356 # when chopping in the middle, distribute $len into left and right part
2357 # return early if chopping wouldn't make string shorter
2358 if ($where eq 'center') {
2359 return $str if ($len + 5 >= length($str)); # filler is length 5
2362 return $str if ($len + 4 >= length($str)); # filler is length 4
2365 # regexps: ending and beginning with word part up to $add_len
2366 my $endre = qr/.{$len}\w{0,$add_len}/;
2367 my $begre = qr/\w{0,$add_len}.{$len}/;
2369 if ($where eq 'left') {
2370 $str =~ m/^(.*?)($begre)$/;
2371 my ($lead, $body) = ($1, $2);
2372 if (length($lead) > 4) {
2375 return "$lead$body";
2377 } elsif ($where eq 'center') {
2378 $str =~ m/^($endre)(.*)$/;
2379 my ($left, $str) = ($1, $2);
2380 $str =~ m/^(.*?)($begre)$/;
2381 my ($mid, $right) = ($1, $2);
2382 if (length($mid) > 5) {
2385 return "$left$mid$right";
2388 $str =~ m/^($endre)(.*)$/;
2391 if (length($tail) > 4) {
2394 return "$body$tail";
2398 # pass-through email filter, obfuscating it when possible
2399 sub email_obfuscate
{
2403 $str = $email->escape_html($str);
2404 # Stock HTML::Email::Obfuscate version likes to produce
2406 $str =~ s
#<(/?)B>#<$1b>#g;
2409 $str = esc_html
($str);
2410 $str =~ s/@/@/;
2415 # takes the same arguments as chop_str, but also wraps a <span> around the
2416 # result with a title attribute if it does get chopped. Additionally, the
2417 # string is HTML-escaped.
2418 sub chop_and_escape_str
{
2421 my $chopped = chop_str
(@_);
2422 $str = to_utf8
($str);
2423 if ($chopped eq $str) {
2424 return email_obfuscate
($chopped);
2427 $str =~ s/[[:cntrl:]]/?/g;
2428 return $cgi->span({-title
=>$str}, email_obfuscate
($chopped));
2432 # Highlight selected fragments of string, using given CSS class,
2433 # and escape HTML. It is assumed that fragments do not overlap.
2434 # Regions are passed as list of pairs (array references).
2436 # Example: esc_html_hl_regions("foobar", "mark", [ 0, 3 ]) returns
2437 # '<span class="mark">foo</span>bar'
2438 sub esc_html_hl_regions
{
2439 my ($str, $css_class, @sel) = @_;
2440 my %opts = grep { ref($_) ne 'ARRAY' } @sel;
2441 @sel = grep { ref($_) eq 'ARRAY' } @sel;
2442 return esc_html
($str, %opts) unless @sel;
2448 my ($begin, $end) = @
$s;
2450 # Don't create empty <span> elements.
2451 next if $end <= $begin;
2453 my $escaped = esc_html
(substr($str, $begin, $end - $begin),
2456 $out .= esc_html
(substr($str, $pos, $begin - $pos), %opts)
2457 if ($begin - $pos > 0);
2458 $out .= $cgi->span({-class => $css_class}, $escaped);
2462 $out .= esc_html
(substr($str, $pos), %opts)
2463 if ($pos < length($str));
2468 # return positions of beginning and end of each match
2470 my ($str, $regexp) = @_;
2471 return unless (defined $str && defined $regexp);
2474 while ($str =~ /$regexp/g) {
2475 push @matches, [$-[0], $+[0]];
2480 # highlight match (if any), and escape HTML
2481 sub esc_html_match_hl
{
2482 my ($str, $regexp) = @_;
2483 return esc_html
($str) unless defined $regexp;
2485 my @matches = matchpos_list
($str, $regexp);
2486 return esc_html
($str) unless @matches;
2488 return esc_html_hl_regions
($str, 'match', @matches);
2492 # highlight match (if any) of shortened string, and escape HTML
2493 sub esc_html_match_hl_chopped
{
2494 my ($str, $chopped, $regexp) = @_;
2495 return esc_html_match_hl
($str, $regexp) unless defined $chopped;
2497 my @matches = matchpos_list
($str, $regexp);
2498 return esc_html
($chopped) unless @matches;
2500 # filter matches so that we mark chopped string
2501 my $tail = "... "; # see chop_str
2502 unless ($chopped =~ s/\Q$tail\E$//) {
2505 my $chop_len = length($chopped);
2506 my $tail_len = length($tail);
2509 for my $m (@matches) {
2510 if ($m->[0] > $chop_len) {
2511 push @filtered, [ $chop_len, $chop_len + $tail_len ] if ($tail_len > 0);
2513 } elsif ($m->[1] > $chop_len) {
2514 push @filtered, [ $m->[0], $chop_len + $tail_len ];
2520 return esc_html_hl_regions
($chopped . $tail, 'match', @filtered);
2523 ## ----------------------------------------------------------------------
2524 ## functions returning short strings
2526 # CSS class for given age epoch value (in seconds)
2527 # and reference time (optional, defaults to now) as second value
2529 my ($age_epoch, $time_now) = @_;
2530 return "noage" unless defined $age_epoch;
2531 defined $time_now or $time_now = time;
2532 my $age = $time_now - $age_epoch;
2534 if ($age < 60*60*2) {
2536 } elsif ($age < 60*60*24*2) {
2543 # convert age epoch in seconds to "nn units ago" string
2544 # reference time used is now unless second argument passed in
2545 # to get the old behavior, pass 0 as the first argument and
2546 # the time in seconds as the second
2548 my ($age_epoch, $time_now) = @_;
2549 return "unknown" unless defined $age_epoch;
2550 return "<?gitweb age_string $age_epoch?>" if $cache_mode_active;
2551 defined $time_now or $time_now = time;
2552 my $age = $time_now - $age_epoch;
2555 if ($age > 60*60*24*365*2) {
2556 $age_str = (int $age/60/60/24/365);
2557 $age_str .= " years ago";
2558 } elsif ($age > 60*60*24*(365/12)*2) {
2559 $age_str = int $age/60/60/24/(365/12);
2560 $age_str .= " months ago";
2561 } elsif ($age > 60*60*24*7*2) {
2562 $age_str = int $age/60/60/24/7;
2563 $age_str .= " weeks ago";
2564 } elsif ($age > 60*60*24*2) {
2565 $age_str = int $age/60/60/24;
2566 $age_str .= " days ago";
2567 } elsif ($age > 60*60*2) {
2568 $age_str = int $age/60/60;
2569 $age_str .= " hours ago";
2570 } elsif ($age > 60*2) {
2571 $age_str = int $age/60;
2572 $age_str .= " min ago";
2573 } elsif ($age > 2) {
2574 $age_str = int $age;
2575 $age_str .= " sec ago";
2577 $age_str .= " right now";
2582 # returns age_string if the age is <= 2 weeks otherwise an absolute date
2583 # this is typically shown to the user directly with the age_string_age as a title
2584 sub age_string_date
{
2585 my ($age_epoch, $time_now) = @_;
2586 return "unknown" unless defined $age_epoch;
2587 return "<?gitweb age_string_date $age_epoch?>" if $cache_mode_active;
2588 defined $time_now or $time_now = time;
2589 my $age = $time_now - $age_epoch;
2591 if ($age > 60*60*24*7*2) {
2592 my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($age_epoch);
2593 return sprintf "%4i-%02u-%02i", 1900 + $year, $mon+1, $mday;
2595 return age_string
($age_epoch, $time_now);
2599 # returns an absolute date if the age is <= 2 weeks otherwise age_string
2600 # this is typically used for the 'title' attribute so it will show as a tooltip
2601 sub age_string_age
{
2602 my ($age_epoch, $time_now) = @_;
2603 return "unknown" unless defined $age_epoch;
2604 return "<?gitweb age_string_age $age_epoch?>" if $cache_mode_active;
2605 defined $time_now or $time_now = time;
2606 my $age = $time_now - $age_epoch;
2608 if ($age > 60*60*24*7*2) {
2609 return age_string
($age_epoch, $time_now);
2611 my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($age_epoch);
2612 return sprintf "%4i-%02u-%02i", 1900 + $year, $mon+1, $mday;
2617 S_IFINVALID
=> 0030000,
2618 S_IFGITLINK
=> 0160000,
2621 # submodule/subproject, a commit object reference
2625 return (($mode & S_IFMT
) == S_IFGITLINK
)
2628 # convert file mode in octal to symbolic file mode string
2630 my $mode = oct shift;
2632 if (S_ISGITLINK
($mode)) {
2633 return 'm---------';
2634 } elsif (S_ISDIR
($mode & S_IFMT
)) {
2635 return 'drwxr-xr-x';
2636 } elsif (S_ISLNK
($mode)) {
2637 return 'lrwxrwxrwx';
2638 } elsif (S_ISREG
($mode)) {
2639 # git cares only about the executable bit
2640 if ($mode & S_IXUSR
) {
2641 return '-rwxr-xr-x';
2643 return '-rw-r--r--';
2646 return '----------';
2650 # convert file mode in octal to file type string
2654 if ($mode !~ m/^[0-7]+$/) {
2660 if (S_ISGITLINK
($mode)) {
2662 } elsif (S_ISDIR
($mode & S_IFMT
)) {
2664 } elsif (S_ISLNK
($mode)) {
2666 } elsif (S_ISREG
($mode)) {
2673 # convert file mode in octal to file type description string
2674 sub file_type_long
{
2677 if ($mode !~ m/^[0-7]+$/) {
2683 if (S_ISGITLINK
($mode)) {
2685 } elsif (S_ISDIR
($mode & S_IFMT
)) {
2687 } elsif (S_ISLNK
($mode)) {
2689 } elsif (S_ISREG
($mode)) {
2690 if ($mode & S_IXUSR
) {
2691 return "executable";
2701 ## ----------------------------------------------------------------------
2702 ## functions returning short HTML fragments, or transforming HTML fragments
2703 ## which don't belong to other sections
2705 # format line of commit message.
2706 sub format_log_line_html
{
2709 $line = esc_html
($line, -nbsp
=>1);
2710 $line =~ s
{\b([0-9a
-fA
-F
]{8,40})\b}{
2711 $cgi->a({-href
=> href
(action
=>"object", hash
=>$1),
2712 -class => "text"}, $1);
2713 }eg
unless $line =~ /^\s*git-svn-id:/;
2718 # format marker of refs pointing to given object
2720 # the destination action is chosen based on object type and current context:
2721 # - for annotated tags, we choose the tag view unless it's the current view
2722 # already, in which case we go to shortlog view
2723 # - for other refs, we keep the current view if we're in history, shortlog or
2724 # log view, and select shortlog otherwise
2725 sub format_ref_marker
{
2726 my ($refs, $id) = @_;
2729 if (defined $refs->{$id}) {
2730 foreach my $ref (@
{$refs->{$id}}) {
2731 # this code exploits the fact that non-lightweight tags are the
2732 # only indirect objects, and that they are the only objects for which
2733 # we want to use tag instead of shortlog as action
2734 my ($type, $name) = qw();
2735 my $indirect = ($ref =~ s/\^\{\}$//);
2736 # e.g. tags/v2.6.11 or heads/next
2737 if ($ref =~ m!^(.*?)s?/(.*)$!) {
2746 $class .= " indirect" if $indirect;
2748 my $dest_action = "shortlog";
2751 $dest_action = "tag" unless $action eq "tag";
2752 } elsif ($action =~ /^(history|(short)?log)$/) {
2753 $dest_action = $action;
2757 $dest .= "refs/" unless $ref =~ m
!^refs
/!;
2760 my $link = $cgi->a({
2762 action
=>$dest_action,
2766 $markers .= "<span class=\"".esc_attr
($class)."\" title=\"".esc_attr
($ref)."\">" .
2772 return '<span class="refs">'. $markers . '</span>';
2778 # format, perhaps shortened and with markers, title line
2779 sub format_subject_html
{
2780 my ($long, $short, $href, $extra) = @_;
2781 $extra = '' unless defined($extra);
2783 if (length($short) < length($long)) {
2785 $long =~ s/[[:cntrl:]]/?/g;
2786 return $cgi->a({-href
=> $href, -class => "list subject",
2787 -title
=> to_utf8
($long)},
2788 esc_html
($short)) . $extra;
2790 return $cgi->a({-href
=> $href, -class => "list subject"},
2791 esc_html
($long)) . $extra;
2795 # Rather than recomputing the url for an email multiple times, we cache it
2796 # after the first hit. This gives a visible benefit in views where the avatar
2797 # for the same email is used repeatedly (e.g. shortlog).
2798 # The cache is shared by all avatar engines (currently gravatar only), which
2799 # are free to use it as preferred. Since only one avatar engine is used for any
2800 # given page, there's no risk for cache conflicts.
2801 our %avatar_cache = ();
2803 # Compute the picon url for a given email, by using the picon search service over at
2804 # http://www.cs.indiana.edu/picons/search.html
2806 my $email = lc shift;
2807 if (!$avatar_cache{$email}) {
2808 my ($user, $domain) = split('@', $email);
2809 $avatar_cache{$email} =
2810 "//www.cs.indiana.edu/cgi-pub/kinzler/piconsearch.cgi/" .
2812 "users+domains+unknown/up/single";
2814 return $avatar_cache{$email};
2817 # Compute the gravatar url for a given email, if it's not in the cache already.
2818 # Gravatar stores only the part of the URL before the size, since that's the
2819 # one computationally more expensive. This also allows reuse of the cache for
2820 # different sizes (for this particular engine).
2822 my $email = lc shift;
2824 $avatar_cache{$email} ||=
2825 "//www.gravatar.com/avatar/" .
2826 Digest
::MD5
::md5_hex
($email) . "?s=";
2827 return $avatar_cache{$email} . $size;
2830 # Insert an avatar for the given $email at the given $size if the feature
2832 sub git_get_avatar
{
2833 my ($email, %opts) = @_;
2834 my $pre_white = ($opts{-pad_before
} ?
" " : "");
2835 my $post_white = ($opts{-pad_after
} ?
" " : "");
2836 $opts{-size
} ||= 'default';
2837 my $size = $avatar_size{$opts{-size
}} || $avatar_size{'default'};
2839 if ($git_avatar eq 'gravatar') {
2840 $url = gravatar_url
($email, $size);
2841 } elsif ($git_avatar eq 'picon') {
2842 $url = picon_url
($email);
2844 # Other providers can be added by extending the if chain, defining $url
2845 # as needed. If no variant puts something in $url, we assume avatars
2846 # are completely disabled/unavailable.
2849 "<img width=\"$size\" " .
2850 "class=\"avatar\" " .
2851 "src=\"".esc_url
($url)."\" " .
2859 sub format_search_author
{
2860 my ($author, $searchtype, $displaytext) = @_;
2861 my $have_search = gitweb_check_feature
('search');
2865 if ($searchtype eq 'author') {
2866 $performed = "authored";
2867 } elsif ($searchtype eq 'committer') {
2868 $performed = "committed";
2871 return $cgi->a({-href
=> href
(action
=>"search", hash
=>$hash,
2872 searchtext
=>$author,
2873 searchtype
=>$searchtype), class=>"list",
2874 title
=>"Search for commits $performed by $author"},
2878 return $displaytext;
2882 # format the author name of the given commit with the given tag
2883 # the author name is chopped and escaped according to the other
2884 # optional parameters (see chop_str).
2885 sub format_author_html
{
2888 my $author = chop_and_escape_str
($co->{'author_name'}, @_);
2889 return "<$tag class=\"author\">" .
2890 format_search_author
($co->{'author_name'}, "author",
2891 git_get_avatar
($co->{'author_email'}, -pad_after
=> 1) .
2896 # format git diff header line, i.e. "diff --(git|combined|cc) ..."
2897 sub format_git_diff_header_line
{
2899 my $diffinfo = shift;
2900 my ($from, $to) = @_;
2902 if ($diffinfo->{'nparents'}) {
2904 $line =~ s!^(diff (.*?) )"?.*$!$1!;
2905 if ($to->{'href'}) {
2906 $line .= $cgi->a({-href
=> $to->{'href'}, -class => "path"},
2907 esc_path
($to->{'file'}));
2908 } else { # file was deleted (no href)
2909 $line .= esc_path
($to->{'file'});
2913 $line =~ s!^(diff (.*?) )"?a/.*$!$1!;
2914 if ($from->{'href'}) {
2915 $line .= $cgi->a({-href
=> $from->{'href'}, -class => "path"},
2916 'a/' . esc_path
($from->{'file'}));
2917 } else { # file was added (no href)
2918 $line .= 'a/' . esc_path
($from->{'file'});
2921 if ($to->{'href'}) {
2922 $line .= $cgi->a({-href
=> $to->{'href'}, -class => "path"},
2923 'b/' . esc_path
($to->{'file'}));
2924 } else { # file was deleted
2925 $line .= 'b/' . esc_path
($to->{'file'});
2929 return "<div class=\"diff header\">$line</div>\n";
2932 # format extended diff header line, before patch itself
2933 sub format_extended_diff_header_line
{
2935 my $diffinfo = shift;
2936 my ($from, $to) = @_;
2939 if ($line =~ s!^((copy|rename) from ).*$!$1! && $from->{'href'}) {
2940 $line .= $cgi->a({-href
=>$from->{'href'}, -class=>"path"},
2941 esc_path
($from->{'file'}));
2943 if ($line =~ s!^((copy|rename) to ).*$!$1! && $to->{'href'}) {
2944 $line .= $cgi->a({-href
=>$to->{'href'}, -class=>"path"},
2945 esc_path
($to->{'file'}));
2947 # match single <mode>
2948 if ($line =~ m/\s(\d{6})$/) {
2949 $line .= '<span class="info"> (' .
2950 file_type_long
($1) .
2954 if ($line =~ m/^index [0-9a-fA-F]{40},[0-9a-fA-F]{40}/) {
2955 # can match only for combined diff
2957 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
2958 if ($from->{'href'}[$i]) {
2959 $line .= $cgi->a({-href
=>$from->{'href'}[$i],
2961 substr($diffinfo->{'from_id'}[$i],0,7));
2966 $line .= ',' if ($i < $diffinfo->{'nparents'} - 1);
2969 if ($to->{'href'}) {
2970 $line .= $cgi->a({-href
=>$to->{'href'}, -class=>"hash"},
2971 substr($diffinfo->{'to_id'},0,7));
2976 } elsif ($line =~ m/^index [0-9a-fA-F]{40}..[0-9a-fA-F]{40}/) {
2977 # can match only for ordinary diff
2978 my ($from_link, $to_link);
2979 if ($from->{'href'}) {
2980 $from_link = $cgi->a({-href
=>$from->{'href'}, -class=>"hash"},
2981 substr($diffinfo->{'from_id'},0,7));
2983 $from_link = '0' x
7;
2985 if ($to->{'href'}) {
2986 $to_link = $cgi->a({-href
=>$to->{'href'}, -class=>"hash"},
2987 substr($diffinfo->{'to_id'},0,7));
2991 my ($from_id, $to_id) = ($diffinfo->{'from_id'}, $diffinfo->{'to_id'});
2992 $line =~ s!$from_id\.\.$to_id!$from_link..$to_link!;
2995 return $line . "<br/>\n";
2998 # format from-file/to-file diff header
2999 sub format_diff_from_to_header
{
3000 my ($from_line, $to_line, $diffinfo, $from, $to, @parents) = @_;
3005 #assert($line =~ m/^---/) if DEBUG;
3006 # no extra formatting for "^--- /dev/null"
3007 if (! $diffinfo->{'nparents'}) {
3008 # ordinary (single parent) diff
3009 if ($line =~ m!^--- "?a/!) {
3010 if ($from->{'href'}) {
3012 $cgi->a({-href
=>$from->{'href'}, -class=>"path"},
3013 esc_path
($from->{'file'}));
3016 esc_path
($from->{'file'});
3019 $result .= qq!<div
class="diff from_file">$line</div
>\n!;
3022 # combined diff (merge commit)
3023 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
3024 if ($from->{'href'}[$i]) {
3026 $cgi->a({-href
=>href
(action
=>"blobdiff",
3027 hash_parent
=>$diffinfo->{'from_id'}[$i],
3028 hash_parent_base
=>$parents[$i],
3029 file_parent
=>$from->{'file'}[$i],
3030 hash
=>$diffinfo->{'to_id'},
3032 file_name
=>$to->{'file'}),
3034 -title
=>"diff" . ($i+1)},
3037 $cgi->a({-href
=>$from->{'href'}[$i], -class=>"path"},
3038 esc_path
($from->{'file'}[$i]));
3040 $line = '--- /dev/null';
3042 $result .= qq!<div
class="diff from_file">$line</div
>\n!;
3047 #assert($line =~ m/^\+\+\+/) if DEBUG;
3048 # no extra formatting for "^+++ /dev/null"
3049 if ($line =~ m!^\+\+\+ "?b/!) {
3050 if ($to->{'href'}) {
3052 $cgi->a({-href
=>$to->{'href'}, -class=>"path"},
3053 esc_path
($to->{'file'}));
3056 esc_path
($to->{'file'});
3059 $result .= qq!<div
class="diff to_file">$line</div
>\n!;
3064 # create note for patch simplified by combined diff
3065 sub format_diff_cc_simplified
{
3066 my ($diffinfo, @parents) = @_;
3069 $result .= "<div class=\"diff header\">" .
3071 if (!is_deleted
($diffinfo)) {
3072 $result .= $cgi->a({-href
=> href
(action
=>"blob",
3074 hash
=>$diffinfo->{'to_id'},
3075 file_name
=>$diffinfo->{'to_file'}),
3077 esc_path
($diffinfo->{'to_file'}));
3079 $result .= esc_path
($diffinfo->{'to_file'});
3081 $result .= "</div>\n" . # class="diff header"
3082 "<div class=\"diff nodifferences\">" .
3084 "</div>\n"; # class="diff nodifferences"
3089 sub diff_line_class
{
3090 my ($line, $from, $to) = @_;
3095 if ($from && $to && ref($from->{'href'}) eq "ARRAY") {
3096 $num_sign = scalar @
{$from->{'href'}};
3099 my @diff_line_classifier = (
3100 { regexp
=> qr/^\@\@{$num_sign} /, class => "chunk_header"},
3101 { regexp
=> qr/^\\/, class => "incomplete" },
3102 { regexp
=> qr/^ {$num_sign}/, class => "ctx" },
3103 # classifier for context must come before classifier add/rem,
3104 # or we would have to use more complicated regexp, for example
3105 # qr/(?= {0,$m}\+)[+ ]{$num_sign}/, where $m = $num_sign - 1;
3106 { regexp
=> qr/^[+ ]{$num_sign}/, class => "add" },
3107 { regexp
=> qr/^[- ]{$num_sign}/, class => "rem" },
3109 for my $clsfy (@diff_line_classifier) {
3110 return $clsfy->{'class'}
3111 if ($line =~ $clsfy->{'regexp'});
3118 # assumes that $from and $to are defined and correctly filled,
3119 # and that $line holds a line of chunk header for unified diff
3120 sub format_unidiff_chunk_header
{
3121 my ($line, $from, $to) = @_;
3123 my ($from_text, $from_start, $from_lines, $to_text, $to_start, $to_lines, $section) =
3124 $line =~ m/^\@{2} (-(\d+)(?:,(\d+))?) (\+(\d+)(?:,(\d+))?) \@{2}(.*)$/;
3126 $from_lines = 0 unless defined $from_lines;
3127 $to_lines = 0 unless defined $to_lines;
3129 if ($from->{'href'}) {
3130 $from_text = $cgi->a({-href
=>"$from->{'href'}#l$from_start",
3131 -class=>"list"}, $from_text);
3133 if ($to->{'href'}) {
3134 $to_text = $cgi->a({-href
=>"$to->{'href'}#l$to_start",
3135 -class=>"list"}, $to_text);
3137 $line = "<span class=\"chunk_info\">@@ $from_text $to_text @@</span>" .
3138 "<span class=\"section\">" . esc_html
($section, -nbsp
=>1) . "</span>";
3142 # assumes that $from and $to are defined and correctly filled,
3143 # and that $line holds a line of chunk header for combined diff
3144 sub format_cc_diff_chunk_header
{
3145 my ($line, $from, $to) = @_;
3147 my ($prefix, $ranges, $section) = $line =~ m/^(\@+) (.*?) \@+(.*)$/;
3148 my (@from_text, @from_start, @from_nlines, $to_text, $to_start, $to_nlines);
3150 @from_text = split(' ', $ranges);
3151 for (my $i = 0; $i < @from_text; ++$i) {
3152 ($from_start[$i], $from_nlines[$i]) =
3153 (split(',', substr($from_text[$i], 1)), 0);
3156 $to_text = pop @from_text;
3157 $to_start = pop @from_start;
3158 $to_nlines = pop @from_nlines;
3160 $line = "<span class=\"chunk_info\">$prefix ";
3161 for (my $i = 0; $i < @from_text; ++$i) {
3162 if ($from->{'href'}[$i]) {
3163 $line .= $cgi->a({-href
=>"$from->{'href'}[$i]#l$from_start[$i]",
3164 -class=>"list"}, $from_text[$i]);
3166 $line .= $from_text[$i];
3170 if ($to->{'href'}) {
3171 $line .= $cgi->a({-href
=>"$to->{'href'}#l$to_start",
3172 -class=>"list"}, $to_text);
3176 $line .= " $prefix</span>" .
3177 "<span class=\"section\">" . esc_html
($section, -nbsp
=>1) . "</span>";
3181 # process patch (diff) line (not to be used for diff headers),
3182 # returning HTML-formatted (but not wrapped) line.
3183 # If the line is passed as a reference, it is treated as HTML and not
3185 sub format_diff_line
{
3186 my ($line, $diff_class, $from, $to) = @_;
3192 $line = untabify
($line);
3194 if ($from && $to && $line =~ m/^\@{2} /) {
3195 $line = format_unidiff_chunk_header
($line, $from, $to);
3196 } elsif ($from && $to && $line =~ m/^\@{3}/) {
3197 $line = format_cc_diff_chunk_header
($line, $from, $to);
3199 $line = esc_html
($line, -nbsp
=>1);
3203 my $diff_classes = "diff diff_body";
3204 $diff_classes .= " $diff_class" if ($diff_class);
3205 $line = "<div class=\"$diff_classes\">$line</div>\n";
3210 # Generates undef or something like "_snapshot_" or "snapshot (_tbz2_ _zip_)",
3211 # linked. Pass the hash of the tree/commit to snapshot.
3212 sub format_snapshot_links
{
3214 my $num_fmts = @snapshot_fmts;
3215 if ($num_fmts > 1) {
3216 # A parenthesized list of links bearing format names.
3217 # e.g. "snapshot (_tar.gz_ _zip_)"
3218 return "<span class=\"snapshots\">snapshot (" . join(' ', map
3225 }, $known_snapshot_formats{$_}{'display'})
3226 , @snapshot_fmts) . ")</span>";
3227 } elsif ($num_fmts == 1) {
3228 # A single "snapshot" link whose tooltip bears the format name.
3230 my ($fmt) = @snapshot_fmts;
3231 return "<span class=\"snapshots\">" .
3236 snapshot_format
=>$fmt
3238 -title
=> "in format: $known_snapshot_formats{$fmt}{'display'}"
3239 }, "snapshot") . "</span>";
3240 } else { # $num_fmts == 0
3245 ## ......................................................................
3246 ## functions returning values to be passed, perhaps after some
3247 ## transformation, to other functions; e.g. returning arguments to href()
3249 # returns hash to be passed to href to generate gitweb URL
3250 # in -title key it returns description of link
3252 my $format = shift || 'Atom';
3253 my %res = (action
=> lc($format));
3254 my $matched_ref = 0;
3256 # feed links are possible only for project views
3257 return unless (defined $project);
3258 # some views should link to OPML, or to generic project feed,
3259 # or don't have specific feed yet (so they should use generic)
3260 return if (!$action || $action =~ /^(?:tags|heads|forks|tag|search)$/x);
3263 # branches refs uses 'refs/' + $get_branch_refs()[x] + '/' prefix
3264 # (fullname) to differentiate from tag links; this also makes
3265 # possible to detect branch links
3266 for my $ref (get_branch_refs
()) {
3267 if ((defined $hash_base && $hash_base =~ m!^refs/\Q$ref\E/(.*)$!) ||
3268 (defined $hash && $hash =~ m!^refs/\Q$ref\E/(.*)$!)) {
3270 $matched_ref = $ref;
3274 # find log type for feed description (title)
3276 if (defined $file_name) {
3277 $type = "history of $file_name";
3278 $type .= "/" if ($action eq 'tree');
3279 $type .= " on '$branch'" if (defined $branch);
3281 $type = "log of $branch" if (defined $branch);
3284 $res{-title
} = $type;
3285 $res{'hash'} = (defined $branch ?
"refs/$matched_ref/$branch" : undef);
3286 $res{'file_name'} = $file_name;
3291 ## ----------------------------------------------------------------------
3292 ## git utility subroutines, invoking git commands
3294 # returns path to the core git executable and the --git-dir parameter as list
3296 $number_of_git_cmds++;
3297 return $GIT, '--git-dir='.$git_dir;
3300 # opens a "-|" cmd pipe handle with 2>/dev/null and returns it
3303 # In order to be compatible with FCGI mode we must use POSIX
3304 # and access the STDERR_FILENO file descriptor directly
3306 use POSIX
qw(STDERR_FILENO dup dup2);
3308 open(my $null, '>', File
::Spec
->devnull) or die "couldn't open devnull: $!";
3309 (my $saveerr = dup
(STDERR_FILENO
)) or die "couldn't dup STDERR: $!";
3310 my $dup2ok = dup2
(fileno($null), STDERR_FILENO
);
3311 close($null) or !$dup2ok or die "couldn't close NULL: $!";
3312 $dup2ok or POSIX
::close($saveerr), die "couldn't dup NULL to STDERR: $!";
3313 my $result = open(my $fd, "-|", @_);
3314 $dup2ok = dup2
($saveerr, STDERR_FILENO
);
3315 POSIX
::close($saveerr) or !$dup2ok or die "couldn't close SAVEERR: $!";
3316 $dup2ok or die "couldn't dup SAVERR to STDERR: $!";
3318 return $result ?
$fd : undef;
3321 # opens a "-|" git_cmd pipe handle with 2>/dev/null and returns it
3323 return cmd_pipe git_cmd
(), @_;
3326 # quote the given arguments for passing them to the shell
3327 # quote_command("command", "arg 1", "arg with ' and ! characters")
3328 # => "'command' 'arg 1' 'arg with '\'' and '\!' characters'"
3329 # Try to avoid using this function wherever possible.
3332 map { my $a = $_; $a =~ s/(['!])/'\\$1'/g; "'$a'" } @_ );
3335 # get HEAD ref of given project as hash
3336 sub git_get_head_hash
{
3337 return git_get_full_hash
(shift, 'HEAD');
3340 sub git_get_full_hash
{
3341 return git_get_hash
(@_);
3344 sub git_get_short_hash
{
3345 return git_get_hash
(@_, '--short=7');
3349 my ($project, $hash, @options) = @_;
3350 my $o_git_dir = $git_dir;
3352 $git_dir = "$projectroot/$project";
3353 if (defined(my $fd = git_cmd_pipe
'rev-parse',
3354 '--verify', '-q', @options, $hash)) {
3356 chomp $retval if defined $retval;
3359 if (defined $o_git_dir) {
3360 $git_dir = $o_git_dir;
3365 # get type of given object
3369 defined(my $fd = git_cmd_pipe
"cat-file", '-t', $hash) or return;
3371 close $fd or return;
3376 # repository configuration
3377 our $config_file = '';
3380 # store multiple values for single key as anonymous array reference
3381 # single values stored directly in the hash, not as [ <value> ]
3382 sub hash_set_multi
{
3383 my ($hash, $key, $value) = @_;
3385 if (!exists $hash->{$key}) {
3386 $hash->{$key} = $value;
3387 } elsif (!ref $hash->{$key}) {
3388 $hash->{$key} = [ $hash->{$key}, $value ];
3390 push @
{$hash->{$key}}, $value;
3394 # return hash of git project configuration
3395 # optionally limited to some section, e.g. 'gitweb'
3396 sub git_parse_project_config
{
3397 my $section_regexp = shift;
3402 defined(my $fh = git_cmd_pipe
"config", '-z', '-l')
3405 while (my $keyval = to_utf8
(scalar <$fh>)) {
3407 my ($key, $value) = split(/\n/, $keyval, 2);
3409 hash_set_multi
(\
%config, $key, $value)
3410 if (!defined $section_regexp || $key =~ /^(?:$section_regexp)\./o);
3417 # convert config value to boolean: 'true' or 'false'
3418 # no value, number > 0, 'true' and 'yes' values are true
3419 # rest of values are treated as false (never as error)
3420 sub config_to_bool
{
3423 return 1 if !defined $val; # section.key
3425 # strip leading and trailing whitespace
3429 return (($val =~ /^\d+$/ && $val) || # section.key = 1
3430 ($val =~ /^(?:true|yes)$/i)); # section.key = true
3433 # convert config value to simple decimal number
3434 # an optional value suffix of 'k', 'm', or 'g' will cause the value
3435 # to be multiplied by 1024, 1048576, or 1073741824
3439 # strip leading and trailing whitespace
3443 if (my ($num, $unit) = ($val =~ /^([0-9]*)([kmg])$/i)) {
3445 # unknown unit is treated as 1
3446 return $num * ($unit eq 'g' ?
1073741824 :
3447 $unit eq 'm' ?
1048576 :
3448 $unit eq 'k' ?
1024 : 1);
3453 # convert config value to array reference, if needed
3454 sub config_to_multi
{
3457 return ref($val) ?
$val : (defined($val) ?
[ $val ] : []);
3460 sub git_get_project_config
{
3461 my ($key, $type) = @_;
3463 return unless defined $git_dir;
3466 return unless ($key);
3467 # only subsection, if exists, is case sensitive,
3468 # and not lowercased by 'git config -z -l'
3469 if (my ($hi, $mi, $lo) = ($key =~ /^([^.]*)\.(.*)\.([^.]*)$/)) {
3471 $key = join(".", lc($hi), $mi, lc($lo));
3472 return if ($lo =~ /\W/ || $hi =~ /\W/);
3476 return if ($key =~ /\W/);
3478 $key =~ s/^gitweb\.//;
3481 if (defined $type) {
3484 unless ($type eq 'bool' || $type eq 'int');
3488 if (!defined $config_file ||
3489 $config_file ne "$git_dir/config") {
3490 %config = git_parse_project_config
('gitweb');
3491 $config_file = "$git_dir/config";
3494 # check if config variable (key) exists
3495 return unless exists $config{"gitweb.$key"};
3498 if (!defined $type) {
3499 return $config{"gitweb.$key"};
3500 } elsif ($type eq 'bool') {
3501 # backward compatibility: 'git config --bool' returns true/false
3502 return config_to_bool
($config{"gitweb.$key"}) ?
'true' : 'false';
3503 } elsif ($type eq 'int') {
3504 return config_to_int
($config{"gitweb.$key"});
3506 return $config{"gitweb.$key"};
3509 # get hash of given path at given ref
3510 sub git_get_hash_by_path
{
3512 my $path = shift || return undef;
3517 defined(my $fd = git_cmd_pipe
"ls-tree", $base, "--", $path)
3518 or die_error
(500, "Open git-ls-tree failed");
3519 my $line = to_utf8
(scalar <$fd>);
3520 close $fd or return undef;
3522 if (!defined $line) {
3523 # there is no tree or hash given by $path at $base
3527 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'
3528 $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t/;
3529 if (defined $type && $type ne $2) {
3530 # type doesn't match
3536 # get path of entry with given hash at given tree-ish (ref)
3537 # used to get 'from' filename for combined diff (merge commit) for renames
3538 sub git_get_path_by_hash
{
3539 my $base = shift || return;
3540 my $hash = shift || return;
3544 defined(my $fd = git_cmd_pipe
"ls-tree", '-r', '-t', '-z', $base)
3546 while (my $line = to_utf8
(scalar <$fd>)) {
3549 #'040000 tree 595596a6a9117ddba9fe379b6b012b558bac8423 gitweb'
3550 #'100644 blob e02e90f0429be0d2a69b76571101f20b8f75530f gitweb/README'
3551 if ($line =~ m/(?:[0-9]+) (?:.+) $hash\t(.+)$/) {
3560 ## ......................................................................
3561 ## git utility functions, directly accessing git repository
3563 # get the value of config variable either from file named as the variable
3564 # itself in the repository ($GIT_DIR/$name file), or from gitweb.$name
3565 # configuration variable in the repository config file.
3566 sub git_get_file_or_project_config
{
3567 my ($path, $name) = @_;
3569 $git_dir = "$projectroot/$path";
3570 open my $fd, '<', "$git_dir/$name"
3571 or return git_get_project_config
($name);
3572 my $conf = to_utf8
(scalar <$fd>);
3574 if (defined $conf) {
3580 sub git_get_project_description
{
3582 return git_get_file_or_project_config
($path, 'description');
3585 sub git_get_project_category
{
3587 return git_get_file_or_project_config
($path, 'category');
3591 # supported formats:
3592 # * $GIT_DIR/ctags/<tagname> file (in 'ctags' subdirectory)
3593 # - if its contents is a number, use it as tag weight,
3594 # - otherwise add a tag with weight 1
3595 # * $GIT_DIR/ctags file, each line is a tag (with weight 1)
3596 # the same value multiple times increases tag weight
3597 # * `gitweb.ctag' multi-valued repo config variable
3598 sub git_get_project_ctags
{
3599 my $project = shift;
3602 $git_dir = "$projectroot/$project";
3603 if (opendir my $dh, "$git_dir/ctags") {
3604 my @files = grep { -f
$_ } map { "$git_dir/ctags/$_" } readdir($dh);
3605 foreach my $tagfile (@files) {
3606 open my $ct, '<', $tagfile
3612 (my $ctag = $tagfile) =~ s
#.*/##;
3613 $ctag = to_utf8
($ctag);
3614 if ($val =~ /^\d+$/) {
3615 $ctags->{$ctag} = $val;
3617 $ctags->{$ctag} = 1;
3622 } elsif (open my $fh, '<', "$git_dir/ctags") {
3623 while (my $line = to_utf8
(scalar <$fh>)) {
3625 $ctags->{$line}++ if $line;
3630 my $taglist = config_to_multi
(git_get_project_config
('ctag'));
3631 foreach my $tag (@
$taglist) {
3639 # return hash, where keys are content tags ('ctags'),
3640 # and values are sum of weights of given tag in every project
3641 sub git_gather_all_ctags
{
3642 my $projects = shift;
3645 foreach my $p (@
$projects) {
3646 foreach my $ct (keys %{$p->{'ctags'}}) {
3647 $ctags->{$ct} += $p->{'ctags'}->{$ct};
3654 sub git_populate_project_tagcloud
{
3655 my ($ctags, $action) = @_;
3657 # First, merge different-cased tags; tags vote on casing
3659 foreach (keys %$ctags) {
3660 $ctags_lc{lc $_}->{count
} += $ctags->{$_};
3661 if (not $ctags_lc{lc $_}->{topcount
}
3662 or $ctags_lc{lc $_}->{topcount
} < $ctags->{$_}) {
3663 $ctags_lc{lc $_}->{topcount
} = $ctags->{$_};
3664 $ctags_lc{lc $_}->{topname
} = $_;
3669 my $matched = $input_params{'ctag_filter'};
3670 if (eval { require HTML
::TagCloud
; 1; }) {
3671 $cloud = HTML
::TagCloud
->new;
3672 foreach my $ctag (sort keys %ctags_lc) {
3673 # Pad the title with spaces so that the cloud looks
3675 my $title = esc_html
($ctags_lc{$ctag}->{topname
});
3676 $title =~ s/ / /g;
3677 $title =~ s/^/ /g;
3678 $title =~ s/$/ /g;
3679 if (defined $matched && $matched eq $ctag) {
3680 $title = qq(<span
class="match">$title</span
>);
3682 $cloud->add($title, href
(-replay
=>1, action
=>$action, ctag_filter
=>$ctag),
3683 $ctags_lc{$ctag}->{count
});
3687 foreach my $ctag (keys %ctags_lc) {
3688 my $title = esc_html
($ctags_lc{$ctag}->{topname
}, -nbsp
=>1);
3689 if (defined $matched && $matched eq $ctag) {
3690 $title = qq(<span
class="match">$title</span
>);
3692 $cloud->{$ctag}{count
} = $ctags_lc{$ctag}->{count
};
3693 $cloud->{$ctag}{ctag
} =
3694 $cgi->a({-href
=>href
(-replay
=>1, action
=>$action, ctag_filter
=>$ctag)}, $title);
3700 sub git_show_project_tagcloud
{
3701 my ($cloud, $count) = @_;
3702 if (ref $cloud eq 'HTML::TagCloud') {
3703 return $cloud->html_and_css($count);
3705 my @tags = sort { $cloud->{$a}->{'count'} <=> $cloud->{$b}->{'count'} } keys %$cloud;
3707 '<div id="htmltagcloud"'.($project ?
'' : ' align="center"').'>' .
3709 $cloud->{$_}->{'ctag'}
3710 } splice(@tags, 0, $count)) .
3715 sub git_get_project_url_list
{
3718 $git_dir = "$projectroot/$path";
3719 open my $fd, '<', "$git_dir/cloneurl"
3720 or return wantarray ?
3721 @
{ config_to_multi
(git_get_project_config
('url')) } :
3722 config_to_multi
(git_get_project_config
('url'));
3723 my @git_project_url_list = map { chomp; to_utf8
($_) } <$fd>;
3726 return wantarray ?
@git_project_url_list : \
@git_project_url_list;
3729 sub git_get_projects_list
{
3730 my $filter = shift || '';
3731 my $paranoid = shift;
3734 if (-d
$projects_list) {
3735 # search in directory
3736 my $dir = $projects_list;
3737 # remove the trailing "/"
3739 my $pfxlen = length("$dir");
3740 my $pfxdepth = ($dir =~ tr!/!!);
3741 # when filtering, search only given subdirectory
3742 if ($filter && !$paranoid) {
3748 follow_fast
=> 1, # follow symbolic links
3749 follow_skip
=> 2, # ignore duplicates
3750 dangling_symlinks
=> 0, # ignore dangling symlinks, silently
3753 our $project_maxdepth;
3755 # skip project-list toplevel, if we get it.
3756 return if (m!^[/.]$!);
3757 # only directories can be git repositories
3758 return unless (-d
$_);
3759 # don't traverse too deep (Find is super slow on os x)
3760 # $project_maxdepth excludes depth of $projectroot
3761 if (($File::Find
::name
=~ tr!/!!) - $pfxdepth > $project_maxdepth) {
3762 $File::Find
::prune
= 1;
3766 my $path = substr($File::Find
::name
, $pfxlen + 1);
3767 # paranoidly only filter here
3768 if ($paranoid && $filter && $path !~ m!^\Q$filter\E/!) {
3771 # we check related file in $projectroot
3772 if (check_export_ok
("$projectroot/$path")) {
3773 push @list, { path
=> $path };
3774 $File::Find
::prune
= 1;
3779 } elsif (-f
$projects_list) {
3780 # read from file(url-encoded):
3781 # 'git%2Fgit.git Linus+Torvalds'
3782 # 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin'
3783 # 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman'
3784 open my $fd, '<', $projects_list or return;
3786 while (my $line = <$fd>) {
3788 my ($path, $owner) = split ' ', $line;
3789 $path = unescape
($path);
3790 $owner = unescape
($owner);
3791 if (!defined $path) {
3794 # if $filter is rpovided, check if $path begins with $filter
3795 if ($filter && $path !~ m!^\Q$filter\E/!) {
3798 if (check_export_ok
("$projectroot/$path")) {
3803 $pr->{'owner'} = to_utf8
($owner);
3813 # written with help of Tree::Trie module (Perl Artistic License, GPL compatibile)
3814 # as side effects it sets 'forks' field to list of forks for forked projects
3815 sub filter_forks_from_projects_list
{
3816 my $projects = shift;
3818 my %trie; # prefix tree of directories (path components)
3819 # generate trie out of those directories that might contain forks
3820 foreach my $pr (@
$projects) {
3821 my $path = $pr->{'path'};
3822 $path =~ s/\.git$//; # forks of 'repo.git' are in 'repo/' directory
3823 next if ($path =~ m!/$!); # skip non-bare repositories, e.g. 'repo/.git'
3824 next unless ($path); # skip '.git' repository: tests, git-instaweb
3825 next unless (-d
"$projectroot/$path"); # containing directory exists
3826 $pr->{'forks'} = []; # there can be 0 or more forks of project
3829 my @dirs = split('/', $path);
3830 # walk the trie, until either runs out of components or out of trie
3832 while (scalar @dirs &&
3833 exists($ref->{$dirs[0]})) {
3834 $ref = $ref->{shift @dirs};
3836 # create rest of trie structure from rest of components
3837 foreach my $dir (@dirs) {
3838 $ref = $ref->{$dir} = {};
3840 # create end marker, store $pr as a data
3841 $ref->{''} = $pr if (!exists $ref->{''});
3844 # filter out forks, by finding shortest prefix match for paths
3847 foreach my $pr (@
$projects) {
3851 foreach my $dir (split('/', $pr->{'path'})) {
3852 if (exists $ref->{''}) {
3853 # found [shortest] prefix, is a fork - skip it
3854 push @
{$ref->{''}{'forks'}}, $pr;
3857 if (!exists $ref->{$dir}) {
3858 # not in trie, cannot have prefix, not a fork
3859 push @filtered, $pr;
3862 # If the dir is there, we just walk one step down the trie.
3863 $ref = $ref->{$dir};
3865 # we ran out of trie
3866 # (shouldn't happen: it's either no match, or end marker)
3867 push @filtered, $pr;
3873 # note: fill_project_list_info must be run first,
3874 # for 'descr_long' and 'ctags' to be filled
3875 sub search_projects_list
{
3876 my ($projlist, %opts) = @_;
3877 my $tagfilter = $opts{'tagfilter'};
3878 my $search_re = $opts{'search_regexp'};
3881 unless ($tagfilter || $search_re);
3883 # searching projects require filling to be run before it;
3884 fill_project_list_info
($projlist,
3885 $tagfilter ?
'ctags' : (),
3886 $search_re ?
('path', 'descr') : ());
3889 foreach my $pr (@
$projlist) {
3892 next unless ref($pr->{'ctags'}) eq 'HASH';
3894 grep { lc($_) eq lc($tagfilter) } keys %{$pr->{'ctags'}};
3898 my $path = $pr->{'path'};
3899 $path =~ s/\.git$//; # should not be included in search
3901 $path =~ /$search_re/ ||
3902 $pr->{'descr_long'} =~ /$search_re/;
3905 push @projects, $pr;
3911 our $gitweb_project_owner = undef;
3912 sub git_get_project_list_from_file
{
3914 return if (defined $gitweb_project_owner);
3916 $gitweb_project_owner = {};
3917 # read from file (url-encoded):
3918 # 'git%2Fgit.git Linus+Torvalds'
3919 # 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin'
3920 # 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman'
3921 if (-f
$projects_list) {
3922 open(my $fd, '<', $projects_list);
3923 while (my $line = <$fd>) {
3925 my ($pr, $ow) = split ' ', $line;
3926 $pr = unescape
($pr);
3927 $ow = unescape
($ow);
3928 $gitweb_project_owner->{$pr} = to_utf8
($ow);
3934 sub git_get_project_owner
{
3938 return undef unless $proj;
3939 $git_dir = "$projectroot/$proj";
3941 if (defined $project && $proj eq $project) {
3942 $owner = git_get_project_config
('owner');
3944 if (!defined $owner && !defined $gitweb_project_owner) {
3945 git_get_project_list_from_file
();
3947 if (!defined $owner && exists $gitweb_project_owner->{$proj}) {
3948 $owner = $gitweb_project_owner->{$proj};
3950 if (!defined $owner && (!defined $project || $proj ne $project)) {
3951 $owner = git_get_project_config
('owner');
3953 if (!defined $owner) {
3954 $owner = get_file_owner
("$git_dir");
3960 sub parse_activity_date
{
3963 if ($dstr =~ /^\s*([-+]?\d+)(?:\s+([-+]\d{4}))?\s*$/) {
3967 if ($dstr =~ /^\s*(\d{4})-(\d{2})-(\d{2})[Tt _](\d{1,2}):(\d{2}):(\d{2})(?:[ _]?([Zz]|(?:[-+]\d{1,2}:?\d{2})))?\s*$/) {
3968 my ($Y,$m,$d,$H,$M,$S,$z) = ($1,$2,$3,$4,$5,$6,$7||'');
3969 my $seconds = timegm
(0+$S, 0+$M, 0+$H, 0+$d, $m-1, $Y-1900);
3970 defined($z) && $z ne '' or $z = 'Z';
3972 substr($z,1,0) = '0' if length($z) == 4;
3974 if (uc($z) ne 'Z') {
3975 $off = 60 * (60 * (0+substr($z,1,2)) + (0+substr($z,3,2)));
3976 $off = -$off if substr($z,0,1) eq '-';
3978 return $seconds - $off;
3983 # If $quick is true only look at $lastactivity_file
3984 sub git_get_last_activity
{
3985 my ($path, $quick) = @_;
3988 $git_dir = "$projectroot/$path";
3989 if ($lastactivity_file && open($fd, "<", "$git_dir/$lastactivity_file")) {
3990 my $activity = <$fd>;
3992 return (undef) unless defined $activity;
3994 return (undef) if $activity eq '';
3995 if (my $timestamp = parse_activity_date
($activity)) {
3996 return ($timestamp);
3999 return (undef) if $quick;
4000 defined($fd = git_cmd_pipe
'for-each-ref',
4001 '--format=%(committer)',
4002 '--sort=-committerdate',
4004 map { "refs/$_" } get_branch_refs
()) or return;
4005 my $most_recent = <$fd>;
4006 close $fd or return (undef);
4007 if (defined $most_recent &&
4008 $most_recent =~ / (\d+) [-+][01]\d\d\d$/) {
4010 return ($timestamp);
4015 # Implementation note: when a single remote is wanted, we cannot use 'git
4016 # remote show -n' because that command always work (assuming it's a remote URL
4017 # if it's not defined), and we cannot use 'git remote show' because that would
4018 # try to make a network roundtrip. So the only way to find if that particular
4019 # remote is defined is to walk the list provided by 'git remote -v' and stop if
4020 # and when we find what we want.
4021 sub git_get_remotes_list
{
4025 my $fd = git_cmd_pipe
'remote', '-v';
4027 while (my $remote = to_utf8
(scalar <$fd>)) {
4029 $remote =~ s!\t(.*?)\s+\((\w+)\)$!!;
4030 next if $wanted and not $remote eq $wanted;
4031 my ($url, $key) = ($1, $2);
4033 $remotes{$remote} ||= { 'heads' => [] };
4034 $remotes{$remote}{$key} = $url;
4036 close $fd or return;
4037 return wantarray ?
%remotes : \
%remotes;
4040 # Takes a hash of remotes as first parameter and fills it by adding the
4041 # available remote heads for each of the indicated remotes.
4042 sub fill_remote_heads
{
4043 my $remotes = shift;
4044 my @heads = map { "remotes/$_" } keys %$remotes;
4045 my @remoteheads = git_get_heads_list
(undef, @heads);
4046 foreach my $remote (keys %$remotes) {
4047 $remotes->{$remote}{'heads'} = [ grep {
4048 $_->{'name'} =~ s!^$remote/!!
4053 sub git_get_references
{
4054 my $type = shift || "";
4056 # 5dc01c595e6c6ec9ccda4f6f69c131c0dd945f8c refs/tags/v2.6.11
4057 # c39ae07f393806ccf406ef966e9a15afc43cc36a refs/tags/v2.6.11^{}
4058 defined(my $fd = git_cmd_pipe
"show-ref", "--dereference",
4059 ($type ?
("--", "refs/$type") : ())) # use -- <pattern> if $type
4062 while (my $line = to_utf8
(scalar <$fd>)) {
4064 if ($line =~ m!^([0-9a-fA-F]{40})\srefs/($type.*)$!) {
4065 if (defined $refs{$1}) {
4066 push @
{$refs{$1}}, $2;
4072 close $fd or return;
4076 sub git_get_rev_name_tags
{
4077 my $hash = shift || return undef;
4079 defined(my $fd = git_cmd_pipe
"name-rev", "--tags", $hash)
4081 my $name_rev = to_utf8
(scalar <$fd>);
4084 if ($name_rev =~ m
|^$hash tags
/(.*)$|) {
4087 # catches also '$hash undefined' output
4092 ## ----------------------------------------------------------------------
4093 ## parse to hash functions
4097 my $tz = shift || "-0000";
4100 my @months = ("Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec");
4101 my @days = ("Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat");
4102 my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($epoch);
4103 $date{'hour'} = $hour;
4104 $date{'minute'} = $min;
4105 $date{'mday'} = $mday;
4106 $date{'day'} = $days[$wday];
4107 $date{'month'} = $months[$mon];
4108 $date{'rfc2822'} = sprintf "%s, %d %s %4d %02d:%02d:%02d +0000",
4109 $days[$wday], $mday, $months[$mon], 1900+$year, $hour ,$min, $sec;
4110 $date{'mday-time'} = sprintf "%d %s %02d:%02d",
4111 $mday, $months[$mon], $hour ,$min;
4112 $date{'iso-8601'} = sprintf "%04d-%02d-%02dT%02d:%02d:%02dZ",
4113 1900+$year, 1+$mon, $mday, $hour ,$min, $sec;
4115 my ($tz_sign, $tz_hour, $tz_min) =
4116 ($tz =~ m/^([-+])(\d\d)(\d\d)$/);
4117 $tz_sign = ($tz_sign eq '-' ?
-1 : +1);
4118 my $local = $epoch + $tz_sign*((($tz_hour*60) + $tz_min)*60);
4119 ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($local);
4120 $date{'hour_local'} = $hour;
4121 $date{'minute_local'} = $min;
4122 $date{'mday_local'} = $mday;
4123 $date{'tz_local'} = $tz;
4124 $date{'iso-tz'} = sprintf("%04d-%02d-%02d %02d:%02d:%02d %s",
4125 1900+$year, $mon+1, $mday,
4126 $hour, $min, $sec, $tz);
4130 sub parse_file_date
{
4132 my $mtime = (stat("$projectroot/$project/$file"))[9];
4133 return () unless defined $mtime;
4134 my $tzoffset = timegm
((localtime($mtime))[0..5]) - $mtime;
4136 if ($tzoffset <= 0) {
4140 $tzoffset = int($tzoffset/60);
4141 $tzstring .= sprintf("%02d%02d", int($tzoffset/60), $tzoffset%60);
4142 return parse_date
($mtime, $tzstring);
4150 defined(my $fd = git_cmd_pipe
"cat-file", "tag", $tag_id) or return;
4151 $tag{'id'} = $tag_id;
4152 while (my $line = to_utf8
(scalar <$fd>)) {
4154 if ($line =~ m/^object ([0-9a-fA-F]{40})$/) {
4155 $tag{'object'} = $1;
4156 } elsif ($line =~ m/^type (.+)$/) {
4158 } elsif ($line =~ m/^tag (.+)$/) {
4160 } elsif ($line =~ m/^tagger (.*) ([0-9]+) (.*)$/) {
4161 $tag{'author'} = $1;
4162 $tag{'author_epoch'} = $2;
4163 $tag{'author_tz'} = $3;
4164 if ($tag{'author'} =~ m/^([^<]+) <([^>]*)>/) {
4165 $tag{'author_name'} = $1;
4166 $tag{'author_email'} = $2;
4168 $tag{'author_name'} = $tag{'author'};
4170 } elsif ($line =~ m/--BEGIN/) {
4171 push @comment, $line;
4173 } elsif ($line eq "") {
4177 push @comment, map(to_utf8
($_), <$fd>);
4178 $tag{'comment'} = \
@comment;
4179 close $fd or return;
4180 if (!defined $tag{'name'}) {
4186 sub parse_commit_text
{
4187 my ($commit_text, $withparents) = @_;
4188 my @commit_lines = split '\n', $commit_text;
4191 pop @commit_lines; # Remove '\0'
4193 if (! @commit_lines) {
4197 my $header = shift @commit_lines;
4198 if ($header !~ m/^[0-9a-fA-F]{40}/) {
4201 ($co{'id'}, my @parents) = split ' ', $header;
4202 while (my $line = shift @commit_lines) {
4203 last if $line eq "\n";
4204 if ($line =~ m/^tree ([0-9a-fA-F]{40})$/) {
4206 } elsif ((!defined $withparents) && ($line =~ m/^parent ([0-9a-fA-F]{40})$/)) {
4208 } elsif ($line =~ m/^author (.*) ([0-9]+) (.*)$/) {
4209 $co{'author'} = to_utf8
($1);
4210 $co{'author_epoch'} = $2;
4211 $co{'author_tz'} = $3;
4212 if ($co{'author'} =~ m/^([^<]+) <([^>]*)>/) {
4213 $co{'author_name'} = $1;
4214 $co{'author_email'} = $2;
4216 $co{'author_name'} = $co{'author'};
4218 } elsif ($line =~ m/^committer (.*) ([0-9]+) (.*)$/) {
4219 $co{'committer'} = to_utf8
($1);
4220 $co{'committer_epoch'} = $2;
4221 $co{'committer_tz'} = $3;
4222 if ($co{'committer'} =~ m/^([^<]+) <([^>]*)>/) {
4223 $co{'committer_name'} = $1;
4224 $co{'committer_email'} = $2;
4226 $co{'committer_name'} = $co{'committer'};
4230 if (!defined $co{'tree'}) {
4233 $co{'parents'} = \
@parents;
4234 $co{'parent'} = $parents[0];
4236 @commit_lines = map to_utf8
($_), @commit_lines;
4237 foreach my $title (@commit_lines) {
4240 $co{'title'} = chop_str
($title, 80, 5);
4241 # remove leading stuff of merges to make the interesting part visible
4242 if (length($title) > 50) {
4243 $title =~ s/^Automatic //;
4244 $title =~ s/^merge (of|with) /Merge ... /i;
4245 if (length($title) > 50) {
4246 $title =~ s/(http|rsync):\/\///;
4248 if (length($title) > 50) {
4249 $title =~ s/(master|www|rsync)\.//;
4251 if (length($title) > 50) {
4252 $title =~ s/kernel.org:?//;
4254 if (length($title) > 50) {
4255 $title =~ s/\/pub\/scm//;
4258 $co{'title_short'} = chop_str
($title, 50, 5);
4262 if (! defined $co{'title'} || $co{'title'} eq "") {
4263 $co{'title'} = $co{'title_short'} = '(no commit message)';
4265 # remove added spaces
4266 foreach my $line (@commit_lines) {
4269 $co{'comment'} = \
@commit_lines;
4271 my $age_epoch = $co{'committer_epoch'};
4272 $co{'age_epoch'} = $age_epoch;
4273 my $time_now = time;
4274 $co{'age_string'} = age_string
($age_epoch, $time_now);
4275 $co{'age_string_date'} = age_string_date
($age_epoch, $time_now);
4276 $co{'age_string_age'} = age_string_age
($age_epoch, $time_now);
4281 my ($commit_id) = @_;
4286 defined(my $fd = git_cmd_pipe
"rev-list",
4292 or die_error
(500, "Open git-rev-list failed");
4293 %co = parse_commit_text
(<$fd>, 1);
4300 my ($commit_id, $maxcount, $skip, $filename, @args) = @_;
4308 defined(my $fd = git_cmd_pipe
"rev-list",
4311 ("--max-count=" . $maxcount),
4312 ("--skip=" . $skip),
4316 ($filename ?
($filename) : ()))
4317 or die_error
(500, "Open git-rev-list failed");
4318 while (my $line = <$fd>) {
4319 my %co = parse_commit_text
($line);
4324 return wantarray ?
@cos : \
@cos;
4327 # parse line of git-diff-tree "raw" output
4328 sub parse_difftree_raw_line
{
4332 # ':100644 100644 03b218260e99b78c6df0ed378e59ed9205ccc96d 3b93d5e7cc7f7dd4ebed13a5cc1a4ad976fc94d8 M ls-files.c'
4333 # ':100644 100644 7f9281985086971d3877aca27704f2aaf9c448ce bc190ebc71bbd923f2b728e505408f5e54bd073a M rev-tree.c'
4334 if ($line =~ m/^:([0-7]{6}) ([0-7]{6}) ([0-9a-fA-F]{40}) ([0-9a-fA-F]{40}) (.)([0-9]{0,3})\t(.*)$/) {
4335 $res{'from_mode'} = $1;
4336 $res{'to_mode'} = $2;
4337 $res{'from_id'} = $3;
4339 $res{'status'} = $5;
4340 $res{'similarity'} = $6;
4341 if ($res{'status'} eq 'R' || $res{'status'} eq 'C') { # renamed or copied
4342 ($res{'from_file'}, $res{'to_file'}) = map { unquote
($_) } split("\t", $7);
4344 $res{'from_file'} = $res{'to_file'} = $res{'file'} = unquote
($7);
4347 # '::100755 100755 100755 60e79ca1b01bc8b057abe17ddab484699a7f5fdb 94067cc5f73388f33722d52ae02f44692bc07490 94067cc5f73388f33722d52ae02f44692bc07490 MR git-gui/git-gui.sh'
4348 # combined diff (for merge commit)
4349 elsif ($line =~ s/^(::+)((?:[0-7]{6} )+)((?:[0-9a-fA-F]{40} )+)([a-zA-Z]+)\t(.*)$//) {
4350 $res{'nparents'} = length($1);
4351 $res{'from_mode'} = [ split(' ', $2) ];
4352 $res{'to_mode'} = pop @
{$res{'from_mode'}};
4353 $res{'from_id'} = [ split(' ', $3) ];
4354 $res{'to_id'} = pop @
{$res{'from_id'}};
4355 $res{'status'} = [ split('', $4) ];
4356 $res{'to_file'} = unquote
($5);
4358 # 'c512b523472485aef4fff9e57b229d9d243c967f'
4359 elsif ($line =~ m/^([0-9a-fA-F]{40})$/) {
4360 $res{'commit'} = $1;
4363 return wantarray ?
%res : \
%res;
4366 # wrapper: return parsed line of git-diff-tree "raw" output
4367 # (the argument might be raw line, or parsed info)
4368 sub parsed_difftree_line
{
4369 my $line_or_ref = shift;
4371 if (ref($line_or_ref) eq "HASH") {
4372 # pre-parsed (or generated by hand)
4373 return $line_or_ref;
4375 return parse_difftree_raw_line
($line_or_ref);
4379 # parse line of git-ls-tree output
4380 sub parse_ls_tree_line
{
4386 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa 16717 panic.c'
4387 $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40}) +(-|[0-9]+)\t(.+)$/s;
4396 $res{'name'} = unquote
($5);
4399 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'
4400 $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t(.+)$/s;
4408 $res{'name'} = unquote
($4);
4412 return wantarray ?
%res : \
%res;
4415 # generates _two_ hashes, references to which are passed as 2 and 3 argument
4416 sub parse_from_to_diffinfo
{
4417 my ($diffinfo, $from, $to, @parents) = @_;
4419 if ($diffinfo->{'nparents'}) {
4421 $from->{'file'} = [];
4422 $from->{'href'} = [];
4423 fill_from_file_info
($diffinfo, @parents)
4424 unless exists $diffinfo->{'from_file'};
4425 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
4426 $from->{'file'}[$i] =
4427 defined $diffinfo->{'from_file'}[$i] ?
4428 $diffinfo->{'from_file'}[$i] :
4429 $diffinfo->{'to_file'};
4430 if ($diffinfo->{'status'}[$i] ne "A") { # not new (added) file
4431 $from->{'href'}[$i] = href
(action
=>"blob",
4432 hash_base
=>$parents[$i],
4433 hash
=>$diffinfo->{'from_id'}[$i],
4434 file_name
=>$from->{'file'}[$i]);
4436 $from->{'href'}[$i] = undef;
4440 # ordinary (not combined) diff
4441 $from->{'file'} = $diffinfo->{'from_file'};
4442 if ($diffinfo->{'status'} ne "A") { # not new (added) file
4443 $from->{'href'} = href
(action
=>"blob", hash_base
=>$hash_parent,
4444 hash
=>$diffinfo->{'from_id'},
4445 file_name
=>$from->{'file'});
4447 delete $from->{'href'};
4451 $to->{'file'} = $diffinfo->{'to_file'};
4452 if (!is_deleted
($diffinfo)) { # file exists in result
4453 $to->{'href'} = href
(action
=>"blob", hash_base
=>$hash,
4454 hash
=>$diffinfo->{'to_id'},
4455 file_name
=>$to->{'file'});
4457 delete $to->{'href'};
4461 ## ......................................................................
4462 ## parse to array of hashes functions
4464 sub git_get_heads_list
{
4465 my ($limit, @classes) = @_;
4466 @classes = get_branch_refs
() unless @classes;
4467 my @patterns = map { "refs/$_" } @classes;
4470 defined(my $fd = git_cmd_pipe
'for-each-ref',
4471 ($limit ?
'--count='.($limit+1) : ()), '--sort=-committerdate',
4472 '--format=%(objectname) %(refname) %(subject)%00%(committer)',
4475 while (my $line = to_utf8
(scalar <$fd>)) {
4479 my ($refinfo, $committerinfo) = split(/\0/, $line);
4480 my ($hash, $name, $title) = split(' ', $refinfo, 3);
4481 my ($committer, $epoch, $tz) =
4482 ($committerinfo =~ /^(.*) ([0-9]+) (.*)$/);
4483 $ref_item{'fullname'} = $name;
4484 my $strip_refs = join '|', map { quotemeta } get_branch_refs
();
4485 $name =~ s!^refs/($strip_refs|remotes)/!!;
4486 $ref_item{'name'} = $name;
4487 # for refs neither in 'heads' nor 'remotes' we want to
4488 # show their ref dir
4489 my $ref_dir = (defined $1) ?
$1 : '';
4490 if ($ref_dir ne '' and $ref_dir ne 'heads' and $ref_dir ne 'remotes') {
4491 $ref_item{'name'} .= ' (' . $ref_dir . ')';
4494 $ref_item{'id'} = $hash;
4495 $ref_item{'title'} = $title || '(no commit message)';
4496 $ref_item{'epoch'} = $epoch;
4498 $ref_item{'age'} = age_string
($ref_item{'epoch'});
4500 $ref_item{'age'} = "unknown";
4503 push @headslist, \
%ref_item;
4507 return wantarray ?
@headslist : \
@headslist;
4510 sub git_get_tags_list
{
4513 my $all = shift || 0;
4514 my $order = shift || $default_refs_order;
4515 my $sortkey = $all && $order eq 'name' ?
'refname' : '-creatordate';
4517 defined(my $fd = git_cmd_pipe
'for-each-ref',
4518 ($limit ?
'--count='.($limit+1) : ()), "--sort=$sortkey",
4519 '--format=%(objectname) %(objecttype) %(refname) '.
4520 '%(*objectname) %(*objecttype) %(subject)%00%(creator)',
4521 ($all ?
'refs' : 'refs/tags'))
4523 while (my $line = to_utf8
(scalar <$fd>)) {
4527 my ($refinfo, $creatorinfo) = split(/\0/, $line);
4528 my ($id, $type, $name, $refid, $reftype, $title) = split(' ', $refinfo, 6);
4529 my ($creator, $epoch, $tz) =
4530 ($creatorinfo =~ /^(.*) ([0-9]+) (.*)$/);
4531 $ref_item{'fullname'} = $name;
4532 $name =~ s!^refs/!! if $all;
4533 $name =~ s!^refs/tags/!! unless $all;
4535 $ref_item{'type'} = $type;
4536 $ref_item{'id'} = $id;
4537 $ref_item{'name'} = $name;
4538 if ($type eq "tag") {
4539 $ref_item{'subject'} = $title;
4540 $ref_item{'reftype'} = $reftype;
4541 $ref_item{'refid'} = $refid;
4543 $ref_item{'reftype'} = $type;
4544 $ref_item{'refid'} = $id;
4547 if ($type eq "tag" || $type eq "commit") {
4548 $ref_item{'epoch'} = $epoch;
4550 $ref_item{'age'} = age_string
($ref_item{'epoch'});
4552 $ref_item{'age'} = "unknown";
4556 push @tagslist, \
%ref_item;
4560 return wantarray ?
@tagslist : \
@tagslist;
4563 ## ----------------------------------------------------------------------
4564 ## filesystem-related functions
4566 sub get_file_owner
{
4569 my ($dev, $ino, $mode, $nlink, $st_uid, $st_gid, $rdev, $size) = stat($path);
4570 my ($name, $passwd, $uid, $gid, $quota, $comment, $gcos, $dir, $shell) = getpwuid($st_uid);
4571 if (!defined $gcos) {
4575 $owner =~ s/[,;].*$//;
4576 return to_utf8
($owner);
4579 # assume that file exists
4581 my $filename = shift;
4583 open my $fd, '<', $filename;
4590 # return undef on failure
4591 sub collect_output
{
4592 defined(my $fd = cmd_pipe
@_) or return undef;
4597 my $result = join('', map({ to_utf8
($_) } <$fd>));
4598 close $fd or return undef;
4602 # return undef on failure
4603 # return '' if only comments
4604 sub collect_html_file
{
4605 my $filename = shift;
4607 open my $fd, '<', $filename or return undef;
4608 my $result = join('', map({ to_utf8
($_) } <$fd>));
4609 close $fd or return undef;
4610 return undef unless defined($result);
4612 $test =~ s/<!--(?:[^-]|(?:-(?!-)))*-->//gs;
4614 return $test eq '' ?
'' : $result;
4617 ## ......................................................................
4618 ## mimetype related functions
4620 sub mimetype_guess_file
{
4621 my $filename = shift;
4622 my $mimemap = shift;
4623 my $rawmode = shift;
4624 -r
$mimemap or return undef;
4627 open(my $mh, '<', $mimemap) or return undef;
4629 next if m/^#/; # skip comments
4630 my ($mimetype, @exts) = split(/\s+/);
4631 foreach my $ext (@exts) {
4632 $mimemap{$ext} = $mimetype;
4638 $ext = $1 if $filename =~ /\.([^.]*)$/;
4639 $ans = $mimemap{$ext} if $ext;
4642 $ans = 'text/html' if $l eq 'application/xhtml+xml';
4644 $ans = 'text/xml' if $l =~ m
!^application
/[^\s
:;,=]+\
+xml
$! ||
4645 $l eq 'image/svg+xml' ||
4646 $l eq 'application/xml-dtd' ||
4647 $l eq 'application/xml-external-parsed-entity';
4653 sub mimetype_guess
{
4654 my $filename = shift;
4655 my $rawmode = shift;
4657 $filename =~ /\./ or return undef;
4659 if ($mimetypes_file) {
4660 my $file = $mimetypes_file;
4661 if ($file !~ m!^/!) { # if it is relative path
4662 # it is relative to project
4663 $file = "$projectroot/$project/$file";
4665 $mime = mimetype_guess_file
($filename, $file, $rawmode);
4667 $mime ||= mimetype_guess_file
($filename, '/etc/mime.types', $rawmode);
4673 my $filename = shift;
4674 my $rawmode = shift;
4677 # The -T/-B file operators produce the wrong result unless a perlio
4678 # layer is present when the file handle is a pipe that delivers less
4679 # than 512 bytes of data before reaching EOF.
4681 # If we are running in a Perl that uses the stdio layer rather than the
4682 # unix+perlio layers we will end up adding a perlio layer on top of the
4683 # stdio layer and get a second level of buffering. This is harmless
4684 # and it makes the -T/-B file operators work properly in all cases.
4686 binmode $fd, ":perlio" or die_error
(500, "Adding perlio layer failed")
4687 unless grep /^perlio$/, PerlIO
::get_layers
($fd);
4689 $mime = mimetype_guess
($filename, $rawmode) if defined $filename;
4691 if (!$mime && $filename) {
4692 if ($filename =~ m/\.html?$/i) {
4693 $mime = 'text/html';
4694 } elsif ($filename =~ m/\.xht(?:ml)?$/i) {
4695 $mime = 'text/html';
4696 } elsif ($filename =~ m/\.te?xt?$/i) {
4697 $mime = 'text/plain';
4698 } elsif ($filename =~ m/\.(?:markdown|md)$/i) {
4699 $mime = 'text/plain';
4700 } elsif ($filename =~ m/\.png$/i) {
4701 $mime = 'image/png';
4702 } elsif ($filename =~ m/\.gif$/i) {
4703 $mime = 'image/gif';
4704 } elsif ($filename =~ m/\.jpe?g$/i) {
4705 $mime = 'image/jpeg';
4706 } elsif ($filename =~ m/\.svgz?$/i) {
4707 $mime = 'image/svg+xml';
4712 return $default_blob_plain_mimetype || 'application/octet-stream' unless $fd || $mime;
4714 $mime = -T
$fd ?
'text/plain' : 'application/octet-stream' unless $mime;
4722 return scalar($data =~ /^[\x00-\x7f]*$/);
4727 return utf8
::decode
($data);
4730 sub extract_html_charset
{
4731 return undef unless $_[0] && "$_[0]</head>" =~ m
#<head(?:\s+[^>]*)?(?<!/)>(.*?)</head\s*>#is;
4733 return $2 if $head =~ m
#<meta\s+charset\s*=\s*(['"])\s*([a-z0-9(:)_.+-]+)\s*\1\s*/?>#is;
4734 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) {
4735 my %kv = (lc($1) => $3, lc($4) => $6);
4736 my ($he, $c) = (lc($kv{'http-equiv'}), $kv{'content'});
4737 return $1 if $he && $c && $he eq 'content-type' &&
4738 $c =~ m!\s*text/html\s*;\s*charset\s*=\s*([a-z0-9(:)_.+-]+)\s*$!is;
4743 sub blob_contenttype
{
4744 my ($fd, $file_name, $type) = @_;
4746 $type ||= blob_mimetype
($fd, $file_name, 1);
4747 return $type unless $type =~ m!^text/.+!i;
4748 my ($leader, $charset, $htmlcharset);
4749 if ($fd && read($fd, $leader, 32768)) {{
4750 $charset='US-ASCII' if is_ascii
($leader);
4751 return ("$type; charset=UTF-8", $leader) if !$charset && is_valid_utf8
($leader);
4752 $charset='ISO-8859-1' unless $charset;
4753 $htmlcharset = extract_html_charset
($leader) if $type eq 'text/html';
4754 if ($htmlcharset && $charset ne 'US-ASCII') {
4755 $htmlcharset = undef if $htmlcharset =~ /^(?:utf-8|us-ascii)$/i
4758 return ("$type; charset=$htmlcharset", $leader) if $htmlcharset;
4759 my $defcharset = $default_text_plain_charset || '';
4760 $defcharset =~ s/^\s+//;
4761 $defcharset =~ s/\s+$//;
4762 $defcharset = '' if $charset && $charset ne 'US-ASCII' && $defcharset =~ /^(?:utf-8|us-ascii)$/i;
4763 return ("$type; charset=" . ($defcharset || 'ISO-8859-1'), $leader);
4766 # peek the first upto 128 bytes off a file handle
4774 return '' unless $fd && read($fd, $prefix128, 128);
4776 # In the general case, we're guaranteed only to be able to ungetc one
4777 # character (provided, of course, we actually got a character first).
4781 # 1) we are dealing with a :perlio layer since blob_mimetype will have
4782 # already been called at least once on the file handle before us
4784 # 2) we have an $fd positioned at the start of the input stream and
4785 # therefore know we were positioned at a buffer boundary before
4786 # reading the initial upto 128 bytes
4788 # 3) the buffer size is at least 512 bytes
4790 # 4) we are careful to only unget raw bytes
4792 # 5) we are attempting to unget exactly the same number of bytes we got
4794 # Given the above conditions we will ALWAYS be able to safely unget
4795 # the $prefix128 value we just got.
4797 # In fact, we could read up to 511 bytes and still be sure.
4798 # (Reading 512 might pop us into the next internal buffer, but probably
4799 # not since that could break the always able to unget at least the one
4800 # you just got guarantee.)
4802 map {$fd->ungetc(ord($_))} reverse(split //, $prefix128);
4807 # guess file syntax for syntax highlighting; return undef if no highlighting
4808 # the name of syntax can (in the future) depend on syntax highlighter used
4809 sub guess_file_syntax
{
4810 my ($fd, $mimetype, $file_name) = @_;
4811 return undef unless $fd && defined $file_name &&
4812 defined $mimetype && $mimetype =~ m!^text/.+!i;
4813 my $basename = basename
($file_name, '.in');
4814 return $highlight_basename{$basename}
4815 if exists $highlight_basename{$basename};
4817 # Peek to see if there's a shebang or xml line.
4818 # We always operate on bytes when testing this.
4821 my $shebang = peek128bytes
($fd);
4822 if (length($shebang) >= 4 && $shebang =~ /^#!/) { # 4 would be '#!/x'
4823 foreach my $key (keys %highlight_shebang) {
4824 my $ar = ref($highlight_shebang{$key}) ?
4825 $highlight_shebang{$key} :
4826 [$highlight_shebang{key
}];
4827 map {return $key if $shebang =~ /$_/} @
$ar;
4830 return 'xml' if $shebang =~ m!^\s*<\?xml\s!; # "xml" must be lowercase
4833 $basename =~ /\.([^.]*)$/;
4834 my $ext = $1 or return undef;
4835 return $highlight_ext{$ext}
4836 if exists $highlight_ext{$ext};
4841 # run highlighter and return FD of its output,
4842 # or return original FD if no highlighting
4843 sub run_highlighter
{
4844 my ($fd, $syntax) = @_;
4845 return $fd unless $fd && !eof($fd) && defined $highlight_bin && defined $syntax;
4847 defined(my $hifd = cmd_pipe
$posix_shell_bin, '-c',
4848 quote_command
(git_cmd
(), "cat-file", "blob", $hash)." | ".
4849 quote_command
($highlight_bin).
4850 " --replace-tabs=8 --fragment --syntax $syntax")
4851 or die_error
(500, "Couldn't open file or run syntax highlighter");
4853 # just in case, should not happen as we tested !eof($fd) above
4854 return $fd if close($hifd);
4857 !$! or die_error
(500, "Couldn't close syntax highighter pipe");
4859 # leaving us with the only possibility a non-zero exit status (possibly a signal);
4860 # instead of dying horribly on this, just skip the highlighting
4861 # but do output a message about it to STDERR that will end up in the log
4862 print STDERR
"warning: skipping failed highlight for --syntax $syntax: ".
4863 sprintf("child exit status 0x%x\n", $?
);
4870 ## ======================================================================
4871 ## functions printing HTML: header, footer, error page
4873 sub get_page_title
{
4874 my $title = to_utf8
($site_name);
4876 unless (defined $project) {
4877 if (defined $project_filter) {
4878 $title .= " - projects in '" . esc_path
($project_filter) . "'";
4882 $title .= " - " . to_utf8
($project);
4884 return $title unless (defined $action);
4885 my $action_print = $action eq 'blame_incremental' ?
'blame' : $action;
4886 $title .= "/$action_print"; # $action is US-ASCII (7bit ASCII)
4888 return $title unless (defined $file_name);
4889 $title .= " - " . esc_path
($file_name);
4890 if ($action eq "tree" && $file_name !~ m
|/$|) {
4897 sub get_content_type_html
{
4898 # We do not ever emit application/xhtml+xml since that gives us
4899 # no benefits and it makes many browsers (e.g. Firefox) exceedingly
4900 # strict, which is troublesome for example when showing user-supplied
4901 # README.html files.
4905 sub print_feed_meta
{
4906 if (defined $project) {
4907 my %href_params = get_feed_info
();
4908 if (!exists $href_params{'-title'}) {
4909 $href_params{'-title'} = 'log';
4912 foreach my $format (qw(RSS Atom)) {
4913 my $type = lc($format);
4915 '-rel' => 'alternate',
4916 '-title' => esc_attr
("$project - $href_params{'-title'} - $format feed"),
4917 '-type' => "application/$type+xml"
4920 $href_params{'extra_options'} = undef;
4921 $href_params{'action'} = $type;
4922 $link_attr{'-href'} = href
(%href_params);
4924 "rel=\"$link_attr{'-rel'}\" ".
4925 "title=\"$link_attr{'-title'}\" ".
4926 "href=\"$link_attr{'-href'}\" ".
4927 "type=\"$link_attr{'-type'}\" ".
4930 $href_params{'extra_options'} = '--no-merges';
4931 $link_attr{'-href'} = href
(%href_params);
4932 $link_attr{'-title'} .= ' (no merges)';
4934 "rel=\"$link_attr{'-rel'}\" ".
4935 "title=\"$link_attr{'-title'}\" ".
4936 "href=\"$link_attr{'-href'}\" ".
4937 "type=\"$link_attr{'-type'}\" ".
4942 printf('<link rel="alternate" title="%s projects list" '.
4943 'href="%s" type="text/plain; charset=utf-8" />'."\n",
4944 esc_attr
($site_name), href
(project
=>undef, action
=>"project_index"));
4945 printf('<link rel="alternate" title="%s projects feeds" '.
4946 'href="%s" type="text/x-opml" />'."\n",
4947 esc_attr
($site_name), href
(project
=>undef, action
=>"opml"));
4951 sub compute_stylesheet_links
{
4952 return "<?gitweb compute_stylesheet_links?>" if $cache_mode_active;
4954 # include each stylesheet that exists, providing backwards capability
4955 # for those people who defined $stylesheet in a config file
4956 if (defined $stylesheet) {
4957 return '<link rel="stylesheet" type="text/css" href="'.esc_url
($stylesheet).'"/>'."\n";
4960 foreach my $stylesheet (@stylesheets) {
4961 next unless $stylesheet;
4962 $sheets .= '<link rel="stylesheet" type="text/css" href="'.esc_url
($stylesheet).'"/>'."\n";
4968 sub print_header_links
{
4971 print compute_stylesheet_links
();
4973 if ($status eq '200 OK');
4974 if (defined $favicon) {
4975 print qq(<link rel
="shortcut icon" href
=").esc_url($favicon).qq(" type
="image/png" />\n);
4979 sub print_nav_breadcrumbs_path
{
4980 my $dirprefix = undef;
4981 while (my $part = shift) {
4982 $dirprefix .= "/" if defined $dirprefix;
4983 $dirprefix .= $part;
4984 print $cgi->a({-href
=> href
(project
=> undef,
4985 project_filter
=> $dirprefix,
4986 action
=> "project_list")},
4987 esc_html
($part)) . " / ";
4991 sub print_nav_breadcrumbs
{
4994 for my $crumb (@extra_breadcrumbs, [ $home_link_str => $home_link ]) {
4995 print $cgi->a({-href
=> esc_url
($crumb->[1])}, $crumb->[0]) . " / ";
4997 if (defined $project) {
4998 my @dirname = split '/', $project;
4999 my $projectbasename = pop @dirname;
5000 print_nav_breadcrumbs_path
(@dirname);
5001 print $cgi->a({-href
=> href
(action
=>"summary")}, esc_html
($projectbasename));
5002 if (defined $action) {
5003 my $action_print = $action ;
5004 $action_print = 'blame' if $action_print eq 'blame_incremental';
5005 if (defined $opts{-action_extra
}) {
5006 $action_print = $cgi->a({-href
=> href
(action
=>$action)},
5009 print " / $action_print";
5011 if (defined $opts{-action_extra
}) {
5012 print " / $opts{-action_extra}";
5015 } elsif (defined $project_filter) {
5016 print_nav_breadcrumbs_path
(split '/', $project_filter);
5020 sub print_search_form
{
5021 if (!defined $searchtext) {
5025 if (defined $hash_base) {
5026 $search_hash = $hash_base;
5027 } elsif (defined $hash) {
5028 $search_hash = $hash;
5030 $search_hash = "HEAD";
5032 # We can't use href() here because we need to encode the
5033 # URL parameters into the form, not into the action link.
5034 my $action = $my_uri;
5035 my $use_pathinfo = gitweb_check_feature
('pathinfo');
5036 if ($use_pathinfo) {
5037 # See notes about doubled / in href()
5039 $action .= "/".esc_path_info
($project);
5041 print $cgi->start_form(-method
=> "get", -action
=> $action) .
5042 "<div class=\"search\">\n" .
5044 $cgi->input({-name
=>"p", -value
=>$project, -type
=>"hidden"}) . "\n") .
5045 $cgi->input({-name
=>"a", -value
=>"search", -type
=>"hidden"}) . "\n" .
5046 $cgi->input({-name
=>"h", -value
=>$search_hash, -type
=>"hidden"}) . "\n" .
5047 $cgi->popup_menu(-name
=> 'st', -default => 'commit',
5048 -values => ['commit', 'grep', 'author', 'committer', 'pickaxe']) .
5049 $cgi->sup($cgi->a({-href
=> href
(action
=>"search_help"),
5050 -title
=> "search help" },
5051 "<span style=\"padding-bottom:1em\">? </span>")) . " search:\n",
5052 $cgi->textfield(-name
=> "s", -value
=> $searchtext, -override
=> 1) . "\n" .
5053 "<span title=\"Extended regular expression\">" .
5054 $cgi->checkbox(-name
=> 'sr', -value
=> 1, -label
=> 're',
5055 -checked
=> $search_use_regexp) .
5058 $cgi->end_form() . "\n";
5061 sub git_header_html
{
5062 my $status = shift || "200 OK";
5063 my $expires = shift;
5066 my $title = get_page_title
();
5067 my $content_type = get_content_type_html
();
5068 print $cgi->header(-type
=>$content_type, -charset
=> 'utf-8',
5069 -status
=> $status, -expires
=> $expires)
5070 unless ($opts{'-no_http_header'});
5071 my $mod_perl_version = $ENV{'MOD_PERL'} ?
" $ENV{'MOD_PERL'}" : '';
5073 <?xml version="1.0" encoding="utf-8"?>
5074 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
5075 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US" lang="en-US">
5076 <!-- git web interface version $version, (C) 2005-2006, Kay Sievers <kay.sievers\@vrfy.org>, Christian Gierke -->
5077 <!-- git core binaries version $git_version -->
5079 <meta http-equiv="content-type" content="$content_type; charset=utf-8"/>
5080 <meta name="generator" content="gitweb/$version git/$git_version$mod_perl_version"/>
5081 <meta name="robots" content="index, nofollow"/>
5082 <title>$title</title>
5083 <script type="text/javascript">/* <![CDATA[ */
5084 function fixBlameLinks() {
5085 var allLinks = document.getElementsByTagName("a");
5086 for (var i = 0; i < allLinks.length; i++) {
5087 var link = allLinks.item(i);
5088 if (link.className == 'blamelink')
5089 link.href = link.href.replace("/blame/", "/blame_incremental/");
5094 # the stylesheet, favicon etc urls won't work correctly with path_info
5095 # unless we set the appropriate base URL
5096 if ($ENV{'PATH_INFO'}) {
5097 print "<base href=\"".esc_url
($base_url)."\" />\n";
5099 print_header_links
($status);
5101 if (defined $site_html_head_string) {
5102 print to_utf8
($site_html_head_string);
5106 "<body><span class=\"body\">\n";
5108 if (defined $site_header && -f
$site_header) {
5109 insert_file
($site_header);
5112 print "<div class=\"page_header\">\n";
5113 print "<span class=\"logo-container\"><span class=\"logo-default\">";
5114 if (defined $logo) {
5115 print $cgi->a({-href
=> esc_url
($logo_url),
5116 -title
=> $logo_label,
5117 -class => "logo-link"},
5118 $cgi->img({-src
=> esc_url
($logo),
5119 -width
=> 72, -height
=> 27,
5121 -class => "logo"}));
5123 print "</span></span><span class=\"banner-container\">";
5124 print_nav_breadcrumbs
(%opts);
5125 print "</span></div>\n";
5127 my $have_search = gitweb_check_feature
('search');
5128 if (defined $project && $have_search) {
5129 print_search_form
();
5133 sub compute_timed_interval
{
5134 return "<?gitweb compute_timed_interval?>" if $cache_mode_active;
5135 return tv_interval
($t0, [ gettimeofday
() ]);
5138 sub compute_commands_count
{
5139 return "<?gitweb compute_commands_count?>" if $cache_mode_active;
5140 my $s = $number_of_git_cmds == 1 ?
'' : 's';
5141 return '<span id="generating_cmd">'.
5142 $number_of_git_cmds.
5143 "</span> git command$s";
5146 sub git_footer_html
{
5147 my $feed_class = 'rss_logo';
5149 print "<div class=\"page_footer\">\n";
5150 if (defined $project) {
5151 my $descr = git_get_project_description
($project);
5152 if (defined $descr) {
5153 print "<div class=\"page_footer_text\">" . esc_html
($descr) . "</div>\n";
5156 my %href_params = get_feed_info
();
5157 if (!%href_params) {
5158 $feed_class .= ' generic';
5160 $href_params{'-title'} ||= 'log';
5162 foreach my $format (qw(RSS Atom)) {
5163 $href_params{'action'} = lc($format);
5164 print $cgi->a({-href
=> href
(%href_params),
5165 -title
=> "$href_params{'-title'} $format feed",
5166 -class => $feed_class}, $format)."\n";
5170 print $cgi->a({-href
=> href
(project
=>undef, action
=>"opml",
5171 project_filter
=> $project_filter),
5172 -class => $feed_class}, "OPML") . " ";
5173 print $cgi->a({-href
=> href
(project
=>undef, action
=>"project_index",
5174 project_filter
=> $project_filter),
5175 -class => $feed_class}, "TXT") . "\n";
5177 print "</div>\n"; # class="page_footer"
5179 if (defined $t0 && gitweb_check_feature
('timed')) {
5180 print "<div id=\"generating_info\">\n";
5181 print 'This page took '.
5182 '<span id="generating_time" class="time_span">'.
5183 compute_timed_interval
().
5186 compute_commands_count
().
5188 print "</div>\n"; # class="page_footer"
5191 if (defined $site_footer && -f
$site_footer) {
5192 insert_file
($site_footer);
5195 print qq!<script type
="text/javascript" src
="!.esc_url($javascript).qq!"></script
>\n!;
5196 if (defined $action &&
5197 $action eq 'blame_incremental') {
5198 print qq!<script type
="text/javascript">\n!.
5199 qq!startBlame
("!. href(action=>"blame_data
", -replay=>1) .qq!",\n!.
5200 qq! "!. href() .qq!");\n!.
5203 my ($jstimezone, $tz_cookie, $datetime_class) =
5204 gitweb_get_feature
('javascript-timezone');
5206 print qq!<script type
="text/javascript">\n!.
5207 qq!window
.onload
= function
() {\n!;
5208 if (gitweb_check_feature
('blame_incremental')) {
5209 print qq! fixBlameLinks
();\n!;
5211 if (gitweb_check_feature
('javascript-actions')) {
5212 print qq! fixLinks
();\n!;
5214 if ($jstimezone && $tz_cookie && $datetime_class) {
5215 print qq! var tz_cookie
= { name
: '$tz_cookie', expires
: 14, path
: '/' };\n!. # in days
5216 qq! onloadTZSetup
('$jstimezone', tz_cookie
, '$datetime_class');\n!;
5222 print "</span></body>\n" .
5226 # die_error(<http_status_code>, <error_message>[, <detailed_html_description>])
5227 # Example: die_error(404, 'Hash not found')
5228 # By convention, use the following status codes (as defined in RFC 2616):
5229 # 400: Invalid or missing CGI parameters, or
5230 # requested object exists but has wrong type.
5231 # 403: Requested feature (like "pickaxe" or "snapshot") not enabled on
5232 # this server or project.
5233 # 404: Requested object/revision/project doesn't exist.
5234 # 500: The server isn't configured properly, or
5235 # an internal error occurred (e.g. failed assertions caused by bugs), or
5236 # an unknown error occurred (e.g. the git binary died unexpectedly).
5237 # 503: The server is currently unavailable (because it is overloaded,
5238 # or down for maintenance). Generally, this is a temporary state.
5240 my $status = shift || 500;
5241 my $error = esc_html
(shift) || "Internal Server Error";
5245 my %http_responses = (
5246 400 => '400 Bad Request',
5247 403 => '403 Forbidden',
5248 404 => '404 Not Found',
5249 500 => '500 Internal Server Error',
5250 503 => '503 Service Unavailable',
5252 git_header_html
($http_responses{$status}, undef, %opts);
5254 <div class="page_body">
5259 if (defined $extra) {
5267 unless ($opts{'-error_handler'});
5270 ## ----------------------------------------------------------------------
5271 ## functions printing or outputting HTML: navigation
5273 # $content is wrapped in a span with class 'tab'
5274 # If $selected is true it also has class 'selected'
5275 # If $disabled is true it also has class 'disabled'
5276 # Whether or not a tab can be disabled and selected at the same time
5277 # is up to the caller
5278 # If $extra_classes is non-empty, it is a whitespace-separated list of
5279 # additional class names to include
5280 # Note that $content MUST already be html-escaped as needed because
5281 # it is included verbatim. And so are any extra class names.
5283 my ($content, $selected, $disabled, $extra_classes) = @_;
5284 my @classes = ("tab");
5285 push(@classes, "selected") if $selected;
5286 push(@classes, "disabled") if $disabled;
5287 push(@classes, split(' ', $extra_classes)) if $extra_classes;
5288 return "<span class=\"" . join(" ", @classes) . "\">". $content . "</span>";
5291 sub git_print_page_nav
{
5292 my ($current, $suppress, $head, $treehead, $treebase, $extra) = @_;
5293 $extra = '' if !defined $extra; # pager or formats
5294 $extra = "<span class=\"page_nav_sub\">".$extra."</span>" if $extra ne '';
5296 my @navs = qw(summary log commit commitdiff tree refs);
5298 @navs = grep { $_ ne $suppress } @navs;
5301 my %arg = map { $_ => {action
=>$_} } @navs;
5302 if (defined $head) {
5303 for (qw(commit commitdiff)) {
5304 $arg{$_}{'hash'} = $head;
5306 if ($current =~ m/^(tree | log | shortlog | commit | commitdiff | search)$/x) {
5307 $arg{'log'}{'hash'} = $head;
5311 $arg{'log'}{'action'} = 'shortlog';
5312 if ($current eq 'log') {
5313 $current = 'shortlog';
5314 } elsif ($current eq 'shortlog') {
5317 $arg{'tree'}{'hash'} = $treehead if defined $treehead;
5318 $arg{'tree'}{'hash_base'} = $treebase if defined $treebase;
5320 my @actions = gitweb_get_feature
('actions');
5321 my $escname = $project;
5322 $escname =~ s/[+]/%2B/g;
5325 'n' => $project, # project name
5326 'f' => $git_dir, # project path within filesystem
5327 'h' => $treehead || '', # current hash ('h' parameter)
5328 'b' => $treebase || '', # hash base ('hb' parameter)
5329 'e' => $escname, # project name with '+' escaped
5332 my ($label, $link, $pos) = splice(@actions,0,3);
5334 @navs = map { $_ eq $pos ?
($_, $label) : $_ } @navs;
5336 $link =~ s/%([%nfhbe])/$repl{$1}/g;
5337 $arg{$label}{'_href'} = $link;
5340 print "<div class=\"page_nav\">\n" .
5342 map { $_ eq $current ?
5344 tabspan
($cgi->a({-href
=> ($arg{$_}{_href
} ?
$arg{$_}{_href
} : href
(%{$arg{$_}}))}, "$_"))
5346 print "<br/>\n$extra<br/>\n" .
5350 # returns a submenu for the nagivation of the refs views (tags, heads,
5351 # remotes) with the current view disabled and the remotes view only
5352 # available if the feature is enabled
5353 sub format_ref_views
{
5355 my @ref_views = qw{tags heads
};
5356 push @ref_views, 'remotes' if gitweb_check_feature
('remote_heads');
5357 return join $barsep, map {
5358 $_ eq $current ? tabspan
($_, 1) :
5359 tabspan
($cgi->a({-href
=> href
(action
=>$_)}, $_))
5363 sub format_paging_nav
{
5364 my ($action, $page, $has_next_link) = @_;
5365 my $paging_nav = "<span class=\"paging_nav\">";
5368 $paging_nav .= tabspan
(
5369 $cgi->a({-href
=> href
(-replay
=>1, page
=>undef)}, "first")) .
5371 $cgi->a({-href
=> href
(-replay
=>1, page
=>$page-1),
5372 -accesskey
=> "p", -title
=> "Alt-p"}, "prev"));
5374 $paging_nav .= tabspan
("first", 1).${mdotsep
}.tabspan
("prev", 0, 1);
5377 if ($has_next_link) {
5378 $paging_nav .= $mdotsep . tabspan
(
5379 $cgi->a({-href
=> href
(-replay
=>1, page
=>$page+1),
5380 -accesskey
=> "n", -title
=> "Alt-n"}, "next"));
5382 $paging_nav .= ${mdotsep
}.tabspan
("next", 0, 1);
5385 return $paging_nav."</span>";
5388 sub format_log_nav
{
5389 my ($action, $page, $has_next_link, $extra) = @_;
5391 defined $extra or $extra = '';
5392 $extra eq '' or $extra .= $barsep;
5394 if ($action eq 'shortlog') {
5395 $paging_nav .= tabspan
('shortlog', 1);
5397 $paging_nav .= tabspan
($cgi->a({-href
=> href
(action
=>'shortlog', -replay
=>1)}, 'shortlog'));
5399 $paging_nav .= $barsep;
5400 if ($action eq 'log') {
5401 $paging_nav .= tabspan
('fulllog', 1);
5403 $paging_nav .= tabspan
($cgi->a({-href
=> href
(action
=>'log', -replay
=>1)}, 'fulllog'));
5406 $paging_nav .= $barsep . $extra . format_paging_nav
($action, $page, $has_next_link);
5410 ## ......................................................................
5411 ## functions printing or outputting HTML: div
5413 sub git_print_header_div
{
5414 my ($action, $title, $hash, $hash_base, $extra) = @_;
5416 defined $extra or $extra = '';
5418 $args{'action'} = $action;
5419 $args{'hash'} = $hash if $hash;
5420 $args{'hash_base'} = $hash_base if $hash_base;
5422 my $link1 = $cgi->a({-href
=> href
(%args), -class => "title"},
5423 $title ?
$title : $action);
5424 my $link2 = $cgi->a({-href
=> href
(%args), -class => "cover"}, "");
5425 print "<div class=\"header\">\n" . '<span class="title">' .
5426 $link1 . $extra . $link2 . '</span>' . "\n</div>\n";
5429 sub format_repo_url
{
5430 my ($name, $url) = @_;
5431 return "<tr class=\"metadata_url\"><td>$name</td><td>$url</td></tr>\n";
5434 # Group output by placing it in a DIV element and adding a header.
5435 # Options for start_div() can be provided by passing a hash reference as the
5436 # first parameter to the function.
5437 # Options to git_print_header_div() can be provided by passing an array
5438 # reference. This must follow the options to start_div if they are present.
5439 # The content can be a scalar, which is output as-is, a scalar reference, which
5440 # is output after html escaping, an IO handle passed either as *handle or
5441 # *handle{IO}, or a function reference. In the latter case all following
5442 # parameters will be taken as argument to the content function call.
5443 sub git_print_section
{
5444 my ($div_args, $header_args, $content);
5446 if (ref($arg) eq 'HASH') {
5450 if (ref($arg) eq 'ARRAY') {
5451 $header_args = $arg;
5456 print $cgi->start_div($div_args);
5457 git_print_header_div
(@
$header_args);
5459 if (ref($content) eq 'CODE') {
5461 } elsif (ref($content) eq 'SCALAR') {
5462 print esc_html
($$content);
5463 } elsif (ref($content) eq 'GLOB' or ref($content) eq 'IO::Handle') {
5464 while (<$content>) {
5467 } elsif (!ref($content) && defined($content)) {
5471 print $cgi->end_div;
5474 sub format_timestamp_html
{
5476 my $useatnight = shift;
5477 defined($useatnight) or $useatnight = 1;
5478 my $strtime = $date->{'rfc2822'};
5480 my (undef, undef, $datetime_class) =
5481 gitweb_get_feature
('javascript-timezone');
5482 if ($datetime_class) {
5483 $strtime = qq!<span
class="$datetime_class">$strtime</span
>!;
5486 my $localtime_format = '(%d %02d:%02d %s)';
5487 if ($useatnight && $date->{'hour_local'} < 6) {
5488 $localtime_format = '(%d <span class="atnight">%02d:%02d</span> %s)';
5491 sprintf($localtime_format, $date->{'mday_local'},
5492 $date->{'hour_local'}, $date->{'minute_local'}, $date->{'tz_local'});
5497 sub format_lastrefresh_row
{
5498 return "<?gitweb format_lastrefresh_row?>" if $cache_mode_active;
5499 my %rd = parse_file_date
('.last_refresh');
5500 if (defined $rd{'rfc2822'}) {
5501 return "<tr id=\"metadata_lrefresh\"><td>last refresh</td>" .
5502 "<td>".format_timestamp_html
(\
%rd,0)."</td></tr>";
5507 # Outputs the author name and date in long form
5508 sub git_print_authorship
{
5511 my $tag = $opts{-tag
} || 'div';
5512 my $author = $co->{'author_name'};
5514 my %ad = parse_date
($co->{'author_epoch'}, $co->{'author_tz'});
5515 print "<$tag class=\"author_date\">" .
5516 format_search_author
($author, "author", esc_html
($author)) .
5517 " [".format_timestamp_html
(\
%ad)."]".
5518 git_get_avatar
($co->{'author_email'}, -pad_before
=> 1) .
5522 # Outputs table rows containing the full author or committer information,
5523 # in the format expected for 'commit' view (& similar).
5524 # Parameters are a commit hash reference, followed by the list of people
5525 # to output information for. If the list is empty it defaults to both
5526 # author and committer.
5527 sub git_print_authorship_rows
{
5529 # too bad we can't use @people = @_ || ('author', 'committer')
5531 @people = ('author', 'committer') unless @people;
5532 foreach my $who (@people) {
5533 my %wd = parse_date
($co->{"${who}_epoch"}, $co->{"${who}_tz"});
5534 print "<tr><td>$who</td><td>" .
5535 format_search_author
($co->{"${who}_name"}, $who,
5536 esc_html
($co->{"${who}_name"})) . " " .
5537 format_search_author
($co->{"${who}_email"}, $who,
5538 esc_html
("<" . $co->{"${who}_email"} . ">")) .
5539 "</td><td rowspan=\"2\">" .
5540 git_get_avatar
($co->{"${who}_email"}, -size
=> 'double') .
5544 format_timestamp_html
(\
%wd) .
5550 sub git_print_page_path
{
5556 print "<div class=\"page_path\">";
5557 print $cgi->a({-href
=> href
(action
=>"tree", hash_base
=>$hb),
5558 -title
=> 'tree root'}, to_utf8
("[$project]"));
5560 if (defined $name) {
5561 my @dirname = split '/', $name;
5562 my $basename = pop @dirname;
5565 foreach my $dir (@dirname) {
5566 $fullname .= ($fullname ?
'/' : '') . $dir;
5567 print $cgi->a({-href
=> href
(action
=>"tree", file_name
=>$fullname,
5569 -title
=> $fullname}, esc_path
($dir));
5572 if (defined $type && $type eq 'blob') {
5573 print $cgi->a({-href
=> href
(action
=>"blob_plain", file_name
=>$file_name,
5575 -title
=> $name}, esc_path
($basename));
5576 } elsif (defined $type && $type eq 'tree') {
5577 print $cgi->a({-href
=> href
(action
=>"tree", file_name
=>$file_name,
5579 -title
=> $name}, esc_path
($basename));
5582 print esc_path
($basename);
5585 print "<br/></div>\n";
5592 if ($opts{'-remove_title'}) {
5593 # remove title, i.e. first line of log
5596 # remove leading empty lines
5597 while (defined $log->[0] && $log->[0] eq "") {
5602 my $skip_blank_line = 0;
5603 foreach my $line (@
$log) {
5604 if ($line =~ m/^\s*([A-Z][-A-Za-z]*-[Bb]y|C[Cc]): /) {
5605 if (! $opts{'-remove_signoff'}) {
5606 print "<span class=\"signoff\">" . esc_html
($line) . "</span><br/>\n";
5607 $skip_blank_line = 1;
5612 if ($line =~ m
,\s
*([a
-z
]*link): (https?
://\S
+),i
) {
5613 if (! $opts{'-remove_signoff'}) {
5614 print "<span class=\"signoff\">" . esc_html
($1) . ": " .
5615 "<a href=\"" . esc_html
($2) . "\">" . esc_html
($2) . "</a>" .
5617 $skip_blank_line = 1;
5622 # print only one empty line
5623 # do not print empty line after signoff
5625 next if ($skip_blank_line);
5626 $skip_blank_line = 1;
5628 $skip_blank_line = 0;
5631 print format_log_line_html
($line) . "<br/>\n";
5634 if ($opts{'-final_empty_line'}) {
5635 # end with single empty line
5636 print "<br/>\n" unless $skip_blank_line;
5640 # return link target (what link points to)
5641 sub git_get_link_target
{
5646 defined(my $fd = git_cmd_pipe
"cat-file", "blob", $hash)
5650 $link_target = to_utf8
(scalar <$fd>);
5655 return $link_target;
5658 # given link target, and the directory (basedir) the link is in,
5659 # return target of link relative to top directory (top tree);
5660 # return undef if it is not possible (including absolute links).
5661 sub normalize_link_target
{
5662 my ($link_target, $basedir) = @_;
5664 # absolute symlinks (beginning with '/') cannot be normalized
5665 return if (substr($link_target, 0, 1) eq '/');
5667 # normalize link target to path from top (root) tree (dir)
5670 $path = $basedir . '/' . $link_target;
5672 # we are in top (root) tree (dir)
5673 $path = $link_target;
5676 # remove //, /./, and /../
5678 foreach my $part (split('/', $path)) {
5679 # discard '.' and ''
5680 next if (!$part || $part eq '.');
5682 if ($part eq '..') {
5686 # link leads outside repository (outside top dir)
5690 push @path_parts, $part;
5693 $path = join('/', @path_parts);
5698 # print tree entry (row of git_tree), but without encompassing <tr> element
5699 sub git_print_tree_entry
{
5700 my ($t, $basedir, $hash_base, $have_blame) = @_;
5703 $base_key{'hash_base'} = $hash_base if defined $hash_base;
5705 # The format of a table row is: mode list link. Where mode is
5706 # the mode of the entry, list is the name of the entry, an href,
5707 # and link is the action links of the entry.
5709 print "<td class=\"mode\">" . mode_str
($t->{'mode'}) . "</td>\n";
5710 if (exists $t->{'size'}) {
5711 print "<td class=\"size\">$t->{'size'}</td>\n";
5713 if ($t->{'type'} eq "blob") {
5714 print "<td class=\"list\">" .
5715 $cgi->a({-href
=> href
(action
=>"blob", hash
=>$t->{'hash'},
5716 file_name
=>"$basedir$t->{'name'}", %base_key),
5717 -class => "list"}, esc_path
($t->{'name'}));
5718 if (S_ISLNK
(oct $t->{'mode'})) {
5719 my $link_target = git_get_link_target
($t->{'hash'});
5721 my $norm_target = normalize_link_target
($link_target, $basedir);
5722 if (defined $norm_target) {
5724 $cgi->a({-href
=> href
(action
=>"object", hash_base
=>$hash_base,
5725 file_name
=>$norm_target),
5726 -title
=> $norm_target}, esc_path
($link_target));
5728 print " -> " . esc_path
($link_target);
5733 print "<td class=\"link\">";
5734 print $cgi->a({-href
=> href
(action
=>"blob", hash
=>$t->{'hash'},
5735 file_name
=>"$basedir$t->{'name'}", %base_key)},
5739 $cgi->a({-href
=> href
(action
=>"blame", hash
=>$t->{'hash'},
5740 file_name
=>"$basedir$t->{'name'}", %base_key),
5741 -class => "blamelink"},
5744 if (defined $hash_base) {
5746 $cgi->a({-href
=> href
(action
=>"history", hash_base
=>$hash_base,
5747 hash
=>$t->{'hash'}, file_name
=>"$basedir$t->{'name'}")},
5751 $cgi->a({-href
=> href
(action
=>"blob_plain", hash_base
=>$hash_base,
5752 file_name
=>"$basedir$t->{'name'}")},
5756 } elsif ($t->{'type'} eq "tree") {
5757 print "<td class=\"list\">";
5758 print $cgi->a({-href
=> href
(action
=>"tree", hash
=>$t->{'hash'},
5759 file_name
=>"$basedir$t->{'name'}",
5761 esc_path
($t->{'name'}));
5763 print "<td class=\"link\">";
5764 print $cgi->a({-href
=> href
(action
=>"tree", hash
=>$t->{'hash'},
5765 file_name
=>"$basedir$t->{'name'}",
5768 if (defined $hash_base) {
5770 $cgi->a({-href
=> href
(action
=>"history", hash_base
=>$hash_base,
5771 file_name
=>"$basedir$t->{'name'}")},
5776 # unknown object: we can only present history for it
5777 # (this includes 'commit' object, i.e. submodule support)
5778 print "<td class=\"list\">" .
5779 esc_path
($t->{'name'}) .
5781 print "<td class=\"link\">";
5782 if (defined $hash_base) {
5783 print $cgi->a({-href
=> href
(action
=>"history",
5784 hash_base
=>$hash_base,
5785 file_name
=>"$basedir$t->{'name'}")},
5792 ## ......................................................................
5793 ## functions printing large fragments of HTML
5795 # get pre-image filenames for merge (combined) diff
5796 sub fill_from_file_info
{
5797 my ($diff, @parents) = @_;
5799 $diff->{'from_file'} = [ ];
5800 $diff->{'from_file'}[$diff->{'nparents'} - 1] = undef;
5801 for (my $i = 0; $i < $diff->{'nparents'}; $i++) {
5802 if ($diff->{'status'}[$i] eq 'R' ||
5803 $diff->{'status'}[$i] eq 'C') {
5804 $diff->{'from_file'}[$i] =
5805 git_get_path_by_hash
($parents[$i], $diff->{'from_id'}[$i]);
5812 # is current raw difftree line of file deletion
5814 my $diffinfo = shift;
5816 return $diffinfo->{'to_id'} eq ('0' x
40);
5819 # does patch correspond to [previous] difftree raw line
5820 # $diffinfo - hashref of parsed raw diff format
5821 # $patchinfo - hashref of parsed patch diff format
5822 # (the same keys as in $diffinfo)
5823 sub is_patch_split
{
5824 my ($diffinfo, $patchinfo) = @_;
5826 return defined $diffinfo && defined $patchinfo
5827 && $diffinfo->{'to_file'} eq $patchinfo->{'to_file'};
5831 sub git_difftree_body
{
5832 my ($difftree, $hash, @parents) = @_;
5833 my ($parent) = $parents[0];
5834 my $have_blame = gitweb_check_feature
('blame');
5835 print "<div class=\"list_head\">\n";
5836 if ($#{$difftree} > 10) {
5837 print(($#{$difftree} + 1) . " files changed:\n");
5841 print "<table class=\"" .
5842 (@parents > 1 ?
"combined " : "") .
5845 # header only for combined diff in 'commitdiff' view
5846 my $has_header = @
$difftree && @parents > 1 && $action eq 'commitdiff';
5849 print "<thead><tr>\n" .
5850 "<th></th><th></th>\n"; # filename, patchN link
5851 for (my $i = 0; $i < @parents; $i++) {
5852 my $par = $parents[$i];
5854 $cgi->a({-href
=> href
(action
=>"commitdiff",
5855 hash
=>$hash, hash_parent
=>$par),
5856 -title
=> 'commitdiff to parent number ' .
5857 ($i+1) . ': ' . substr($par,0,7)},
5861 print "</tr></thead>\n<tbody>\n";
5866 foreach my $line (@
{$difftree}) {
5867 my $diff = parsed_difftree_line
($line);
5870 print "<tr class=\"dark\">\n";
5872 print "<tr class=\"light\">\n";
5876 if (exists $diff->{'nparents'}) { # combined diff
5878 fill_from_file_info
($diff, @parents)
5879 unless exists $diff->{'from_file'};
5881 if (!is_deleted
($diff)) {
5882 # file exists in the result (child) commit
5884 $cgi->a({-href
=> href
(action
=>"blob", hash
=>$diff->{'to_id'},
5885 file_name
=>$diff->{'to_file'},
5887 -class => "list"}, esc_path
($diff->{'to_file'})) .
5891 esc_path
($diff->{'to_file'}) .
5895 if ($action eq 'commitdiff') {
5898 print "<td class=\"link\">" .
5899 $cgi->a({-href
=> href
(-anchor
=>"patch$patchno")},
5905 my $has_history = 0;
5906 my $not_deleted = 0;
5907 for (my $i = 0; $i < $diff->{'nparents'}; $i++) {
5908 my $hash_parent = $parents[$i];
5909 my $from_hash = $diff->{'from_id'}[$i];
5910 my $from_path = $diff->{'from_file'}[$i];
5911 my $status = $diff->{'status'}[$i];
5913 $has_history ||= ($status ne 'A');
5914 $not_deleted ||= ($status ne 'D');
5916 if ($status eq 'A') {
5917 print "<td class=\"link\" align=\"right\">$barsep</td>\n";
5918 } elsif ($status eq 'D') {
5919 print "<td class=\"link\">" .
5920 $cgi->a({-href
=> href
(action
=>"blob",
5923 file_name
=>$from_path)},
5927 if ($diff->{'to_id'} eq $from_hash) {
5928 print "<td class=\"link nochange\">";
5930 print "<td class=\"link\">";
5932 print $cgi->a({-href
=> href
(action
=>"blobdiff",
5933 hash
=>$diff->{'to_id'},
5934 hash_parent
=>$from_hash,
5936 hash_parent_base
=>$hash_parent,
5937 file_name
=>$diff->{'to_file'},
5938 file_parent
=>$from_path)},
5944 print "<td class=\"link\">";
5946 print $cgi->a({-href
=> href
(action
=>"blob",
5947 hash
=>$diff->{'to_id'},
5948 file_name
=>$diff->{'to_file'},
5951 print $barsep if ($has_history);
5954 print $cgi->a({-href
=> href
(action
=>"history",
5955 file_name
=>$diff->{'to_file'},
5962 next; # instead of 'else' clause, to avoid extra indent
5964 # else ordinary diff
5966 my ($to_mode_oct, $to_mode_str, $to_file_type);
5967 my ($from_mode_oct, $from_mode_str, $from_file_type);
5968 if ($diff->{'to_mode'} ne ('0' x
6)) {
5969 $to_mode_oct = oct $diff->{'to_mode'};
5970 if (S_ISREG
($to_mode_oct)) { # only for regular file
5971 $to_mode_str = sprintf("%04o", $to_mode_oct & 0777); # permission bits
5973 $to_file_type = file_type
($diff->{'to_mode'});
5975 if ($diff->{'from_mode'} ne ('0' x
6)) {
5976 $from_mode_oct = oct $diff->{'from_mode'};
5977 if (S_ISREG
($from_mode_oct)) { # only for regular file
5978 $from_mode_str = sprintf("%04o", $from_mode_oct & 0777); # permission bits
5980 $from_file_type = file_type
($diff->{'from_mode'});
5983 if ($diff->{'status'} eq "A") { # created
5984 my $mode_chng = "<span class=\"file_status new\">[new $to_file_type";
5985 $mode_chng .= " with mode: $to_mode_str" if $to_mode_str;
5986 $mode_chng .= "]</span>";
5988 print $cgi->a({-href
=> href
(action
=>"blob", hash
=>$diff->{'to_id'},
5989 hash_base
=>$hash, file_name
=>$diff->{'file'}),
5990 -class => "list"}, esc_path
($diff->{'file'}));
5992 print "<td>$mode_chng</td>\n";
5993 print "<td class=\"link\">";
5994 if ($action eq 'commitdiff') {
5997 print $cgi->a({-href
=> href
(-anchor
=>"patch$patchno")},
6001 print $cgi->a({-href
=> href
(action
=>"blob", hash
=>$diff->{'to_id'},
6002 hash_base
=>$hash, file_name
=>$diff->{'file'})},
6006 } elsif ($diff->{'status'} eq "D") { # deleted
6007 my $mode_chng = "<span class=\"file_status deleted\">[deleted $from_file_type]</span>";
6009 print $cgi->a({-href
=> href
(action
=>"blob", hash
=>$diff->{'from_id'},
6010 hash_base
=>$parent, file_name
=>$diff->{'file'}),
6011 -class => "list"}, esc_path
($diff->{'file'}));
6013 print "<td>$mode_chng</td>\n";
6014 print "<td class=\"link\">";
6015 if ($action eq 'commitdiff') {
6018 print $cgi->a({-href
=> href
(-anchor
=>"patch$patchno")},
6022 print $cgi->a({-href
=> href
(action
=>"blob", hash
=>$diff->{'from_id'},
6023 hash_base
=>$parent, file_name
=>$diff->{'file'})},
6026 print $cgi->a({-href
=> href
(action
=>"blame", hash_base
=>$parent,
6027 file_name
=>$diff->{'file'}),
6028 -class => "blamelink"},
6031 print $cgi->a({-href
=> href
(action
=>"history", hash_base
=>$parent,
6032 file_name
=>$diff->{'file'})},
6036 } elsif ($diff->{'status'} eq "M" || $diff->{'status'} eq "T") { # modified, or type changed
6037 my $mode_chnge = "";
6038 if ($diff->{'from_mode'} != $diff->{'to_mode'}) {
6039 $mode_chnge = "<span class=\"file_status mode_chnge\">[changed";
6040 if ($from_file_type ne $to_file_type) {
6041 $mode_chnge .= " from $from_file_type to $to_file_type";
6043 if (($from_mode_oct & 0777) != ($to_mode_oct & 0777)) {
6044 if ($from_mode_str && $to_mode_str) {
6045 $mode_chnge .= " mode: $from_mode_str->$to_mode_str";
6046 } elsif ($to_mode_str) {
6047 $mode_chnge .= " mode: $to_mode_str";
6050 $mode_chnge .= "]</span>\n";
6053 print $cgi->a({-href
=> href
(action
=>"blob", hash
=>$diff->{'to_id'},
6054 hash_base
=>$hash, file_name
=>$diff->{'file'}),
6055 -class => "list"}, esc_path
($diff->{'file'}));
6057 print "<td>$mode_chnge</td>\n";
6058 print "<td class=\"link\">";
6059 if ($action eq 'commitdiff') {
6062 print $cgi->a({-href
=> href
(-anchor
=>"patch$patchno")},
6065 } elsif ($diff->{'to_id'} ne $diff->{'from_id'}) {
6066 # "commit" view and modified file (not onlu mode changed)
6067 print $cgi->a({-href
=> href
(action
=>"blobdiff",
6068 hash
=>$diff->{'to_id'}, hash_parent
=>$diff->{'from_id'},
6069 hash_base
=>$hash, hash_parent_base
=>$parent,
6070 file_name
=>$diff->{'file'})},
6074 print $cgi->a({-href
=> href
(action
=>"blob", hash
=>$diff->{'to_id'},
6075 hash_base
=>$hash, file_name
=>$diff->{'file'})},
6078 print $cgi->a({-href
=> href
(action
=>"blame", hash_base
=>$hash,
6079 file_name
=>$diff->{'file'}),
6080 -class => "blamelink"},
6083 print $cgi->a({-href
=> href
(action
=>"history", hash_base
=>$hash,
6084 file_name
=>$diff->{'file'})},
6088 } elsif ($diff->{'status'} eq "R" || $diff->{'status'} eq "C") { # renamed or copied
6089 my %status_name = ('R' => 'moved', 'C' => 'copied');
6090 my $nstatus = $status_name{$diff->{'status'}};
6092 if ($diff->{'from_mode'} != $diff->{'to_mode'}) {
6093 # mode also for directories, so we cannot use $to_mode_str
6094 $mode_chng = sprintf(", mode: %04o", $to_mode_oct & 0777);
6097 $cgi->a({-href
=> href
(action
=>"blob", hash_base
=>$hash,
6098 hash
=>$diff->{'to_id'}, file_name
=>$diff->{'to_file'}),
6099 -class => "list"}, esc_path
($diff->{'to_file'})) . "</td>\n" .
6100 "<td><span class=\"file_status $nstatus\">[$nstatus from " .
6101 $cgi->a({-href
=> href
(action
=>"blob", hash_base
=>$parent,
6102 hash
=>$diff->{'from_id'}, file_name
=>$diff->{'from_file'}),
6103 -class => "list"}, esc_path
($diff->{'from_file'})) .
6104 " with " . (int $diff->{'similarity'}) . "% similarity$mode_chng]</span></td>\n" .
6105 "<td class=\"link\">";
6106 if ($action eq 'commitdiff') {
6109 print $cgi->a({-href
=> href
(-anchor
=>"patch$patchno")},
6112 } elsif ($diff->{'to_id'} ne $diff->{'from_id'}) {
6113 # "commit" view and modified file (not only pure rename or copy)
6114 print $cgi->a({-href
=> href
(action
=>"blobdiff",
6115 hash
=>$diff->{'to_id'}, hash_parent
=>$diff->{'from_id'},
6116 hash_base
=>$hash, hash_parent_base
=>$parent,
6117 file_name
=>$diff->{'to_file'}, file_parent
=>$diff->{'from_file'})},
6121 print $cgi->a({-href
=> href
(action
=>"blob", hash
=>$diff->{'to_id'},
6122 hash_base
=>$parent, file_name
=>$diff->{'to_file'})},
6125 print $cgi->a({-href
=> href
(action
=>"blame", hash_base
=>$hash,
6126 file_name
=>$diff->{'to_file'}),
6127 -class => "blamelink"},
6130 print $cgi->a({-href
=> href
(action
=>"history", hash_base
=>$hash,
6131 file_name
=>$diff->{'to_file'})},
6135 } # we should not encounter Unmerged (U) or Unknown (X) status
6138 print "</tbody>" if $has_header;
6142 # Print context lines and then rem/add lines in a side-by-side manner.
6143 sub print_sidebyside_diff_lines
{
6144 my ($ctx, $rem, $add) = @_;
6146 # print context block before add/rem block
6149 '<div class="chunk_block ctx">',
6150 '<div class="old">',
6153 '<div class="new">',
6162 '<div class="chunk_block rem">',
6163 '<div class="old">',
6170 '<div class="chunk_block add">',
6171 '<div class="new">',
6177 '<div class="chunk_block chg">',
6178 '<div class="old">',
6181 '<div class="new">',
6188 # Print context lines and then rem/add lines in inline manner.
6189 sub print_inline_diff_lines
{
6190 my ($ctx, $rem, $add) = @_;
6192 print @
$ctx, @
$rem, @
$add;
6195 # Format removed and added line, mark changed part and HTML-format them.
6196 # Implementation is based on contrib/diff-highlight
6197 sub format_rem_add_lines_pair
{
6198 my ($rem, $add, $num_parents) = @_;
6200 # We need to untabify lines before split()'ing them;
6201 # otherwise offsets would be invalid.
6204 $rem = untabify
($rem);
6205 $add = untabify
($add);
6207 my @rem = split(//, $rem);
6208 my @add = split(//, $add);
6209 my ($esc_rem, $esc_add);
6210 # Ignore leading +/- characters for each parent.
6211 my ($prefix_len, $suffix_len) = ($num_parents, 0);
6212 my ($prefix_has_nonspace, $suffix_has_nonspace);
6214 my $shorter = (@rem < @add) ?
@rem : @add;
6215 while ($prefix_len < $shorter) {
6216 last if ($rem[$prefix_len] ne $add[$prefix_len]);
6218 $prefix_has_nonspace = 1 if ($rem[$prefix_len] !~ /\s/);
6222 while ($prefix_len + $suffix_len < $shorter) {
6223 last if ($rem[-1 - $suffix_len] ne $add[-1 - $suffix_len]);
6225 $suffix_has_nonspace = 1 if ($rem[-1 - $suffix_len] !~ /\s/);
6229 # Mark lines that are different from each other, but have some common
6230 # part that isn't whitespace. If lines are completely different, don't
6231 # mark them because that would make output unreadable, especially if
6232 # diff consists of multiple lines.
6233 if ($prefix_has_nonspace || $suffix_has_nonspace) {
6234 $esc_rem = esc_html_hl_regions
($rem, 'marked',
6235 [$prefix_len, @rem - $suffix_len], -nbsp
=>1);
6236 $esc_add = esc_html_hl_regions
($add, 'marked',
6237 [$prefix_len, @add - $suffix_len], -nbsp
=>1);
6239 $esc_rem = esc_html
($rem, -nbsp
=>1);
6240 $esc_add = esc_html
($add, -nbsp
=>1);
6243 return format_diff_line
(\
$esc_rem, 'rem'),
6244 format_diff_line
(\
$esc_add, 'add');
6247 # HTML-format diff context, removed and added lines.
6248 sub format_ctx_rem_add_lines
{
6249 my ($ctx, $rem, $add, $num_parents) = @_;
6250 my (@new_ctx, @new_rem, @new_add);
6251 my $can_highlight = 0;
6252 my $is_combined = ($num_parents > 1);
6254 # Highlight if every removed line has a corresponding added line.
6255 if (@
$add > 0 && @
$add == @
$rem) {
6258 # Highlight lines in combined diff only if the chunk contains
6259 # diff between the same version, e.g.
6266 # Otherwise the highlightling would be confusing.
6268 for (my $i = 0; $i < @
$add; $i++) {
6269 my $prefix_rem = substr($rem->[$i], 0, $num_parents);
6270 my $prefix_add = substr($add->[$i], 0, $num_parents);
6272 $prefix_rem =~ s/-/+/g;
6274 if ($prefix_rem ne $prefix_add) {
6282 if ($can_highlight) {
6283 for (my $i = 0; $i < @
$add; $i++) {
6284 my ($line_rem, $line_add) = format_rem_add_lines_pair
(
6285 $rem->[$i], $add->[$i], $num_parents);
6286 push @new_rem, $line_rem;
6287 push @new_add, $line_add;
6290 @new_rem = map { format_diff_line
($_, 'rem') } @
$rem;
6291 @new_add = map { format_diff_line
($_, 'add') } @
$add;
6294 @new_ctx = map { format_diff_line
($_, 'ctx') } @
$ctx;
6296 return (\
@new_ctx, \
@new_rem, \
@new_add);
6299 # Print context lines and then rem/add lines.
6300 sub print_diff_lines
{
6301 my ($ctx, $rem, $add, $diff_style, $num_parents) = @_;
6302 my $is_combined = $num_parents > 1;
6304 ($ctx, $rem, $add) = format_ctx_rem_add_lines
($ctx, $rem, $add,
6307 if ($diff_style eq 'sidebyside' && !$is_combined) {
6308 print_sidebyside_diff_lines
($ctx, $rem, $add);
6310 # default 'inline' style and unknown styles
6311 print_inline_diff_lines
($ctx, $rem, $add);
6315 sub print_diff_chunk
{
6316 my ($diff_style, $num_parents, $from, $to, @chunk) = @_;
6317 my (@ctx, @rem, @add);
6319 # The class of the previous line.
6320 my $prev_class = '';
6322 return unless @chunk;
6324 # incomplete last line might be among removed or added lines,
6325 # or both, or among context lines: find which
6326 for (my $i = 1; $i < @chunk; $i++) {
6327 if ($chunk[$i][0] eq 'incomplete') {
6328 $chunk[$i][0] = $chunk[$i-1][0];
6333 push @chunk, ["", ""];
6335 foreach my $line_info (@chunk) {
6336 my ($class, $line) = @
$line_info;
6338 # print chunk headers
6339 if ($class && $class eq 'chunk_header') {
6340 print format_diff_line
($line, $class, $from, $to);
6344 ## print from accumulator when have some add/rem lines or end
6345 # of chunk (flush context lines), or when have add and rem
6346 # lines and new block is reached (otherwise add/rem lines could
6348 if (!$class || ((@rem || @add) && $class eq 'ctx') ||
6349 (@rem && @add && $class ne $prev_class)) {
6350 print_diff_lines
(\
@ctx, \
@rem, \
@add,
6351 $diff_style, $num_parents);
6352 @ctx = @rem = @add = ();
6355 ## adding lines to accumulator
6358 # rem, add or change
6359 if ($class eq 'rem') {
6361 } elsif ($class eq 'add') {
6365 if ($class eq 'ctx') {
6369 $prev_class = $class;
6373 sub git_patchset_body
{
6374 my ($fd, $diff_style, $difftree, $hash, @hash_parents) = @_;
6375 my ($hash_parent) = $hash_parents[0];
6377 my $is_combined = (@hash_parents > 1);
6379 my $patch_number = 0;
6384 my @chunk; # for side-by-side diff
6386 print "<div class=\"patchset\">\n";
6388 # skip to first patch
6389 while ($patch_line = to_utf8
(scalar <$fd>)) {
6392 last if ($patch_line =~ m/^diff /);
6396 while ($patch_line) {
6398 # parse "git diff" header line
6399 if ($patch_line =~ m/^diff --git (\"(?:[^\\\"]*(?:\\.[^\\\"]*)*)\"|[^ "]*) (.*)$/) {
6400 # $1 is from_name, which we do not use
6401 $to_name = unquote
($2);
6402 $to_name =~ s!^b/!!;
6403 } elsif ($patch_line =~ m/^diff --(cc|combined) ("?.*"?)$/) {
6404 # $1 is 'cc' or 'combined', which we do not use
6405 $to_name = unquote
($2);
6410 # check if current patch belong to current raw line
6411 # and parse raw git-diff line if needed
6412 if (is_patch_split
($diffinfo, { 'to_file' => $to_name })) {
6413 # this is continuation of a split patch
6414 print "<div class=\"patch cont\">\n";
6416 # advance raw git-diff output if needed
6417 $patch_idx++ if defined $diffinfo;
6419 # read and prepare patch information
6420 $diffinfo = parsed_difftree_line
($difftree->[$patch_idx]);
6422 # compact combined diff output can have some patches skipped
6423 # find which patch (using pathname of result) we are at now;
6425 while ($to_name ne $diffinfo->{'to_file'}) {
6426 print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n" .
6427 format_diff_cc_simplified
($diffinfo, @hash_parents) .
6428 "</div>\n"; # class="patch"
6433 last if $patch_idx > $#$difftree;
6434 $diffinfo = parsed_difftree_line
($difftree->[$patch_idx]);
6438 # modifies %from, %to hashes
6439 parse_from_to_diffinfo
($diffinfo, \
%from, \
%to, @hash_parents);
6441 # this is first patch for raw difftree line with $patch_idx index
6442 # we index @$difftree array from 0, but number patches from 1
6443 print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n";
6447 #assert($patch_line =~ m/^diff /) if DEBUG;
6448 #assert($patch_line !~ m!$/$!) if DEBUG; # is chomp-ed
6450 # print "git diff" header
6451 print format_git_diff_header_line
($patch_line, $diffinfo,
6454 # print extended diff header
6455 print "<div class=\"diff extended_header\">\n";
6457 while ($patch_line = to_utf8
(scalar<$fd>)) {
6460 last EXTENDED_HEADER
if ($patch_line =~ m/^--- |^diff /);
6462 print format_extended_diff_header_line
($patch_line, $diffinfo,
6465 print "</div>\n"; # class="diff extended_header"
6467 # from-file/to-file diff header
6468 if (! $patch_line) {
6469 print "</div>\n"; # class="patch"
6472 next PATCH
if ($patch_line =~ m/^diff /);
6473 #assert($patch_line =~ m/^---/) if DEBUG;
6475 my $last_patch_line = $patch_line;
6476 $patch_line = to_utf8
(scalar <$fd>);
6478 #assert($patch_line =~ m/^\+\+\+/) if DEBUG;
6480 print format_diff_from_to_header
($last_patch_line, $patch_line,
6481 $diffinfo, \
%from, \
%to,
6486 while ($patch_line = to_utf8
(scalar <$fd>)) {
6489 next PATCH
if ($patch_line =~ m/^diff /);
6491 my $class = diff_line_class
($patch_line, \
%from, \
%to);
6493 if ($class eq 'chunk_header') {
6494 print_diff_chunk
($diff_style, scalar @hash_parents, \
%from, \
%to, @chunk);
6498 push @chunk, [ $class, $patch_line ];
6503 print_diff_chunk
($diff_style, scalar @hash_parents, \
%from, \
%to, @chunk);
6506 print "</div>\n"; # class="patch"
6509 # for compact combined (--cc) format, with chunk and patch simplification
6510 # the patchset might be empty, but there might be unprocessed raw lines
6511 for (++$patch_idx if $patch_number > 0;
6512 $patch_idx < @
$difftree;
6514 # read and prepare patch information
6515 $diffinfo = parsed_difftree_line
($difftree->[$patch_idx]);
6517 # generate anchor for "patch" links in difftree / whatchanged part
6518 print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n" .
6519 format_diff_cc_simplified
($diffinfo, @hash_parents) .
6520 "</div>\n"; # class="patch"
6525 if ($patch_number == 0) {
6526 if (@hash_parents > 1) {
6527 print "<div class=\"diff nodifferences\">Trivial merge</div>\n";
6529 print "<div class=\"diff nodifferences\">No differences found</div>\n";
6533 print "</div>\n"; # class="patchset"
6536 # . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
6538 sub git_project_search_form
{
6539 my ($searchtext, $search_use_regexp) = @_;
6542 if ($project_filter) {
6543 $limit = " in '$project_filter'";
6546 print "<div class=\"projsearch\">\n";
6547 print $cgi->start_form(-method
=> 'get', -action
=> $my_uri) .
6548 $cgi->hidden(-name
=> 'a', -value
=> 'project_list') . "\n";
6549 print $cgi->hidden(-name
=> 'pf', -value
=> $project_filter). "\n"
6550 if (defined $project_filter);
6551 print $cgi->textfield(-name
=> 's', -value
=> $searchtext,
6552 -title
=> "Search project by name and description$limit",
6553 -size
=> 60) . "\n" .
6554 "<span title=\"Extended regular expression\">" .
6555 $cgi->checkbox(-name
=> 'sr', -value
=> 1, -label
=> 're',
6556 -checked
=> $search_use_regexp) .
6558 $cgi->submit(-name
=> 'btnS', -value
=> 'Search') .
6559 $cgi->end_form() . "\n" .
6560 "<span class=\"projectlist_link\">" .
6561 $cgi->a({-href
=> href
(project
=> undef, searchtext
=> undef,
6562 action
=> 'project_list',
6563 project_filter
=> $project_filter)},
6564 esc_html
("List all projects$limit")) . "</span><br />\n";
6565 print "<span class=\"projectlist_link\">" .
6566 $cgi->a({-href
=> href
(project
=> undef, searchtext
=> undef,
6567 action
=> 'project_list',
6568 project_filter
=> undef)},
6569 esc_html
("List all projects")) . "</span>\n" if $project_filter;
6573 # entry for given @keys needs filling if at least one of keys in list
6574 # is not present in %$project_info
6575 sub project_info_needs_filling
{
6576 my ($project_info, @keys) = @_;
6578 # return List::MoreUtils::any { !exists $project_info->{$_} } @keys;
6579 foreach my $key (@keys) {
6580 if (!exists $project_info->{$key}) {
6587 sub git_cache_file_format
{
6588 return GITWEB_CACHE_FORMAT
.
6589 (gitweb_check_feature
('forks') ?
" (forks)" : "");
6592 sub git_retrieve_cache_file
{
6593 my $cache_file = shift;
6595 use Storable
qw(retrieve);
6597 if ((my $dump = eval { retrieve
($cache_file) })) {
6599 ref($dump) eq 'ARRAY' &&
6601 ref($$dump[1]) eq 'ARRAY' &&
6602 @
{$$dump[1]} == 2 &&
6603 ref(${$$dump[1]}[0]) eq 'ARRAY' &&
6604 ref(${$$dump[1]}[1]) eq 'HASH' &&
6605 $$dump[0] eq git_cache_file_format
();
6611 sub git_store_cache_file
{
6612 my ($cache_file, $cachedata) = @_;
6614 use File
::Basename
qw(dirname);
6616 use POSIX
qw(:fcntl_h);
6617 use Storable
qw(store_fd);
6620 my $cache_d = dirname
($cache_file);
6622 umask($mask & ~0070) if $cache_grpshared;
6623 if ((-d
$cache_d || mkdir($cache_d, $cache_grpshared ?
0770 : 0700)) &&
6624 sysopen(my $fd, "$cache_file.lock", O_WRONLY
|O_CREAT
|O_EXCL
, $cache_grpshared ?
0660 : 0600)) {
6625 store_fd
([git_cache_file_format
(), $cachedata], $fd);
6627 rename "$cache_file.lock", $cache_file;
6628 $result = stat($cache_file)->mtime;
6630 umask($mask) if $cache_grpshared;
6634 sub verify_cached_project
{
6635 my ($hashref, $path) = @_;
6636 return undef unless $path;
6637 delete $$hashref{$path}, return undef unless is_valid_project
($path);
6638 return $$hashref{$path} if exists $$hashref{$path};
6640 # A valid project was requested but it's not yet in the cache
6641 # Manufacture a minimal project entry (path, name, description)
6642 # Also provide age, but only if it's available via $lastactivity_file
6644 my %proj = ('path' => $path);
6645 my $val = git_get_project_description
($path);
6646 defined $val or $val = '';
6647 $proj{'descr_long'} = $val;
6648 $proj{'descr'} = chop_str
($val, $projects_list_description_width, 5);
6649 unless ($omit_owner) {
6650 $val = git_get_project_owner
($path);
6651 defined $val or $val = '';
6652 $proj{'owner'} = $val;
6654 unless ($omit_age_column) {
6655 ($val) = git_get_last_activity
($path, 1);
6656 $proj{'age_epoch'} = $val if defined $val;
6658 $$hashref{$path} = \
%proj;
6662 sub git_filter_cached_projects
{
6663 my ($cache, $projlist, $verify) = @_;
6664 my $hashref = $$cache[1];
6666 sub {verify_cached_project
($hashref, $_[0])} :
6667 sub {$$hashref{$_[0]}};
6669 my $c = &$sub($_->{'path'});
6670 defined $c ?
($_ = $c) : ()
6674 # fills project list info (age, description, owner, category, forks, etc.)
6675 # for each project in the list, removing invalid projects from
6676 # returned list, or fill only specified info.
6678 # Invalid projects are removed from the returned list if and only if you
6679 # ask 'age_epoch' to be filled, because they are the only fields
6680 # that run unconditionally git command that requires repository, and
6681 # therefore do always check if project repository is invalid.
6684 # * fill_project_list_info(\@project_list, 'descr_long', 'ctags')
6685 # ensures that 'descr_long' and 'ctags' fields are filled
6686 # * @project_list = fill_project_list_info(\@project_list)
6687 # ensures that all fields are filled (and invalid projects removed)
6689 # NOTE: modifies $projlist, but does not remove entries from it
6690 sub fill_project_list_info
{
6691 my ($projlist, @wanted_keys) = @_;
6693 my $rebuild = @wanted_keys && $wanted_keys[0] eq 'rebuild-cache' && shift @wanted_keys;
6694 return fill_project_list_info_uncached
($projlist, @wanted_keys)
6695 unless $projlist_cache_lifetime && $projlist_cache_lifetime > 0;
6699 my $cache_lifetime = $rebuild ?
0 : $projlist_cache_lifetime;
6700 my $cache_file = "$cache_dir/$projlist_cache_name";
6706 if ($cache_lifetime && -f
$cache_file) {
6707 $cache_mtime = stat($cache_file)->mtime;
6708 $cache_dump = undef if $cache_mtime &&
6709 (!$cache_dump_mtime || $cache_dump_mtime != $cache_mtime);
6711 if (defined $cache_mtime && # caching is on and $cache_file exists
6712 $cache_mtime + $cache_lifetime*60 > $now &&
6713 ($cache_dump || ($cache_dump = git_retrieve_cache_file
($cache_file)))) {
6715 $cache_dump_mtime = $cache_mtime;
6716 $stale = $now - $cache_mtime;
6717 my $verify = ($action eq 'summary' || $action eq 'forks') &&
6718 gitweb_check_feature
('forks');
6719 @projects = git_filter_cached_projects
($cache_dump, $projlist, $verify);
6721 } else { # Cache miss.
6722 if (defined $cache_mtime) {
6723 # Postpone timeout by two minutes so that we get
6724 # enough time to do our job, or to be more exact
6725 # make cache expire after two minutes from now.
6726 my $time = $now - $cache_lifetime*60 + 120;
6727 utime $time, $time, $cache_file;
6729 my @all_projects = git_get_projects_list
();
6730 my %all_projects_filled = map { ( $_->{'path'} => $_ ) }
6731 fill_project_list_info_uncached
(\
@all_projects);
6732 map { $all_projects_filled{$_->{'path'}} = $_ }
6733 filter_forks_from_projects_list
([values(%all_projects_filled)])
6734 if gitweb_check_feature
('forks');
6735 $cache_dump = [[sort {$a->{'path'} cmp $b->{'path'}} values(%all_projects_filled)],
6736 \
%all_projects_filled];
6737 $cache_dump_mtime = git_store_cache_file
($cache_file, $cache_dump);
6738 @projects = git_filter_cached_projects
($cache_dump, $projlist);
6741 if ($cache_lifetime && $stale > 0) {
6742 print "<div class=\"stale_info\">Cached version (${stale}s old)</div>\n"
6743 unless $shown_stale_message;
6744 $shown_stale_message = 1;
6750 sub fill_project_list_info_uncached
{
6751 my ($projlist, @wanted_keys) = @_;
6753 my $filter_set = sub { return @_; };
6755 my %wanted_keys = map { $_ => 1 } @wanted_keys;
6756 $filter_set = sub { return grep { $wanted_keys{$_} } @_; };
6759 my $show_ctags = gitweb_check_feature
('ctags');
6761 foreach my $pr (@
$projlist) {
6762 if (project_info_needs_filling
($pr, $filter_set->('age_epoch'))) {
6763 my (@activity) = git_get_last_activity
($pr->{'path'});
6764 unless (@activity) {
6767 ($pr->{'age_epoch'}) = @activity;
6769 if (project_info_needs_filling
($pr, $filter_set->('descr', 'descr_long'))) {
6770 my $descr = git_get_project_description
($pr->{'path'}) || "";
6771 $descr = to_utf8
($descr);
6772 $pr->{'descr_long'} = $descr;
6773 $pr->{'descr'} = chop_str
($descr, $projects_list_description_width, 5);
6775 if (project_info_needs_filling
($pr, $filter_set->('owner'))) {
6776 $pr->{'owner'} = git_get_project_owner
("$pr->{'path'}") || "";
6779 project_info_needs_filling
($pr, $filter_set->('ctags'))) {
6780 $pr->{'ctags'} = git_get_project_ctags
($pr->{'path'});
6782 if ($projects_list_group_categories &&
6783 project_info_needs_filling
($pr, $filter_set->('category'))) {
6784 my $cat = git_get_project_category
($pr->{'path'}) ||
6785 $project_list_default_category;
6786 $pr->{'category'} = to_utf8
($cat);
6789 push @projects, $pr;
6795 sub sort_projects_list
{
6796 my ($projlist, $order) = @_;
6800 return sub { lc($a->{$key}) cmp lc($b->{$key}) };
6803 sub order_reverse_num_then_undef
{
6806 defined $a->{$key} ?
6807 (defined $b->{$key} ?
$b->{$key} <=> $a->{$key} : -1) :
6808 (defined $b->{$key} ?
1 : 0)
6813 project
=> order_str
('path'),
6814 descr
=> order_str
('descr_long'),
6815 owner
=> order_str
('owner'),
6816 age
=> order_reverse_num_then_undef
('age_epoch'),
6819 my $ordering = $orderings{$order};
6820 return defined $ordering ?
sort $ordering @
$projlist : @
$projlist;
6823 # returns a hash of categories, containing the list of project
6824 # belonging to each category
6825 sub build_projlist_by_category
{
6826 my ($projlist, $from, $to) = @_;
6829 $from = 0 unless defined $from;
6830 $to = $#$projlist if (!defined $to || $#$projlist < $to);
6832 for (my $i = $from; $i <= $to; $i++) {
6833 my $pr = $projlist->[$i];
6834 push @
{$categories{ $pr->{'category'} }}, $pr;
6837 return wantarray ?
%categories : \
%categories;
6840 # print 'sort by' <th> element, generating 'sort by $name' replay link
6841 # if that order is not selected
6843 print format_sort_th
(@_);
6846 sub format_sort_th
{
6847 my ($name, $order, $header) = @_;
6849 $header ||= ucfirst($name);
6851 if ($order eq $name) {
6852 $sort_th .= "<th>$header</th>\n";
6854 $sort_th .= "<th>" .
6855 $cgi->a({-href
=> href
(-replay
=>1, order
=>$name),
6856 -class => "header"}, $header) .
6863 sub git_project_list_rows
{
6864 my ($projlist, $from, $to, $check_forks) = @_;
6866 $from = 0 unless defined $from;
6867 $to = $#$projlist if (!defined $to || $#$projlist < $to);
6871 for (my $i = $from; $i <= $to; $i++) {
6872 my $pr = $projlist->[$i];
6875 print "<tr class=\"dark\">\n";
6877 print "<tr class=\"light\">\n";
6883 if ($pr->{'forks'}) {
6884 my $nforks = scalar @
{$pr->{'forks'}};
6885 my $s = $nforks == 1 ?
'' : 's';
6887 print $cgi->a({-href
=> href
(project
=>$pr->{'path'}, action
=>"forks"),
6888 -title
=> "$nforks fork$s"}, "+");
6890 print $cgi->span({-title
=> "$nforks fork$s"}, "+");
6895 my $path = $pr->{'path'};
6896 my $dotgit = $path =~ s/\.git$// ?
'.git' : '';
6897 print "<td>" . $cgi->a({-href
=> href
(project
=>$pr->{'path'}, action
=>"summary"),
6899 esc_html_match_hl
($path, $search_regexp).$dotgit) .
6901 "<td>" . $cgi->a({-href
=> href
(project
=>$pr->{'path'}, action
=>"summary"),
6903 -title
=> $pr->{'descr_long'}},
6905 ? esc_html_match_hl_chopped
($pr->{'descr_long'},
6906 $pr->{'descr'}, $search_regexp)
6907 : esc_html
($pr->{'descr'})) .
6909 unless ($omit_owner) {
6910 print "<td><i>" . ($owner_link_hook
6911 ?
$cgi->a({-href
=> $owner_link_hook->($pr->{'owner'}), -class => "list"},
6912 chop_and_escape_str
($pr->{'owner'}, 15))
6913 : chop_and_escape_str
($pr->{'owner'}, 15)) . "</i></td>\n";
6915 unless ($omit_age_column) {
6916 my ($age_epoch, $age_string) = ($pr->{'age_epoch'});
6917 $age_string = defined $age_epoch ? age_string
($age_epoch, $now) : "No commits";
6918 print "<td class=\"". age_class
($age_epoch, $now) . "\">" . $age_string . "</td>\n";
6920 print"<td class=\"link\">" .
6921 $cgi->a({-href
=> href
(project
=>$pr->{'path'}, action
=>"summary")}, "summary") . $barsep .
6922 $cgi->a({-href
=> href
(project
=>$pr->{'path'}, action
=>"shortlog")}, "log") . $barsep .
6923 $cgi->a({-href
=> href
(project
=>$pr->{'path'}, action
=>"tree")}, "tree") .
6924 ($pr->{'forks'} ?
$barsep . $cgi->a({-href
=> href
(project
=>$pr->{'path'}, action
=>"forks")}, "forks") : '') .
6930 sub git_project_list_body
{
6931 # actually uses global variable $project
6932 my ($projlist, $order, $from, $to, $extra, $no_header, $ctags_action, $keep_top) = @_;
6933 my @projects = @
$projlist;
6935 my $check_forks = gitweb_check_feature
('forks');
6936 my $show_ctags = gitweb_check_feature
('ctags');
6937 my $tagfilter = $show_ctags ?
$input_params{'ctag_filter'} : undef;
6938 $check_forks = undef
6939 if ($tagfilter || $search_regexp);
6941 # filtering out forks before filling info allows us to do less work
6943 @projects = filter_forks_from_projects_list
(\
@projects);
6944 push @projects, { 'path' => "$project_filter.git" }
6945 if $project_filter && $keep_top && is_valid_project
("$project_filter.git");
6947 # search_projects_list pre-fills required info
6948 @projects = search_projects_list
(\
@projects,
6949 'search_regexp' => $search_regexp,
6950 'tagfilter' => $tagfilter)
6951 if ($tagfilter || $search_regexp);
6953 my @all_fields = ('descr', 'descr_long', 'ctags', 'category');
6954 push @all_fields, 'age_epoch' unless($omit_age_column);
6955 push @all_fields, 'owner' unless($omit_owner);
6956 @projects = fill_project_list_info
(\
@projects, @all_fields);
6958 $order ||= $default_projects_order;
6959 $from = 0 unless defined $from;
6960 $to = $#projects if (!defined $to || $#projects < $to);
6965 "<b>No such projects found</b><br />\n".
6966 "Click ".$cgi->a({-href
=>href
(project
=>undef,action
=>'project_list')},"here")." to view all projects<br />\n".
6967 "</center>\n<br />\n";
6971 @projects = sort_projects_list
(\
@projects, $order);
6974 my $ctags = git_gather_all_ctags
(\
@projects);
6975 my $cloud = git_populate_project_tagcloud
($ctags, $ctags_action||'project_list');
6976 print git_show_project_tagcloud
($cloud, 64);
6979 print "<table class=\"project_list\">\n";
6980 unless ($no_header) {
6983 print "<th></th>\n";
6985 print_sort_th
('project', $order, 'Project');
6986 print_sort_th
('descr', $order, 'Description');
6987 print_sort_th
('owner', $order, 'Owner') unless $omit_owner;
6988 print_sort_th
('age', $order, 'Last Change') unless $omit_age_column;
6989 print "<th></th>\n" . # for links
6993 if ($projects_list_group_categories) {
6994 # only display categories with projects in the $from-$to window
6995 @projects = sort {$a->{'category'} cmp $b->{'category'}} @projects[$from..$to];
6996 my %categories = build_projlist_by_category
(\
@projects, $from, $to);
6997 foreach my $cat (sort keys %categories) {
6998 unless ($cat eq "") {
7001 print "<td></td>\n";
7003 print "<td class=\"category\" colspan=\"5\">".esc_html
($cat)."</td>\n";
7007 git_project_list_rows
($categories{$cat}, undef, undef, $check_forks);
7010 git_project_list_rows
(\
@projects, $from, $to, $check_forks);
7013 if (defined $extra) {
7014 print "<tr class=\"extra\">\n";
7016 print "<td></td>\n";
7018 print "<td colspan=\"5\" class=\"extra\">$extra</td>\n" .
7025 # uses global variable $project
7026 my ($commitlist, $from, $to, $refs, $extra) = @_;
7028 $from = 0 unless defined $from;
7029 $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
7031 for (my $i = 0; $i <= $to; $i++) {
7032 my %co = %{$commitlist->[$i]};
7034 my $commit = $co{'id'};
7035 my $ref = format_ref_marker
($refs, $commit);
7036 git_print_header_div
('commit',
7037 "<span class=\"age\">$co{'age_string'}</span>" .
7038 esc_html
($co{'title'}),
7039 $commit, undef, $ref);
7040 print "<div class=\"title_text\">\n" .
7041 "<div class=\"log_link\">\n" .
7042 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$commit)}, "commit") .
7044 $cgi->a({-href
=> href
(action
=>"commitdiff", hash
=>$commit)}, "commitdiff") .
7046 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$commit, hash_base
=>$commit)}, "tree") .
7049 git_print_authorship
(\
%co, -tag
=> 'span');
7050 print "<br/>\n</div>\n";
7052 print "<div class=\"log_body\">\n";
7053 git_print_log
($co{'comment'}, -final_empty_line
=> 1);
7057 print "<div class=\"page_nav_trailer\">\n";
7063 sub git_shortlog_body
{
7064 # uses global variable $project
7065 my ($commitlist, $from, $to, $refs, $extra) = @_;
7067 $from = 0 unless defined $from;
7068 $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
7070 print "<table class=\"shortlog\">\n";
7072 for (my $i = $from; $i <= $to; $i++) {
7073 my %co = %{$commitlist->[$i]};
7074 my $commit = $co{'id'};
7075 my $ref = format_ref_marker
($refs, $commit);
7077 print "<tr class=\"dark\">\n";
7079 print "<tr class=\"light\">\n";
7082 # git_summary() used print "<td><i>$co{'age_string'}</i></td>\n" .
7083 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
7084 format_author_html
('td', \
%co, 10) . "<td>";
7085 print format_subject_html
($co{'title'}, $co{'title_short'},
7086 href
(action
=>"commit", hash
=>$commit), $ref);
7088 "<td class=\"link\">" .
7089 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$commit)}, "commit") . $barsep .
7090 $cgi->a({-href
=> href
(action
=>"commitdiff", hash
=>$commit)}, "commitdiff") . $barsep .
7091 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$commit, hash_base
=>$commit)}, "tree");
7092 my $snapshot_links = format_snapshot_links
($commit);
7093 if (defined $snapshot_links) {
7094 print $barsep . $snapshot_links;
7099 if (defined $extra) {
7100 print "<tr class=\"extra\">\n" .
7101 "<td colspan=\"4\" class=\"extra\">$extra</td>\n" .
7107 sub git_history_body
{
7108 # Warning: assumes constant type (blob or tree) during history
7109 my ($commitlist, $from, $to, $refs, $extra,
7110 $file_name, $file_hash, $ftype) = @_;
7112 $from = 0 unless defined $from;
7113 $to = $#{$commitlist} unless (defined $to && $to <= $#{$commitlist});
7115 print "<table class=\"history\">\n";
7117 for (my $i = $from; $i <= $to; $i++) {
7118 my %co = %{$commitlist->[$i]};
7122 my $commit = $co{'id'};
7124 my $ref = format_ref_marker
($refs, $commit);
7127 print "<tr class=\"dark\">\n";
7129 print "<tr class=\"light\">\n";
7132 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
7133 # shortlog: format_author_html('td', \%co, 10)
7134 format_author_html
('td', \
%co, 15, 3) . "<td>";
7135 # originally git_history used chop_str($co{'title'}, 50)
7136 print format_subject_html
($co{'title'}, $co{'title_short'},
7137 href
(action
=>"commit", hash
=>$commit), $ref);
7139 "<td class=\"link\">" .
7140 $cgi->a({-href
=> href
(action
=>$ftype, hash_base
=>$commit, file_name
=>$file_name)}, $ftype) . $barsep .
7141 $cgi->a({-href
=> href
(action
=>"commitdiff", hash
=>$commit)}, "commitdiff");
7143 if ($ftype eq 'blob') {
7144 my $blob_current = $file_hash;
7145 my $blob_parent = git_get_hash_by_path
($commit, $file_name);
7146 if (defined $blob_current && defined $blob_parent &&
7147 $blob_current ne $blob_parent) {
7149 $cgi->a({-href
=> href
(action
=>"blobdiff",
7150 hash
=>$blob_current, hash_parent
=>$blob_parent,
7151 hash_base
=>$hash_base, hash_parent_base
=>$commit,
7152 file_name
=>$file_name)},
7159 if (defined $extra) {
7160 print "<tr class=\"extra\">\n" .
7161 "<td colspan=\"4\" class=\"extra\">$extra</td>\n" .
7168 # uses global variable $project
7169 my ($taglist, $from, $to, $extra, $head_at, $full, $order) = @_;
7170 $from = 0 unless defined $from;
7171 $to = $#{$taglist} if (!defined $to || $#{$taglist} < $to);
7172 $order ||= $default_refs_order;
7174 print "<table class=\"tags\">\n";
7176 print "<tr class=\"tags_header\">\n";
7177 print_sort_th
('age', $order, 'Last Change');
7178 print_sort_th
('name', $order, 'Name');
7179 print "<th></th>\n" . # for comment
7180 "<th></th>\n" . # for tag
7181 "<th></th>\n" . # for links
7185 for (my $i = $from; $i <= $to; $i++) {
7186 my $entry = $taglist->[$i];
7188 my $comment = $tag{'subject'};
7190 if (defined $comment) {
7191 $comment_short = chop_str
($comment, 30, 5);
7193 my $curr = defined $head_at && $tag{'id'} eq $head_at;
7195 print "<tr class=\"dark\">\n";
7197 print "<tr class=\"light\">\n";
7200 if (defined $tag{'age'}) {
7201 print "<td><i>$tag{'age'}</i></td>\n";
7203 print "<td></td>\n";
7205 print(($curr ?
"<td class=\"current_head\">" : "<td>") .
7206 $cgi->a({-href
=> href
(action
=>$tag{'reftype'}, hash
=>$tag{'refid'}),
7207 -class => "list name"}, esc_html
($tag{'name'})) .
7210 if (defined $comment) {
7211 print format_subject_html
($comment, $comment_short,
7212 href
(action
=>"tag", hash
=>$tag{'id'}));
7215 "<td class=\"selflink\">";
7216 if ($tag{'type'} eq "tag") {
7217 print $cgi->a({-href
=> href
(action
=>"tag", hash
=>$tag{'id'})}, "tag");
7222 "<td class=\"link\">" . $barsep .
7223 $cgi->a({-href
=> href
(action
=>$tag{'reftype'}, hash
=>$tag{'refid'})}, $tag{'reftype'});
7224 if ($tag{'reftype'} eq "commit") {
7225 print $barsep . $cgi->a({-href
=> href
(action
=>"shortlog", hash
=>$tag{'fullname'})}, "log");
7226 print $barsep . $cgi->a({-href
=> href
(action
=>"tree", hash
=>$tag{'fullname'})}, "tree") if $full;
7227 } elsif ($tag{'reftype'} eq "blob") {
7228 print $barsep . $cgi->a({-href
=> href
(action
=>"blob_plain", hash
=>$tag{'refid'})}, "raw");
7233 if (defined $extra) {
7234 print "<tr class=\"extra\">\n" .
7235 "<td colspan=\"5\" class=\"extra\">$extra</td>\n" .
7241 sub git_heads_body
{
7242 # uses global variable $project
7243 my ($headlist, $head_at, $from, $to, $extra) = @_;
7244 $from = 0 unless defined $from;
7245 $to = $#{$headlist} if (!defined $to || $#{$headlist} < $to);
7247 print "<table class=\"heads\">\n";
7249 for (my $i = $from; $i <= $to; $i++) {
7250 my $entry = $headlist->[$i];
7252 my $curr = defined $head_at && $ref{'id'} eq $head_at;
7254 print "<tr class=\"dark\">\n";
7256 print "<tr class=\"light\">\n";
7259 print "<td><i>$ref{'age'}</i></td>\n" .
7260 ($curr ?
"<td class=\"current_head\">" : "<td>") .
7261 $cgi->a({-href
=> href
(action
=>"shortlog", hash
=>$ref{'fullname'}),
7262 -class => "list name"},esc_html
($ref{'name'})) .
7264 "<td class=\"link\">" .
7265 $cgi->a({-href
=> href
(action
=>"shortlog", hash
=>$ref{'fullname'})}, "log") . $barsep .
7266 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$ref{'fullname'}, hash_base
=>$ref{'fullname'})}, "tree") .
7270 if (defined $extra) {
7271 print "<tr class=\"extra\">\n" .
7272 "<td colspan=\"3\" class=\"extra\">$extra</td>\n" .
7278 # Display a single remote block
7279 sub git_remote_block
{
7280 my ($remote, $rdata, $limit, $head) = @_;
7282 my $heads = $rdata->{'heads'};
7283 my $fetch = $rdata->{'fetch'};
7284 my $push = $rdata->{'push'};
7286 my $urls_table = "<table class=\"projects_list\">\n" ;
7288 if (defined $fetch) {
7289 if ($fetch eq $push) {
7290 $urls_table .= format_repo_url
("URL", $fetch);
7292 $urls_table .= format_repo_url
("Fetch URL", $fetch);
7293 $urls_table .= format_repo_url
("Push URL", $push) if defined $push;
7295 } elsif (defined $push) {
7296 $urls_table .= format_repo_url
("Push URL", $push);
7298 $urls_table .= format_repo_url
("", "No remote URL");
7301 $urls_table .= "</table>\n";
7304 if (defined $limit && $limit < @
$heads) {
7305 $dots = $cgi->a({-href
=> href
(action
=>"remotes", hash
=>$remote)}, "...");
7309 git_heads_body
($heads, $head, 0, $limit, $dots);
7312 # Display a list of remote names with the respective fetch and push URLs
7313 sub git_remotes_list
{
7314 my ($remotedata, $limit) = @_;
7315 print "<table class=\"heads\">\n";
7317 my @remotes = sort keys %$remotedata;
7319 my $limited = $limit && $limit < @remotes;
7321 $#remotes = $limit - 1 if $limited;
7323 while (my $remote = shift @remotes) {
7324 my $rdata = $remotedata->{$remote};
7325 my $fetch = $rdata->{'fetch'};
7326 my $push = $rdata->{'push'};
7328 print "<tr class=\"dark\">\n";
7330 print "<tr class=\"light\">\n";
7334 $cgi->a({-href
=> href
(action
=>'remotes', hash
=>$remote),
7335 -class=> "list name"},esc_html
($remote)) .
7337 print "<td class=\"link\">" .
7338 (defined $fetch ?
$cgi->a({-href
=> $fetch}, "fetch") : "fetch") .
7340 (defined $push ?
$cgi->a({-href
=> $push}, "push") : "push") .
7348 "<td colspan=\"3\">" .
7349 $cgi->a({-href
=> href
(action
=>"remotes")}, "...") .
7350 "</td>\n" . "</tr>\n";
7356 # Display remote heads grouped by remote, unless there are too many
7357 # remotes, in which case we only display the remote names
7358 sub git_remotes_body
{
7359 my ($remotedata, $limit, $head) = @_;
7360 if ($limit and $limit < keys %$remotedata) {
7361 git_remotes_list
($remotedata, $limit);
7363 fill_remote_heads
($remotedata);
7364 while (my ($remote, $rdata) = each %$remotedata) {
7365 git_print_section
({-class=>"remote", -id
=>$remote},
7366 ["remotes", $remote, $remote], sub {
7367 git_remote_block
($remote, $rdata, $limit, $head);
7373 sub git_search_message
{
7377 if ($searchtype eq 'commit') {
7378 $greptype = "--grep=";
7379 } elsif ($searchtype eq 'author') {
7380 $greptype = "--author=";
7381 } elsif ($searchtype eq 'committer') {
7382 $greptype = "--committer=";
7384 $greptype .= $searchtext;
7385 my @commitlist = parse_commits
($hash, 101, (100 * $page), undef,
7386 $greptype, '--regexp-ignore-case',
7387 $search_use_regexp ?
'--extended-regexp' : '--fixed-strings');
7389 my $paging_nav = "<span class=\"paging_nav\">";
7391 $paging_nav .= tabspan
(
7392 $cgi->a({-href
=> href
(-replay
=>1, page
=>undef)},
7395 $cgi->a({-href
=> href
(-replay
=>1, page
=>$page-1),
7396 -accesskey
=> "p", -title
=> "Alt-p"}, "prev"));
7398 $paging_nav .= tabspan
("first", 1, 0).${mdotsep
}.tabspan
("prev", 0, 1);
7401 if ($#commitlist >= 100) {
7402 $next_link = tabspan
(
7403 $cgi->a({-href
=> href
(-replay
=>1, page
=>$page+1),
7404 -accesskey
=> "n", -title
=> "Alt-n"}, "next"));
7405 $paging_nav .= "${mdotsep}$next_link";
7407 $paging_nav .= ${mdotsep
}.tabspan
("next", 0, 1);
7412 git_print_page_nav
('','', $hash,$co{'tree'},$hash, $paging_nav."</span>");
7413 git_print_header_div
('commit', esc_html
($co{'title'}), $hash);
7414 if ($page == 0 && !@commitlist) {
7415 print "<p>No match.</p>\n";
7417 git_search_grep_body
(\
@commitlist, 0, 99, $next_link);
7423 sub git_search_changes
{
7427 defined(my $fd = git_cmd_pipe
'--no-pager', 'log', @diff_opts,
7428 '--pretty=format:%H', '--no-abbrev', '--raw', "-S$searchtext",
7429 ($search_use_regexp ?
'--pickaxe-regex' : ()))
7430 or die_error
(500, "Open git-log failed");
7434 git_print_page_nav
('','', $hash,$co{'tree'},$hash);
7435 git_print_header_div
('commit', esc_html
($co{'title'}), $hash);
7437 print "<table class=\"pickaxe search\">\n";
7441 while (my $line = to_utf8
(scalar <$fd>)) {
7445 my %set = parse_difftree_raw_line
($line);
7446 if (defined $set{'commit'}) {
7447 # finish previous commit
7450 "<td class=\"link\">" .
7451 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$co{'id'})},
7454 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$co{'tree'},
7455 hash_base
=>$co{'id'})},
7462 print "<tr class=\"dark\">\n";
7464 print "<tr class=\"light\">\n";
7467 %co = parse_commit
($set{'commit'});
7468 my $author = chop_and_escape_str
($co{'author_name'}, 15, 5);
7469 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
7470 "<td><i>$author</i></td>\n" .
7472 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$co{'id'}),
7473 -class => "list subject"},
7474 chop_and_escape_str
($co{'title'}, 50) . "<br/>");
7475 } elsif (defined $set{'to_id'}) {
7476 next if ($set{'to_id'} =~ m/^0{40}$/);
7478 print $cgi->a({-href
=> href
(action
=>"blob", hash_base
=>$co{'id'},
7479 hash
=>$set{'to_id'}, file_name
=>$set{'to_file'}),
7481 "<span class=\"match\">" . esc_path
($set{'file'}) . "</span>") .
7487 # finish last commit (warning: repetition!)
7490 "<td class=\"link\">" .
7491 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$co{'id'})},
7494 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$co{'tree'},
7495 hash_base
=>$co{'id'})},
7506 sub git_search_files
{
7510 defined(my $fd = git_cmd_pipe
'grep', '-n', '-z',
7511 $search_use_regexp ?
('-E', '-i') : '-F',
7512 $searchtext, $co{'tree'})
7513 or die_error
(500, "Open git-grep failed");
7517 git_print_page_nav
('','', $hash,$co{'tree'},$hash);
7518 git_print_header_div
('commit', esc_html
($co{'title'}), $hash);
7520 print "<table class=\"grep_search\">\n";
7525 while (my $line = to_utf8
(scalar <$fd>)) {
7527 my ($file, $lno, $ltext, $binary);
7528 last if ($matches++ > 1000);
7529 if ($line =~ /^Binary file (.+) matches$/) {
7533 ($file, $lno, $ltext) = split(/\0/, $line, 3);
7534 $file =~ s/^$co{'tree'}://;
7536 if ($file ne $lastfile) {
7537 $lastfile and print "</td></tr>\n";
7539 print "<tr class=\"dark\">\n";
7541 print "<tr class=\"light\">\n";
7543 $file_href = href
(action
=>"blob", hash_base
=>$co{'id'},
7545 print "<td class=\"list\">".
7546 $cgi->a({-href
=> $file_href, -class => "list"}, esc_path
($file));
7547 print "</td><td>\n";
7551 print "<div class=\"binary\">Binary file</div>\n";
7553 $ltext = untabify
($ltext);
7554 if ($ltext =~ m/^(.*)($search_regexp)(.*)$/i) {
7555 $ltext = esc_html
($1, -nbsp
=>1);
7556 $ltext .= '<span class="match">';
7557 $ltext .= esc_html
($2, -nbsp
=>1);
7558 $ltext .= '</span>';
7559 $ltext .= esc_html
($3, -nbsp
=>1);
7561 $ltext = esc_html
($ltext, -nbsp
=>1);
7563 print "<div class=\"pre\">" .
7564 $cgi->a({-href
=> $file_href.'#l'.$lno,
7565 -class => "linenr"}, sprintf('%4i', $lno)) .
7566 ' ' . $ltext . "</div>\n";
7570 print "</td></tr>\n";
7571 if ($matches > 1000) {
7572 print "<div class=\"diff nodifferences\">Too many matches, listing trimmed</div>\n";
7575 print "<div class=\"diff nodifferences\">No matches found</div>\n";
7584 sub git_search_grep_body
{
7585 my ($commitlist, $from, $to, $extra) = @_;
7586 $from = 0 unless defined $from;
7587 $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
7589 print "<table class=\"commit_search\">\n";
7591 for (my $i = $from; $i <= $to; $i++) {
7592 my %co = %{$commitlist->[$i]};
7596 my $commit = $co{'id'};
7598 print "<tr class=\"dark\">\n";
7600 print "<tr class=\"light\">\n";
7603 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
7604 format_author_html
('td', \
%co, 15, 5) .
7606 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$co{'id'}),
7607 -class => "list subject"},
7608 chop_and_escape_str
($co{'title'}, 50) . "<br/>");
7609 my $comment = $co{'comment'};
7610 foreach my $line (@
$comment) {
7611 if ($line =~ m/^(.*?)($search_regexp)(.*)$/i) {
7612 my ($lead, $match, $trail) = ($1, $2, $3);
7613 $match = chop_str
($match, 70, 5, 'center');
7614 my $contextlen = int((80 - length($match))/2);
7615 $contextlen = 30 if ($contextlen > 30);
7616 $lead = chop_str
($lead, $contextlen, 10, 'left');
7617 $trail = chop_str
($trail, $contextlen, 10, 'right');
7619 $lead = esc_html
($lead);
7620 $match = esc_html
($match);
7621 $trail = esc_html
($trail);
7623 print "$lead<span class=\"match\">$match</span>$trail<br />";
7627 "<td class=\"link\">" .
7628 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$co{'id'})}, "commit") .
7630 $cgi->a({-href
=> href
(action
=>"commitdiff", hash
=>$co{'id'})}, "commitdiff") .
7632 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$co{'tree'}, hash_base
=>$co{'id'})}, "tree");
7636 if (defined $extra) {
7637 print "<tr class=\"extra\">\n" .
7638 "<td colspan=\"3\" class=\"extra\">$extra</td>\n" .
7644 ## ======================================================================
7645 ## ======================================================================
7648 sub git_project_list_load
{
7649 my $empty_list_ok = shift;
7650 my $order = $input_params{'order'};
7651 if (defined $order && $order !~ m/none|project|descr|owner|age/) {
7652 die_error
(400, "Unknown order parameter");
7655 my @list = git_get_projects_list
($project_filter, $strict_export);
7656 if ($project_filter && (!@list || !gitweb_check_feature
('forks'))) {
7657 push @list, { 'path' => "$project_filter.git" }
7658 if is_valid_project
("$project_filter.git");
7661 die_error
(404, "No projects found") unless $empty_list_ok;
7664 return (\
@list, $order);
7668 my ($projlist, $order);
7670 if ($frontpage_no_project_list) {
7672 $project_filter = undef;
7674 ($projlist, $order) = git_project_list_load
(1);
7677 if (defined $home_text && -f
$home_text) {
7678 print "<div class=\"index_include\">\n";
7679 insert_file
($home_text);
7682 git_project_search_form
($searchtext, $search_use_regexp);
7683 if ($frontpage_no_project_list) {
7684 my $show_ctags = gitweb_check_feature
('ctags');
7685 if ($frontpage_no_project_list == 1 and $show_ctags) {
7686 my @projects = git_get_projects_list
($project_filter, $strict_export);
7687 @projects = filter_forks_from_projects_list
(\
@projects) if gitweb_check_feature
('forks');
7688 @projects = fill_project_list_info
(\
@projects, 'ctags');
7689 my $ctags = git_gather_all_ctags
(\
@projects);
7690 my $cloud = git_populate_project_tagcloud
($ctags, 'project_list');
7691 print git_show_project_tagcloud
($cloud, 64);
7694 git_project_list_body
($projlist, $order, undef, undef, undef, undef, undef, 1);
7699 sub git_project_list
{
7700 my ($projlist, $order) = git_project_list_load
();
7702 if (!$frontpage_no_project_list && defined $home_text && -f
$home_text) {
7703 print "<div class=\"index_include\">\n";
7704 insert_file
($home_text);
7707 git_project_search_form
();
7708 git_project_list_body
($projlist, $order, undef, undef, undef, undef, undef, 1);
7713 my $order = $input_params{'order'};
7714 if (defined $order && $order !~ m/none|project|descr|owner|age/) {
7715 die_error
(400, "Unknown order parameter");
7718 my $filter = $project;
7719 $filter =~ s/\.git$//;
7720 my @list = git_get_projects_list
($filter);
7722 die_error
(404, "No forks found");
7726 git_print_page_nav
('','');
7727 git_print_header_div
('summary', "$project forks");
7728 git_project_list_body
(\
@list, $order, undef, undef, undef, undef, 'forks');
7732 sub git_project_index
{
7733 my @projects = git_get_projects_list
($project_filter, $strict_export);
7735 die_error
(404, "No projects found");
7739 -type
=> 'text/plain',
7740 -charset
=> 'utf-8',
7741 -content_disposition
=> 'inline; filename="index.aux"');
7743 foreach my $pr (@projects) {
7744 if (!exists $pr->{'owner'}) {
7745 $pr->{'owner'} = git_get_project_owner
("$pr->{'path'}");
7748 my ($path, $owner) = ($pr->{'path'}, $pr->{'owner'});
7749 # quote as in CGI::Util::encode, but keep the slash, and use '+' for ' '
7750 $path =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf
("%%%02X", ord($1))/eg
;
7751 $owner =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf
("%%%02X", ord($1))/eg
;
7755 print "$path $owner\n";
7760 my $descr = git_get_project_description
($project) || "none";
7761 my %co = parse_commit
("HEAD");
7762 my %cd = %co ? parse_date
($co{'committer_epoch'}, $co{'committer_tz'}) : ();
7763 my $head = $co{'id'};
7764 my $remote_heads = gitweb_check_feature
('remote_heads');
7766 my $owner = git_get_project_owner
($project);
7767 my $homepage = git_get_project_config
('homepage');
7768 my $base_url = git_get_project_config
('baseurl');
7770 my $refs = git_get_references
();
7771 # These get_*_list functions return one more to allow us to see if
7772 # there are more ...
7773 my @taglist = git_get_tags_list
(16);
7774 my @headlist = git_get_heads_list
(16);
7775 my %remotedata = $remote_heads ? git_get_remotes_list
() : ();
7777 my $check_forks = gitweb_check_feature
('forks');
7780 # find forks of a project
7781 my $filter = $project;
7782 $filter =~ s/\.git$//;
7783 @forklist = git_get_projects_list
($filter);
7784 # filter out forks of forks
7785 @forklist = filter_forks_from_projects_list
(\
@forklist)
7790 git_print_page_nav
('summary','', $head);
7792 if ($check_forks and $project =~ m
#/#) {
7793 my $xproject = $project; $xproject =~ s
#/[^/]+$#.git#; #
7794 my $r = $cgi->a({-href
=> href
(project
=> $xproject, action
=> 'summary')}, $xproject);
7796 <div class="forkinfo">
7797 This project is a fork of the $r project. If you have that one
7798 already cloned locally, you can use
7799 <pre>git clone --reference /path/to/your/$xproject/incarnation mirror_URL</pre>
7800 to save bandwidth during cloning.
7805 print "<div class=\"title\"> </div>\n";
7806 print "<table class=\"projects_list\">\n" .
7807 "<tr id=\"metadata_desc\"><td>description</td><td>" . esc_html
($descr) . "</td></tr>\n";
7809 print "<tr id=\"metadata_homepage\"><td>homepage URL</td><td>" . $cgi->a({-href
=> $homepage}, $homepage) . "</td></tr>\n";
7812 print "<tr id=\"metadata_baseurl\"><td>repository URL</td><td>" . esc_html
($base_url) . "</td></tr>\n";
7814 if ($owner and not $omit_owner) {
7815 print "<tr id=\"metadata_owner\"><td>owner</td><td>" . ($owner_link_hook
7816 ?
$cgi->a({-href
=> $owner_link_hook->($owner)}, email_obfuscate
($owner))
7817 : email_obfuscate
($owner)) . "</td></tr>\n";
7819 if (defined $cd{'rfc2822'}) {
7820 print "<tr id=\"metadata_lchange\"><td>last change</td>" .
7821 "<td>".format_timestamp_html
(\
%cd)."</td></tr>\n";
7823 print format_lastrefresh_row
(), "\n";
7825 # use per project git URL list in $projectroot/$project/cloneurl
7826 # or make project git URL from git base URL and project name
7827 my $url_tag = $base_url ?
"mirror URL" : "URL";
7828 my @url_list = git_get_project_url_list
($project);
7829 @url_list = map { "$_/$project" } @git_base_url_list unless @url_list;
7830 foreach my $git_url (@url_list) {
7831 next unless $git_url;
7832 print format_repo_url
($url_tag, $git_url);
7835 @url_list = map { "$_/$project" } @git_base_push_urls;
7836 if ((git_get_project_config
("showpush", '--bool')||'false') eq "true" ||
7837 -f
"$projectroot/$project/.nofetch") {
7838 $url_tag = "push URL";
7839 foreach my $git_push_url (@url_list) {
7840 next unless $git_push_url;
7841 my $hint = $https_hint_html && $git_push_url =~ /^https:/i ?
7842 " $https_hint_html" : '';
7843 print "<tr class=\"metadata_pushurl\"><td>$url_tag</td><td>$git_push_url$hint</td></tr>\n";
7848 if (defined($git_base_bundles_url) && -d
"$projectroot/$project/bundles") {
7849 my $projname = $project;
7850 $projname =~ s
|^.*/||;
7851 my $url = "$git_base_bundles_url/$project/bundles";
7852 print format_repo_url
(
7854 "<a rel='nofollow' href='$url'>$projname downloadable bundles</a>");
7858 my $show_ctags = gitweb_check_feature
('ctags');
7860 my $ctags = git_get_project_ctags
($project);
7861 if (%$ctags || $show_ctags !~ /^\d+$/) {
7862 # without ability to add tags, don't show if there are none
7863 my $cloud = git_populate_project_tagcloud
($ctags, 'project_list');
7864 print "<tr id=\"metadata_ctags\">" .
7865 "<td style=\"vertical-align:middle\">content tags<br />";
7866 print "</td>\n<td>" unless %$ctags;
7867 print "<form action=\"$show_ctags\" method=\"post\" style=\"white-space:nowrap\">" .
7868 "<input type=\"hidden\" name=\"p\" value=\"$project\"/>" .
7869 "add: <input type=\"text\" name=\"t\" size=\"8\" /></form>"
7870 unless $show_ctags =~ /^\d+$/;
7871 print "</td>\n<td>" if %$ctags;
7872 print git_show_project_tagcloud
($cloud, 48)."</td>" .
7879 # If XSS prevention is on, we don't include README.html.
7880 # TODO: Allow a readme in some safe format.
7881 if (!$prevent_xss) {
7882 my $readme = -s
"$projectroot/$project/README.html"
7883 ? collect_html_file
("$projectroot/$project/README.html")
7884 : collect_output
($git_automatic_readme_html, "$projectroot/$project");
7885 if (defined($readme)) {
7886 $readme =~ s/^\s+//s;
7887 $readme =~ s/\s+$//s;
7888 print "<div class=\"title\">readme</div>\n",
7889 "<div id=\"readme\" class=\"readme\">\n",
7896 # we need to request one more than 16 (0..15) to check if
7898 my @commitlist = $head ? parse_commits
($head, 17) : ();
7900 git_print_header_div
('shortlog');
7901 git_shortlog_body
(\
@commitlist, 0, 15, $refs,
7902 $#commitlist <= 15 ?
undef :
7903 $cgi->a({-href
=> href
(action
=>"shortlog")}, "..."));
7907 git_print_header_div
('tags');
7908 git_tags_body
(\
@taglist, 0, 15,
7909 $#taglist <= 15 ?
undef :
7910 $cgi->a({-href
=> href
(action
=>"tags")}, "..."));
7914 git_print_header_div
('heads');
7915 git_heads_body
(\
@headlist, $head, 0, 15,
7916 $#headlist <= 15 ?
undef :
7917 $cgi->a({-href
=> href
(action
=>"heads")}, "..."));
7921 git_print_header_div
('remotes');
7922 git_remotes_body
(\
%remotedata, 15, $head);
7926 git_print_header_div
('forks');
7927 git_project_list_body
(\
@forklist, 'age', 0, 15,
7928 $#forklist <= 15 ?
undef :
7929 $cgi->a({-href
=> href
(action
=>"forks")}, "..."),
7930 'no_header', 'forks');
7937 my %tag = parse_tag
($hash);
7940 die_error
(404, "Unknown tag object");
7944 $fullhash = $hash if $hash =~ m/^[0-9a-fA-F]{40}$/;
7945 $fullhash = git_get_full_hash
($project, $hash) unless $fullhash;
7947 my $head = git_get_head_hash
($project);
7949 git_print_page_nav
('','', $head,undef,$head);
7950 git_print_header_div
('commit', esc_html
($tag{'name'}), $hash);
7951 print "<div class=\"title_text\">\n" .
7952 "<table class=\"object_header\">\n" .
7953 "<tr><td>tag</td><td class=\"sha1\">$fullhash</td></tr>\n" .
7955 "<td>object</td>\n" .
7956 "<td>" . $cgi->a({-class => "list", -href
=> href
(action
=>$tag{'type'}, hash
=>$tag{'object'})},
7957 $tag{'object'}) . "</td>\n" .
7958 "<td class=\"link\">" . $cgi->a({-href
=> href
(action
=>$tag{'type'}, hash
=>$tag{'object'})},
7959 $tag{'type'}) . "</td>\n" .
7961 if (defined($tag{'author'})) {
7962 git_print_authorship_rows
(\
%tag, 'author');
7964 print "</table>\n\n" .
7966 print "<div class=\"page_body\">";
7967 my $comment = $tag{'comment'};
7968 foreach my $line (@
$comment) {
7970 print esc_html
($line, -nbsp
=>1) . "<br/>\n";
7976 sub git_blame_common
{
7977 my $format = shift || 'porcelain';
7978 if ($format eq 'porcelain' && $input_params{'javascript'}) {
7979 $format = 'incremental';
7980 $action = 'blame_incremental'; # for page title etc
7984 gitweb_check_feature
('blame')
7985 or die_error
(403, "Blame view not allowed");
7988 die_error
(400, "No file name given") unless $file_name;
7989 $hash_base ||= git_get_head_hash
($project);
7990 die_error
(404, "Couldn't find base commit") unless $hash_base;
7991 my %co = parse_commit
($hash_base)
7992 or die_error
(404, "Commit not found");
7994 if (!defined $hash) {
7995 $hash = git_get_hash_by_path
($hash_base, $file_name, "blob")
7996 or die_error
(404, "Error looking up file");
7998 $ftype = git_get_type
($hash);
7999 if ($ftype !~ "blob") {
8000 die_error
(400, "Object is not a blob");
8005 if ($format eq 'incremental') {
8006 # get file contents (as base)
8007 defined($fd = git_cmd_pipe
'cat-file', 'blob', $hash)
8008 or die_error
(500, "Open git-cat-file failed");
8009 } elsif ($format eq 'data') {
8010 # run git-blame --incremental
8011 defined($fd = git_cmd_pipe
"blame", "--incremental",
8012 $hash_base, "--", $file_name)
8013 or die_error
(500, "Open git-blame --incremental failed");
8015 # run git-blame --porcelain
8016 defined($fd = git_cmd_pipe
"blame", '-p',
8017 $hash_base, '--', $file_name)
8018 or die_error
(500, "Open git-blame --porcelain failed");
8021 # incremental blame data returns early
8022 if ($format eq 'data') {
8024 -type
=>"text/plain", -charset
=> "utf-8",
8025 -status
=> "200 OK");
8026 local $| = 1; # output autoflush
8031 or print "ERROR $!\n";
8034 if (defined $t0 && gitweb_check_feature
('timed')) {
8036 tv_interval
($t0, [ gettimeofday
() ]).
8037 ' '.$number_of_git_cmds;
8046 my $formats_nav = tabspan
(
8047 $cgi->a({-href
=> href
(action
=>"blob", -replay
=>1)},
8051 $cgi->a({-href
=> href
(action
=>"history", -replay
=>1)},
8054 $cgi->a({-href
=> href
(action
=>$action, file_name
=>$file_name)},
8056 git_print_page_nav
('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
8057 git_print_header_div
('commit', esc_html
($co{'title'}), $hash_base);
8058 git_print_page_path
($file_name, $ftype, $hash_base);
8061 if ($format eq 'incremental') {
8062 print "<noscript>\n<div class=\"error\"><center><b>\n".
8063 "This page requires JavaScript to run.\n Use ".
8064 $cgi->a({-href
=> href
(action
=>'blame',javascript
=>0,-replay
=>1)},
8067 "</b></center></div>\n</noscript>\n";
8069 print qq!<div id
="progress_bar" style
="width: 100%; background-color: yellow"></div
>\n!;
8072 print qq!<div
class="page_body">\n!;
8073 print qq!<div id
="progress_info">... / ...</div
>\n!
8074 if ($format eq 'incremental');
8075 print qq!<table id
="blame_table" class="blame" width
="100%">\n!.
8076 #qq!<col width="5.5em" /><col width="2.5em" /><col width="*" />\n!.
8078 qq!<tr
><th nowrap
="nowrap" style
="white-space:nowrap">!.
8079 qq!Commit
 <a href="javascript:extra_blame_columns()" id="columns_expander" !.
8080 qq!title
="toggles blame author information display">[+]</a></th
>!.
8081 qq!<th
class="extra_column">Author
</th><th class="extra_column">Date</th
>!.
8082 qq!<th
>Line
</th><th width="100%">Data</th
></tr
>\n!.
8086 my @rev_color = qw(light dark);
8087 my $num_colors = scalar(@rev_color);
8088 my $current_color = 0;
8090 if ($format eq 'incremental') {
8091 my $color_class = $rev_color[$current_color];
8096 while (my $line = to_utf8
(scalar <$fd>)) {
8100 print qq!<tr id
="l$linenr" class="$color_class">!.
8101 qq!<td
class="sha1"><a href
=""> </a></td
>!.
8102 qq!<td
class="extra_column" nowrap
="nowrap"></td
>!.
8103 qq!<td
class="extra_column" nowrap
="nowrap"></td
>!.
8104 qq!<td
class="linenr">!.
8105 qq!<a
class="linenr" href
="">$linenr</a></td
>!;
8106 print qq!<td
class="pre">! . esc_html
($line) . "</td>\n";
8110 } else { # porcelain, i.e. ordinary blame
8111 my %metainfo = (); # saves information about commits
8115 while (my $line = to_utf8
(scalar <$fd>)) {
8117 # the header: <SHA-1> <src lineno> <dst lineno> [<lines in group>]
8118 # no <lines in group> for subsequent lines in group of lines
8119 my ($full_rev, $orig_lineno, $lineno, $group_size) =
8120 ($line =~ /^([0-9a-f]{40}) (\d+) (\d+)(?: (\d+))?$/);
8121 if (!exists $metainfo{$full_rev}) {
8122 $metainfo{$full_rev} = { 'nprevious' => 0 };
8124 my $meta = $metainfo{$full_rev};
8126 while ($data = to_utf8
(scalar <$fd>)) {
8128 last if ($data =~ s/^\t//); # contents of line
8129 if ($data =~ /^(\S+)(?: (.*))?$/) {
8130 $meta->{$1} = $2 unless exists $meta->{$1};
8132 if ($data =~ /^previous /) {
8133 $meta->{'nprevious'}++;
8136 my $short_rev = substr($full_rev, 0, 8);
8137 my $author = $meta->{'author'};
8139 parse_date
($meta->{'author-time'}, $meta->{'author-tz'});
8140 my $date = $date{'iso-tz'};
8142 $current_color = ($current_color + 1) % $num_colors;
8144 my $tr_class = $rev_color[$current_color];
8145 $tr_class .= ' boundary' if (exists $meta->{'boundary'});
8146 $tr_class .= ' no-previous' if ($meta->{'nprevious'} == 0);
8147 $tr_class .= ' multiple-previous' if ($meta->{'nprevious'} > 1);
8148 print "<tr id=\"l$lineno\" class=\"$tr_class\">\n";
8150 my $rowspan = $group_size > 1 ?
" rowspan=\"$group_size\"" : "";
8151 print "<td class=\"sha1\"";
8152 print " title=\"". esc_html
($author) . ", $date\"";
8154 print $cgi->a({-href
=> href
(action
=>"commit",
8156 file_name
=>$file_name)},
8157 esc_html
($short_rev));
8158 if ($group_size >= 2) {
8159 my @author_initials = ($author =~ /\b([[:upper:]])\B/g);
8160 if (@author_initials) {
8162 esc_html
(join('', @author_initials));
8167 print "<td class=\"extra_column\" nowrap=\"nowrap\"$rowspan>". esc_html
($author) . "</td>";
8168 print "<td class=\"extra_column\" nowrap=\"nowrap\"$rowspan>". $date . "</td>";
8170 # 'previous' <sha1 of parent commit> <filename at commit>
8171 if (exists $meta->{'previous'} &&
8172 $meta->{'previous'} =~ /^([a-fA-F0-9]{40}) (.*)$/) {
8173 $meta->{'parent'} = $1;
8174 $meta->{'file_parent'} = unquote
($2);
8177 exists($meta->{'parent'}) ?
8178 $meta->{'parent'} : $full_rev;
8179 my $linenr_filename =
8180 exists($meta->{'file_parent'}) ?
8181 $meta->{'file_parent'} : unquote
($meta->{'filename'});
8182 my $blamed = href
(action
=> 'blame',
8183 file_name
=> $linenr_filename,
8184 hash_base
=> $linenr_commit);
8185 print "<td class=\"linenr\">";
8186 print $cgi->a({ -href
=> "$blamed#l$orig_lineno",
8187 -class => "linenr" },
8190 print "<td class=\"pre\">" . esc_html
($data) . "</td>\n";
8198 "</table>\n"; # class="blame"
8199 print "</div>\n"; # class="blame_body"
8201 or print "Reading blob failed\n";
8210 sub git_blame_incremental
{
8211 git_blame_common
('incremental');
8214 sub git_blame_data
{
8215 git_blame_common
('data');
8219 my $head = git_get_head_hash
($project);
8221 git_print_page_nav
('','', $head,undef,$head,format_ref_views
('tags'));
8222 git_print_header_div
('summary', $project);
8224 my @tagslist = git_get_tags_list
();
8226 git_tags_body
(\
@tagslist);
8232 my $order = $input_params{'order'};
8233 if (defined $order && $order !~ m/age|name/) {
8234 die_error
(400, "Unknown order parameter");
8237 my $head = git_get_head_hash
($project);
8239 git_print_page_nav
('','', $head,undef,$head,format_ref_views
('refs'));
8240 git_print_header_div
('summary', $project);
8242 my @refslist = git_get_tags_list
(undef, 1, $order);
8244 git_tags_body
(\
@refslist, undef, undef, undef, $head, 1, $order);
8250 my $head = git_get_head_hash
($project);
8252 git_print_page_nav
('','', $head,undef,$head,format_ref_views
('heads'));
8253 git_print_header_div
('summary', $project);
8255 my @headslist = git_get_heads_list
();
8257 git_heads_body
(\
@headslist, $head);
8262 # used both for single remote view and for list of all the remotes
8264 gitweb_check_feature
('remote_heads')
8265 or die_error
(403, "Remote heads view is disabled");
8267 my $head = git_get_head_hash
($project);
8268 my $remote = $input_params{'hash'};
8270 my $remotedata = git_get_remotes_list
($remote);
8271 die_error
(500, "Unable to get remote information") unless defined $remotedata;
8273 unless (%$remotedata) {
8274 die_error
(404, defined $remote ?
8275 "Remote $remote not found" :
8276 "No remotes found");
8279 git_header_html
(undef, undef, -action_extra
=> $remote);
8280 git_print_page_nav
('', '', $head, undef, $head,
8281 format_ref_views
($remote ?
'' : 'remotes'));
8283 fill_remote_heads
($remotedata);
8284 if (defined $remote) {
8285 git_print_header_div
('remotes', "$remote remote for $project");
8286 git_remote_block
($remote, $remotedata->{$remote}, undef, $head);
8288 git_print_header_div
('summary', "$project remotes");
8289 git_remotes_body
($remotedata, undef, $head);
8295 sub git_blob_plain
{
8299 if (!defined $hash) {
8300 if (defined $file_name) {
8301 my $base = $hash_base || git_get_head_hash
($project);
8302 $hash = git_get_hash_by_path
($base, $file_name, "blob")
8303 or die_error
(404, "Cannot find file");
8305 die_error
(400, "No file name defined");
8307 } elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) {
8308 # blobs defined by non-textual hash id's can be cached
8312 defined(my $fd = git_cmd_pipe
"cat-file", "blob", $hash)
8313 or die_error
(500, "Open git-cat-file blob '$hash' failed");
8316 # content-type (can include charset)
8318 ($type, $leader) = blob_contenttype
($fd, $file_name, $type);
8320 # "save as" filename, even when no $file_name is given
8321 my $save_as = "$hash";
8322 if (defined $file_name) {
8323 $save_as = $file_name;
8324 } elsif ($type =~ m/^text\//) {
8328 # With XSS prevention on, blobs of all types except a few known safe
8329 # ones are served with "Content-Disposition: attachment" to make sure
8330 # they don't run in our security domain. For certain image types,
8331 # blob view writes an <img> tag referring to blob_plain view, and we
8332 # want to be sure not to break that by serving the image as an
8333 # attachment (though Firefox 3 doesn't seem to care).
8334 my $sandbox = $prevent_xss &&
8335 $type !~ m!^(?:text/[a-z]+|image/(?:gif|png|jpeg))(?:[ ;]|$)!;
8337 # serve text/* as text/plain
8339 ($type =~ m!^text/[a-z]+\b(.*)$! ||
8340 ($type =~ m!^[a-z]+/[a-z]\+xml\b(.*)$! && -T
$fd))) {
8342 $rest = defined $rest ?
$rest : '';
8343 $type = "text/plain$rest";
8348 -expires
=> $expires,
8349 -content_disposition
=>
8350 ($sandbox ?
'attachment' : 'inline')
8351 . '; filename="' . $save_as . '"');
8352 binmode STDOUT
, ':raw';
8354 print $leader if defined $leader;
8356 while (read($fd, $buf, 32768)) {
8359 binmode STDOUT
, ':utf8'; # as set at the beginning of gitweb.cgi
8368 if (!defined $hash) {
8369 if (defined $file_name) {
8370 my $base = $hash_base || git_get_head_hash
($project);
8371 $hash = git_get_hash_by_path
($base, $file_name, "blob")
8372 or die_error
(404, "Cannot find file");
8375 die_error
(400, "No file name defined");
8377 } elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) {
8378 # blobs defined by non-textual hash id's can be cached
8382 $fullhash = git_get_full_hash
($project, $hash) unless $fullhash;
8384 my $have_blame = gitweb_check_feature
('blame');
8385 defined(my $fd = git_cmd_pipe
"cat-file", "blob", $hash)
8386 or die_error
(500, "Couldn't cat $file_name, $hash");
8388 my $mimetype = blob_mimetype
($fd, $file_name);
8389 # use 'blob_plain' (aka 'raw') view for files that cannot be displayed
8390 if ($mimetype !~ m!^(?:text/|image/(?:gif|png|jpeg)$)! && -B
$fd) {
8392 return git_blob_plain
($mimetype);
8394 # we can have blame only for text/* mimetype
8395 $have_blame &&= ($mimetype =~ m!^text/!);
8397 my $highlight = gitweb_check_feature
('highlight') && defined $highlight_bin;
8398 my $syntax = guess_file_syntax
($fd, $mimetype, $file_name) if $highlight;
8399 my $highlight_mode_active;
8400 ($fd, $highlight_mode_active) = run_highlighter
($fd, $syntax) if $syntax;
8402 git_header_html
(undef, $expires);
8403 my $formats_nav = '';
8404 if (defined $hash_base && (my %co = parse_commit
($hash_base))) {
8405 if (defined $file_name) {
8407 $formats_nav .= tabspan
(
8408 $cgi->a({-href
=> href
(action
=>"blame", -replay
=>1),
8409 -class => "blamelink"},
8413 $formats_nav .= tabspan
(
8414 $cgi->a({-href
=> href
(action
=>"history", -replay
=>1)},
8417 $cgi->a({-href
=> href
(action
=>"blob_plain", -replay
=>1)},
8420 $cgi->a({-href
=> href
(action
=>"blob",
8421 hash_base
=>"HEAD", file_name
=>$file_name)},
8424 $formats_nav .= tabspan
(
8425 $cgi->a({-href
=> href
(action
=>"blob_plain", -replay
=>1)},
8428 git_print_page_nav
('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
8429 git_print_header_div
('commit', esc_html
($co{'title'}), $hash_base);
8431 print "<div class=\"page_nav\">\n" .
8432 "<br/><br/></div>\n" .
8433 "<div class=\"title\">".esc_html
($hash)."</div>\n";
8435 git_print_page_path
($file_name, "blob", $hash_base);
8436 print "<div class=\"title_text\">\n" .
8437 "<table class=\"object_header\">\n";
8438 print "<tr><td>blob</td><td class=\"sha1\">$fullhash</td></tr>\n";
8441 print "<div class=\"page_body\">\n";
8442 if ($mimetype =~ m!^image/!) {
8443 print qq!<img
class="blob" type
="!.esc_attr($mimetype).qq!"!;
8445 print qq! alt
="!.esc_attr($file_name).qq!" title
="!.esc_attr($file_name).qq!"!;
8448 href(action=>"blob_plain
", hash=>$hash,
8449 hash_base=>$hash_base, file_name=>$file_name) .
8453 while (my $line = to_utf8
(scalar <$fd>)) {
8456 $line = untabify
($line);
8457 printf qq!<div
class="pre"><a id
="l%i" href
="%s#l%i" class="linenr">%4i</a> %s</div
>\n!,
8458 $nr, esc_attr
(href
(-replay
=> 1)), $nr, $nr,
8459 $highlight_mode_active ? sanitize
($line) : esc_html
($line, -nbsp
=>1);
8463 or print "Reading blob failed.\n";
8470 if (!defined $hash_base) {
8471 $hash_base = "HEAD";
8473 if (!defined $hash) {
8474 if (defined $file_name) {
8475 $hash = git_get_hash_by_path
($hash_base, $file_name, "tree");
8481 die_error
(404, "No such tree") unless defined($hash);
8482 $fullhash = $hash if !$fullhash && $hash =~ m/^[0-9a-fA-F]{40}$/;
8483 $fullhash = git_get_full_hash
($project, $hash) unless $fullhash;
8485 my $show_sizes = gitweb_check_feature
('show-sizes');
8486 my $have_blame = gitweb_check_feature
('blame');
8491 defined(my $fd = git_cmd_pipe
"ls-tree", '-z',
8492 ($show_sizes ?
'-l' : ()), @extra_options, $hash)
8493 or die_error
(500, "Open git-ls-tree failed");
8494 @entries = map { chomp; to_utf8
($_) } <$fd>;
8496 or die_error
(404, "Reading tree failed");
8501 if (defined $hash_base && (my %co = parse_commit
($hash_base))) {
8502 my $refs = git_get_references
();
8503 my $ref = format_ref_marker
($refs, $co{'id'});
8505 if (defined $file_name) {
8507 tabspan
($cgi->a({-href
=> href
(action
=>"history", -replay
=>1)},
8509 tabspan
($cgi->a({-href
=> href
(action
=>"tree",
8510 hash_base
=>"HEAD", file_name
=>$file_name)},
8513 my $snapshot_links = format_snapshot_links
($hash);
8514 if (defined $snapshot_links) {
8515 # FIXME: Should be available when we have no hash base as well.
8516 push @views_nav, $snapshot_links;
8518 git_print_page_nav
('tree','', $hash_base, undef, undef,
8519 join($barsep, @views_nav));
8520 git_print_header_div
('commit', esc_html
($co{'title'}), $hash_base, undef, $ref);
8523 print "<div class=\"page_nav\">\n";
8524 print "<br/><br/></div>\n";
8525 print "<div class=\"title\">".esc_html
($hash)."</div>\n";
8527 if (defined $file_name) {
8528 $basedir = $file_name;
8529 if ($basedir ne '' && substr($basedir, -1) ne '/') {
8532 git_print_page_path
($file_name, 'tree', $hash_base);
8534 print "<div class=\"title_text\">\n" .
8535 "<table class=\"object_header\">\n";
8536 print "<tr><td>tree</td><td class=\"sha1\">$fullhash</td></tr>\n";
8539 print "<div class=\"page_body\">\n";
8540 print "<table class=\"tree\">\n";
8542 # '..' (top directory) link if possible
8543 if (defined $hash_base &&
8544 defined $file_name && $file_name =~ m![^/]+$!) {
8546 print "<tr class=\"dark\">\n";
8548 print "<tr class=\"light\">\n";
8552 my $up = $file_name;
8553 $up =~ s!/?[^/]+$!!;
8554 undef $up unless $up;
8555 # based on git_print_tree_entry
8556 print '<td class="mode">' . mode_str
('040000') . "</td>\n";
8557 print '<td class="size"> </td>'."\n" if $show_sizes;
8558 print '<td class="list">';
8559 print $cgi->a({-href
=> href
(action
=>"tree",
8560 hash_base
=>$hash_base,
8564 print "<td class=\"link\"></td>\n";
8568 foreach my $line (@entries) {
8569 my %t = parse_ls_tree_line
($line, -z
=> 1, -l
=> $show_sizes);
8572 print "<tr class=\"dark\">\n";
8574 print "<tr class=\"light\">\n";
8578 git_print_tree_entry
(\
%t, $basedir, $hash_base, $have_blame);
8582 print "</table>\n" .
8587 sub sanitize_for_filename
{
8591 $name =~ s/[^[:alnum:]_.-]//g;
8597 my ($project, $hash) = @_;
8599 # path/to/project.git -> project
8600 # path/to/project/.git -> project
8601 my $name = to_utf8
($project);
8602 $name =~ s
,([^/])/*\
.git
$,$1,;
8603 $name = sanitize_for_filename
(basename
($name));
8606 if ($hash =~ /^[0-9a-fA-F]+$/) {
8607 # shorten SHA-1 hash
8608 my $full_hash = git_get_full_hash
($project, $hash);
8609 if ($full_hash =~ /^$hash/ && length($hash) > 7) {
8610 $ver = git_get_short_hash
($project, $hash);
8612 } elsif ($hash =~ m!^refs/tags/(.*)$!) {
8613 # tags don't need shortened SHA-1 hash
8616 # branches and other need shortened SHA-1 hash
8617 my $strip_refs = join '|', map { quotemeta } get_branch_refs
();
8618 if ($hash =~ m!^refs/($strip_refs|remotes)/(.*)$!) {
8619 my $ref_dir = (defined $1) ?
$1 : '';
8622 $ref_dir = sanitize_for_filename
($ref_dir);
8623 # for refs neither in heads nor remotes we want to
8624 # add a ref dir to archive name
8625 if ($ref_dir ne '' and $ref_dir ne 'heads' and $ref_dir ne 'remotes') {
8626 $ver = $ref_dir . '-' . $ver;
8629 $ver .= '-' . git_get_short_hash
($project, $hash);
8631 # special case of sanitization for filename - we change
8632 # slashes to dots instead of dashes
8633 # in case of hierarchical branch names
8635 $ver =~ s/[^[:alnum:]_.-]//g;
8637 # name = project-version_string
8638 $name = "$name-$ver";
8640 return wantarray ?
($name, $name) : $name;
8643 sub exit_if_unmodified_since
{
8644 my ($latest_epoch) = @_;
8647 my $if_modified = $cgi->http('IF_MODIFIED_SINCE');
8648 if (defined $if_modified) {
8650 if (eval { require HTTP
::Date
; 1; }) {
8651 $since = HTTP
::Date
::str2time
($if_modified);
8652 } elsif (eval { require Time
::ParseDate
; 1; }) {
8653 $since = Time
::ParseDate
::parsedate
($if_modified, GMT
=> 1);
8655 if (defined $since && $latest_epoch <= $since) {
8656 my %latest_date = parse_date
($latest_epoch);
8658 -last_modified
=> $latest_date{'rfc2822'},
8659 -status
=> '304 Not Modified');
8666 my $format = $input_params{'snapshot_format'};
8667 if (!@snapshot_fmts) {
8668 die_error
(403, "Snapshots not allowed");
8670 # default to first supported snapshot format
8671 $format ||= $snapshot_fmts[0];
8672 if ($format !~ m/^[a-z0-9]+$/) {
8673 die_error
(400, "Invalid snapshot format parameter");
8674 } elsif (!exists($known_snapshot_formats{$format})) {
8675 die_error
(400, "Unknown snapshot format");
8676 } elsif ($known_snapshot_formats{$format}{'disabled'}) {
8677 die_error
(403, "Snapshot format not allowed");
8678 } elsif (!grep($_ eq $format, @snapshot_fmts)) {
8679 die_error
(403, "Unsupported snapshot format");
8682 my $type = git_get_type
("$hash^{}");
8684 die_error
(404, 'Object does not exist');
8685 } elsif ($type eq 'blob') {
8686 die_error
(400, 'Object is not a tree-ish');
8689 my ($name, $prefix) = snapshot_name
($project, $hash);
8690 my $filename = "$name$known_snapshot_formats{$format}{'suffix'}";
8692 my %co = parse_commit
($hash);
8693 exit_if_unmodified_since
($co{'committer_epoch'}) if %co;
8696 git_cmd
(), 'archive',
8697 "--format=$known_snapshot_formats{$format}{'format'}",
8698 "--prefix=$prefix/", $hash);
8699 if (exists $known_snapshot_formats{$format}{'compressor'}) {
8700 @cmd = ($posix_shell_bin, '-c', quote_command
(@cmd) .
8701 ' | ' . quote_command
(@
{$known_snapshot_formats{$format}{'compressor'}}));
8704 $filename =~ s/(["\\])/\\$1/g;
8707 %latest_date = parse_date
($co{'committer_epoch'}, $co{'committer_tz'});
8711 -type
=> $known_snapshot_formats{$format}{'type'},
8712 -content_disposition
=> 'inline; filename="' . $filename . '"',
8713 %co ?
(-last_modified
=> $latest_date{'rfc2822'}) : (),
8714 -status
=> '200 OK');
8716 defined(my $fd = cmd_pipe
@cmd)
8717 or die_error
(500, "Execute git-archive failed");
8719 binmode STDOUT
, ':raw';
8722 while (read($fd, $buf, 32768)) {
8725 binmode STDOUT
, ':utf8'; # as set at the beginning of gitweb.cgi
8730 sub git_log_generic
{
8731 my ($fmt_name, $body_subr, $base, $parent, $file_name, $file_hash) = @_;
8733 my $head = git_get_head_hash
($project);
8734 if (!defined $base) {
8737 if (!defined $page) {
8740 my $refs = git_get_references
();
8742 my $commit_hash = $base;
8743 if (defined $parent) {
8744 $commit_hash = "$parent..$base";
8747 parse_commits
($commit_hash, 101, (100 * $page),
8748 defined $file_name ?
($file_name, "--full-history") : ());
8751 if (!defined $file_hash && defined $file_name) {
8752 # some commits could have deleted file in question,
8753 # and not have it in tree, but one of them has to have it
8754 for (my $i = 0; $i < @commitlist; $i++) {
8755 $file_hash = git_get_hash_by_path
($commitlist[$i]{'id'}, $file_name);
8756 last if defined $file_hash;
8759 if (defined $file_hash) {
8760 $ftype = git_get_type
($file_hash);
8762 if (defined $file_name && !defined $ftype) {
8763 die_error
(500, "Unknown type of object");
8766 if (defined $file_name) {
8767 %co = parse_commit
($base)
8768 or die_error
(404, "Unknown commit object");
8773 if ($#commitlist >= 100) {
8775 $cgi->a({-href
=> href
(-replay
=>1, page
=>$page+1),
8776 -accesskey
=> "n", -title
=> "Alt-n"}, "next");
8779 my ($patch_max) = gitweb_get_feature
('patches');
8780 if ($patch_max && !defined $file_name) {
8781 if ($patch_max < 0 || @commitlist <= $patch_max) {
8782 $extra = $cgi->a({-href
=> href
(action
=>"patches", -replay
=>1)},
8786 my $paging_nav = format_log_nav
($fmt_name, $page, $#commitlist >= 100, $extra);
8789 local $action = 'fulllog';
8792 git_print_page_nav
($fmt_name,'', $hash,$hash,$hash, $paging_nav);
8793 if (defined $file_name) {
8794 git_print_header_div
('commit', esc_html
($co{'title'}), $base);
8796 git_print_header_div
('summary', $project)
8798 git_print_page_path
($file_name, $ftype, $hash_base)
8799 if (defined $file_name);
8801 $body_subr->(\
@commitlist, 0, 99, $refs, $next_link,
8802 $file_name, $file_hash, $ftype);
8808 git_log_generic
('log', \
&git_log_body
,
8809 $hash, $hash_parent);
8813 $hash ||= $hash_base || "HEAD";
8814 my %co = parse_commit
($hash)
8815 or die_error
(404, "Unknown commit object");
8817 my $parent = $co{'parent'};
8818 my $parents = $co{'parents'}; # listref
8820 # we need to prepare $formats_nav before any parameter munging
8822 if (!defined $parent) {
8824 $formats_nav .= '<span class="parents none">(initial)</span>';
8825 } elsif (@
$parents == 1) {
8826 # single parent commit
8828 '<span class="parents single">(parent: ' .
8829 $cgi->a({-href
=> href
(action
=>"commit",
8831 esc_html
(substr($parent, 0, 7))) .
8836 '<span class="parents multiple">(merge: ' .
8838 $cgi->a({-href
=> href
(action
=>"commit",
8840 esc_html
(substr($_, 0, 7)));
8844 if (gitweb_check_feature
('patches') && @
$parents <= 1) {
8845 $formats_nav .= $barsep . tabspan
(
8846 $cgi->a({-href
=> href
(action
=>"patch", -replay
=>1)},
8850 if (!defined $parent) {
8854 defined(my $fd = git_cmd_pipe
"diff-tree", '-r', "--no-commit-id",
8856 (@
$parents <= 1 ?
$parent : '-c'),
8858 or die_error
(500, "Open git-diff-tree failed");
8859 @difftree = map { chomp; to_utf8
($_) } <$fd>;
8860 close $fd or die_error
(404, "Reading git-diff-tree failed");
8862 # non-textual hash id's can be cached
8864 if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
8867 my $refs = git_get_references
();
8868 my $ref = format_ref_marker
($refs, $co{'id'});
8870 git_header_html
(undef, $expires);
8871 git_print_page_nav
('commit', '',
8872 $hash, $co{'tree'}, $hash,
8875 if (defined $co{'parent'}) {
8876 git_print_header_div
('commitdiff', esc_html
($co{'title'}), $hash, undef, $ref);
8878 git_print_header_div
('tree', esc_html
($co{'title'}), $co{'tree'}, $hash, $ref);
8880 print "<div class=\"title_text\">\n" .
8881 "<table class=\"object_header\">\n";
8882 print "<tr><td>commit</td><td class=\"sha1\">$co{'id'}</td></tr>\n";
8883 git_print_authorship_rows
(\
%co);
8886 "<td class=\"sha1\">" .
8887 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$co{'tree'}, hash_base
=>$hash),
8888 class => "list"}, $co{'tree'}) .
8890 "<td class=\"link\">" .
8891 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$co{'tree'}, hash_base
=>$hash)},
8893 my $snapshot_links = format_snapshot_links
($hash);
8894 if (defined $snapshot_links) {
8895 print $barsep . $snapshot_links;
8900 foreach my $par (@
$parents) {
8903 "<td class=\"sha1\">" .
8904 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$par),
8905 class => "list"}, $par) .
8907 "<td class=\"link\">" .
8908 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$par)}, "commit") .
8910 $cgi->a({-href
=> href
(action
=>"commitdiff", hash
=>$hash, hash_parent
=>$par)}, "diff") .
8917 print "<div class=\"page_body\">\n";
8918 git_print_log
($co{'comment'});
8921 git_difftree_body
(\
@difftree, $hash, @
$parents);
8927 # object is defined by:
8928 # - hash or hash_base alone
8929 # - hash_base and file_name
8932 # - hash or hash_base alone
8933 if ($hash || ($hash_base && !defined $file_name)) {
8934 my $object_id = $hash || $hash_base;
8936 defined(my $fd = git_cmd_pipe
'cat-file', '-t', $object_id)
8937 or die_error
(404, "Object does not exist");
8941 or die_error
(404, "Object does not exist");
8943 # - hash_base and file_name
8944 } elsif ($hash_base && defined $file_name) {
8945 $file_name =~ s
,/+$,,;
8947 system(git_cmd
(), "cat-file", '-e', $hash_base) == 0
8948 or die_error
(404, "Base object does not exist");
8950 # here errors should not happen
8951 defined(my $fd = git_cmd_pipe
"ls-tree", $hash_base, "--", $file_name)
8952 or die_error
(500, "Open git-ls-tree failed");
8953 my $line = to_utf8
(scalar <$fd>);
8956 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'
8957 unless ($line && $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t/) {
8958 die_error
(404, "File or directory for given base does not exist");
8963 die_error
(400, "Not enough information to find object");
8966 print $cgi->redirect(-uri
=> href
(action
=>$type, -full
=>1,
8967 hash
=>$hash, hash_base
=>$hash_base,
8968 file_name
=>$file_name),
8969 -status
=> '302 Found');
8973 my $format = shift || 'html';
8974 my $diff_style = $input_params{'diff_style'} || 'inline';
8981 # preparing $fd and %diffinfo for git_patchset_body
8983 if (defined $hash_base && defined $hash_parent_base) {
8984 if (defined $file_name) {
8986 defined($fd = git_cmd_pipe
"diff-tree", '-r', @diff_opts,
8987 $hash_parent_base, $hash_base,
8988 "--", (defined $file_parent ?
$file_parent : ()), $file_name)
8989 or die_error
(500, "Open git-diff-tree failed");
8990 @difftree = map { chomp; to_utf8
($_) } <$fd>;
8992 or die_error
(404, "Reading git-diff-tree failed");
8994 or die_error
(404, "Blob diff not found");
8996 } elsif (defined $hash &&
8997 $hash =~ /[0-9a-fA-F]{40}/) {
8998 # try to find filename from $hash
9000 # read filtered raw output
9001 defined($fd = git_cmd_pipe
"diff-tree", '-r', @diff_opts,
9002 $hash_parent_base, $hash_base, "--")
9003 or die_error
(500, "Open git-diff-tree failed");
9005 # ':100644 100644 03b21826... 3b93d5e7... M ls-files.c'
9007 grep { /^:[0-7]{6} [0-7]{6} [0-9a-fA-F]{40} $hash/ }
9008 map { chomp; to_utf8
($_) } <$fd>;
9010 or die_error
(404, "Reading git-diff-tree failed");
9012 or die_error
(404, "Blob diff not found");
9015 die_error
(400, "Missing one of the blob diff parameters");
9018 if (@difftree > 1) {
9019 die_error
(400, "Ambiguous blob diff specification");
9022 %diffinfo = parse_difftree_raw_line
($difftree[0]);
9023 $file_parent ||= $diffinfo{'from_file'} || $file_name;
9024 $file_name ||= $diffinfo{'to_file'};
9026 $hash_parent ||= $diffinfo{'from_id'};
9027 $hash ||= $diffinfo{'to_id'};
9029 # non-textual hash id's can be cached
9030 if ($hash_base =~ m/^[0-9a-fA-F]{40}$/ &&
9031 $hash_parent_base =~ m/^[0-9a-fA-F]{40}$/) {
9036 defined($fd = git_cmd_pipe
"diff-tree", '-r', @diff_opts,
9037 '-p', ($format eq 'html' ?
"--full-index" : ()),
9038 $hash_parent_base, $hash_base,
9039 "--", (defined $file_parent ?
$file_parent : ()), $file_name)
9040 or die_error
(500, "Open git-diff-tree failed");
9043 # old/legacy style URI -- not generated anymore since 1.4.3.
9045 die_error
('404 Not Found', "Missing one of the blob diff parameters")
9049 if ($format eq 'html') {
9051 $cgi->a({-href
=> href
(action
=>"blobdiff_plain", -replay
=>1)},
9053 $formats_nav .= diff_style_nav
($diff_style);
9054 git_header_html
(undef, $expires);
9055 if (defined $hash_base && (my %co = parse_commit
($hash_base))) {
9056 git_print_page_nav
('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
9057 git_print_header_div
('commit', esc_html
($co{'title'}), $hash_base);
9059 print "<div class=\"page_nav\"><br/>$formats_nav<br/></div>\n";
9060 print "<div class=\"title\">".esc_html
("$hash vs $hash_parent")."</div>\n";
9062 if (defined $file_name) {
9063 git_print_page_path
($file_name, "blob", $hash_base);
9065 print "<div class=\"page_path\"></div>\n";
9068 } elsif ($format eq 'plain') {
9070 -type
=> 'text/plain',
9071 -charset
=> 'utf-8',
9072 -expires
=> $expires,
9073 -content_disposition
=> 'inline; filename="' . "$file_name" . '.patch"');
9075 print "X-Git-Url: " . $cgi->self_url() . "\n\n";
9078 die_error
(400, "Unknown blobdiff format");
9082 if ($format eq 'html') {
9083 print "<div class=\"page_body\">\n";
9085 git_patchset_body
($fd, $diff_style,
9086 [ \
%diffinfo ], $hash_base, $hash_parent_base);
9089 print "</div>\n"; # class="page_body"
9093 while (my $line = to_utf8
(scalar <$fd>)) {
9094 $line =~ s!a/($hash|$hash_parent)!'a/'.esc_path($diffinfo{'from_file'})!eg;
9095 $line =~ s!b/($hash|$hash_parent)!'b/'.esc_path($diffinfo{'to_file'})!eg;
9099 last if $line =~ m!^\+\+\+!;
9108 sub git_blobdiff_plain
{
9109 git_blobdiff
('plain');
9112 # assumes that it is added as later part of already existing navigation,
9113 # so it returns "| foo | bar" rather than just "foo | bar"
9114 sub diff_style_nav
{
9115 my ($diff_style, $is_combined) = @_;
9116 $diff_style ||= 'inline';
9118 return "" if ($is_combined);
9120 my @styles = (inline
=> 'inline', 'sidebyside' => 'side by side');
9121 my %styles = @styles;
9123 @styles[ map { $_ * 2 } 0..$#styles/2 ];
9125 return $barsep . '<span class="diffstyles">' . join($barsep,
9128 '<span class="diffstyle selected">' . $styles{$_} . '</span>' :
9129 '<span class="diffstyle">' .
9130 $cgi->a({-href
=> href
(-replay
=>1, diff_style
=> $_)}, $styles{$_}) .
9132 } @styles) . '</span>';
9135 sub git_commitdiff
{
9137 my $format = $params{-format
} || 'html';
9138 my $diff_style = $input_params{'diff_style'} || 'inline';
9140 my ($patch_max) = gitweb_get_feature
('patches');
9141 if ($format eq 'patch') {
9142 die_error
(403, "Patch view not allowed") unless $patch_max;
9145 $hash ||= $hash_base || "HEAD";
9146 my %co = parse_commit
($hash)
9147 or die_error
(404, "Unknown commit object");
9149 # choose format for commitdiff for merge
9150 if (! defined $hash_parent && @
{$co{'parents'}} > 1) {
9151 $hash_parent = '--cc';
9153 # we need to prepare $formats_nav before almost any parameter munging
9155 if ($format eq 'html') {
9156 $formats_nav = tabspan
(
9157 $cgi->a({-href
=> href
(action
=>"commitdiff_plain", -replay
=>1)},
9159 if ($patch_max && @
{$co{'parents'}} <= 1) {
9160 $formats_nav .= $barsep . tabspan
(
9161 $cgi->a({-href
=> href
(action
=>"patch", -replay
=>1)},
9164 $formats_nav .= diff_style_nav
($diff_style, @
{$co{'parents'}} > 1);
9166 if (defined $hash_parent &&
9167 $hash_parent ne '-c' && $hash_parent ne '--cc') {
9168 # commitdiff with two commits given
9169 my $hash_parent_short = $hash_parent;
9170 if ($hash_parent =~ m/^[0-9a-fA-F]{40}$/) {
9171 $hash_parent_short = substr($hash_parent, 0, 7);
9173 $formats_nav .= $spcsep . '<span class="parents multiple">' .
9175 for (my $i = 0; $i < @
{$co{'parents'}}; $i++) {
9176 if ($co{'parents'}[$i] eq $hash_parent) {
9177 $formats_nav .= ' parent ' . ($i+1);
9181 $formats_nav .= ': ' .
9182 $cgi->a({-href
=> href
(-replay
=>1,
9183 hash
=>$hash_parent, hash_base
=>undef)},
9184 esc_html
($hash_parent_short)) .
9186 } elsif (!$co{'parent'}) {
9188 $formats_nav .= $spcsep . '<span class="parents none">(initial)</span>';
9189 } elsif (scalar @
{$co{'parents'}} == 1) {
9190 # single parent commit
9191 $formats_nav .= $spcsep .
9192 '<span class="parents single">(parent: ' .
9193 $cgi->a({-href
=> href
(-replay
=>1,
9194 hash
=>$co{'parent'}, hash_base
=>undef)},
9195 esc_html
(substr($co{'parent'}, 0, 7))) .
9199 if ($hash_parent eq '--cc') {
9200 $formats_nav .= $barsep . tabspan
(
9201 $cgi->a({-href
=> href
(-replay
=>1,
9202 hash
=>$hash, hash_parent
=>'-c')},
9204 } else { # $hash_parent eq '-c'
9205 $formats_nav .= $barsep . tabspan
(
9206 $cgi->a({-href
=> href
(-replay
=>1,
9207 hash
=>$hash, hash_parent
=>'--cc')},
9210 $formats_nav .= $spcsep .
9211 '<span class="parents multiple">(merge: ' .
9213 $cgi->a({-href
=> href
(-replay
=>1,
9214 hash
=>$_, hash_base
=>undef)},
9215 esc_html
(substr($_, 0, 7)));
9216 } @
{$co{'parents'}} ) .
9221 my $hash_parent_param = $hash_parent;
9222 if (!defined $hash_parent_param) {
9223 # --cc for multiple parents, --root for parentless
9224 $hash_parent_param =
9225 @
{$co{'parents'}} > 1 ?
'--cc' : $co{'parent'} || '--root';
9231 if ($format eq 'html') {
9232 defined($fd = git_cmd_pipe
"diff-tree", '-r', @diff_opts,
9233 "--no-commit-id", "--patch-with-raw", "--full-index",
9234 $hash_parent_param, $hash, "--")
9235 or die_error
(500, "Open git-diff-tree failed");
9237 while (my $line = to_utf8
(scalar <$fd>)) {
9239 # empty line ends raw part of diff-tree output
9241 push @difftree, scalar parse_difftree_raw_line
($line);
9244 } elsif ($format eq 'plain') {
9245 defined($fd = git_cmd_pipe
"diff-tree", '-r', @diff_opts,
9246 '-p', $hash_parent_param, $hash, "--")
9247 or die_error
(500, "Open git-diff-tree failed");
9248 } elsif ($format eq 'patch') {
9249 # For commit ranges, we limit the output to the number of
9250 # patches specified in the 'patches' feature.
9251 # For single commits, we limit the output to a single patch,
9252 # diverging from the git-format-patch default.
9253 my @commit_spec = ();
9255 if ($patch_max > 0) {
9256 push @commit_spec, "-$patch_max";
9258 push @commit_spec, '-n', "$hash_parent..$hash";
9260 if ($params{-single
}) {
9261 push @commit_spec, '-1';
9263 if ($patch_max > 0) {
9264 push @commit_spec, "-$patch_max";
9266 push @commit_spec, "-n";
9268 push @commit_spec, '--root', $hash;
9270 defined($fd = git_cmd_pipe
"format-patch", @diff_opts,
9271 '--encoding=utf8', '--stdout', @commit_spec)
9272 or die_error
(500, "Open git-format-patch failed");
9274 die_error
(400, "Unknown commitdiff format");
9277 # non-textual hash id's can be cached
9279 if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
9283 # write commit message
9284 if ($format eq 'html') {
9285 my $refs = git_get_references
();
9286 my $ref = format_ref_marker
($refs, $co{'id'});
9288 git_header_html
(undef, $expires);
9289 git_print_page_nav
('commitdiff','', $hash,$co{'tree'},$hash, $formats_nav);
9290 git_print_header_div
('commit', esc_html
($co{'title'}), $hash, undef, $ref);
9291 print "<div class=\"title_text\">\n" .
9292 "<table class=\"object_header\">\n";
9293 git_print_authorship_rows
(\
%co);
9296 print "<div class=\"page_body\">\n";
9297 if (@
{$co{'comment'}} > 1) {
9298 print "<div class=\"log\">\n";
9299 git_print_log
($co{'comment'}, -final_empty_line
=> 1, -remove_title
=> 1);
9300 print "</div>\n"; # class="log"
9303 } elsif ($format eq 'plain') {
9304 my $refs = git_get_references
("tags");
9305 my $tagname = git_get_rev_name_tags
($hash);
9306 my $filename = basename
($project) . "-$hash.patch";
9309 -type
=> 'text/plain',
9310 -charset
=> 'utf-8',
9311 -expires
=> $expires,
9312 -content_disposition
=> 'inline; filename="' . "$filename" . '"');
9313 my %ad = parse_date
($co{'author_epoch'}, $co{'author_tz'});
9314 print "From: " . to_utf8
($co{'author'}) . "\n";
9315 print "Date: $ad{'rfc2822'} ($ad{'tz_local'})\n";
9316 print "Subject: " . to_utf8
($co{'title'}) . "\n";
9318 print "X-Git-Tag: $tagname\n" if $tagname;
9319 print "X-Git-Url: " . $cgi->self_url() . "\n\n";
9321 foreach my $line (@
{$co{'comment'}}) {
9322 print to_utf8
($line) . "\n";
9325 } elsif ($format eq 'patch') {
9326 my $filename = basename
($project) . "-$hash.patch";
9329 -type
=> 'text/plain',
9330 -charset
=> 'utf-8',
9331 -expires
=> $expires,
9332 -content_disposition
=> 'inline; filename="' . "$filename" . '"');
9336 if ($format eq 'html') {
9337 my $use_parents = !defined $hash_parent ||
9338 $hash_parent eq '-c' || $hash_parent eq '--cc';
9339 git_difftree_body
(\
@difftree, $hash,
9340 $use_parents ? @
{$co{'parents'}} : $hash_parent);
9343 git_patchset_body
($fd, $diff_style,
9345 $use_parents ? @
{$co{'parents'}} : $hash_parent);
9347 print "</div>\n"; # class="page_body"
9350 } elsif ($format eq 'plain') {
9355 or print "Reading git-diff-tree failed\n";
9356 } elsif ($format eq 'patch') {
9361 or print "Reading git-format-patch failed\n";
9365 sub git_commitdiff_plain
{
9366 git_commitdiff
(-format
=> 'plain');
9369 # format-patch-style patches
9371 git_commitdiff
(-format
=> 'patch', -single
=> 1);
9375 git_commitdiff
(-format
=> 'patch');
9379 git_log_generic
('history', \
&git_history_body
,
9380 $hash_base, $hash_parent_base,
9385 $searchtype ||= 'commit';
9387 # check if appropriate features are enabled
9388 gitweb_check_feature
('search')
9389 or die_error
(403, "Search is disabled");
9390 if ($searchtype eq 'pickaxe') {
9391 # pickaxe may take all resources of your box and run for several minutes
9392 # with every query - so decide by yourself how public you make this feature
9393 gitweb_check_feature
('pickaxe')
9394 or die_error
(403, "Pickaxe search is disabled");
9396 if ($searchtype eq 'grep') {
9397 # grep search might be potentially CPU-intensive, too
9398 gitweb_check_feature
('grep')
9399 or die_error
(403, "Grep search is disabled");
9402 if (!defined $searchtext) {
9403 die_error
(400, "Text field is empty");
9405 if (!defined $hash) {
9406 $hash = git_get_head_hash
($project);
9408 my %co = parse_commit
($hash);
9410 die_error
(404, "Unknown commit object");
9412 if (!defined $page) {
9416 if ($searchtype eq 'commit' ||
9417 $searchtype eq 'author' ||
9418 $searchtype eq 'committer') {
9419 git_search_message
(%co);
9420 } elsif ($searchtype eq 'pickaxe') {
9421 git_search_changes
(%co);
9422 } elsif ($searchtype eq 'grep') {
9423 git_search_files
(%co);
9425 die_error
(400, "Unknown search type");
9429 sub git_search_help
{
9431 git_print_page_nav
('','', $hash,$hash,$hash);
9433 <div class="search_help">
9434 <p><strong>Pattern</strong> is by default a normal string that is matched precisely (but without
9435 regard to case, except in the case of pickaxe). However, when you check the <em>re</em> checkbox,
9436 the pattern entered is recognized as the POSIX extended
9437 <a href="http://en.wikipedia.org/wiki/Regular_expression">regular expression</a> (also case
9440 <dt><b>commit</b></dt>
9441 <dd>The commit messages and authorship information will be scanned for the given pattern.</dd>
9443 my $have_grep = gitweb_check_feature
('grep');
9446 <dt><b>grep</b></dt>
9447 <dd>All files in the currently selected tree (HEAD unless you are explicitly browsing
9448 a different one) are searched for the given pattern. On large trees, this search can take
9449 a while and put some strain on the server, so please use it with some consideration. Note that
9450 due to git-grep peculiarity, currently if regexp mode is turned off, the matches are
9451 case-sensitive.</dd>
9455 <dt><b>author</b></dt>
9456 <dd>Name and e-mail of the change author and date of birth of the patch will be scanned for the given pattern.</dd>
9457 <dt><b>committer</b></dt>
9458 <dd>Name and e-mail of the committer and date of commit will be scanned for the given pattern.</dd>
9460 my $have_pickaxe = gitweb_check_feature
('pickaxe');
9461 if ($have_pickaxe) {
9463 <dt><b>pickaxe</b></dt>
9464 <dd>All commits that caused the string to appear or disappear from any file (changes that
9465 added, removed or "modified" the string) will be listed. This search can take a while and
9466 takes a lot of strain on the server, so please use it wisely. Note that since you may be
9467 interested even in changes just changing the case as well, this search is case sensitive.</dd>
9470 print "</dl>\n</div>\n";
9475 git_log_generic
('shortlog', \
&git_shortlog_body
,
9476 $hash, $hash_parent);
9479 ## ......................................................................
9480 ## feeds (RSS, Atom; OPML)
9483 my $format = shift || 'atom';
9484 my $have_blame = gitweb_check_feature
('blame');
9486 # Atom: http://www.atomenabled.org/developers/syndication/
9487 # RSS: http://www.notestips.com/80256B3A007F2692/1/NAMO5P9UPQ
9488 if ($format ne 'rss' && $format ne 'atom') {
9489 die_error
(400, "Unknown web feed format");
9492 # log/feed of current (HEAD) branch, log of given branch, history of file/directory
9493 my $head = $hash || 'HEAD';
9494 my @commitlist = parse_commits
($head, 150, 0, $file_name);
9498 my $content_type = "application/$format+xml";
9499 if (defined $cgi->http('HTTP_ACCEPT') &&
9500 $cgi->Accept('text/xml') > $cgi->Accept($content_type)) {
9501 # browser (feed reader) prefers text/xml
9502 $content_type = 'text/xml';
9504 if (defined($commitlist[0])) {
9505 %latest_commit = %{$commitlist[0]};
9506 my $latest_epoch = $latest_commit{'committer_epoch'};
9507 exit_if_unmodified_since
($latest_epoch);
9508 %latest_date = parse_date
($latest_epoch, $latest_commit{'committer_tz'});
9511 -type
=> $content_type,
9512 -charset
=> 'utf-8',
9513 %latest_date ?
(-last_modified
=> $latest_date{'rfc2822'}) : (),
9514 -status
=> '200 OK');
9516 # Optimization: skip generating the body if client asks only
9517 # for Last-Modified date.
9518 return if ($cgi->request_method() eq 'HEAD');
9521 my $title = "$site_name - $project/$action";
9522 my $feed_type = 'log';
9523 if (defined $hash) {
9524 $title .= " - '$hash'";
9525 $feed_type = 'branch log';
9526 if (defined $file_name) {
9527 $title .= " :: $file_name";
9528 $feed_type = 'history';
9530 } elsif (defined $file_name) {
9531 $title .= " - $file_name";
9532 $feed_type = 'history';
9534 $title .= " $feed_type";
9535 $title = esc_html
($title);
9536 my $descr = git_get_project_description
($project);
9537 if (defined $descr) {
9538 $descr = esc_html
($descr);
9540 $descr = "$project " .
9541 ($format eq 'rss' ?
'RSS' : 'Atom') .
9544 my $owner = git_get_project_owner
($project);
9545 $owner = esc_html
($owner);
9549 if (defined $file_name) {
9550 $alt_url = href
(-full
=>1, action
=>"history", hash
=>$hash, file_name
=>$file_name);
9551 } elsif (defined $hash) {
9552 $alt_url = href
(-full
=>1, action
=>"log", hash
=>$hash);
9554 $alt_url = href
(-full
=>1, action
=>"summary");
9556 print qq!<?xml version
="1.0" encoding
="utf-8"?
>\n!;
9557 if ($format eq 'rss') {
9559 <rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
9562 print "<title>$title</title>\n" .
9563 "<link>$alt_url</link>\n" .
9564 "<description>$descr</description>\n" .
9565 "<language>en</language>\n" .
9566 # project owner is responsible for 'editorial' content
9567 "<managingEditor>$owner</managingEditor>\n";
9568 if (defined $logo || defined $favicon) {
9569 # prefer the logo to the favicon, since RSS
9570 # doesn't allow both
9571 my $img = esc_url
($logo || $favicon);
9573 "<url>$img</url>\n" .
9574 "<title>$title</title>\n" .
9575 "<link>$alt_url</link>\n" .
9579 print "<pubDate>$latest_date{'rfc2822'}</pubDate>\n";
9580 print "<lastBuildDate>$latest_date{'rfc2822'}</lastBuildDate>\n";
9582 print "<generator>gitweb v.$version/$git_version</generator>\n";
9583 } elsif ($format eq 'atom') {
9585 <feed xmlns="http://www.w3.org/2005/Atom">
9587 print "<title>$title</title>\n" .
9588 "<subtitle>$descr</subtitle>\n" .
9589 '<link rel="alternate" type="text/html" href="' .
9590 $alt_url . '" />' . "\n" .
9591 '<link rel="self" type="' . $content_type . '" href="' .
9592 $cgi->self_url() . '" />' . "\n" .
9593 "<id>" . href
(-full
=>1) . "</id>\n" .
9594 # use project owner for feed author
9595 '<author><name>'. email_obfuscate
($owner) . '</name></author>\n';
9596 if (defined $favicon) {
9597 print "<icon>" . esc_url
($favicon) . "</icon>\n";
9599 if (defined $logo) {
9600 # not twice as wide as tall: 72 x 27 pixels
9601 print "<logo>" . esc_url
($logo) . "</logo>\n";
9603 if (! %latest_date) {
9604 # dummy date to keep the feed valid until commits trickle in:
9605 print "<updated>1970-01-01T00:00:00Z</updated>\n";
9607 print "<updated>$latest_date{'iso-8601'}</updated>\n";
9609 print "<generator version='$version/$git_version'>gitweb</generator>\n";
9613 for (my $i = 0; $i <= $#commitlist; $i++) {
9614 my %co = %{$commitlist[$i]};
9615 my $commit = $co{'id'};
9616 # we read 150, we always show 30 and the ones more recent than 48 hours
9617 if (($i >= 20) && ((time - $co{'author_epoch'}) > 48*60*60)) {
9620 my %cd = parse_date
($co{'author_epoch'}, $co{'author_tz'});
9622 # get list of changed files
9623 defined(my $fd = git_cmd_pipe
"diff-tree", '-r', @diff_opts,
9624 $co{'parent'} || "--root",
9625 $co{'id'}, "--", (defined $file_name ?
$file_name : ()))
9627 my @difftree = map { chomp; to_utf8
($_) } <$fd>;
9631 # print element (entry, item)
9632 my $co_url = href
(-full
=>1, action
=>"commitdiff", hash
=>$commit);
9633 if ($format eq 'rss') {
9635 "<title>" . esc_html
($co{'title'}) . "</title>\n" .
9636 "<author>" . esc_html
($co{'author'}) . "</author>\n" .
9637 "<pubDate>$cd{'rfc2822'}</pubDate>\n" .
9638 "<guid isPermaLink=\"true\">$co_url</guid>\n" .
9639 "<link>$co_url</link>\n" .
9640 "<description>" . esc_html
($co{'title'}) . "</description>\n" .
9641 "<content:encoded>" .
9643 } elsif ($format eq 'atom') {
9645 "<title type=\"html\">" . esc_html
($co{'title'}) . "</title>\n" .
9646 "<updated>$cd{'iso-8601'}</updated>\n" .
9648 " <name>" . esc_html
($co{'author_name'}) . "</name>\n";
9649 if ($co{'author_email'}) {
9650 print " <email>" . esc_html
($co{'author_email'}) . "</email>\n";
9652 print "</author>\n" .
9653 # use committer for contributor
9655 " <name>" . esc_html
($co{'committer_name'}) . "</name>\n";
9656 if ($co{'committer_email'}) {
9657 print " <email>" . esc_html
($co{'committer_email'}) . "</email>\n";
9659 print "</contributor>\n" .
9660 "<published>$cd{'iso-8601'}</published>\n" .
9661 "<link rel=\"alternate\" type=\"text/html\" href=\"$co_url\" />\n" .
9662 "<id>$co_url</id>\n" .
9663 "<content type=\"xhtml\" xml:base=\"" . esc_url
($my_url) . "\">\n" .
9664 "<div xmlns=\"http://www.w3.org/1999/xhtml\">\n";
9666 my $comment = $co{'comment'};
9668 foreach my $line (@
$comment) {
9669 $line = esc_html
($line);
9672 print "</pre><ul>\n";
9673 foreach my $difftree_line (@difftree) {
9674 my %difftree = parse_difftree_raw_line
($difftree_line);
9675 next if !$difftree{'from_id'};
9677 my $file = $difftree{'file'} || $difftree{'to_file'};
9681 $cgi->a({-href
=> href
(-full
=>1, action
=>"blobdiff",
9682 hash
=>$difftree{'to_id'}, hash_parent
=>$difftree{'from_id'},
9683 hash_base
=>$co{'id'}, hash_parent_base
=>$co{'parent'},
9684 file_name
=>$file, file_parent
=>$difftree{'from_file'}),
9685 -title
=> "diff"}, 'D');
9687 print $cgi->a({-href
=> href
(-full
=>1, action
=>"blame",
9688 file_name
=>$file, hash_base
=>$commit),
9689 -class => "blamelink",
9690 -title
=> "blame"}, 'B');
9692 # if this is not a feed of a file history
9693 if (!defined $file_name || $file_name ne $file) {
9694 print $cgi->a({-href
=> href
(-full
=>1, action
=>"history",
9695 file_name
=>$file, hash
=>$commit),
9696 -title
=> "history"}, 'H');
9698 $file = esc_path
($file);
9702 if ($format eq 'rss') {
9703 print "</ul>]]>\n" .
9704 "</content:encoded>\n" .
9706 } elsif ($format eq 'atom') {
9707 print "</ul>\n</div>\n" .
9714 if ($format eq 'rss') {
9715 print "</channel>\n</rss>\n";
9716 } elsif ($format eq 'atom') {
9730 my @list = git_get_projects_list
($project_filter, $strict_export);
9732 die_error
(404, "No projects found");
9736 -type
=> 'text/xml',
9737 -charset
=> 'utf-8',
9738 -content_disposition
=> 'inline; filename="opml.xml"');
9740 my $title = esc_html
($site_name);
9741 my $filter = " within subdirectory ";
9742 if (defined $project_filter) {
9743 $filter .= esc_html
($project_filter);
9748 <?xml version="1.0" encoding="utf-8"?>
9749 <opml version="1.0">
9751 <title>$title OPML Export$filter</title>
9754 <outline text="git RSS feeds">
9757 foreach my $pr (@list) {
9759 my $head = git_get_head_hash
($proj{'path'});
9760 if (!defined $head) {
9763 $git_dir = "$projectroot/$proj{'path'}";
9764 my %co = parse_commit
($head);
9769 my $path = esc_html
(chop_str
($proj{'path'}, 25, 5));
9770 my $rss = href
('project' => $proj{'path'}, 'action' => 'rss', -full
=> 1);
9771 my $html = href
('project' => $proj{'path'}, 'action' => 'summary', -full
=> 1);
9772 print "<outline type=\"rss\" text=\"$path\" title=\"$path\" xmlUrl=\"$rss\" htmlUrl=\"$html\"/>\n";