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);
22 use constant GITWEB_CACHE_FORMAT
=> "Gitweb Cache Format 3";
23 binmode STDOUT
, ':utf8';
25 if (!defined($CGI::VERSION
) || $CGI::VERSION
< 4.08) {
26 eval 'sub CGI::multi_param { CGI::param(@_) }'
29 our $t0 = [ gettimeofday
() ];
30 our $number_of_git_cmds = 0;
33 CGI
->compile() if $ENV{'MOD_PERL'};
36 our $version = "++GIT_VERSION++";
38 our ($my_url, $my_uri, $base_url, $path_info, $home_link);
42 our $my_url = $cgi->url();
43 our $my_uri = $cgi->url(-absolute
=> 1);
45 # Base URL for relative URLs in gitweb ($logo, $favicon, ...),
46 # needed and used only for URLs with nonempty PATH_INFO
47 our $base_url = $my_url;
49 # When the script is used as DirectoryIndex, the URL does not contain the name
50 # of the script file itself, and $cgi->url() fails to strip PATH_INFO, so we
51 # have to do it ourselves. We make $path_info global because it's also used
54 # Another issue with the script being the DirectoryIndex is that the resulting
55 # $my_url data is not the full script URL: this is good, because we want
56 # generated links to keep implying the script name if it wasn't explicitly
57 # indicated in the URL we're handling, but it means that $my_url cannot be used
59 # Therefore, if we needed to strip PATH_INFO, then we know that we have
60 # to build the base URL ourselves:
61 our $path_info = decode_utf8
($ENV{"PATH_INFO"});
63 # $path_info has already been URL-decoded by the web server, but
64 # $my_url and $my_uri have not. URL-decode them so we can properly
66 $my_url = unescape
($my_url);
67 $my_uri = unescape
($my_uri);
68 if ($my_url =~ s
,\Q
$path_info\E
$,, &&
69 $my_uri =~ s
,\Q
$path_info\E
$,, &&
70 defined $ENV{'SCRIPT_NAME'}) {
71 $base_url = $cgi->url(-base
=> 1) . $ENV{'SCRIPT_NAME'};
75 # target of the home link on top of all pages
76 our $home_link = $my_uri || "/";
79 # core git executable to use
80 # this can just be "git" if your webserver has a sensible PATH
81 our $GIT = "++GIT_BINDIR++/git";
83 # absolute fs-path which will be prepended to the project path
84 #our $projectroot = "/pub/scm";
85 our $projectroot = "++GITWEB_PROJECTROOT++";
87 # fs traversing limit for getting project list
88 # the number is relative to the projectroot
89 our $project_maxdepth = "++GITWEB_PROJECT_MAXDEPTH++";
91 # string of the home link on top of all pages
92 our $home_link_str = "++GITWEB_HOME_LINK_STR++";
94 # extra breadcrumbs preceding the home link
95 our @extra_breadcrumbs = ();
97 # name of your site or organization to appear in page titles
98 # replace this with something more descriptive for clearer bookmarks
99 our $site_name = "++GITWEB_SITENAME++"
100 || ($ENV{'SERVER_NAME'} || "Untitled") . " Git";
102 # html snippet to include in the <head> section of each page
103 our $site_html_head_string = "++GITWEB_SITE_HTML_HEAD_STRING++";
104 # filename of html text to include at top of each page
105 our $site_header = "++GITWEB_SITE_HEADER++";
106 # html text to include at home page
107 our $home_text = "++GITWEB_HOMETEXT++";
108 # filename of html text to include at bottom of each page
109 our $site_footer = "++GITWEB_SITE_FOOTER++";
112 our @stylesheets = ("++GITWEB_CSS++");
113 # URI of a single stylesheet, which can be overridden in GITWEB_CONFIG.
114 our $stylesheet = undef;
115 # URI of GIT logo (72x27 size)
116 our $logo = "++GITWEB_LOGO++";
117 # URI of GIT favicon, assumed to be image/png type
118 our $favicon = "++GITWEB_FAVICON++";
119 # URI of gitweb.js (JavaScript code for gitweb)
120 our $javascript = "++GITWEB_JS++";
122 # URI and label (title) of GIT logo link
123 #our $logo_url = "http://www.kernel.org/pub/software/scm/git/docs/";
124 #our $logo_label = "git documentation";
125 our $logo_url = "http://git-scm.com/";
126 our $logo_label = "git homepage";
128 # source of projects list
129 our $projects_list = "++GITWEB_LIST++";
131 # the width (in characters) of the projects list "Description" column
132 our $projects_list_description_width = 25;
134 # group projects by category on the projects list
135 # (enabled if this variable evaluates to true)
136 our $projects_list_group_categories = 0;
138 # default category if none specified
139 # (leave the empty string for no category)
140 our $project_list_default_category = "";
142 # default order of projects list
143 # valid values are none, project, descr, owner, and age
144 our $default_projects_order = "project";
146 # default order of refs list
147 # valid values are age and name
148 our $default_refs_order = "age";
150 # show repository only if this file exists
151 # (only effective if this variable evaluates to true)
152 our $export_ok = "++GITWEB_EXPORT_OK++";
154 # don't generate age column on the projects list page
155 our $omit_age_column = 0;
157 # use contents of this file (in iso, iso-strict or raw format) as
158 # the last activity data if it exists and is a valid date
159 our $lastactivity_file = undef;
161 # don't generate information about owners of repositories
164 # owner link hook given owner name (full and NOT obfuscated)
165 # should return full URL-escaped link to attach to owner, for example:
166 # sub { return "/showowner.cgi?owner=".CGI::Util::escape($_[0]); }
167 our $owner_link_hook = undef;
169 # show repository only if this subroutine returns true
170 # when given the path to the project, for example:
171 # sub { return -e "$_[0]/git-daemon-export-ok"; }
172 our $export_auth_hook = undef;
174 # only allow viewing of repositories also shown on the overview page
175 our $strict_export = "++GITWEB_STRICT_EXPORT++";
177 # list of git base URLs used for URL to where fetch project from,
178 # i.e. full URL is "$git_base_url/$project"
179 our @git_base_url_list = grep { $_ ne '' } ("++GITWEB_BASE_URL++");
181 # URLs designated for pushing new changes, extended by the
182 # project name (i.e. "$git_base_push_url[0]/$project")
183 our @git_base_push_urls = ();
185 # https hint html inserted right after any https push URL (undef for none)
186 our $https_hint_html = undef;
188 # default blob_plain mimetype and default charset for text/plain blob
189 our $default_blob_plain_mimetype = 'application/octet-stream';
190 our $default_text_plain_charset = undef;
192 # file to use for guessing MIME types before trying /etc/mime.types
193 # (relative to the current git repository)
194 our $mimetypes_file = undef;
196 # assume this charset if line contains non-UTF-8 characters;
197 # it should be valid encoding (see Encoding::Supported(3pm) for list),
198 # for which encoding all byte sequences are valid, for example
199 # 'iso-8859-1' aka 'latin1' (it is decoded without checking, so it
200 # could be even 'utf-8' for the old behavior)
201 our $fallback_encoding = 'latin1';
203 # rename detection options for git-diff and git-diff-tree
204 # - default is '-M', with the cost proportional to
205 # (number of removed files) * (number of new files).
206 # - more costly is '-C' (which implies '-M'), with the cost proportional to
207 # (number of changed files + number of removed files) * (number of new files)
208 # - even more costly is '-C', '--find-copies-harder' with cost
209 # (number of files in the original tree) * (number of new files)
210 # - one might want to include '-B' option, e.g. '-B', '-M'
211 our @diff_opts = ('-M'); # taken from git_commit
213 # cache directory (relative to $GIT_DIR) for project-specific html page caches.
214 # the directory must exist and be writable by the process running gitweb.
215 # additionally some actions must be selected for caching in %html_cache_actions
216 # - default is 'htmlcache'
217 our $html_cache_dir = 'htmlcache';
219 # which actions to cache in $html_cache_dir
220 # if $html_cache_dir exists (relative to $GIT_DIR) and is writable by the
221 # process running gitweb, then any actions selected here will have their output
222 # cached and the cache file will be returned instead of regenerating the page
223 # if it exists. For this to be useful, an external process must remove the
224 # cached file or create a $action.changed file whenever the information it
225 # contains becomes out of date so that it will be regenerated the next time
226 # it's requested. Creating the $action.changed file is preferred as it avoids
227 # race conditions where the data changes while the cache is being regenerated.
229 # currently only caching of the summary page is supported
230 # - to enable caching of the summary page use:
231 # $html_cache_actions{'summary'} = 1;
232 our %html_cache_actions = ();
234 # Disables features that would allow repository owners to inject script into
236 our $prevent_xss = 0;
238 # Path to a POSIX shell. Needed to run $highlight_bin and a snapshot compressor.
239 # Only used when highlight is enabled or snapshots with compressors are enabled.
240 our $posix_shell_bin = "++POSIX_SHELL_BIN++";
242 # Path to the highlight executable to use (must be the one from
243 # http://www.andre-simon.de due to assumptions about parameters and output).
244 # Useful if highlight is not installed on your webserver's PATH.
245 # [Default: highlight]
246 our $highlight_bin = "++HIGHLIGHT_BIN++";
248 # Whether to include project list on the gitweb front page; 0 means yes,
249 # 1 means no list but show tag cloud if enabled (all projects still need
250 # to be scanned, unless the info is cached), 2 means no list and no tag cloud
252 our $frontpage_no_project_list = 0;
254 # projects list cache for busy sites with many projects;
255 # if you set this to non-zero, it will be used as the cached
256 # index lifetime in minutes
258 # the cached list version is stored in $cache_dir/$cache_name and can
259 # be tweaked by other scripts running with the same uid as gitweb -
260 # use this ONLY at secure installations; only single gitweb project
261 # root per system is supported, unless you tweak configuration!
262 our $projlist_cache_lifetime = 0; # in minutes
263 # FHS compliant $cache_dir would be "/var/cache/gitweb"
265 (defined $ENV{'TMPDIR'} ?
$ENV{'TMPDIR'} : '/tmp').'/gitweb';
266 our $projlist_cache_name = 'gitweb.index.cache';
267 our $cache_grpshared = 0;
269 # information about snapshot formats that gitweb is capable of serving
270 our %known_snapshot_formats = (
272 # 'display' => display name,
273 # 'type' => mime type,
274 # 'suffix' => filename suffix,
275 # 'format' => --format for git-archive,
276 # 'compressor' => [compressor command and arguments]
277 # (array reference, optional)
278 # 'disabled' => boolean (optional)}
281 'display' => 'tar.gz',
282 'type' => 'application/x-gzip',
283 'suffix' => '.tar.gz',
285 'compressor' => ['gzip', '-n']},
288 'display' => 'tar.bz2',
289 'type' => 'application/x-bzip2',
290 'suffix' => '.tar.bz2',
292 'compressor' => ['bzip2']},
295 'display' => 'tar.xz',
296 'type' => 'application/x-xz',
297 'suffix' => '.tar.xz',
299 'compressor' => ['xz'],
304 'type' => 'application/x-zip',
309 # Aliases so we understand old gitweb.snapshot values in repository
311 our %known_snapshot_format_aliases = (
316 # backward compatibility: legacy gitweb config support
317 'x-gzip' => undef, 'gz' => undef,
318 'x-bzip2' => undef, 'bz2' => undef,
319 'x-zip' => undef, '' => undef,
322 # Pixel sizes for icons and avatars. If the default font sizes or lineheights
323 # are changed, it may be appropriate to change these values too via
330 # Used to set the maximum load that we will still respond to gitweb queries.
331 # If server load exceed this value then return "503 server busy" error.
332 # If gitweb cannot determined server load, it is taken to be 0.
333 # Leave it undefined (or set to 'undef') to turn off load checking.
336 # configuration for 'highlight' (http://www.andre-simon.de/)
338 our %highlight_basename = (
341 'SConstruct' => 'py', # SCons equivalent of Makefile
342 'Makefile' => 'make',
343 'makefile' => 'make',
344 'GNUmakefile' => 'make',
345 'BSDmakefile' => 'make',
347 # match by shebang regex
348 our %highlight_shebang = (
349 # Each entry has a key which is the syntax to use and
350 # a value which is either a qr regex or an array of qr regexs to match
351 # against the first 128 (less if the blob is shorter) BYTES of the blob.
352 # We match /usr/bin/env items separately to require "/usr/bin/env" and
353 # allow a limited subset of NAME=value items to appear.
354 'awk' => [ qr
,^#!\s*/(?:\w+/)*(?:[gnm]?awk)(?:\s|$),mo,
355 qr
,^#!\s*/usr/bin/env\s+(?:\w+=\w*\s+)*(?:[gnm]?awk)(?:\s|$),mo ],
356 'make' => [ qr
,^#!\s*/(?:\w+/)*(?:g?make)(?:\s|$),mo,
357 qr
,^#!\s*/usr/bin/env\s+(?:\w+=\w*\s+)*(?:g?make)(?:\s|$),mo ],
358 'php' => [ qr
,^#!\s*/(?:\w+/)*(?:php)(?:\s|$),mo,
359 qr
,^#!\s*/usr/bin/env\s+(?:\w+=\w*\s+)*(?:php)(?:\s|$),mo ],
360 'pl' => [ qr
,^#!\s*/(?:\w+/)*(?:perl)(?:\s|$),mo,
361 qr
,^#!\s*/usr/bin/env\s+(?:\w+=\w*\s+)*(?:perl)(?:\s|$),mo ],
362 'py' => [ qr
,^#!\s*/(?:\w+/)*(?:python)(?:\s|$),mo,
363 qr
,^#!\s*/usr/bin/env\s+(?:\w+=\w*\s+)*(?:python)(?:\s|$),mo ],
364 'sh' => [ qr
,^#!\s*/(?:\w+/)*(?:[bd]ash|t?csh|[akz]?sh)(?:\s|$),mo,
365 qr
,^#!\s*/usr/bin/env\s+(?:\w+=\w*\s+)*(?:[bd]ash|t?csh|[akz]?sh)(?:\s|$),mo ],
366 'rb' => [ qr
,^#!\s*/(?:\w+/)*(?:perl)(?:\s|$),mo,
367 qr
,^#!\s*/usr/bin/env\s+(?:\w+=\w*\s+)*(?:perl)(?:\s|$),mo ],
370 our %highlight_ext = (
371 # main extensions, defining name of syntax;
372 # see files in /usr/share/highlight/langDefs/ directory
373 (map { $_ => $_ } qw(
374 4gl a4c abnf abp ada agda ahk ampl amtrix applescript arc
375 arm as asm asp aspect ats au3 avenue awk bat bb bbcode bib
376 bms bnf boo c cb cfc chl clipper clojure clp cob cs css d
377 diff dot dylan e ebnf erl euphoria exp f90 flx for frink fs
378 go haskell hcl html httpd hx icl icn idl idlang ili
379 inc_luatex ini inp io iss j java js jsp lbn ldif lgt lhs
380 lisp lotos ls lsl lua ly make mel mercury mib miranda ml mo
381 mod2 mod3 mpl ms mssql n nas nbc nice nrx nsi nut nxc oberon
382 objc octave oorexx os oz pas php pike pl pl1 pov pro
383 progress ps ps1 psl pure py pyx q qmake qu r rb rebol rexx
384 rnc s sas sc scala scilab sh sma smalltalk sml sno spec spn
385 sql sybase tcl tcsh tex ttcn3 vala vb verilog vhd xml xpp y
387 # alternate extensions, see /etc/highlight/filetypes.conf
388 (map { $_ => '4gl' } qw(informix)),
389 (map { $_ => 'a4c' } qw(ascend)),
390 (map { $_ => 'abp' } qw(abp4)),
391 (map { $_ => 'ada' } qw(a adb ads gnad)),
392 (map { $_ => 'ahk' } qw(autohotkey)),
393 (map { $_ => 'ampl' } qw(dat run)),
394 (map { $_ => 'amtrix' } qw(hnd s4 s4h s4t t4)),
395 (map { $_ => 'as' } qw(actionscript)),
396 (map { $_ => 'asm' } qw(29k 68s 68x a51 assembler x68 x86)),
397 (map { $_ => 'asp' } qw(asa)),
398 (map { $_ => 'aspect' } qw(was wud)),
399 (map { $_ => 'ats' } qw(dats)),
400 (map { $_ => 'au3' } qw(autoit)),
401 (map { $_ => 'bat' } qw(cmd)),
402 (map { $_ => 'bb' } qw(blitzbasic)),
403 (map { $_ => 'bib' } qw(bibtex)),
404 (map { $_ => 'c' } qw(c++ cc cpp cu cxx h hh hpp hxx)),
405 (map { $_ => 'cb' } qw(clearbasic)),
406 (map { $_ => 'cfc' } qw(cfm coldfusion)),
407 (map { $_ => 'chl' } qw(chill)),
408 (map { $_ => 'cob' } qw(cbl cobol)),
409 (map { $_ => 'cs' } qw(csharp)),
410 (map { $_ => 'diff' } qw(patch)),
411 (map { $_ => 'dot' } qw(graphviz)),
412 (map { $_ => 'e' } qw(eiffel se)),
413 (map { $_ => 'erl' } qw(erlang hrl)),
414 (map { $_ => 'euphoria' } qw(eu ew ex exu exw wxu)),
415 (map { $_ => 'exp' } qw(express)),
416 (map { $_ => 'f90' } qw(f95)),
417 (map { $_ => 'flx' } qw(felix)),
418 (map { $_ => 'for' } qw(f f77 ftn)),
419 (map { $_ => 'fs' } qw(fsharp fsx)),
420 (map { $_ => 'haskell' } qw(hs)),
421 (map { $_ => 'html' } qw(htm xhtml)),
422 (map { $_ => 'hx' } qw(haxe)),
423 (map { $_ => 'icl' } qw(clean)),
424 (map { $_ => 'icn' } qw(icon)),
425 (map { $_ => 'ili' } qw(interlis)),
426 (map { $_ => 'inp' } qw(fame)),
427 (map { $_ => 'iss' } qw(innosetup)),
428 (map { $_ => 'j' } qw(jasmin)),
429 (map { $_ => 'java' } qw(groovy grv)),
430 (map { $_ => 'lbn' } qw(luban)),
431 (map { $_ => 'lgt' } qw(logtalk)),
432 (map { $_ => 'lisp' } qw(cl clisp el lsp sbcl scom)),
433 (map { $_ => 'ls' } qw(lotus)),
434 (map { $_ => 'lsl' } qw(lindenscript)),
435 (map { $_ => 'ly' } qw(lilypond)),
436 (map { $_ => 'make' } qw(mak mk kmk)),
437 (map { $_ => 'mel' } qw(maya)),
438 (map { $_ => 'mib' } qw(smi snmp)),
439 (map { $_ => 'ml' } qw(mli ocaml)),
440 (map { $_ => 'mo' } qw(modelica)),
441 (map { $_ => 'mod2' } qw(def mod)),
442 (map { $_ => 'mod3' } qw(i3 m3)),
443 (map { $_ => 'mpl' } qw(maple)),
444 (map { $_ => 'n' } qw(nemerle)),
445 (map { $_ => 'nas' } qw(nasal)),
446 (map { $_ => 'nrx' } qw(netrexx)),
447 (map { $_ => 'nsi' } qw(nsis)),
448 (map { $_ => 'nut' } qw(squirrel)),
449 (map { $_ => 'oberon' } qw(ooc)),
450 (map { $_ => 'objc' } qw(M m mm)),
451 (map { $_ => 'php' } qw(php3 php4 php5 php6)),
452 (map { $_ => 'pike' } qw(pmod)),
453 (map { $_ => 'pl' } qw(perl plex plx pm)),
454 (map { $_ => 'pl1' } qw(bdy ff fp fpp rpp sf sp spb spe spp sps wf wp wpb wpp wps)),
455 (map { $_ => 'progress' } qw(i p w)),
456 (map { $_ => 'py' } qw(python)),
457 (map { $_ => 'pyx' } qw(pyrex)),
458 (map { $_ => 'rb' } qw(pp rjs ruby)),
459 (map { $_ => 'rexx' } qw(rex rx the)),
460 (map { $_ => 'sc' } qw(paradox)),
461 (map { $_ => 'scilab' } qw(sce sci)),
462 (map { $_ => 'sh' } qw(bash ebuild eclass ksh zsh)),
463 (map { $_ => 'sma' } qw(small)),
464 (map { $_ => 'smalltalk' } qw(gst sq st)),
465 (map { $_ => 'sno' } qw(snobal)),
466 (map { $_ => 'sybase' } qw(sp)),
467 (map { $_ => 'tcl' } qw(itcl wish)),
468 (map { $_ => 'tex' } qw(cls sty)),
469 (map { $_ => 'vb' } qw(bas basic bi vbs)),
470 (map { $_ => 'verilog' } qw(v)),
471 (map { $_ => 'xml' } qw(dtd ecf ent hdr hub jnlp nrm plist resx sgm sgml svg tld vxml wml xsd xsl)),
472 (map { $_ => 'y' } qw(bison)),
475 # You define site-wide feature defaults here; override them with
476 # $GITWEB_CONFIG as necessary.
479 # 'sub' => feature-sub (subroutine),
480 # 'override' => allow-override (boolean),
481 # 'default' => [ default options...] (array reference)}
483 # if feature is overridable (it means that allow-override has true value),
484 # then feature-sub will be called with default options as parameters;
485 # return value of feature-sub indicates if to enable specified feature
487 # if there is no 'sub' key (no feature-sub), then feature cannot be
490 # use gitweb_get_feature(<feature>) to retrieve the <feature> value
491 # (an array) or gitweb_check_feature(<feature>) to check if <feature>
494 # Enable the 'blame' blob view, showing the last commit that modified
495 # each line in the file. This can be very CPU-intensive.
497 # To enable system wide have in $GITWEB_CONFIG
498 # $feature{'blame'}{'default'} = [1];
499 # To have project specific config enable override in $GITWEB_CONFIG
500 # $feature{'blame'}{'override'} = 1;
501 # and in project config gitweb.blame = 0|1;
503 'sub' => sub { feature_bool
('blame', @_) },
507 # Enable the 'incremental blame' blob view, which uses javascript to
508 # incrementally show the revisions of lines as they are discovered
509 # in the history. It is better for large histories, files and slow
510 # servers, but requires javascript in the client and can slow down the
511 # browser on large files.
513 # To enable system wide have in $GITWEB_CONFIG
514 # $feature{'blame_incremental'}{'default'} = [1];
515 # To have project specific config enable override in $GITWEB_CONFIG
516 # $feature{'blame_incremental'}{'override'} = 1;
517 # and in project config gitweb.blame_incremental = 0|1;
518 'blame_incremental' => {
519 'sub' => sub { feature_bool
('blame_incremental', @_) },
523 # Enable the 'snapshot' link, providing a compressed archive of any
524 # tree. This can potentially generate high traffic if you have large
527 # Value is a list of formats defined in %known_snapshot_formats that
529 # To disable system wide have in $GITWEB_CONFIG
530 # $feature{'snapshot'}{'default'} = [];
531 # To have project specific config enable override in $GITWEB_CONFIG
532 # $feature{'snapshot'}{'override'} = 1;
533 # and in project config, a comma-separated list of formats or "none"
534 # to disable. Example: gitweb.snapshot = tbz2,zip;
536 'sub' => \
&feature_snapshot
,
538 'default' => ['tgz']},
540 # Enable text search, which will list the commits which match author,
541 # committer or commit text to a given string. Enabled by default.
542 # Project specific override is not supported.
544 # Note that this controls all search features, which means that if
545 # it is disabled, then 'grep' and 'pickaxe' search would also be
551 # Enable grep search, which will list the files in currently selected
552 # tree containing the given string. Enabled by default. This can be
553 # potentially CPU-intensive, of course.
554 # Note that you need to have 'search' feature enabled too.
556 # To enable system wide have in $GITWEB_CONFIG
557 # $feature{'grep'}{'default'} = [1];
558 # To have project specific config enable override in $GITWEB_CONFIG
559 # $feature{'grep'}{'override'} = 1;
560 # and in project config gitweb.grep = 0|1;
562 'sub' => sub { feature_bool
('grep', @_) },
566 # Enable the pickaxe search, which will list the commits that modified
567 # a given string in a file. This can be practical and quite faster
568 # alternative to 'blame', but still potentially CPU-intensive.
569 # Note that you need to have 'search' feature enabled too.
571 # To enable system wide have in $GITWEB_CONFIG
572 # $feature{'pickaxe'}{'default'} = [1];
573 # To have project specific config enable override in $GITWEB_CONFIG
574 # $feature{'pickaxe'}{'override'} = 1;
575 # and in project config gitweb.pickaxe = 0|1;
577 'sub' => sub { feature_bool
('pickaxe', @_) },
581 # Enable showing size of blobs in a 'tree' view, in a separate
582 # column, similar to what 'ls -l' does. This cost a bit of IO.
584 # To disable system wide have in $GITWEB_CONFIG
585 # $feature{'show-sizes'}{'default'} = [0];
586 # To have project specific config enable override in $GITWEB_CONFIG
587 # $feature{'show-sizes'}{'override'} = 1;
588 # and in project config gitweb.showsizes = 0|1;
590 'sub' => sub { feature_bool
('showsizes', @_) },
594 # Make gitweb use an alternative format of the URLs which can be
595 # more readable and natural-looking: project name is embedded
596 # directly in the path and the query string contains other
597 # auxiliary information. All gitweb installations recognize
598 # URL in either format; this configures in which formats gitweb
601 # To enable system wide have in $GITWEB_CONFIG
602 # $feature{'pathinfo'}{'default'} = [1];
603 # Project specific override is not supported.
605 # Note that you will need to change the default location of CSS,
606 # favicon, logo and possibly other files to an absolute URL. Also,
607 # if gitweb.cgi serves as your indexfile, you will need to force
608 # $my_uri to contain the script name in your $GITWEB_CONFIG (and you
609 # will also likely want to set $home_link if you're setting $my_uri).
614 # Make gitweb consider projects in project root subdirectories
615 # to be forks of existing projects. Given project $projname.git,
616 # projects matching $projname/*.git will not be shown in the main
617 # projects list, instead a '+' mark will be added to $projname
618 # there and a 'forks' view will be enabled for the project, listing
619 # all the forks. If project list is taken from a file, forks have
620 # to be listed after the main project.
622 # To enable system wide have in $GITWEB_CONFIG
623 # $feature{'forks'}{'default'} = [1];
624 # Project specific override is not supported.
629 # Insert custom links to the action bar of all project pages.
630 # This enables you mainly to link to third-party scripts integrating
631 # into gitweb; e.g. git-browser for graphical history representation
632 # or custom web-based repository administration interface.
634 # The 'default' value consists of a list of triplets in the form
635 # (label, link, position) where position is the label after which
636 # to insert the link and link is a format string where %n expands
637 # to the project name, %f to the project path within the filesystem,
638 # %h to the current hash (h gitweb parameter) and %b to the current
639 # hash base (hb gitweb parameter); %% expands to %. %e expands to the
640 # project name where all '+' characters have been replaced with '%2B'.
642 # To enable system wide have in $GITWEB_CONFIG e.g.
643 # $feature{'actions'}{'default'} = [('graphiclog',
644 # '/git-browser/by-commit.html?r=%n', 'summary')];
645 # Project specific override is not supported.
650 # Allow gitweb scan project content tags of project repository,
651 # and display the popular Web 2.0-ish "tag cloud" near the projects
652 # list. Note that this is something COMPLETELY different from the
655 # gitweb by itself can show existing tags, but it does not handle
656 # tagging itself; you need to do it externally, outside gitweb.
657 # The format is described in git_get_project_ctags() subroutine.
658 # You may want to install the HTML::TagCloud Perl module to get
659 # a pretty tag cloud instead of just a list of tags.
661 # To enable system wide have in $GITWEB_CONFIG
662 # $feature{'ctags'}{'default'} = [1];
663 # Project specific override is not supported.
665 # A value of 0 means no ctags display or editing. A value of
666 # 1 enables ctags display but never editing. A non-empty value
667 # that is not a string of digits enables ctags display AND the
668 # ability to add tags using a form that uses method POST and
669 # an action value set to the configured 'ctags' value.
674 # The maximum number of patches in a patchset generated in patch
675 # view. Set this to 0 or undef to disable patch view, or to a
676 # negative number to remove any limit.
678 # To disable system wide have in $GITWEB_CONFIG
679 # $feature{'patches'}{'default'} = [0];
680 # To have project specific config enable override in $GITWEB_CONFIG
681 # $feature{'patches'}{'override'} = 1;
682 # and in project config gitweb.patches = 0|n;
683 # where n is the maximum number of patches allowed in a patchset.
685 'sub' => \
&feature_patches
,
689 # Avatar support. When this feature is enabled, views such as
690 # shortlog or commit will display an avatar associated with
691 # the email of the committer(s) and/or author(s).
693 # Currently available providers are gravatar and picon.
694 # If an unknown provider is specified, the feature is disabled.
696 # Gravatar depends on Digest::MD5.
697 # Picon currently relies on the indiana.edu database.
699 # To enable system wide have in $GITWEB_CONFIG
700 # $feature{'avatar'}{'default'} = ['<provider>'];
701 # where <provider> is either gravatar or picon.
702 # To have project specific config enable override in $GITWEB_CONFIG
703 # $feature{'avatar'}{'override'} = 1;
704 # and in project config gitweb.avatar = <provider>;
706 'sub' => \
&feature_avatar
,
710 # Enable displaying how much time and how many git commands
711 # it took to generate and display page. Disabled by default.
712 # Project specific override is not supported.
717 # Enable turning some links into links to actions which require
718 # JavaScript to run (like 'blame_incremental'). Not enabled by
719 # default. Project specific override is currently not supported.
720 'javascript-actions' => {
724 # Enable and configure ability to change common timezone for dates
725 # in gitweb output via JavaScript. Enabled by default.
726 # Project specific override is not supported.
727 'javascript-timezone' => {
730 'local', # default timezone: 'utc', 'local', or '(-|+)HHMM' format,
731 # or undef to turn off this feature
732 'gitweb_tz', # name of cookie where to store selected timezone
733 'datetime', # CSS class used to mark up dates for manipulation
736 # Syntax highlighting support. This is based on Daniel Svensson's
737 # and Sham Chukoury's work in gitweb-xmms2.git.
738 # It requires the 'highlight' program present in $PATH,
739 # and therefore is disabled by default.
741 # To enable system wide have in $GITWEB_CONFIG
742 # $feature{'highlight'}{'default'} = [1];
745 'sub' => sub { feature_bool
('highlight', @_) },
749 # Enable displaying of remote heads in the heads list
751 # To enable system wide have in $GITWEB_CONFIG
752 # $feature{'remote_heads'}{'default'} = [1];
753 # To have project specific config enable override in $GITWEB_CONFIG
754 # $feature{'remote_heads'}{'override'} = 1;
755 # and in project config gitweb.remoteheads = 0|1;
757 'sub' => sub { feature_bool
('remote_heads', @_) },
761 # Enable showing branches under other refs in addition to heads
763 # To set system wide extra branch refs have in $GITWEB_CONFIG
764 # $feature{'extra-branch-refs'}{'default'} = ['dirs', 'of', 'choice'];
765 # To have project specific config enable override in $GITWEB_CONFIG
766 # $feature{'extra-branch-refs'}{'override'} = 1;
767 # and in project config gitweb.extrabranchrefs = dirs of choice
768 # Every directory is separated with whitespace.
770 'extra-branch-refs' => {
771 'sub' => \
&feature_extra_branch_refs
,
776 sub gitweb_get_feature
{
778 return unless exists $feature{$name};
779 my ($sub, $override, @defaults) = (
780 $feature{$name}{'sub'},
781 $feature{$name}{'override'},
782 @
{$feature{$name}{'default'}});
783 # project specific override is possible only if we have project
784 our $git_dir; # global variable, declared later
785 if (!$override || !defined $git_dir) {
789 warn "feature $name is not overridable";
792 return $sub->(@defaults);
795 # A wrapper to check if a given feature is enabled.
796 # With this, you can say
798 # my $bool_feat = gitweb_check_feature('bool_feat');
799 # gitweb_check_feature('bool_feat') or somecode;
803 # my ($bool_feat) = gitweb_get_feature('bool_feat');
804 # (gitweb_get_feature('bool_feat'))[0] or somecode;
806 sub gitweb_check_feature
{
807 return (gitweb_get_feature
(@_))[0];
813 my ($val) = git_get_project_config
($key, '--bool');
817 } elsif ($val eq 'true') {
819 } elsif ($val eq 'false') {
824 sub feature_snapshot
{
827 my ($val) = git_get_project_config
('snapshot');
830 @fmts = ($val eq 'none' ?
() : split /\s*[,\s]\s*/, $val);
836 sub feature_patches
{
837 my @val = (git_get_project_config
('patches', '--int'));
847 my @val = (git_get_project_config
('avatar'));
849 return @val ?
@val : @_;
852 sub feature_extra_branch_refs
{
853 my (@branch_refs) = @_;
854 my $values = git_get_project_config
('extrabranchrefs');
857 $values = config_to_multi
($values);
859 foreach my $value (@
{$values}) {
860 push @branch_refs, split /\s+/, $value;
867 # checking HEAD file with -e is fragile if the repository was
868 # initialized long time ago (i.e. symlink HEAD) and was pack-ref'ed
870 sub check_head_link
{
872 my $headfile = "$dir/HEAD";
873 return ((-e
$headfile) ||
874 (-l
$headfile && readlink($headfile) =~ /^refs\/heads\
//));
877 sub check_export_ok
{
879 return (check_head_link
($dir) &&
880 (!$export_ok || -e
"$dir/$export_ok") &&
881 (!$export_auth_hook || $export_auth_hook->($dir)));
884 # process alternate names for backward compatibility
885 # filter out unsupported (unknown) snapshot formats
886 sub filter_snapshot_fmts
{
890 exists $known_snapshot_format_aliases{$_} ?
891 $known_snapshot_format_aliases{$_} : $_} @fmts;
893 exists $known_snapshot_formats{$_} &&
894 !$known_snapshot_formats{$_}{'disabled'}} @fmts;
897 sub filter_and_validate_refs
{
899 my %unique_refs = ();
901 foreach my $ref (@refs) {
902 die_error
(500, "Invalid ref '$ref' in 'extra-branch-refs' feature") unless (is_valid_ref_format
($ref));
903 # 'heads' are added implicitly in get_branch_refs().
904 $unique_refs{$ref} = 1 if ($ref ne 'heads');
906 return sort keys %unique_refs;
909 # If it is set to code reference, it is code that it is to be run once per
910 # request, allowing updating configurations that change with each request,
911 # while running other code in config file only once.
913 # Otherwise, if it is false then gitweb would process config file only once;
914 # if it is true then gitweb config would be run for each request.
915 our $per_request_config = 1;
917 # If true and fileno STDIN is 0 and getsockname succeeds, then FCGI mode will
918 # be activated automatically as though the --fcgi option was given.
921 # read and parse gitweb config file given by its parameter.
922 # returns true on success, false on recoverable error, allowing
923 # to chain this subroutine, using first file that exists.
924 # dies on errors during parsing config file, as it is unrecoverable.
925 sub read_config_file
{
926 my $filename = shift;
927 return unless defined $filename;
928 # die if there are errors parsing config file
937 our ($GITWEB_CONFIG, $GITWEB_CONFIG_SYSTEM, $GITWEB_CONFIG_COMMON);
938 sub evaluate_gitweb_config
{
939 our $GITWEB_CONFIG = $ENV{'GITWEB_CONFIG'} || "++GITWEB_CONFIG++";
940 our $GITWEB_CONFIG_SYSTEM = $ENV{'GITWEB_CONFIG_SYSTEM'} || "++GITWEB_CONFIG_SYSTEM++";
941 our $GITWEB_CONFIG_COMMON = $ENV{'GITWEB_CONFIG_COMMON'} || "++GITWEB_CONFIG_COMMON++";
943 # Protect against duplications of file names, to not read config twice.
944 # Only one of $GITWEB_CONFIG and $GITWEB_CONFIG_SYSTEM is used, so
945 # there possibility of duplication of filename there doesn't matter.
946 $GITWEB_CONFIG = "" if ($GITWEB_CONFIG eq $GITWEB_CONFIG_COMMON);
947 $GITWEB_CONFIG_SYSTEM = "" if ($GITWEB_CONFIG_SYSTEM eq $GITWEB_CONFIG_COMMON);
949 # Common system-wide settings for convenience.
950 # Those settings can be ovverriden by GITWEB_CONFIG or GITWEB_CONFIG_SYSTEM.
951 read_config_file
($GITWEB_CONFIG_COMMON);
953 # Use first config file that exists. This means use the per-instance
954 # GITWEB_CONFIG if exists, otherwise use GITWEB_SYSTEM_CONFIG.
955 read_config_file
($GITWEB_CONFIG) and return;
956 read_config_file
($GITWEB_CONFIG_SYSTEM);
961 sub evaluate_encoding
{
962 my $requested = $fallback_encoding || 'ISO-8859-1';
963 my $obj = Encode
::find_encoding
($requested) or
964 die_error
(400, "Requested fallback encoding not found");
965 if ($obj->name eq 'iso-8859-1') {
966 # Use Windows-1252 instead as required by the HTML 5 standard
967 my $altobj = Encode
::find_encoding
('Windows-1252');
968 $obj = $altobj if $altobj;
970 $encode_object = $obj;
973 sub evaluate_email_obfuscate
{
976 if (!$email && eval { require HTML
::Email
::Obfuscate
; 1 }) {
977 $email = HTML
::Email
::Obfuscate
->new(lite
=> 1);
981 # Get loadavg of system, to compare against $maxload.
982 # Currently it requires '/proc/loadavg' present to get loadavg;
983 # if it is not present it returns 0, which means no load checking.
985 if( -e
'/proc/loadavg' ){
986 open my $fd, '<', '/proc/loadavg'
988 my @load = split(/\s+/, scalar <$fd>);
991 # The first three columns measure CPU and IO utilization of the last one,
992 # five, and 10 minute periods. The fourth column shows the number of
993 # currently running processes and the total number of processes in the m/n
994 # format. The last column displays the last process ID used.
995 return $load[0] || 0;
997 # additional checks for load average should go here for things that don't export
1003 # version of the core git binary
1005 sub evaluate_git_version
{
1006 our $git_version = $version;
1010 if (defined $maxload && get_loadavg
() > $maxload) {
1011 die_error
(503, "The load average on the server is too high");
1015 # ======================================================================
1016 # input validation and dispatch
1018 # input parameters can be collected from a variety of sources (presently, CGI
1019 # and PATH_INFO), so we define an %input_params hash that collects them all
1020 # together during validation: this allows subsequent uses (e.g. href()) to be
1021 # agnostic of the parameter origin
1023 our %input_params = ();
1025 # input parameters are stored with the long parameter name as key. This will
1026 # also be used in the href subroutine to convert parameters to their CGI
1027 # equivalent, and since the href() usage is the most frequent one, we store
1028 # the name -> CGI key mapping here, instead of the reverse.
1030 # XXX: Warning: If you touch this, check the search form for updating,
1033 our @cgi_param_mapping = (
1037 file_parent
=> "fp",
1039 hash_parent
=> "hp",
1041 hash_parent_base
=> "hpb",
1046 snapshot_format
=> "sf",
1048 extra_options
=> "opt",
1049 search_use_regexp
=> "sr",
1052 project_filter
=> "pf",
1053 # this must be last entry (for manipulation from JavaScript)
1056 our %cgi_param_mapping = @cgi_param_mapping;
1058 # we will also need to know the possible actions, for validation
1060 "blame" => \
&git_blame
,
1061 "blame_incremental" => \
&git_blame_incremental
,
1062 "blame_data" => \
&git_blame_data
,
1063 "blobdiff" => \
&git_blobdiff
,
1064 "blobdiff_plain" => \
&git_blobdiff_plain
,
1065 "blob" => \
&git_blob
,
1066 "blob_plain" => \
&git_blob_plain
,
1067 "commitdiff" => \
&git_commitdiff
,
1068 "commitdiff_plain" => \
&git_commitdiff_plain
,
1069 "commit" => \
&git_commit
,
1070 "forks" => \
&git_forks
,
1071 "heads" => \
&git_heads
,
1072 "history" => \
&git_history
,
1074 "patch" => \
&git_patch
,
1075 "patches" => \
&git_patches
,
1076 "refs" => \
&git_refs
,
1077 "remotes" => \
&git_remotes
,
1079 "atom" => \
&git_atom
,
1080 "search" => \
&git_search
,
1081 "search_help" => \
&git_search_help
,
1082 "shortlog" => \
&git_shortlog
,
1083 "summary" => \
&git_summary
,
1085 "tags" => \
&git_tags
,
1086 "tree" => \
&git_tree
,
1087 "snapshot" => \
&git_snapshot
,
1088 "object" => \
&git_object
,
1089 # those below don't need $project
1090 "opml" => \
&git_opml
,
1091 "frontpage" => \
&git_frontpage
,
1092 "project_list" => \
&git_project_list
,
1093 "project_index" => \
&git_project_index
,
1096 # the only actions we will allow to be cached
1097 our %supported_cache_actions = map {( $_ => 1 )} qw(summary);
1099 # finally, we have the hash of allowed extra_options for the commands that
1101 our %allowed_options = (
1102 "--no-merges" => [ qw(rss atom log shortlog history) ],
1105 # fill %input_params with the CGI parameters. All values except for 'opt'
1106 # should be single values, but opt can be an array. We should probably
1107 # build an array of parameters that can be multi-valued, but since for the time
1108 # being it's only this one, we just single it out
1109 sub evaluate_query_params
{
1112 while (my ($name, $symbol) = each %cgi_param_mapping) {
1113 if ($symbol eq 'opt') {
1114 $input_params{$name} = [ map { decode_utf8
($_) } $cgi->multi_param($symbol) ];
1116 $input_params{$name} = decode_utf8
($cgi->param($symbol));
1120 # Backwards compatibility - by_tag= <=> t=
1121 if ($input_params{'ctag'}) {
1122 $input_params{'ctag_filter'} = $input_params{'ctag'};
1126 # now read PATH_INFO and update the parameter list for missing parameters
1127 sub evaluate_path_info
{
1128 return if defined $input_params{'project'};
1129 return if !$path_info;
1130 $path_info =~ s
,^/+,,;
1131 return if !$path_info;
1133 # find which part of PATH_INFO is project
1134 my $project = $path_info;
1135 $project =~ s
,/+$,,;
1136 while ($project && !check_head_link
("$projectroot/$project")) {
1137 $project =~ s
,/*[^/]*$,,;
1139 return unless $project;
1140 $input_params{'project'} = $project;
1142 # do not change any parameters if an action is given using the query string
1143 return if $input_params{'action'};
1144 $path_info =~ s
,^\Q
$project\E
/*,,;
1146 # next, check if we have an action
1147 my $action = $path_info;
1148 $action =~ s
,/.*$,,;
1149 if (exists $actions{$action}) {
1150 $path_info =~ s
,^$action/*,,;
1151 $input_params{'action'} = $action;
1154 # list of actions that want hash_base instead of hash, but can have no
1155 # pathname (f) parameter
1161 # we want to catch, among others
1162 # [$hash_parent_base[:$file_parent]..]$hash_parent[:$file_name]
1163 my ($parentrefname, $parentpathname, $refname, $pathname) =
1164 ($path_info =~ /^(?:(.+?)(?::(.+))?\.\.)?([^:]+?)?(?::(.+))?$/);
1166 # first, analyze the 'current' part
1167 if (defined $pathname) {
1168 # we got "branch:filename" or "branch:dir/"
1169 # we could use git_get_type(branch:pathname), but:
1170 # - it needs $git_dir
1171 # - it does a git() call
1172 # - the convention of terminating directories with a slash
1173 # makes it superfluous
1174 # - embedding the action in the PATH_INFO would make it even
1176 $pathname =~ s
,^/+,,;
1177 if (!$pathname || substr($pathname, -1) eq "/") {
1178 $input_params{'action'} ||= "tree";
1179 $pathname =~ s
,/$,,;
1181 # the default action depends on whether we had parent info
1183 if ($parentrefname) {
1184 $input_params{'action'} ||= "blobdiff_plain";
1186 $input_params{'action'} ||= "blob_plain";
1189 $input_params{'hash_base'} ||= $refname;
1190 $input_params{'file_name'} ||= $pathname;
1191 } elsif (defined $refname) {
1192 # we got "branch". In this case we have to choose if we have to
1193 # set hash or hash_base.
1195 # Most of the actions without a pathname only want hash to be
1196 # set, except for the ones specified in @wants_base that want
1197 # hash_base instead. It should also be noted that hand-crafted
1198 # links having 'history' as an action and no pathname or hash
1199 # set will fail, but that happens regardless of PATH_INFO.
1200 if (defined $parentrefname) {
1201 # if there is parent let the default be 'shortlog' action
1202 # (for http://git.example.com/repo.git/A..B links); if there
1203 # is no parent, dispatch will detect type of object and set
1204 # action appropriately if required (if action is not set)
1205 $input_params{'action'} ||= "shortlog";
1207 if ($input_params{'action'} &&
1208 grep { $_ eq $input_params{'action'} } @wants_base) {
1209 $input_params{'hash_base'} ||= $refname;
1211 $input_params{'hash'} ||= $refname;
1215 # next, handle the 'parent' part, if present
1216 if (defined $parentrefname) {
1217 # a missing pathspec defaults to the 'current' filename, allowing e.g.
1218 # someproject/blobdiff/oldrev..newrev:/filename
1219 if ($parentpathname) {
1220 $parentpathname =~ s
,^/+,,;
1221 $parentpathname =~ s
,/$,,;
1222 $input_params{'file_parent'} ||= $parentpathname;
1224 $input_params{'file_parent'} ||= $input_params{'file_name'};
1226 # we assume that hash_parent_base is wanted if a path was specified,
1227 # or if the action wants hash_base instead of hash
1228 if (defined $input_params{'file_parent'} ||
1229 grep { $_ eq $input_params{'action'} } @wants_base) {
1230 $input_params{'hash_parent_base'} ||= $parentrefname;
1232 $input_params{'hash_parent'} ||= $parentrefname;
1236 # for the snapshot action, we allow URLs in the form
1237 # $project/snapshot/$hash.ext
1238 # where .ext determines the snapshot and gets removed from the
1239 # passed $refname to provide the $hash.
1241 # To be able to tell that $refname includes the format extension, we
1242 # require the following two conditions to be satisfied:
1243 # - the hash input parameter MUST have been set from the $refname part
1244 # of the URL (i.e. they must be equal)
1245 # - the snapshot format MUST NOT have been defined already (e.g. from
1247 # It's also useless to try any matching unless $refname has a dot,
1248 # so we check for that too
1249 if (defined $input_params{'action'} &&
1250 $input_params{'action'} eq 'snapshot' &&
1251 defined $refname && index($refname, '.') != -1 &&
1252 $refname eq $input_params{'hash'} &&
1253 !defined $input_params{'snapshot_format'}) {
1254 # We loop over the known snapshot formats, checking for
1255 # extensions. Allowed extensions are both the defined suffix
1256 # (which includes the initial dot already) and the snapshot
1257 # format key itself, with a prepended dot
1258 while (my ($fmt, $opt) = each %known_snapshot_formats) {
1259 my $hash = $refname;
1260 unless ($hash =~ s/(\Q$opt->{'suffix'}\E|\Q.$fmt\E)$//) {
1264 # a valid suffix was found, so set the snapshot format
1265 # and reset the hash parameter
1266 $input_params{'snapshot_format'} = $fmt;
1267 $input_params{'hash'} = $hash;
1268 # we also set the format suffix to the one requested
1269 # in the URL: this way a request for e.g. .tgz returns
1270 # a .tgz instead of a .tar.gz
1271 $known_snapshot_formats{$fmt}{'suffix'} = $sfx;
1277 our ($action, $project, $file_name, $file_parent, $hash, $hash_parent, $hash_base,
1278 $hash_parent_base, @extra_options, $page, $searchtype, $search_use_regexp,
1279 $searchtext, $search_regexp, $project_filter);
1280 sub evaluate_and_validate_params
{
1281 our $action = $input_params{'action'};
1282 if (defined $action) {
1283 if (!is_valid_action
($action)) {
1284 die_error
(400, "Invalid action parameter");
1288 # parameters which are pathnames
1289 our $project = $input_params{'project'};
1290 if (defined $project) {
1291 if (!is_valid_project
($project)) {
1293 die_error
(404, "No such project");
1297 our $project_filter = $input_params{'project_filter'};
1298 if (defined $project_filter) {
1299 if (!is_valid_pathname
($project_filter)) {
1300 die_error
(404, "Invalid project_filter parameter");
1304 our $file_name = $input_params{'file_name'};
1305 if (defined $file_name) {
1306 if (!is_valid_pathname
($file_name)) {
1307 die_error
(400, "Invalid file parameter");
1311 our $file_parent = $input_params{'file_parent'};
1312 if (defined $file_parent) {
1313 if (!is_valid_pathname
($file_parent)) {
1314 die_error
(400, "Invalid file parent parameter");
1318 # parameters which are refnames
1319 our $hash = $input_params{'hash'};
1320 if (defined $hash) {
1321 if (!is_valid_refname
($hash)) {
1322 die_error
(400, "Invalid hash parameter");
1326 our $hash_parent = $input_params{'hash_parent'};
1327 if (defined $hash_parent) {
1328 if (!is_valid_refname
($hash_parent)) {
1329 die_error
(400, "Invalid hash parent parameter");
1333 our $hash_base = $input_params{'hash_base'};
1334 if (defined $hash_base) {
1335 if (!is_valid_refname
($hash_base)) {
1336 die_error
(400, "Invalid hash base parameter");
1340 our @extra_options = @
{$input_params{'extra_options'}};
1341 # @extra_options is always defined, since it can only be (currently) set from
1342 # CGI, and $cgi->param() returns the empty array in array context if the param
1344 foreach my $opt (@extra_options) {
1345 if (not exists $allowed_options{$opt}) {
1346 die_error
(400, "Invalid option parameter");
1348 if (not grep(/^$action$/, @
{$allowed_options{$opt}})) {
1349 die_error
(400, "Invalid option parameter for this action");
1353 our $hash_parent_base = $input_params{'hash_parent_base'};
1354 if (defined $hash_parent_base) {
1355 if (!is_valid_refname
($hash_parent_base)) {
1356 die_error
(400, "Invalid hash parent base parameter");
1361 our $page = $input_params{'page'};
1362 if (defined $page) {
1363 if ($page =~ m/[^0-9]/) {
1364 die_error
(400, "Invalid page parameter");
1368 our $searchtype = $input_params{'searchtype'};
1369 if (defined $searchtype) {
1370 if ($searchtype =~ m/[^a-z]/) {
1371 die_error
(400, "Invalid searchtype parameter");
1375 our $search_use_regexp = $input_params{'search_use_regexp'};
1377 our $searchtext = $input_params{'searchtext'};
1378 our $search_regexp = undef;
1379 if (defined $searchtext) {
1380 if (length($searchtext) < 2) {
1381 die_error
(403, "At least two characters are required for search parameter");
1383 if ($search_use_regexp) {
1384 $search_regexp = $searchtext;
1385 if (!eval { qr/$search_regexp/; 1; }) {
1386 (my $error = $@
) =~ s/ at \S+ line \d+.*\n?//;
1387 die_error
(400, "Invalid search regexp '$search_regexp'",
1391 $search_regexp = quotemeta $searchtext;
1396 # path to the current git repository
1398 sub evaluate_git_dir
{
1399 our $git_dir = "$projectroot/$project" if $project;
1402 our (@snapshot_fmts, $git_avatar, @extra_branch_refs);
1403 sub configure_gitweb_features
{
1404 # list of supported snapshot formats
1405 our @snapshot_fmts = gitweb_get_feature
('snapshot');
1406 @snapshot_fmts = filter_snapshot_fmts
(@snapshot_fmts);
1408 # check that the avatar feature is set to a known provider name,
1409 # and for each provider check if the dependencies are satisfied.
1410 # if the provider name is invalid or the dependencies are not met,
1411 # reset $git_avatar to the empty string.
1412 our ($git_avatar) = gitweb_get_feature
('avatar');
1413 if ($git_avatar eq 'gravatar') {
1414 $git_avatar = '' unless (eval { require Digest
::MD5
; 1; });
1415 } elsif ($git_avatar eq 'picon') {
1421 our @extra_branch_refs = gitweb_get_feature
('extra-branch-refs');
1422 @extra_branch_refs = filter_and_validate_refs
(@extra_branch_refs);
1425 sub get_branch_refs
{
1426 return ('heads', @extra_branch_refs);
1429 # custom error handler: 'die <message>' is Internal Server Error
1430 sub handle_errors_html
{
1431 my $msg = shift; # it is already HTML escaped
1433 # to avoid infinite loop where error occurs in die_error,
1434 # change handler to default handler, disabling handle_errors_html
1435 set_message
("Error occurred when inside die_error:\n$msg");
1437 # you cannot jump out of die_error when called as error handler;
1438 # the subroutine set via CGI::Carp::set_message is called _after_
1439 # HTTP headers are already written, so it cannot write them itself
1440 die_error
(undef, undef, $msg, -error_handler
=> 1, -no_http_header
=> 1);
1442 set_message
(\
&handle_errors_html
);
1444 our $shown_stale_message = 0;
1445 our $cache_dump = undef;
1446 our $cache_dump_mtime = undef;
1450 $shown_stale_message = 0;
1451 if (!defined $action) {
1452 if (defined $hash) {
1453 $action = git_get_type
($hash);
1454 $action or die_error
(404, "Object does not exist");
1455 } elsif (defined $hash_base && defined $file_name) {
1456 $action = git_get_type
("$hash_base:$file_name");
1457 $action or die_error
(404, "File or directory does not exist");
1458 } elsif (defined $project) {
1459 $action = 'summary';
1461 $action = 'frontpage';
1464 if (!defined($actions{$action})) {
1465 die_error
(400, "Unknown action");
1467 if ($action !~ m/^(?:opml|frontpage|project_list|project_index)$/ &&
1469 die_error
(400, "Project needed");
1472 my $cached_page = $supported_cache_actions{$action}
1473 ? cached_action_page
($action)
1475 print($cached_page), return if $cached_page;
1476 local *SAVEOUT
= *STDOUT
;
1477 my $caching_page = $supported_cache_actions{$action}
1478 ? cached_action_start
($action)
1481 $actions{$action}->();
1483 if ($caching_page) {
1484 $cached_page = cached_action_finish
($action);
1486 if (!$cached_page) {
1487 # Some other failure, redo without cache
1488 print STDERR
"re-generating $action page after caching failure\n";
1489 $actions{$action}->();
1497 our $t0 = [ gettimeofday
() ]
1499 our $number_of_git_cmds = 0;
1502 our $first_request = 1;
1503 our $evaluate_uri_force = undef;
1507 # Only allow GET and HEAD methods
1508 if (!$ENV{'REQUEST_METHOD'} || ($ENV{'REQUEST_METHOD'} ne 'GET' && $ENV{'REQUEST_METHOD'} ne 'HEAD')) {
1510 Status: 405 Method Not Allowed
1511 Content-Type: text/plain
1514 405 Method Not Allowed
1520 &$evaluate_uri_force() if $evaluate_uri_force;
1521 if ($per_request_config) {
1522 if (ref($per_request_config) eq 'CODE') {
1523 $per_request_config->();
1524 } elsif (!$first_request) {
1525 evaluate_gitweb_config
();
1526 evaluate_email_obfuscate
();
1531 # $projectroot and $projects_list might be set in gitweb config file
1532 $projects_list ||= $projectroot;
1534 evaluate_query_params
();
1535 evaluate_path_info
();
1536 evaluate_and_validate_params
();
1539 configure_gitweb_features
();
1544 our $is_last_request = sub { 1 };
1545 our ($pre_dispatch_hook, $post_dispatch_hook, $pre_listen_hook);
1549 our $fcgi_raw_mode = 0;
1550 sub configure_as_fcgi
{
1551 return if $fcgi_mode;
1556 # We have gone to great effort to make sure that all incoming data has
1557 # been converted from whatever format it was in into UTF-8. We have
1558 # even taken care to make sure the output handle is in ':utf8' mode.
1559 # Now along comes FCGI and blows it with:
1561 # Use of wide characters in FCGI::Stream::PRINT is deprecated
1562 # and will stop wprking[sic] in a future version of FCGI
1564 # To fix this we replace FCGI::Stream::PRINT with our own routine that
1565 # first encodes everything and then calls the original routine, but
1566 # not if $fcgi_raw_mode is true (then we just call the original routine).
1568 # Note that we could do this by using utf8::is_utf8 to check instead
1569 # of having a $fcgi_raw_mode global, but that would be slower to run
1570 # the test on each element and much slower than skipping the conversion
1571 # entirely when we know we're outputting raw bytes.
1572 my $orig = \
&FCGI
::Stream
::PRINT
;
1573 undef *FCGI
::Stream
::PRINT
;
1574 *FCGI
::Stream
::PRINT
= sub {
1575 @_ = (shift, map {my $x=$_; utf8
::encode
($x); $x} @_)
1576 unless $fcgi_raw_mode;
1580 our $CGI = 'CGI::Fast';
1584 my $request_number = 0;
1585 # let each child service 100 requests
1586 our $is_last_request = sub { ++$request_number > 100 };
1589 my $script_name = $ENV{'SCRIPT_NAME'} || $ENV{'SCRIPT_FILENAME'} || __FILE__
;
1591 if $script_name =~ /\.fcgi$/
1592 or $auto_fcgi && defined fileno STDIN
&& fileno STDIN
== 0 && getsockname(STDIN
);
1594 return unless (@ARGV);
1596 require Getopt
::Long
;
1597 Getopt
::Long
::GetOptions
(
1598 'fastcgi|fcgi|f' => \
&configure_as_fcgi
,
1599 'nproc|n=i' => sub {
1600 my ($arg, $val) = @_;
1601 return unless eval { require FCGI
::ProcManager
; 1; };
1602 my $proc_manager = FCGI
::ProcManager
->new({
1603 n_processes
=> $val,
1605 our $pre_listen_hook = sub { $proc_manager->pm_manage() };
1606 our $pre_dispatch_hook = sub { $proc_manager->pm_pre_dispatch() };
1607 our $post_dispatch_hook = sub { $proc_manager->pm_post_dispatch() };
1613 evaluate_gitweb_config
();
1614 evaluate_encoding
();
1615 evaluate_email_obfuscate
();
1616 evaluate_git_version
();
1617 my ($mu, $hl, $subroutine) = ($my_uri, $home_link, '');
1618 $subroutine .= '$my_uri = $mu;' if defined $my_uri && $my_uri ne '';
1619 $subroutine .= '$home_link = $hl;' if defined $home_link && $home_link ne '';
1620 $evaluate_uri_force = eval "sub {$subroutine}" if $subroutine;
1624 $pre_listen_hook->()
1625 if $pre_listen_hook;
1628 while ($cgi = $CGI->new()) {
1629 $pre_dispatch_hook->()
1630 if $pre_dispatch_hook;
1634 $post_dispatch_hook->()
1635 if $post_dispatch_hook;
1638 last REQUEST
if ($is_last_request->());
1647 if (defined caller) {
1648 # wrapped in a subroutine processing requests,
1649 # e.g. mod_perl with ModPerl::Registry, or PSGI with Plack::App::WrapCGI
1652 # pure CGI script, serving single request
1656 ## ======================================================================
1659 # possible values of extra options
1660 # -full => 0|1 - use absolute/full URL ($my_uri/$my_url as base)
1661 # -replay => 1 - start from a current view (replay with modifications)
1662 # -path_info => 0|1 - don't use/use path_info URL (if possible)
1663 # -anchor => ANCHOR - add #ANCHOR to end of URL, implies -replay if used alone
1666 # default is to use -absolute url() i.e. $my_uri
1667 my $href = $params{-full
} ?
$my_url : $my_uri;
1669 # implicit -replay, must be first of implicit params
1670 $params{-replay
} = 1 if (keys %params == 1 && $params{-anchor
});
1672 $params{'project'} = $project unless exists $params{'project'};
1674 if ($params{-replay
}) {
1675 while (my ($name, $symbol) = each %cgi_param_mapping) {
1676 if (!exists $params{$name}) {
1677 $params{$name} = $input_params{$name};
1682 my $use_pathinfo = gitweb_check_feature
('pathinfo');
1683 if (defined $params{'project'} &&
1684 (exists $params{-path_info
} ?
$params{-path_info
} : $use_pathinfo)) {
1685 # try to put as many parameters as possible in PATH_INFO:
1688 # - hash_parent or hash_parent_base:/file_parent
1689 # - hash or hash_base:/filename
1690 # - the snapshot_format as an appropriate suffix
1692 # When the script is the root DirectoryIndex for the domain,
1693 # $href here would be something like http://gitweb.example.com/
1694 # Thus, we strip any trailing / from $href, to spare us double
1695 # slashes in the final URL
1698 # Then add the project name, if present
1699 $href .= "/".esc_path_info
($params{'project'});
1700 delete $params{'project'};
1702 # since we destructively absorb parameters, we keep this
1703 # boolean that remembers if we're handling a snapshot
1704 my $is_snapshot = $params{'action'} eq 'snapshot';
1706 # Summary just uses the project path URL, any other action is
1708 if (defined $params{'action'}) {
1709 $href .= "/".esc_path_info
($params{'action'})
1710 unless $params{'action'} eq 'summary';
1711 delete $params{'action'};
1714 # Next, we put hash_parent_base:/file_parent..hash_base:/file_name,
1715 # stripping nonexistent or useless pieces
1716 $href .= "/" if ($params{'hash_base'} || $params{'hash_parent_base'}
1717 || $params{'hash_parent'} || $params{'hash'});
1718 if (defined $params{'hash_base'}) {
1719 if (defined $params{'hash_parent_base'}) {
1720 $href .= esc_path_info
($params{'hash_parent_base'});
1721 # skip the file_parent if it's the same as the file_name
1722 if (defined $params{'file_parent'}) {
1723 if (defined $params{'file_name'} && $params{'file_parent'} eq $params{'file_name'}) {
1724 delete $params{'file_parent'};
1725 } elsif ($params{'file_parent'} !~ /\.\./) {
1726 $href .= ":/".esc_path_info
($params{'file_parent'});
1727 delete $params{'file_parent'};
1731 delete $params{'hash_parent'};
1732 delete $params{'hash_parent_base'};
1733 } elsif (defined $params{'hash_parent'}) {
1734 $href .= esc_path_info
($params{'hash_parent'}). "..";
1735 delete $params{'hash_parent'};
1738 $href .= esc_path_info
($params{'hash_base'});
1739 if (defined $params{'file_name'} && $params{'file_name'} !~ /\.\./) {
1740 $href .= ":/".esc_path_info
($params{'file_name'});
1741 delete $params{'file_name'};
1743 delete $params{'hash'};
1744 delete $params{'hash_base'};
1745 } elsif (defined $params{'hash'}) {
1746 $href .= esc_path_info
($params{'hash'});
1747 delete $params{'hash'};
1750 # If the action was a snapshot, we can absorb the
1751 # snapshot_format parameter too
1753 my $fmt = $params{'snapshot_format'};
1754 # snapshot_format should always be defined when href()
1755 # is called, but just in case some code forgets, we
1756 # fall back to the default
1757 $fmt ||= $snapshot_fmts[0];
1758 $href .= $known_snapshot_formats{$fmt}{'suffix'};
1759 delete $params{'snapshot_format'};
1763 # now encode the parameters explicitly
1765 for (my $i = 0; $i < @cgi_param_mapping; $i += 2) {
1766 my ($name, $symbol) = ($cgi_param_mapping[$i], $cgi_param_mapping[$i+1]);
1767 if (defined $params{$name}) {
1768 if (ref($params{$name}) eq "ARRAY") {
1769 foreach my $par (@
{$params{$name}}) {
1770 push @result, $symbol . "=" . esc_param
($par);
1773 push @result, $symbol . "=" . esc_param
($params{$name});
1777 $href .= "?" . join(';', @result) if scalar @result;
1779 # final transformation: trailing spaces must be escaped (URI-encoded)
1780 $href =~ s/(\s+)$/CGI::escape($1)/e;
1782 if ($params{-anchor
}) {
1783 $href .= "#".esc_param
($params{-anchor
});
1790 ## ======================================================================
1791 ## validation, quoting/unquoting and escaping
1793 sub is_valid_action
{
1795 return undef unless exists $actions{$input};
1799 sub is_valid_project
{
1802 return unless defined $input;
1803 if (!is_valid_pathname
($input) ||
1804 !(-d
"$projectroot/$input") ||
1805 !check_export_ok
("$projectroot/$input") ||
1806 ($strict_export && !project_in_list
($input))) {
1813 sub is_valid_pathname
{
1816 return undef unless defined $input;
1817 # no '.' or '..' as elements of path, i.e. no '.' or '..'
1818 # at the beginning, at the end, and between slashes.
1819 # also this catches doubled slashes
1820 if ($input =~ m!(^|/)(|\.|\.\.)(/|$)!) {
1823 # no null characters
1824 if ($input =~ m!\0!) {
1830 sub is_valid_ref_format
{
1833 return undef unless defined $input;
1834 # restrictions on ref name according to git-check-ref-format
1835 if ($input =~ m!(/\.|\.\.|[\000-\040\177 ~^:?*\[]|/$)!) {
1841 sub is_valid_refname
{
1844 return undef unless defined $input;
1845 # textual hashes are O.K.
1846 if ($input =~ m/^[0-9a-fA-F]{40}$/) {
1849 # it must be correct pathname
1850 is_valid_pathname
($input) or return undef;
1851 # check git-check-ref-format restrictions
1852 is_valid_ref_format
($input) or return undef;
1856 # decode sequences of octets in utf8 into Perl's internal form,
1857 # which is utf-8 with utf8 flag set if needed. gitweb writes out
1858 # in utf-8 thanks to "binmode STDOUT, ':utf8'" at beginning
1861 return undef unless defined $str;
1863 if (utf8
::is_utf8
($str) || utf8
::decode
($str)) {
1866 return $encode_object->decode($str, Encode
::FB_DEFAULT
);
1870 # quote unsafe chars, but keep the slash, even when it's not
1871 # correct, but quoted slashes look too horrible in bookmarks
1874 return undef unless defined $str;
1875 $str =~ s/([^A-Za-z0-9\-_.~()\/:@ ]+)/CGI
::escape
($1)/eg
;
1880 # the quoting rules for path_info fragment are slightly different
1883 return undef unless defined $str;
1885 # path_info doesn't treat '+' as space (specially), but '?' must be escaped
1886 $str =~ s/([^A-Za-z0-9\-_.~();\/;:@&= +]+)/CGI
::escape
($1)/eg
;
1891 # quote unsafe chars in whole URL, so some characters cannot be quoted
1894 return undef unless defined $str;
1895 $str =~ s/([^A-Za-z0-9\-_.~();\/;?:@&= ]+)/CGI
::escape
($1)/eg
;
1900 # quote unsafe characters in HTML attributes
1903 # for XHTML conformance escaping '"' to '"' is not enough
1904 return esc_html
(@_);
1907 # replace invalid utf8 character with SUBSTITUTION sequence
1912 return undef unless defined $str;
1914 $str = to_utf8
($str);
1915 $str = $cgi->escapeHTML($str);
1916 if ($opts{'-nbsp'}) {
1917 $str =~ s/ / /g;
1920 $str =~ s
|([[:cntrl
:]])|(($1 ne "\t") ? quot_cec
($1) : $1)|eg
;
1924 # quote control characters and escape filename to HTML
1929 return undef unless defined $str;
1931 $str = to_utf8
($str);
1932 $str = $cgi->escapeHTML($str);
1933 if ($opts{'-nbsp'}) {
1934 $str =~ s/ / /g;
1937 $str =~ s
|([[:cntrl
:]])|quot_cec
($1)|eg
;
1941 # Sanitize for use in XHTML + application/xml+xhtm (valid XML 1.0)
1945 return undef unless defined $str;
1947 $str = to_utf8
($str);
1949 $str =~ s
|([[:cntrl
:]])|(index("\t\n\r", $1) != -1 ?
$1 : quot_cec
($1))|eg
;
1953 # Make control characters "printable", using character escape codes (CEC)
1957 my %es = ( # character escape codes, aka escape sequences
1958 "\t" => '\t', # tab (HT)
1959 "\n" => '\n', # line feed (LF)
1960 "\r" => '\r', # carrige return (CR)
1961 "\f" => '\f', # form feed (FF)
1962 "\b" => '\b', # backspace (BS)
1963 "\a" => '\a', # alarm (bell) (BEL)
1964 "\e" => '\e', # escape (ESC)
1965 "\013" => '\v', # vertical tab (VT)
1966 "\000" => '\0', # nul character (NUL)
1968 my $chr = ( (exists $es{$cntrl})
1970 : sprintf('\x%02x', ord($cntrl)) );
1971 if ($opts{-nohtml
}) {
1974 return "<span class=\"cntrl\">$chr</span>";
1978 # Alternatively use unicode control pictures codepoints,
1979 # Unicode "printable representation" (PR)
1984 my $chr = sprintf('&#%04d;', 0x2400+ord($cntrl));
1985 if ($opts{-nohtml
}) {
1988 return "<span class=\"cntrl\">$chr</span>";
1992 # git may return quoted and escaped filenames
1998 my %es = ( # character escape codes, aka escape sequences
1999 't' => "\t", # tab (HT, TAB)
2000 'n' => "\n", # newline (NL)
2001 'r' => "\r", # return (CR)
2002 'f' => "\f", # form feed (FF)
2003 'b' => "\b", # backspace (BS)
2004 'a' => "\a", # alarm (bell) (BEL)
2005 'e' => "\e", # escape (ESC)
2006 'v' => "\013", # vertical tab (VT)
2009 if ($seq =~ m/^[0-7]{1,3}$/) {
2010 # octal char sequence
2011 return chr(oct($seq));
2012 } elsif (exists $es{$seq}) {
2013 # C escape sequence, aka character escape code
2016 # quoted ordinary character
2020 if ($str =~ m/^"(.*)"$/) {
2023 $str =~ s/\\([^0-7]|[0-7]{1,3})/unq($1)/eg;
2028 # escape tabs (convert tabs to spaces)
2032 while ((my $pos = index($line, "\t")) != -1) {
2033 if (my $count = (8 - ($pos % 8))) {
2034 my $spaces = ' ' x
$count;
2035 $line =~ s/\t/$spaces/;
2042 sub project_in_list
{
2043 my $project = shift;
2044 my @list = git_get_projects_list
();
2045 return @list && scalar(grep { $_->{'path'} eq $project } @list);
2048 sub cached_action_page
{
2050 my $precious = shift;
2052 use POSIX
qw(fstat strftime);
2054 return undef unless $html_cache_actions{$action} && $html_cache_dir;
2055 my $cache_file = "$projectroot/$project/$html_cache_dir/$action";
2056 return undef if !$precious && -e
"$cache_file.changed";
2057 open my $fd, '<', "$projectroot/$project/$html_cache_dir/$action" or
2060 my $cached_page = <$fd>;
2061 my ($ino, $sz, $mtime) = (fstat
(fileno $fd))[1,7,9];
2062 close $fd && $ino && $sz && $mtime or return undef;
2063 my $offset = length($1) if $cached_page =~ /^(Status\s*:.*\n)/i;
2064 my $hdrs = "Last-Modified: " .
2065 strftime
("%a, %d %b %Y %H:%M:%S GMT", gmtime($mtime)) . "\r\n";
2066 $hdrs .= "ETag: \"" .
2067 sprintf("%x-%x-%x", $ino, $sz, $mtime) . "\"\r\n";
2068 substr($cached_page, $offset||0, 0) = $hdrs;
2069 return $cached_page;
2072 # Caller is responsible for preserving STDOUT beforehand if needed
2073 sub cached_action_start
{
2076 use POSIX
qw(:fcntl_h);
2078 return undef unless $html_cache_actions{$action} && $html_cache_dir;
2080 my $cache_file = "$projectroot/$project/$html_cache_dir/$action";
2081 sysopen(CACHEFILE
, "$cache_file.lock",
2082 O_WRONLY
|O_CREAT
|O_EXCL
, 0664) or return undef;
2083 *STDOUT
= *CACHEFILE
;
2084 unlink "$cache_file.changed";
2088 # Caller is responsible for restoring STDOUT afterward if needed
2089 sub cached_action_finish
{
2094 return undef unless $html_cache_actions{$action} && $html_cache_dir;
2095 my $cache_file = "$projectroot/$project/$html_cache_dir/$action";
2096 close(STDOUT
) or die "couldn't close cache file on STDOUT: $!";
2097 # Do not leave STDOUT file descriptor invalid!
2099 open(NULL
, '>', File
::Spec
->devnull) or die "couldn't open NULL to devnull: $!";
2101 unlink "$cache_file.lock" unless rename "$cache_file.lock", $cache_file;
2102 return cached_action_page
($action, 1);
2105 ## ----------------------------------------------------------------------
2106 ## HTML aware string manipulation
2108 # Try to chop given string on a word boundary between position
2109 # $len and $len+$add_len. If there is no word boundary there,
2110 # chop at $len+$add_len. Do not chop if chopped part plus ellipsis
2111 # (marking chopped part) would be longer than given string.
2115 my $add_len = shift || 10;
2116 my $where = shift || 'right'; # 'left' | 'center' | 'right'
2118 # Make sure perl knows it is utf8 encoded so we don't
2119 # cut in the middle of a utf8 multibyte char.
2120 $str = to_utf8
($str);
2122 # allow only $len chars, but don't cut a word if it would fit in $add_len
2123 # if it doesn't fit, cut it if it's still longer than the dots we would add
2124 # remove chopped character entities entirely
2126 # when chopping in the middle, distribute $len into left and right part
2127 # return early if chopping wouldn't make string shorter
2128 if ($where eq 'center') {
2129 return $str if ($len + 5 >= length($str)); # filler is length 5
2132 return $str if ($len + 4 >= length($str)); # filler is length 4
2135 # regexps: ending and beginning with word part up to $add_len
2136 my $endre = qr/.{$len}\w{0,$add_len}/;
2137 my $begre = qr/\w{0,$add_len}.{$len}/;
2139 if ($where eq 'left') {
2140 $str =~ m/^(.*?)($begre)$/;
2141 my ($lead, $body) = ($1, $2);
2142 if (length($lead) > 4) {
2145 return "$lead$body";
2147 } elsif ($where eq 'center') {
2148 $str =~ m/^($endre)(.*)$/;
2149 my ($left, $str) = ($1, $2);
2150 $str =~ m/^(.*?)($begre)$/;
2151 my ($mid, $right) = ($1, $2);
2152 if (length($mid) > 5) {
2155 return "$left$mid$right";
2158 $str =~ m/^($endre)(.*)$/;
2161 if (length($tail) > 4) {
2164 return "$body$tail";
2168 # pass-through email filter, obfuscating it when possible
2169 sub email_obfuscate
{
2173 $str = $email->escape_html($str);
2174 # Stock HTML::Email::Obfuscate version likes to produce
2176 $str =~ s
#<(/?)B>#<$1b>#g;
2179 $str = esc_html
($str);
2180 $str =~ s/@/@/;
2185 # takes the same arguments as chop_str, but also wraps a <span> around the
2186 # result with a title attribute if it does get chopped. Additionally, the
2187 # string is HTML-escaped.
2188 sub chop_and_escape_str
{
2191 my $chopped = chop_str
(@_);
2192 $str = to_utf8
($str);
2193 if ($chopped eq $str) {
2194 return email_obfuscate
($chopped);
2197 $str =~ s/[[:cntrl:]]/?/g;
2198 return $cgi->span({-title
=>$str}, email_obfuscate
($chopped));
2202 # Highlight selected fragments of string, using given CSS class,
2203 # and escape HTML. It is assumed that fragments do not overlap.
2204 # Regions are passed as list of pairs (array references).
2206 # Example: esc_html_hl_regions("foobar", "mark", [ 0, 3 ]) returns
2207 # '<span class="mark">foo</span>bar'
2208 sub esc_html_hl_regions
{
2209 my ($str, $css_class, @sel) = @_;
2210 my %opts = grep { ref($_) ne 'ARRAY' } @sel;
2211 @sel = grep { ref($_) eq 'ARRAY' } @sel;
2212 return esc_html
($str, %opts) unless @sel;
2218 my ($begin, $end) = @
$s;
2220 # Don't create empty <span> elements.
2221 next if $end <= $begin;
2223 my $escaped = esc_html
(substr($str, $begin, $end - $begin),
2226 $out .= esc_html
(substr($str, $pos, $begin - $pos), %opts)
2227 if ($begin - $pos > 0);
2228 $out .= $cgi->span({-class => $css_class}, $escaped);
2232 $out .= esc_html
(substr($str, $pos), %opts)
2233 if ($pos < length($str));
2238 # return positions of beginning and end of each match
2240 my ($str, $regexp) = @_;
2241 return unless (defined $str && defined $regexp);
2244 while ($str =~ /$regexp/g) {
2245 push @matches, [$-[0], $+[0]];
2250 # highlight match (if any), and escape HTML
2251 sub esc_html_match_hl
{
2252 my ($str, $regexp) = @_;
2253 return esc_html
($str) unless defined $regexp;
2255 my @matches = matchpos_list
($str, $regexp);
2256 return esc_html
($str) unless @matches;
2258 return esc_html_hl_regions
($str, 'match', @matches);
2262 # highlight match (if any) of shortened string, and escape HTML
2263 sub esc_html_match_hl_chopped
{
2264 my ($str, $chopped, $regexp) = @_;
2265 return esc_html_match_hl
($str, $regexp) unless defined $chopped;
2267 my @matches = matchpos_list
($str, $regexp);
2268 return esc_html
($chopped) unless @matches;
2270 # filter matches so that we mark chopped string
2271 my $tail = "... "; # see chop_str
2272 unless ($chopped =~ s/\Q$tail\E$//) {
2275 my $chop_len = length($chopped);
2276 my $tail_len = length($tail);
2279 for my $m (@matches) {
2280 if ($m->[0] > $chop_len) {
2281 push @filtered, [ $chop_len, $chop_len + $tail_len ] if ($tail_len > 0);
2283 } elsif ($m->[1] > $chop_len) {
2284 push @filtered, [ $m->[0], $chop_len + $tail_len ];
2290 return esc_html_hl_regions
($chopped . $tail, 'match', @filtered);
2293 ## ----------------------------------------------------------------------
2294 ## functions returning short strings
2296 # CSS class for given age value (in seconds)
2300 if (!defined $age) {
2302 } elsif ($age < 60*60*2) {
2304 } elsif ($age < 60*60*24*2) {
2311 # convert age in seconds to "nn units ago" string
2316 if ($age > 60*60*24*365*2) {
2317 $age_str = (int $age/60/60/24/365);
2318 $age_str .= " years ago";
2319 } elsif ($age > 60*60*24*(365/12)*2) {
2320 $age_str = int $age/60/60/24/(365/12);
2321 $age_str .= " months ago";
2322 } elsif ($age > 60*60*24*7*2) {
2323 $age_str = int $age/60/60/24/7;
2324 $age_str .= " weeks ago";
2325 } elsif ($age > 60*60*24*2) {
2326 $age_str = int $age/60/60/24;
2327 $age_str .= " days ago";
2328 } elsif ($age > 60*60*2) {
2329 $age_str = int $age/60/60;
2330 $age_str .= " hours ago";
2331 } elsif ($age > 60*2) {
2332 $age_str = int $age/60;
2333 $age_str .= " min ago";
2334 } elsif ($age > 2) {
2335 $age_str = int $age;
2336 $age_str .= " sec ago";
2338 $age_str .= " right now";
2344 S_IFINVALID
=> 0030000,
2345 S_IFGITLINK
=> 0160000,
2348 # submodule/subproject, a commit object reference
2352 return (($mode & S_IFMT
) == S_IFGITLINK
)
2355 # convert file mode in octal to symbolic file mode string
2357 my $mode = oct shift;
2359 if (S_ISGITLINK
($mode)) {
2360 return 'm---------';
2361 } elsif (S_ISDIR
($mode & S_IFMT
)) {
2362 return 'drwxr-xr-x';
2363 } elsif (S_ISLNK
($mode)) {
2364 return 'lrwxrwxrwx';
2365 } elsif (S_ISREG
($mode)) {
2366 # git cares only about the executable bit
2367 if ($mode & S_IXUSR
) {
2368 return '-rwxr-xr-x';
2370 return '-rw-r--r--';
2373 return '----------';
2377 # convert file mode in octal to file type string
2381 if ($mode !~ m/^[0-7]+$/) {
2387 if (S_ISGITLINK
($mode)) {
2389 } elsif (S_ISDIR
($mode & S_IFMT
)) {
2391 } elsif (S_ISLNK
($mode)) {
2393 } elsif (S_ISREG
($mode)) {
2400 # convert file mode in octal to file type description string
2401 sub file_type_long
{
2404 if ($mode !~ m/^[0-7]+$/) {
2410 if (S_ISGITLINK
($mode)) {
2412 } elsif (S_ISDIR
($mode & S_IFMT
)) {
2414 } elsif (S_ISLNK
($mode)) {
2416 } elsif (S_ISREG
($mode)) {
2417 if ($mode & S_IXUSR
) {
2418 return "executable";
2428 ## ----------------------------------------------------------------------
2429 ## functions returning short HTML fragments, or transforming HTML fragments
2430 ## which don't belong to other sections
2432 # format line of commit message.
2433 sub format_log_line_html
{
2436 $line = esc_html
($line, -nbsp
=>1);
2437 $line =~ s
{\b([0-9a
-fA
-F
]{8,40})\b}{
2438 $cgi->a({-href
=> href
(action
=>"object", hash
=>$1),
2439 -class => "text"}, $1);
2440 }eg
unless $line =~ /^\s*git-svn-id:/;
2445 # format marker of refs pointing to given object
2447 # the destination action is chosen based on object type and current context:
2448 # - for annotated tags, we choose the tag view unless it's the current view
2449 # already, in which case we go to shortlog view
2450 # - for other refs, we keep the current view if we're in history, shortlog or
2451 # log view, and select shortlog otherwise
2452 sub format_ref_marker
{
2453 my ($refs, $id) = @_;
2456 if (defined $refs->{$id}) {
2457 foreach my $ref (@
{$refs->{$id}}) {
2458 # this code exploits the fact that non-lightweight tags are the
2459 # only indirect objects, and that they are the only objects for which
2460 # we want to use tag instead of shortlog as action
2461 my ($type, $name) = qw();
2462 my $indirect = ($ref =~ s/\^\{\}$//);
2463 # e.g. tags/v2.6.11 or heads/next
2464 if ($ref =~ m!^(.*?)s?/(.*)$!) {
2473 $class .= " indirect" if $indirect;
2475 my $dest_action = "shortlog";
2478 $dest_action = "tag" unless $action eq "tag";
2479 } elsif ($action =~ /^(history|(short)?log)$/) {
2480 $dest_action = $action;
2484 $dest .= "refs/" unless $ref =~ m
!^refs
/!;
2487 my $link = $cgi->a({
2489 action
=>$dest_action,
2493 $markers .= "<span class=\"".esc_attr
($class)."\" title=\"".esc_attr
($ref)."\">" .
2499 return '<span class="refs">'. $markers . '</span>';
2505 # format, perhaps shortened and with markers, title line
2506 sub format_subject_html
{
2507 my ($long, $short, $href, $extra) = @_;
2508 $extra = '' unless defined($extra);
2510 if (length($short) < length($long)) {
2512 $long =~ s/[[:cntrl:]]/?/g;
2513 return $cgi->a({-href
=> $href, -class => "list subject",
2514 -title
=> to_utf8
($long)},
2515 esc_html
($short)) . $extra;
2517 return $cgi->a({-href
=> $href, -class => "list subject"},
2518 esc_html
($long)) . $extra;
2522 # Rather than recomputing the url for an email multiple times, we cache it
2523 # after the first hit. This gives a visible benefit in views where the avatar
2524 # for the same email is used repeatedly (e.g. shortlog).
2525 # The cache is shared by all avatar engines (currently gravatar only), which
2526 # are free to use it as preferred. Since only one avatar engine is used for any
2527 # given page, there's no risk for cache conflicts.
2528 our %avatar_cache = ();
2530 # Compute the picon url for a given email, by using the picon search service over at
2531 # http://www.cs.indiana.edu/picons/search.html
2533 my $email = lc shift;
2534 if (!$avatar_cache{$email}) {
2535 my ($user, $domain) = split('@', $email);
2536 $avatar_cache{$email} =
2537 "//www.cs.indiana.edu/cgi-pub/kinzler/piconsearch.cgi/" .
2539 "users+domains+unknown/up/single";
2541 return $avatar_cache{$email};
2544 # Compute the gravatar url for a given email, if it's not in the cache already.
2545 # Gravatar stores only the part of the URL before the size, since that's the
2546 # one computationally more expensive. This also allows reuse of the cache for
2547 # different sizes (for this particular engine).
2549 my $email = lc shift;
2551 $avatar_cache{$email} ||=
2552 "//www.gravatar.com/avatar/" .
2553 Digest
::MD5
::md5_hex
($email) . "?s=";
2554 return $avatar_cache{$email} . $size;
2557 # Insert an avatar for the given $email at the given $size if the feature
2559 sub git_get_avatar
{
2560 my ($email, %opts) = @_;
2561 my $pre_white = ($opts{-pad_before
} ?
" " : "");
2562 my $post_white = ($opts{-pad_after
} ?
" " : "");
2563 $opts{-size
} ||= 'default';
2564 my $size = $avatar_size{$opts{-size
}} || $avatar_size{'default'};
2566 if ($git_avatar eq 'gravatar') {
2567 $url = gravatar_url
($email, $size);
2568 } elsif ($git_avatar eq 'picon') {
2569 $url = picon_url
($email);
2571 # Other providers can be added by extending the if chain, defining $url
2572 # as needed. If no variant puts something in $url, we assume avatars
2573 # are completely disabled/unavailable.
2576 "<img width=\"$size\" " .
2577 "class=\"avatar\" " .
2578 "src=\"".esc_url
($url)."\" " .
2586 sub format_search_author
{
2587 my ($author, $searchtype, $displaytext) = @_;
2588 my $have_search = gitweb_check_feature
('search');
2592 if ($searchtype eq 'author') {
2593 $performed = "authored";
2594 } elsif ($searchtype eq 'committer') {
2595 $performed = "committed";
2598 return $cgi->a({-href
=> href
(action
=>"search", hash
=>$hash,
2599 searchtext
=>$author,
2600 searchtype
=>$searchtype), class=>"list",
2601 title
=>"Search for commits $performed by $author"},
2605 return $displaytext;
2609 # format the author name of the given commit with the given tag
2610 # the author name is chopped and escaped according to the other
2611 # optional parameters (see chop_str).
2612 sub format_author_html
{
2615 my $author = chop_and_escape_str
($co->{'author_name'}, @_);
2616 return "<$tag class=\"author\">" .
2617 format_search_author
($co->{'author_name'}, "author",
2618 git_get_avatar
($co->{'author_email'}, -pad_after
=> 1) .
2623 # format git diff header line, i.e. "diff --(git|combined|cc) ..."
2624 sub format_git_diff_header_line
{
2626 my $diffinfo = shift;
2627 my ($from, $to) = @_;
2629 if ($diffinfo->{'nparents'}) {
2631 $line =~ s!^(diff (.*?) )"?.*$!$1!;
2632 if ($to->{'href'}) {
2633 $line .= $cgi->a({-href
=> $to->{'href'}, -class => "path"},
2634 esc_path
($to->{'file'}));
2635 } else { # file was deleted (no href)
2636 $line .= esc_path
($to->{'file'});
2640 $line =~ s!^(diff (.*?) )"?a/.*$!$1!;
2641 if ($from->{'href'}) {
2642 $line .= $cgi->a({-href
=> $from->{'href'}, -class => "path"},
2643 'a/' . esc_path
($from->{'file'}));
2644 } else { # file was added (no href)
2645 $line .= 'a/' . esc_path
($from->{'file'});
2648 if ($to->{'href'}) {
2649 $line .= $cgi->a({-href
=> $to->{'href'}, -class => "path"},
2650 'b/' . esc_path
($to->{'file'}));
2651 } else { # file was deleted
2652 $line .= 'b/' . esc_path
($to->{'file'});
2656 return "<div class=\"diff header\">$line</div>\n";
2659 # format extended diff header line, before patch itself
2660 sub format_extended_diff_header_line
{
2662 my $diffinfo = shift;
2663 my ($from, $to) = @_;
2666 if ($line =~ s!^((copy|rename) from ).*$!$1! && $from->{'href'}) {
2667 $line .= $cgi->a({-href
=>$from->{'href'}, -class=>"path"},
2668 esc_path
($from->{'file'}));
2670 if ($line =~ s!^((copy|rename) to ).*$!$1! && $to->{'href'}) {
2671 $line .= $cgi->a({-href
=>$to->{'href'}, -class=>"path"},
2672 esc_path
($to->{'file'}));
2674 # match single <mode>
2675 if ($line =~ m/\s(\d{6})$/) {
2676 $line .= '<span class="info"> (' .
2677 file_type_long
($1) .
2681 if ($line =~ m/^index [0-9a-fA-F]{40},[0-9a-fA-F]{40}/) {
2682 # can match only for combined diff
2684 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
2685 if ($from->{'href'}[$i]) {
2686 $line .= $cgi->a({-href
=>$from->{'href'}[$i],
2688 substr($diffinfo->{'from_id'}[$i],0,7));
2693 $line .= ',' if ($i < $diffinfo->{'nparents'} - 1);
2696 if ($to->{'href'}) {
2697 $line .= $cgi->a({-href
=>$to->{'href'}, -class=>"hash"},
2698 substr($diffinfo->{'to_id'},0,7));
2703 } elsif ($line =~ m/^index [0-9a-fA-F]{40}..[0-9a-fA-F]{40}/) {
2704 # can match only for ordinary diff
2705 my ($from_link, $to_link);
2706 if ($from->{'href'}) {
2707 $from_link = $cgi->a({-href
=>$from->{'href'}, -class=>"hash"},
2708 substr($diffinfo->{'from_id'},0,7));
2710 $from_link = '0' x
7;
2712 if ($to->{'href'}) {
2713 $to_link = $cgi->a({-href
=>$to->{'href'}, -class=>"hash"},
2714 substr($diffinfo->{'to_id'},0,7));
2718 my ($from_id, $to_id) = ($diffinfo->{'from_id'}, $diffinfo->{'to_id'});
2719 $line =~ s!$from_id\.\.$to_id!$from_link..$to_link!;
2722 return $line . "<br/>\n";
2725 # format from-file/to-file diff header
2726 sub format_diff_from_to_header
{
2727 my ($from_line, $to_line, $diffinfo, $from, $to, @parents) = @_;
2732 #assert($line =~ m/^---/) if DEBUG;
2733 # no extra formatting for "^--- /dev/null"
2734 if (! $diffinfo->{'nparents'}) {
2735 # ordinary (single parent) diff
2736 if ($line =~ m!^--- "?a/!) {
2737 if ($from->{'href'}) {
2739 $cgi->a({-href
=>$from->{'href'}, -class=>"path"},
2740 esc_path
($from->{'file'}));
2743 esc_path
($from->{'file'});
2746 $result .= qq!<div
class="diff from_file">$line</div
>\n!;
2749 # combined diff (merge commit)
2750 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
2751 if ($from->{'href'}[$i]) {
2753 $cgi->a({-href
=>href
(action
=>"blobdiff",
2754 hash_parent
=>$diffinfo->{'from_id'}[$i],
2755 hash_parent_base
=>$parents[$i],
2756 file_parent
=>$from->{'file'}[$i],
2757 hash
=>$diffinfo->{'to_id'},
2759 file_name
=>$to->{'file'}),
2761 -title
=>"diff" . ($i+1)},
2764 $cgi->a({-href
=>$from->{'href'}[$i], -class=>"path"},
2765 esc_path
($from->{'file'}[$i]));
2767 $line = '--- /dev/null';
2769 $result .= qq!<div
class="diff from_file">$line</div
>\n!;
2774 #assert($line =~ m/^\+\+\+/) if DEBUG;
2775 # no extra formatting for "^+++ /dev/null"
2776 if ($line =~ m!^\+\+\+ "?b/!) {
2777 if ($to->{'href'}) {
2779 $cgi->a({-href
=>$to->{'href'}, -class=>"path"},
2780 esc_path
($to->{'file'}));
2783 esc_path
($to->{'file'});
2786 $result .= qq!<div
class="diff to_file">$line</div
>\n!;
2791 # create note for patch simplified by combined diff
2792 sub format_diff_cc_simplified
{
2793 my ($diffinfo, @parents) = @_;
2796 $result .= "<div class=\"diff header\">" .
2798 if (!is_deleted
($diffinfo)) {
2799 $result .= $cgi->a({-href
=> href
(action
=>"blob",
2801 hash
=>$diffinfo->{'to_id'},
2802 file_name
=>$diffinfo->{'to_file'}),
2804 esc_path
($diffinfo->{'to_file'}));
2806 $result .= esc_path
($diffinfo->{'to_file'});
2808 $result .= "</div>\n" . # class="diff header"
2809 "<div class=\"diff nodifferences\">" .
2811 "</div>\n"; # class="diff nodifferences"
2816 sub diff_line_class
{
2817 my ($line, $from, $to) = @_;
2822 if ($from && $to && ref($from->{'href'}) eq "ARRAY") {
2823 $num_sign = scalar @
{$from->{'href'}};
2826 my @diff_line_classifier = (
2827 { regexp
=> qr/^\@\@{$num_sign} /, class => "chunk_header"},
2828 { regexp
=> qr/^\\/, class => "incomplete" },
2829 { regexp
=> qr/^ {$num_sign}/, class => "ctx" },
2830 # classifier for context must come before classifier add/rem,
2831 # or we would have to use more complicated regexp, for example
2832 # qr/(?= {0,$m}\+)[+ ]{$num_sign}/, where $m = $num_sign - 1;
2833 { regexp
=> qr/^[+ ]{$num_sign}/, class => "add" },
2834 { regexp
=> qr/^[- ]{$num_sign}/, class => "rem" },
2836 for my $clsfy (@diff_line_classifier) {
2837 return $clsfy->{'class'}
2838 if ($line =~ $clsfy->{'regexp'});
2845 # assumes that $from and $to are defined and correctly filled,
2846 # and that $line holds a line of chunk header for unified diff
2847 sub format_unidiff_chunk_header
{
2848 my ($line, $from, $to) = @_;
2850 my ($from_text, $from_start, $from_lines, $to_text, $to_start, $to_lines, $section) =
2851 $line =~ m/^\@{2} (-(\d+)(?:,(\d+))?) (\+(\d+)(?:,(\d+))?) \@{2}(.*)$/;
2853 $from_lines = 0 unless defined $from_lines;
2854 $to_lines = 0 unless defined $to_lines;
2856 if ($from->{'href'}) {
2857 $from_text = $cgi->a({-href
=>"$from->{'href'}#l$from_start",
2858 -class=>"list"}, $from_text);
2860 if ($to->{'href'}) {
2861 $to_text = $cgi->a({-href
=>"$to->{'href'}#l$to_start",
2862 -class=>"list"}, $to_text);
2864 $line = "<span class=\"chunk_info\">@@ $from_text $to_text @@</span>" .
2865 "<span class=\"section\">" . esc_html
($section, -nbsp
=>1) . "</span>";
2869 # assumes that $from and $to are defined and correctly filled,
2870 # and that $line holds a line of chunk header for combined diff
2871 sub format_cc_diff_chunk_header
{
2872 my ($line, $from, $to) = @_;
2874 my ($prefix, $ranges, $section) = $line =~ m/^(\@+) (.*?) \@+(.*)$/;
2875 my (@from_text, @from_start, @from_nlines, $to_text, $to_start, $to_nlines);
2877 @from_text = split(' ', $ranges);
2878 for (my $i = 0; $i < @from_text; ++$i) {
2879 ($from_start[$i], $from_nlines[$i]) =
2880 (split(',', substr($from_text[$i], 1)), 0);
2883 $to_text = pop @from_text;
2884 $to_start = pop @from_start;
2885 $to_nlines = pop @from_nlines;
2887 $line = "<span class=\"chunk_info\">$prefix ";
2888 for (my $i = 0; $i < @from_text; ++$i) {
2889 if ($from->{'href'}[$i]) {
2890 $line .= $cgi->a({-href
=>"$from->{'href'}[$i]#l$from_start[$i]",
2891 -class=>"list"}, $from_text[$i]);
2893 $line .= $from_text[$i];
2897 if ($to->{'href'}) {
2898 $line .= $cgi->a({-href
=>"$to->{'href'}#l$to_start",
2899 -class=>"list"}, $to_text);
2903 $line .= " $prefix</span>" .
2904 "<span class=\"section\">" . esc_html
($section, -nbsp
=>1) . "</span>";
2908 # process patch (diff) line (not to be used for diff headers),
2909 # returning HTML-formatted (but not wrapped) line.
2910 # If the line is passed as a reference, it is treated as HTML and not
2912 sub format_diff_line
{
2913 my ($line, $diff_class, $from, $to) = @_;
2919 $line = untabify
($line);
2921 if ($from && $to && $line =~ m/^\@{2} /) {
2922 $line = format_unidiff_chunk_header
($line, $from, $to);
2923 } elsif ($from && $to && $line =~ m/^\@{3}/) {
2924 $line = format_cc_diff_chunk_header
($line, $from, $to);
2926 $line = esc_html
($line, -nbsp
=>1);
2930 my $diff_classes = "diff diff_body";
2931 $diff_classes .= " $diff_class" if ($diff_class);
2932 $line = "<div class=\"$diff_classes\">$line</div>\n";
2937 # Generates undef or something like "_snapshot_" or "snapshot (_tbz2_ _zip_)",
2938 # linked. Pass the hash of the tree/commit to snapshot.
2939 sub format_snapshot_links
{
2941 my $num_fmts = @snapshot_fmts;
2942 if ($num_fmts > 1) {
2943 # A parenthesized list of links bearing format names.
2944 # e.g. "snapshot (_tar.gz_ _zip_)"
2945 return "snapshot (" . join(' ', map
2952 }, $known_snapshot_formats{$_}{'display'})
2953 , @snapshot_fmts) . ")";
2954 } elsif ($num_fmts == 1) {
2955 # A single "snapshot" link whose tooltip bears the format name.
2957 my ($fmt) = @snapshot_fmts;
2963 snapshot_format
=>$fmt
2965 -title
=> "in format: $known_snapshot_formats{$fmt}{'display'}"
2967 } else { # $num_fmts == 0
2972 ## ......................................................................
2973 ## functions returning values to be passed, perhaps after some
2974 ## transformation, to other functions; e.g. returning arguments to href()
2976 # returns hash to be passed to href to generate gitweb URL
2977 # in -title key it returns description of link
2979 my $format = shift || 'Atom';
2980 my %res = (action
=> lc($format));
2981 my $matched_ref = 0;
2983 # feed links are possible only for project views
2984 return unless (defined $project);
2985 # some views should link to OPML, or to generic project feed,
2986 # or don't have specific feed yet (so they should use generic)
2987 return if (!$action || $action =~ /^(?:tags|heads|forks|tag|search)$/x);
2990 # branches refs uses 'refs/' + $get_branch_refs()[x] + '/' prefix
2991 # (fullname) to differentiate from tag links; this also makes
2992 # possible to detect branch links
2993 for my $ref (get_branch_refs
()) {
2994 if ((defined $hash_base && $hash_base =~ m!^refs/\Q$ref\E/(.*)$!) ||
2995 (defined $hash && $hash =~ m!^refs/\Q$ref\E/(.*)$!)) {
2997 $matched_ref = $ref;
3001 # find log type for feed description (title)
3003 if (defined $file_name) {
3004 $type = "history of $file_name";
3005 $type .= "/" if ($action eq 'tree');
3006 $type .= " on '$branch'" if (defined $branch);
3008 $type = "log of $branch" if (defined $branch);
3011 $res{-title
} = $type;
3012 $res{'hash'} = (defined $branch ?
"refs/$matched_ref/$branch" : undef);
3013 $res{'file_name'} = $file_name;
3018 ## ----------------------------------------------------------------------
3019 ## git utility subroutines, invoking git commands
3021 # returns path to the core git executable and the --git-dir parameter as list
3023 $number_of_git_cmds++;
3024 return $GIT, '--git-dir='.$git_dir;
3027 # opens a "-|" cmd pipe handle with 2>/dev/null and returns it
3030 # In order to be compatible with FCGI mode we must use POSIX
3031 # and access the STDERR_FILENO file descriptor directly
3033 use POSIX
qw(STDERR_FILENO dup dup2);
3035 open(my $null, '>', File
::Spec
->devnull) or die "couldn't open devnull: $!";
3036 (my $saveerr = dup
(STDERR_FILENO
)) or die "couldn't dup STDERR: $!";
3037 my $dup2ok = dup2
(fileno($null), STDERR_FILENO
);
3038 close($null) or !$dup2ok or die "couldn't close NULL: $!";
3039 $dup2ok or POSIX
::close($saveerr), die "couldn't dup NULL to STDERR: $!";
3040 my $result = open(my $fd, "-|", @_);
3041 $dup2ok = dup2
($saveerr, STDERR_FILENO
);
3042 POSIX
::close($saveerr) or !$dup2ok or die "couldn't close SAVEERR: $!";
3043 $dup2ok or die "couldn't dup SAVERR to STDERR: $!";
3045 return $result ?
$fd : undef;
3048 # opens a "-|" git_cmd pipe handle with 2>/dev/null and returns it
3050 return cmd_pipe git_cmd
(), @_;
3053 # quote the given arguments for passing them to the shell
3054 # quote_command("command", "arg 1", "arg with ' and ! characters")
3055 # => "'command' 'arg 1' 'arg with '\'' and '\!' characters'"
3056 # Try to avoid using this function wherever possible.
3059 map { my $a = $_; $a =~ s/(['!])/'\\$1'/g; "'$a'" } @_ );
3062 # get HEAD ref of given project as hash
3063 sub git_get_head_hash
{
3064 return git_get_full_hash
(shift, 'HEAD');
3067 sub git_get_full_hash
{
3068 return git_get_hash
(@_);
3071 sub git_get_short_hash
{
3072 return git_get_hash
(@_, '--short=7');
3076 my ($project, $hash, @options) = @_;
3077 my $o_git_dir = $git_dir;
3079 $git_dir = "$projectroot/$project";
3080 if (defined(my $fd = git_cmd_pipe
'rev-parse',
3081 '--verify', '-q', @options, $hash)) {
3083 chomp $retval if defined $retval;
3086 if (defined $o_git_dir) {
3087 $git_dir = $o_git_dir;
3092 # get type of given object
3096 defined(my $fd = git_cmd_pipe
"cat-file", '-t', $hash) or return;
3098 close $fd or return;
3103 # repository configuration
3104 our $config_file = '';
3107 # store multiple values for single key as anonymous array reference
3108 # single values stored directly in the hash, not as [ <value> ]
3109 sub hash_set_multi
{
3110 my ($hash, $key, $value) = @_;
3112 if (!exists $hash->{$key}) {
3113 $hash->{$key} = $value;
3114 } elsif (!ref $hash->{$key}) {
3115 $hash->{$key} = [ $hash->{$key}, $value ];
3117 push @
{$hash->{$key}}, $value;
3121 # return hash of git project configuration
3122 # optionally limited to some section, e.g. 'gitweb'
3123 sub git_parse_project_config
{
3124 my $section_regexp = shift;
3129 defined(my $fh = git_cmd_pipe
"config", '-z', '-l')
3132 while (my $keyval = to_utf8
(scalar <$fh>)) {
3134 my ($key, $value) = split(/\n/, $keyval, 2);
3136 hash_set_multi
(\
%config, $key, $value)
3137 if (!defined $section_regexp || $key =~ /^(?:$section_regexp)\./o);
3144 # convert config value to boolean: 'true' or 'false'
3145 # no value, number > 0, 'true' and 'yes' values are true
3146 # rest of values are treated as false (never as error)
3147 sub config_to_bool
{
3150 return 1 if !defined $val; # section.key
3152 # strip leading and trailing whitespace
3156 return (($val =~ /^\d+$/ && $val) || # section.key = 1
3157 ($val =~ /^(?:true|yes)$/i)); # section.key = true
3160 # convert config value to simple decimal number
3161 # an optional value suffix of 'k', 'm', or 'g' will cause the value
3162 # to be multiplied by 1024, 1048576, or 1073741824
3166 # strip leading and trailing whitespace
3170 if (my ($num, $unit) = ($val =~ /^([0-9]*)([kmg])$/i)) {
3172 # unknown unit is treated as 1
3173 return $num * ($unit eq 'g' ?
1073741824 :
3174 $unit eq 'm' ?
1048576 :
3175 $unit eq 'k' ?
1024 : 1);
3180 # convert config value to array reference, if needed
3181 sub config_to_multi
{
3184 return ref($val) ?
$val : (defined($val) ?
[ $val ] : []);
3187 sub git_get_project_config
{
3188 my ($key, $type) = @_;
3190 return unless defined $git_dir;
3193 return unless ($key);
3194 # only subsection, if exists, is case sensitive,
3195 # and not lowercased by 'git config -z -l'
3196 if (my ($hi, $mi, $lo) = ($key =~ /^([^.]*)\.(.*)\.([^.]*)$/)) {
3198 $key = join(".", lc($hi), $mi, lc($lo));
3199 return if ($lo =~ /\W/ || $hi =~ /\W/);
3203 return if ($key =~ /\W/);
3205 $key =~ s/^gitweb\.//;
3208 if (defined $type) {
3211 unless ($type eq 'bool' || $type eq 'int');
3215 if (!defined $config_file ||
3216 $config_file ne "$git_dir/config") {
3217 %config = git_parse_project_config
('gitweb');
3218 $config_file = "$git_dir/config";
3221 # check if config variable (key) exists
3222 return unless exists $config{"gitweb.$key"};
3225 if (!defined $type) {
3226 return $config{"gitweb.$key"};
3227 } elsif ($type eq 'bool') {
3228 # backward compatibility: 'git config --bool' returns true/false
3229 return config_to_bool
($config{"gitweb.$key"}) ?
'true' : 'false';
3230 } elsif ($type eq 'int') {
3231 return config_to_int
($config{"gitweb.$key"});
3233 return $config{"gitweb.$key"};
3236 # get hash of given path at given ref
3237 sub git_get_hash_by_path
{
3239 my $path = shift || return undef;
3244 defined(my $fd = git_cmd_pipe
"ls-tree", $base, "--", $path)
3245 or die_error
(500, "Open git-ls-tree failed");
3246 my $line = to_utf8
(scalar <$fd>);
3247 close $fd or return undef;
3249 if (!defined $line) {
3250 # there is no tree or hash given by $path at $base
3254 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'
3255 $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t/;
3256 if (defined $type && $type ne $2) {
3257 # type doesn't match
3263 # get path of entry with given hash at given tree-ish (ref)
3264 # used to get 'from' filename for combined diff (merge commit) for renames
3265 sub git_get_path_by_hash
{
3266 my $base = shift || return;
3267 my $hash = shift || return;
3271 defined(my $fd = git_cmd_pipe
"ls-tree", '-r', '-t', '-z', $base)
3273 while (my $line = to_utf8
(scalar <$fd>)) {
3276 #'040000 tree 595596a6a9117ddba9fe379b6b012b558bac8423 gitweb'
3277 #'100644 blob e02e90f0429be0d2a69b76571101f20b8f75530f gitweb/README'
3278 if ($line =~ m/(?:[0-9]+) (?:.+) $hash\t(.+)$/) {
3287 ## ......................................................................
3288 ## git utility functions, directly accessing git repository
3290 # get the value of config variable either from file named as the variable
3291 # itself in the repository ($GIT_DIR/$name file), or from gitweb.$name
3292 # configuration variable in the repository config file.
3293 sub git_get_file_or_project_config
{
3294 my ($path, $name) = @_;
3296 $git_dir = "$projectroot/$path";
3297 open my $fd, '<', "$git_dir/$name"
3298 or return git_get_project_config
($name);
3299 my $conf = to_utf8
(scalar <$fd>);
3301 if (defined $conf) {
3307 sub git_get_project_description
{
3309 return git_get_file_or_project_config
($path, 'description');
3312 sub git_get_project_category
{
3314 return git_get_file_or_project_config
($path, 'category');
3318 # supported formats:
3319 # * $GIT_DIR/ctags/<tagname> file (in 'ctags' subdirectory)
3320 # - if its contents is a number, use it as tag weight,
3321 # - otherwise add a tag with weight 1
3322 # * $GIT_DIR/ctags file, each line is a tag (with weight 1)
3323 # the same value multiple times increases tag weight
3324 # * `gitweb.ctag' multi-valued repo config variable
3325 sub git_get_project_ctags
{
3326 my $project = shift;
3329 $git_dir = "$projectroot/$project";
3330 if (opendir my $dh, "$git_dir/ctags") {
3331 my @files = grep { -f
$_ } map { "$git_dir/ctags/$_" } readdir($dh);
3332 foreach my $tagfile (@files) {
3333 open my $ct, '<', $tagfile
3339 (my $ctag = $tagfile) =~ s
#.*/##;
3340 $ctag = to_utf8
($ctag);
3341 if ($val =~ /^\d+$/) {
3342 $ctags->{$ctag} = $val;
3344 $ctags->{$ctag} = 1;
3349 } elsif (open my $fh, '<', "$git_dir/ctags") {
3350 while (my $line = to_utf8
(scalar <$fh>)) {
3352 $ctags->{$line}++ if $line;
3357 my $taglist = config_to_multi
(git_get_project_config
('ctag'));
3358 foreach my $tag (@
$taglist) {
3366 # return hash, where keys are content tags ('ctags'),
3367 # and values are sum of weights of given tag in every project
3368 sub git_gather_all_ctags
{
3369 my $projects = shift;
3372 foreach my $p (@
$projects) {
3373 foreach my $ct (keys %{$p->{'ctags'}}) {
3374 $ctags->{$ct} += $p->{'ctags'}->{$ct};
3381 sub git_populate_project_tagcloud
{
3382 my ($ctags, $action) = @_;
3384 # First, merge different-cased tags; tags vote on casing
3386 foreach (keys %$ctags) {
3387 $ctags_lc{lc $_}->{count
} += $ctags->{$_};
3388 if (not $ctags_lc{lc $_}->{topcount
}
3389 or $ctags_lc{lc $_}->{topcount
} < $ctags->{$_}) {
3390 $ctags_lc{lc $_}->{topcount
} = $ctags->{$_};
3391 $ctags_lc{lc $_}->{topname
} = $_;
3396 my $matched = $input_params{'ctag_filter'};
3397 if (eval { require HTML
::TagCloud
; 1; }) {
3398 $cloud = HTML
::TagCloud
->new;
3399 foreach my $ctag (sort keys %ctags_lc) {
3400 # Pad the title with spaces so that the cloud looks
3402 my $title = esc_html
($ctags_lc{$ctag}->{topname
});
3403 $title =~ s/ / /g;
3404 $title =~ s/^/ /g;
3405 $title =~ s/$/ /g;
3406 if (defined $matched && $matched eq $ctag) {
3407 $title = qq(<span
class="match">$title</span
>);
3409 $cloud->add($title, href
(-replay
=>1, action
=>$action, ctag_filter
=>$ctag),
3410 $ctags_lc{$ctag}->{count
});
3414 foreach my $ctag (keys %ctags_lc) {
3415 my $title = esc_html
($ctags_lc{$ctag}->{topname
}, -nbsp
=>1);
3416 if (defined $matched && $matched eq $ctag) {
3417 $title = qq(<span
class="match">$title</span
>);
3419 $cloud->{$ctag}{count
} = $ctags_lc{$ctag}->{count
};
3420 $cloud->{$ctag}{ctag
} =
3421 $cgi->a({-href
=>href
(-replay
=>1, action
=>$action, ctag_filter
=>$ctag)}, $title);
3427 sub git_show_project_tagcloud
{
3428 my ($cloud, $count) = @_;
3429 if (ref $cloud eq 'HTML::TagCloud') {
3430 return $cloud->html_and_css($count);
3432 my @tags = sort { $cloud->{$a}->{'count'} <=> $cloud->{$b}->{'count'} } keys %$cloud;
3434 '<div id="htmltagcloud"'.($project ?
'' : ' align="center"').'>' .
3436 $cloud->{$_}->{'ctag'}
3437 } splice(@tags, 0, $count)) .
3442 sub git_get_project_url_list
{
3445 $git_dir = "$projectroot/$path";
3446 open my $fd, '<', "$git_dir/cloneurl"
3447 or return wantarray ?
3448 @
{ config_to_multi
(git_get_project_config
('url')) } :
3449 config_to_multi
(git_get_project_config
('url'));
3450 my @git_project_url_list = map { chomp; to_utf8
($_) } <$fd>;
3453 return wantarray ?
@git_project_url_list : \
@git_project_url_list;
3456 sub git_get_projects_list
{
3457 my $filter = shift || '';
3458 my $paranoid = shift;
3461 if (-d
$projects_list) {
3462 # search in directory
3463 my $dir = $projects_list;
3464 # remove the trailing "/"
3466 my $pfxlen = length("$dir");
3467 my $pfxdepth = ($dir =~ tr!/!!);
3468 # when filtering, search only given subdirectory
3469 if ($filter && !$paranoid) {
3475 follow_fast
=> 1, # follow symbolic links
3476 follow_skip
=> 2, # ignore duplicates
3477 dangling_symlinks
=> 0, # ignore dangling symlinks, silently
3480 our $project_maxdepth;
3482 # skip project-list toplevel, if we get it.
3483 return if (m!^[/.]$!);
3484 # only directories can be git repositories
3485 return unless (-d
$_);
3486 # don't traverse too deep (Find is super slow on os x)
3487 # $project_maxdepth excludes depth of $projectroot
3488 if (($File::Find
::name
=~ tr!/!!) - $pfxdepth > $project_maxdepth) {
3489 $File::Find
::prune
= 1;
3493 my $path = substr($File::Find
::name
, $pfxlen + 1);
3494 # paranoidly only filter here
3495 if ($paranoid && $filter && $path !~ m!^\Q$filter\E/!) {
3498 # we check related file in $projectroot
3499 if (check_export_ok
("$projectroot/$path")) {
3500 push @list, { path
=> $path };
3501 $File::Find
::prune
= 1;
3506 } elsif (-f
$projects_list) {
3507 # read from file(url-encoded):
3508 # 'git%2Fgit.git Linus+Torvalds'
3509 # 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin'
3510 # 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman'
3511 open my $fd, '<', $projects_list or return;
3513 while (my $line = <$fd>) {
3515 my ($path, $owner) = split ' ', $line;
3516 $path = unescape
($path);
3517 $owner = unescape
($owner);
3518 if (!defined $path) {
3521 # if $filter is rpovided, check if $path begins with $filter
3522 if ($filter && $path !~ m!^\Q$filter\E/!) {
3525 if (check_export_ok
("$projectroot/$path")) {
3530 $pr->{'owner'} = to_utf8
($owner);
3540 # written with help of Tree::Trie module (Perl Artistic License, GPL compatibile)
3541 # as side effects it sets 'forks' field to list of forks for forked projects
3542 sub filter_forks_from_projects_list
{
3543 my $projects = shift;
3545 my %trie; # prefix tree of directories (path components)
3546 # generate trie out of those directories that might contain forks
3547 foreach my $pr (@
$projects) {
3548 my $path = $pr->{'path'};
3549 $path =~ s/\.git$//; # forks of 'repo.git' are in 'repo/' directory
3550 next if ($path =~ m!/$!); # skip non-bare repositories, e.g. 'repo/.git'
3551 next unless ($path); # skip '.git' repository: tests, git-instaweb
3552 next unless (-d
"$projectroot/$path"); # containing directory exists
3553 $pr->{'forks'} = []; # there can be 0 or more forks of project
3556 my @dirs = split('/', $path);
3557 # walk the trie, until either runs out of components or out of trie
3559 while (scalar @dirs &&
3560 exists($ref->{$dirs[0]})) {
3561 $ref = $ref->{shift @dirs};
3563 # create rest of trie structure from rest of components
3564 foreach my $dir (@dirs) {
3565 $ref = $ref->{$dir} = {};
3567 # create end marker, store $pr as a data
3568 $ref->{''} = $pr if (!exists $ref->{''});
3571 # filter out forks, by finding shortest prefix match for paths
3574 foreach my $pr (@
$projects) {
3578 foreach my $dir (split('/', $pr->{'path'})) {
3579 if (exists $ref->{''}) {
3580 # found [shortest] prefix, is a fork - skip it
3581 push @
{$ref->{''}{'forks'}}, $pr;
3584 if (!exists $ref->{$dir}) {
3585 # not in trie, cannot have prefix, not a fork
3586 push @filtered, $pr;
3589 # If the dir is there, we just walk one step down the trie.
3590 $ref = $ref->{$dir};
3592 # we ran out of trie
3593 # (shouldn't happen: it's either no match, or end marker)
3594 push @filtered, $pr;
3600 # note: fill_project_list_info must be run first,
3601 # for 'descr_long' and 'ctags' to be filled
3602 sub search_projects_list
{
3603 my ($projlist, %opts) = @_;
3604 my $tagfilter = $opts{'tagfilter'};
3605 my $search_re = $opts{'search_regexp'};
3608 unless ($tagfilter || $search_re);
3610 # searching projects require filling to be run before it;
3611 fill_project_list_info
($projlist,
3612 $tagfilter ?
'ctags' : (),
3613 $search_re ?
('path', 'descr') : ());
3616 foreach my $pr (@
$projlist) {
3619 next unless ref($pr->{'ctags'}) eq 'HASH';
3621 grep { lc($_) eq lc($tagfilter) } keys %{$pr->{'ctags'}};
3625 my $path = $pr->{'path'};
3626 $path =~ s/\.git$//; # should not be included in search
3628 $path =~ /$search_re/ ||
3629 $pr->{'descr_long'} =~ /$search_re/;
3632 push @projects, $pr;
3638 our $gitweb_project_owner = undef;
3639 sub git_get_project_list_from_file
{
3641 return if (defined $gitweb_project_owner);
3643 $gitweb_project_owner = {};
3644 # read from file (url-encoded):
3645 # 'git%2Fgit.git Linus+Torvalds'
3646 # 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin'
3647 # 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman'
3648 if (-f
$projects_list) {
3649 open(my $fd, '<', $projects_list);
3650 while (my $line = <$fd>) {
3652 my ($pr, $ow) = split ' ', $line;
3653 $pr = unescape
($pr);
3654 $ow = unescape
($ow);
3655 $gitweb_project_owner->{$pr} = to_utf8
($ow);
3661 sub git_get_project_owner
{
3662 my $project = shift;
3665 return undef unless $project;
3666 $git_dir = "$projectroot/$project";
3668 if (!defined $gitweb_project_owner) {
3669 git_get_project_list_from_file
();
3672 if (exists $gitweb_project_owner->{$project}) {
3673 $owner = $gitweb_project_owner->{$project};
3675 if (!defined $owner){
3676 $owner = git_get_project_config
('owner');
3678 if (!defined $owner) {
3679 $owner = get_file_owner
("$git_dir");
3685 sub parse_activity_date
{
3690 if ($dstr =~ /^\s*([-+]?\d+)(?:\s+([-+]\d{4}))?\s*$/) {
3694 if ($dstr =~ /^\s*(\d{4})-(\d{2})-(\d{2})[Tt _](\d{1,2}):(\d{2}):(\d{2})(?:[ _]?([Zz]|(?:[-+]\d{1,2}:?\d{2})))?\s*$/) {
3695 my ($Y,$m,$d,$H,$M,$S,$z) = ($1,$2,$3,$4,$5,$6,$7||'');
3696 my $seconds = timegm
(0+$S, 0+$M, 0+$H, 0+$d, $m-1, $Y-1900);
3697 defined($z) && $z ne '' or $z = 'Z';
3699 substr($z,1,0) = '0' if length($z) == 4;
3701 if (uc($z) ne 'Z') {
3702 $off = 60 * (60 * (0+substr($z,1,2)) + (0+substr($z,3,2)));
3703 $off = -$off if substr($z,0,1) eq '-';
3705 return $seconds - $off;
3710 sub git_get_last_activity
{
3714 $git_dir = "$projectroot/$path";
3715 if ($lastactivity_file && open($fd, "<", "$git_dir/$lastactivity_file")) {
3716 my $activity = <$fd>;
3718 if (defined $activity &&
3719 (my $timestamp = parse_activity_date
($activity))) {
3720 return ($timestamp);
3723 defined($fd = git_cmd_pipe
'for-each-ref',
3724 '--format=%(committer)',
3725 '--sort=-committerdate',
3727 map { "refs/$_" } get_branch_refs
()) or return;
3728 my $most_recent = <$fd>;
3729 close $fd or return (undef);
3730 if (defined $most_recent &&
3731 $most_recent =~ / (\d+) [-+][01]\d\d\d$/) {
3733 return ($timestamp);
3738 # Implementation note: when a single remote is wanted, we cannot use 'git
3739 # remote show -n' because that command always work (assuming it's a remote URL
3740 # if it's not defined), and we cannot use 'git remote show' because that would
3741 # try to make a network roundtrip. So the only way to find if that particular
3742 # remote is defined is to walk the list provided by 'git remote -v' and stop if
3743 # and when we find what we want.
3744 sub git_get_remotes_list
{
3748 my $fd = git_cmd_pipe
'remote', '-v';
3750 while (my $remote = to_utf8
(scalar <$fd>)) {
3752 $remote =~ s!\t(.*?)\s+\((\w+)\)$!!;
3753 next if $wanted and not $remote eq $wanted;
3754 my ($url, $key) = ($1, $2);
3756 $remotes{$remote} ||= { 'heads' => [] };
3757 $remotes{$remote}{$key} = $url;
3759 close $fd or return;
3760 return wantarray ?
%remotes : \
%remotes;
3763 # Takes a hash of remotes as first parameter and fills it by adding the
3764 # available remote heads for each of the indicated remotes.
3765 sub fill_remote_heads
{
3766 my $remotes = shift;
3767 my @heads = map { "remotes/$_" } keys %$remotes;
3768 my @remoteheads = git_get_heads_list
(undef, @heads);
3769 foreach my $remote (keys %$remotes) {
3770 $remotes->{$remote}{'heads'} = [ grep {
3771 $_->{'name'} =~ s!^$remote/!!
3776 sub git_get_references
{
3777 my $type = shift || "";
3779 # 5dc01c595e6c6ec9ccda4f6f69c131c0dd945f8c refs/tags/v2.6.11
3780 # c39ae07f393806ccf406ef966e9a15afc43cc36a refs/tags/v2.6.11^{}
3781 defined(my $fd = git_cmd_pipe
"show-ref", "--dereference",
3782 ($type ?
("--", "refs/$type") : ())) # use -- <pattern> if $type
3785 while (my $line = to_utf8
(scalar <$fd>)) {
3787 if ($line =~ m!^([0-9a-fA-F]{40})\srefs/($type.*)$!) {
3788 if (defined $refs{$1}) {
3789 push @
{$refs{$1}}, $2;
3795 close $fd or return;
3799 sub git_get_rev_name_tags
{
3800 my $hash = shift || return undef;
3802 defined(my $fd = git_cmd_pipe
"name-rev", "--tags", $hash)
3804 my $name_rev = to_utf8
(scalar <$fd>);
3807 if ($name_rev =~ m
|^$hash tags
/(.*)$|) {
3810 # catches also '$hash undefined' output
3815 ## ----------------------------------------------------------------------
3816 ## parse to hash functions
3820 my $tz = shift || "-0000";
3823 my @months = ("Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec");
3824 my @days = ("Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat");
3825 my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($epoch);
3826 $date{'hour'} = $hour;
3827 $date{'minute'} = $min;
3828 $date{'mday'} = $mday;
3829 $date{'day'} = $days[$wday];
3830 $date{'month'} = $months[$mon];
3831 $date{'rfc2822'} = sprintf "%s, %d %s %4d %02d:%02d:%02d +0000",
3832 $days[$wday], $mday, $months[$mon], 1900+$year, $hour ,$min, $sec;
3833 $date{'mday-time'} = sprintf "%d %s %02d:%02d",
3834 $mday, $months[$mon], $hour ,$min;
3835 $date{'iso-8601'} = sprintf "%04d-%02d-%02dT%02d:%02d:%02dZ",
3836 1900+$year, 1+$mon, $mday, $hour ,$min, $sec;
3838 my ($tz_sign, $tz_hour, $tz_min) =
3839 ($tz =~ m/^([-+])(\d\d)(\d\d)$/);
3840 $tz_sign = ($tz_sign eq '-' ?
-1 : +1);
3841 my $local = $epoch + $tz_sign*((($tz_hour*60) + $tz_min)*60);
3842 ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($local);
3843 $date{'hour_local'} = $hour;
3844 $date{'minute_local'} = $min;
3845 $date{'mday_local'} = $mday;
3846 $date{'tz_local'} = $tz;
3847 $date{'iso-tz'} = sprintf("%04d-%02d-%02d %02d:%02d:%02d %s",
3848 1900+$year, $mon+1, $mday,
3849 $hour, $min, $sec, $tz);
3853 my %parse_date_rfc2822_month_names;
3855 %parse_date_rfc2822_month_names = (
3856 jan
=> 0, feb
=> 1, mar
=> 2, apr
=> 3, may
=> 4, jun
=> 5,
3857 jul
=> 6, aug
=> 7, sep
=> 8, oct => 9, nov
=> 10, dec
=> 11
3861 sub parse_date_rfc2822
{
3862 my $datestr = shift;
3863 return () unless defined $datestr;
3864 $datestr = $1 if $datestr =~/^[^\s]+,\s*(.*)$/;
3865 return () unless $datestr =~
3866 /^\s*(\d{1,2})\s+([A-Za-z]{3})\s+(\d{4})\s+(\d{1,2}):(\d{2}):(\d{2})\s+([+-]\d{4})\s*$/;
3867 my ($d,$b,$Y,$H,$M,$S,$z) = ($1,$2,$3,$4,$5,$6,$7);
3868 my $m = $parse_date_rfc2822_month_names{lc($b)};
3869 return () unless defined($m);
3870 my $seconds = timegm
(0+$S, 0+$M, 0+$H, 0+$d, 0+$m, $Y-1900);
3871 my $tzoffset = 60 * (60 * (0+substr($z,1,2)) + (0+substr($z,3,2)));
3872 $tzoffset = -$tzoffset if substr($z,0,1) eq '-';
3874 if ($tzoffset >= 0) {
3875 $tzstring = sprintf('+%02d%02d', int($tzoffset / 3600), int(($tzoffset % 3600) / 60));
3877 $tzstring = sprintf('-%02d%02d', int(-$tzoffset / 3600), int((-$tzoffset % 3600) / 60));
3879 return parse_date
($seconds - $tzoffset, $tzstring);
3887 defined(my $fd = git_cmd_pipe
"cat-file", "tag", $tag_id) or return;
3888 $tag{'id'} = $tag_id;
3889 while (my $line = to_utf8
(scalar <$fd>)) {
3891 if ($line =~ m/^object ([0-9a-fA-F]{40})$/) {
3892 $tag{'object'} = $1;
3893 } elsif ($line =~ m/^type (.+)$/) {
3895 } elsif ($line =~ m/^tag (.+)$/) {
3897 } elsif ($line =~ m/^tagger (.*) ([0-9]+) (.*)$/) {
3898 $tag{'author'} = $1;
3899 $tag{'author_epoch'} = $2;
3900 $tag{'author_tz'} = $3;
3901 if ($tag{'author'} =~ m/^([^<]+) <([^>]*)>/) {
3902 $tag{'author_name'} = $1;
3903 $tag{'author_email'} = $2;
3905 $tag{'author_name'} = $tag{'author'};
3907 } elsif ($line =~ m/--BEGIN/) {
3908 push @comment, $line;
3910 } elsif ($line eq "") {
3914 push @comment, map(to_utf8
($_), <$fd>);
3915 $tag{'comment'} = \
@comment;
3916 close $fd or return;
3917 if (!defined $tag{'name'}) {
3923 sub parse_commit_text
{
3924 my ($commit_text, $withparents) = @_;
3925 my @commit_lines = split '\n', $commit_text;
3928 pop @commit_lines; # Remove '\0'
3930 if (! @commit_lines) {
3934 my $header = shift @commit_lines;
3935 if ($header !~ m/^[0-9a-fA-F]{40}/) {
3938 ($co{'id'}, my @parents) = split ' ', $header;
3939 while (my $line = shift @commit_lines) {
3940 last if $line eq "\n";
3941 if ($line =~ m/^tree ([0-9a-fA-F]{40})$/) {
3943 } elsif ((!defined $withparents) && ($line =~ m/^parent ([0-9a-fA-F]{40})$/)) {
3945 } elsif ($line =~ m/^author (.*) ([0-9]+) (.*)$/) {
3946 $co{'author'} = to_utf8
($1);
3947 $co{'author_epoch'} = $2;
3948 $co{'author_tz'} = $3;
3949 if ($co{'author'} =~ m/^([^<]+) <([^>]*)>/) {
3950 $co{'author_name'} = $1;
3951 $co{'author_email'} = $2;
3953 $co{'author_name'} = $co{'author'};
3955 } elsif ($line =~ m/^committer (.*) ([0-9]+) (.*)$/) {
3956 $co{'committer'} = to_utf8
($1);
3957 $co{'committer_epoch'} = $2;
3958 $co{'committer_tz'} = $3;
3959 if ($co{'committer'} =~ m/^([^<]+) <([^>]*)>/) {
3960 $co{'committer_name'} = $1;
3961 $co{'committer_email'} = $2;
3963 $co{'committer_name'} = $co{'committer'};
3967 if (!defined $co{'tree'}) {
3970 $co{'parents'} = \
@parents;
3971 $co{'parent'} = $parents[0];
3973 @commit_lines = map to_utf8
($_), @commit_lines;
3974 foreach my $title (@commit_lines) {
3977 $co{'title'} = chop_str
($title, 80, 5);
3978 # remove leading stuff of merges to make the interesting part visible
3979 if (length($title) > 50) {
3980 $title =~ s/^Automatic //;
3981 $title =~ s/^merge (of|with) /Merge ... /i;
3982 if (length($title) > 50) {
3983 $title =~ s/(http|rsync):\/\///;
3985 if (length($title) > 50) {
3986 $title =~ s/(master|www|rsync)\.//;
3988 if (length($title) > 50) {
3989 $title =~ s/kernel.org:?//;
3991 if (length($title) > 50) {
3992 $title =~ s/\/pub\/scm//;
3995 $co{'title_short'} = chop_str
($title, 50, 5);
3999 if (! defined $co{'title'} || $co{'title'} eq "") {
4000 $co{'title'} = $co{'title_short'} = '(no commit message)';
4002 # remove added spaces
4003 foreach my $line (@commit_lines) {
4006 $co{'comment'} = \
@commit_lines;
4008 my $age = time - $co{'committer_epoch'};
4010 $co{'age_string'} = age_string
($age);
4011 my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($co{'committer_epoch'});
4012 if ($age > 60*60*24*7*2) {
4013 $co{'age_string_date'} = sprintf "%4i-%02u-%02i", 1900 + $year, $mon+1, $mday;
4014 $co{'age_string_age'} = $co{'age_string'};
4016 $co{'age_string_date'} = $co{'age_string'};
4017 $co{'age_string_age'} = sprintf "%4i-%02u-%02i", 1900 + $year, $mon+1, $mday;
4023 my ($commit_id) = @_;
4028 defined(my $fd = git_cmd_pipe
"rev-list",
4034 or die_error
(500, "Open git-rev-list failed");
4035 %co = parse_commit_text
(<$fd>, 1);
4042 my ($commit_id, $maxcount, $skip, $filename, @args) = @_;
4050 defined(my $fd = git_cmd_pipe
"rev-list",
4053 ("--max-count=" . $maxcount),
4054 ("--skip=" . $skip),
4058 ($filename ?
($filename) : ()))
4059 or die_error
(500, "Open git-rev-list failed");
4060 while (my $line = <$fd>) {
4061 my %co = parse_commit_text
($line);
4066 return wantarray ?
@cos : \
@cos;
4069 # parse line of git-diff-tree "raw" output
4070 sub parse_difftree_raw_line
{
4074 # ':100644 100644 03b218260e99b78c6df0ed378e59ed9205ccc96d 3b93d5e7cc7f7dd4ebed13a5cc1a4ad976fc94d8 M ls-files.c'
4075 # ':100644 100644 7f9281985086971d3877aca27704f2aaf9c448ce bc190ebc71bbd923f2b728e505408f5e54bd073a M rev-tree.c'
4076 if ($line =~ m/^:([0-7]{6}) ([0-7]{6}) ([0-9a-fA-F]{40}) ([0-9a-fA-F]{40}) (.)([0-9]{0,3})\t(.*)$/) {
4077 $res{'from_mode'} = $1;
4078 $res{'to_mode'} = $2;
4079 $res{'from_id'} = $3;
4081 $res{'status'} = $5;
4082 $res{'similarity'} = $6;
4083 if ($res{'status'} eq 'R' || $res{'status'} eq 'C') { # renamed or copied
4084 ($res{'from_file'}, $res{'to_file'}) = map { unquote
($_) } split("\t", $7);
4086 $res{'from_file'} = $res{'to_file'} = $res{'file'} = unquote
($7);
4089 # '::100755 100755 100755 60e79ca1b01bc8b057abe17ddab484699a7f5fdb 94067cc5f73388f33722d52ae02f44692bc07490 94067cc5f73388f33722d52ae02f44692bc07490 MR git-gui/git-gui.sh'
4090 # combined diff (for merge commit)
4091 elsif ($line =~ s/^(::+)((?:[0-7]{6} )+)((?:[0-9a-fA-F]{40} )+)([a-zA-Z]+)\t(.*)$//) {
4092 $res{'nparents'} = length($1);
4093 $res{'from_mode'} = [ split(' ', $2) ];
4094 $res{'to_mode'} = pop @
{$res{'from_mode'}};
4095 $res{'from_id'} = [ split(' ', $3) ];
4096 $res{'to_id'} = pop @
{$res{'from_id'}};
4097 $res{'status'} = [ split('', $4) ];
4098 $res{'to_file'} = unquote
($5);
4100 # 'c512b523472485aef4fff9e57b229d9d243c967f'
4101 elsif ($line =~ m/^([0-9a-fA-F]{40})$/) {
4102 $res{'commit'} = $1;
4105 return wantarray ?
%res : \
%res;
4108 # wrapper: return parsed line of git-diff-tree "raw" output
4109 # (the argument might be raw line, or parsed info)
4110 sub parsed_difftree_line
{
4111 my $line_or_ref = shift;
4113 if (ref($line_or_ref) eq "HASH") {
4114 # pre-parsed (or generated by hand)
4115 return $line_or_ref;
4117 return parse_difftree_raw_line
($line_or_ref);
4121 # parse line of git-ls-tree output
4122 sub parse_ls_tree_line
{
4128 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa 16717 panic.c'
4129 $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40}) +(-|[0-9]+)\t(.+)$/s;
4138 $res{'name'} = unquote
($5);
4141 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'
4142 $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t(.+)$/s;
4150 $res{'name'} = unquote
($4);
4154 return wantarray ?
%res : \
%res;
4157 # generates _two_ hashes, references to which are passed as 2 and 3 argument
4158 sub parse_from_to_diffinfo
{
4159 my ($diffinfo, $from, $to, @parents) = @_;
4161 if ($diffinfo->{'nparents'}) {
4163 $from->{'file'} = [];
4164 $from->{'href'} = [];
4165 fill_from_file_info
($diffinfo, @parents)
4166 unless exists $diffinfo->{'from_file'};
4167 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
4168 $from->{'file'}[$i] =
4169 defined $diffinfo->{'from_file'}[$i] ?
4170 $diffinfo->{'from_file'}[$i] :
4171 $diffinfo->{'to_file'};
4172 if ($diffinfo->{'status'}[$i] ne "A") { # not new (added) file
4173 $from->{'href'}[$i] = href
(action
=>"blob",
4174 hash_base
=>$parents[$i],
4175 hash
=>$diffinfo->{'from_id'}[$i],
4176 file_name
=>$from->{'file'}[$i]);
4178 $from->{'href'}[$i] = undef;
4182 # ordinary (not combined) diff
4183 $from->{'file'} = $diffinfo->{'from_file'};
4184 if ($diffinfo->{'status'} ne "A") { # not new (added) file
4185 $from->{'href'} = href
(action
=>"blob", hash_base
=>$hash_parent,
4186 hash
=>$diffinfo->{'from_id'},
4187 file_name
=>$from->{'file'});
4189 delete $from->{'href'};
4193 $to->{'file'} = $diffinfo->{'to_file'};
4194 if (!is_deleted
($diffinfo)) { # file exists in result
4195 $to->{'href'} = href
(action
=>"blob", hash_base
=>$hash,
4196 hash
=>$diffinfo->{'to_id'},
4197 file_name
=>$to->{'file'});
4199 delete $to->{'href'};
4203 ## ......................................................................
4204 ## parse to array of hashes functions
4206 sub git_get_heads_list
{
4207 my ($limit, @classes) = @_;
4208 @classes = get_branch_refs
() unless @classes;
4209 my @patterns = map { "refs/$_" } @classes;
4212 defined(my $fd = git_cmd_pipe
'for-each-ref',
4213 ($limit ?
'--count='.($limit+1) : ()), '--sort=-committerdate',
4214 '--format=%(objectname) %(refname) %(subject)%00%(committer)',
4217 while (my $line = to_utf8
(scalar <$fd>)) {
4221 my ($refinfo, $committerinfo) = split(/\0/, $line);
4222 my ($hash, $name, $title) = split(' ', $refinfo, 3);
4223 my ($committer, $epoch, $tz) =
4224 ($committerinfo =~ /^(.*) ([0-9]+) (.*)$/);
4225 $ref_item{'fullname'} = $name;
4226 my $strip_refs = join '|', map { quotemeta } get_branch_refs
();
4227 $name =~ s!^refs/($strip_refs|remotes)/!!;
4228 $ref_item{'name'} = $name;
4229 # for refs neither in 'heads' nor 'remotes' we want to
4230 # show their ref dir
4231 my $ref_dir = (defined $1) ?
$1 : '';
4232 if ($ref_dir ne '' and $ref_dir ne 'heads' and $ref_dir ne 'remotes') {
4233 $ref_item{'name'} .= ' (' . $ref_dir . ')';
4236 $ref_item{'id'} = $hash;
4237 $ref_item{'title'} = $title || '(no commit message)';
4238 $ref_item{'epoch'} = $epoch;
4240 $ref_item{'age'} = age_string
(time - $ref_item{'epoch'});
4242 $ref_item{'age'} = "unknown";
4245 push @headslist, \
%ref_item;
4249 return wantarray ?
@headslist : \
@headslist;
4252 sub git_get_tags_list
{
4255 my $all = shift || 0;
4256 my $order = shift || $default_refs_order;
4257 my $sortkey = $all && $order eq 'name' ?
'refname' : '-creatordate';
4259 defined(my $fd = git_cmd_pipe
'for-each-ref',
4260 ($limit ?
'--count='.($limit+1) : ()), "--sort=$sortkey",
4261 '--format=%(objectname) %(objecttype) %(refname) '.
4262 '%(*objectname) %(*objecttype) %(subject)%00%(creator)',
4263 ($all ?
'refs' : 'refs/tags'))
4265 while (my $line = to_utf8
(scalar <$fd>)) {
4269 my ($refinfo, $creatorinfo) = split(/\0/, $line);
4270 my ($id, $type, $name, $refid, $reftype, $title) = split(' ', $refinfo, 6);
4271 my ($creator, $epoch, $tz) =
4272 ($creatorinfo =~ /^(.*) ([0-9]+) (.*)$/);
4273 $ref_item{'fullname'} = $name;
4274 $name =~ s!^refs/!! if $all;
4275 $name =~ s!^refs/tags/!! unless $all;
4277 $ref_item{'type'} = $type;
4278 $ref_item{'id'} = $id;
4279 $ref_item{'name'} = $name;
4280 if ($type eq "tag") {
4281 $ref_item{'subject'} = $title;
4282 $ref_item{'reftype'} = $reftype;
4283 $ref_item{'refid'} = $refid;
4285 $ref_item{'reftype'} = $type;
4286 $ref_item{'refid'} = $id;
4289 if ($type eq "tag" || $type eq "commit") {
4290 $ref_item{'epoch'} = $epoch;
4292 $ref_item{'age'} = age_string
(time - $ref_item{'epoch'});
4294 $ref_item{'age'} = "unknown";
4298 push @tagslist, \
%ref_item;
4302 return wantarray ?
@tagslist : \
@tagslist;
4305 ## ----------------------------------------------------------------------
4306 ## filesystem-related functions
4308 sub get_file_owner
{
4311 my ($dev, $ino, $mode, $nlink, $st_uid, $st_gid, $rdev, $size) = stat($path);
4312 my ($name, $passwd, $uid, $gid, $quota, $comment, $gcos, $dir, $shell) = getpwuid($st_uid);
4313 if (!defined $gcos) {
4317 $owner =~ s/[,;].*$//;
4318 return to_utf8
($owner);
4321 # assume that file exists
4323 my $filename = shift;
4325 open my $fd, '<', $filename;
4332 ## ......................................................................
4333 ## mimetype related functions
4335 sub mimetype_guess_file
{
4336 my $filename = shift;
4337 my $mimemap = shift;
4338 my $rawmode = shift;
4339 -r
$mimemap or return undef;
4342 open(my $mh, '<', $mimemap) or return undef;
4344 next if m/^#/; # skip comments
4345 my ($mimetype, @exts) = split(/\s+/);
4346 foreach my $ext (@exts) {
4347 $mimemap{$ext} = $mimetype;
4353 $ext = $1 if $filename =~ /\.([^.]*)$/;
4354 $ans = $mimemap{$ext} if $ext;
4357 $ans = 'text/html' if $l eq 'application/xhtml+xml';
4359 $ans = 'text/xml' if $l =~ m
!^application
/[^\s
:;,=]+\
+xml
$! ||
4360 $l eq 'image/svg+xml' ||
4361 $l eq 'application/xml-dtd' ||
4362 $l eq 'application/xml-external-parsed-entity';
4368 sub mimetype_guess
{
4369 my $filename = shift;
4370 my $rawmode = shift;
4372 $filename =~ /\./ or return undef;
4374 if ($mimetypes_file) {
4375 my $file = $mimetypes_file;
4376 if ($file !~ m!^/!) { # if it is relative path
4377 # it is relative to project
4378 $file = "$projectroot/$project/$file";
4380 $mime = mimetype_guess_file
($filename, $file, $rawmode);
4382 $mime ||= mimetype_guess_file
($filename, '/etc/mime.types', $rawmode);
4388 my $filename = shift;
4389 my $rawmode = shift;
4392 # The -T/-B file operators produce the wrong result unless a perlio
4393 # layer is present when the file handle is a pipe that delivers less
4394 # than 512 bytes of data before reaching EOF.
4396 # If we are running in a Perl that uses the stdio layer rather than the
4397 # unix+perlio layers we will end up adding a perlio layer on top of the
4398 # stdio layer and get a second level of buffering. This is harmless
4399 # and it makes the -T/-B file operators work properly in all cases.
4401 binmode $fd, ":perlio" or die_error
(500, "Adding perlio layer failed")
4402 unless grep /^perlio$/, PerlIO
::get_layers
($fd);
4404 $mime = mimetype_guess
($filename, $rawmode) if defined $filename;
4406 if (!$mime && $filename) {
4407 if ($filename =~ m/\.html?$/i) {
4408 $mime = 'text/html';
4409 } elsif ($filename =~ m/\.xht(?:ml)?$/i) {
4410 $mime = 'text/html';
4411 } elsif ($filename =~ m/\.te?xt?$/i) {
4412 $mime = 'text/plain';
4413 } elsif ($filename =~ m/\.(?:markdown|md)$/i) {
4414 $mime = 'text/plain';
4415 } elsif ($filename =~ m/\.png$/i) {
4416 $mime = 'image/png';
4417 } elsif ($filename =~ m/\.gif$/i) {
4418 $mime = 'image/gif';
4419 } elsif ($filename =~ m/\.jpe?g$/i) {
4420 $mime = 'image/jpeg';
4421 } elsif ($filename =~ m/\.svgz?$/i) {
4422 $mime = 'image/svg+xml';
4427 return $default_blob_plain_mimetype || 'application/octet-stream' unless $fd || $mime;
4429 $mime = -T
$fd ?
'text/plain' : 'application/octet-stream' unless $mime;
4437 return scalar($data =~ /^[\x00-\x7f]*$/);
4442 return utf8
::decode
($data);
4445 sub extract_html_charset
{
4446 return undef unless $_[0] && "$_[0]</head>" =~ m
#<head(?:\s+[^>]*)?(?<!/)>(.*?)</head\s*>#is;
4448 return $2 if $head =~ m
#<meta\s+charset\s*=\s*(['"])\s*([a-z0-9(:)_.+-]+)\s*\1\s*/?>#is;
4449 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) {
4450 my %kv = (lc($1) => $3, lc($4) => $6);
4451 my ($he, $c) = (lc($kv{'http-equiv'}), $kv{'content'});
4452 return $1 if $he && $c && $he eq 'content-type' &&
4453 $c =~ m!\s*text/html\s*;\s*charset\s*=\s*([a-z0-9(:)_.+-]+)\s*$!is;
4458 sub blob_contenttype
{
4459 my ($fd, $file_name, $type) = @_;
4461 $type ||= blob_mimetype
($fd, $file_name, 1);
4462 return $type unless $type =~ m!^text/.+!i;
4463 my ($leader, $charset, $htmlcharset);
4464 if ($fd && read($fd, $leader, 32768)) {{
4465 $charset='US-ASCII' if is_ascii
($leader);
4466 return ("$type; charset=UTF-8", $leader) if !$charset && is_valid_utf8
($leader);
4467 $charset='ISO-8859-1' unless $charset;
4468 $htmlcharset = extract_html_charset
($leader) if $type eq 'text/html';
4469 if ($htmlcharset && $charset ne 'US-ASCII') {
4470 $htmlcharset = undef if $htmlcharset =~ /^(?:utf-8|us-ascii)$/i
4473 return ("$type; charset=$htmlcharset", $leader) if $htmlcharset;
4474 my $defcharset = $default_text_plain_charset || '';
4475 $defcharset =~ s/^\s+//;
4476 $defcharset =~ s/\s+$//;
4477 $defcharset = '' if $charset && $charset ne 'US-ASCII' && $defcharset =~ /^(?:utf-8|us-ascii)$/i;
4478 return ("$type; charset=" . ($defcharset || 'ISO-8859-1'), $leader);
4481 # peek the first upto 128 bytes off a file handle
4489 return '' unless $fd && read($fd, $prefix128, 128);
4491 # In the general case, we're guaranteed only to be able to ungetc one
4492 # character (provided, of course, we actually got a character first).
4496 # 1) we are dealing with a :perlio layer since blob_mimetype will have
4497 # already been called at least once on the file handle before us
4499 # 2) we have an $fd positioned at the start of the input stream and
4500 # therefore know we were positioned at a buffer boundary before
4501 # reading the initial upto 128 bytes
4503 # 3) the buffer size is at least 512 bytes
4505 # 4) we are careful to only unget raw bytes
4507 # 5) we are attempting to unget exactly the same number of bytes we got
4509 # Given the above conditions we will ALWAYS be able to safely unget
4510 # the $prefix128 value we just got.
4512 # In fact, we could read up to 511 bytes and still be sure.
4513 # (Reading 512 might pop us into the next internal buffer, but probably
4514 # not since that could break the always able to unget at least the one
4515 # you just got guarantee.)
4517 map {$fd->ungetc(ord($_))} reverse(split //, $prefix128);
4522 # guess file syntax for syntax highlighting; return undef if no highlighting
4523 # the name of syntax can (in the future) depend on syntax highlighter used
4524 sub guess_file_syntax
{
4525 my ($fd, $mimetype, $file_name) = @_;
4526 return undef unless $fd && defined $file_name &&
4527 defined $mimetype && $mimetype =~ m!^text/.+!i;
4528 my $basename = basename
($file_name, '.in');
4529 return $highlight_basename{$basename}
4530 if exists $highlight_basename{$basename};
4532 # Peek to see if there's a shebang or xml line.
4533 # We always operate on bytes when testing this.
4536 my $shebang = peek128bytes
($fd);
4537 if (length($shebang) >= 4 && $shebang =~ /^#!/) { # 4 would be '#!/x'
4538 foreach my $key (keys %highlight_shebang) {
4539 my $ar = ref($highlight_shebang{$key}) ?
4540 $highlight_shebang{$key} :
4541 [$highlight_shebang{key
}];
4542 map {return $key if $shebang =~ /$_/} @
$ar;
4545 return 'xml' if $shebang =~ m!^\s*<\?xml\s!; # "xml" must be lowercase
4548 $basename =~ /\.([^.]*)$/;
4549 my $ext = $1 or return undef;
4550 return $highlight_ext{$ext}
4551 if exists $highlight_ext{$ext};
4556 # run highlighter and return FD of its output,
4557 # or return original FD if no highlighting
4558 sub run_highlighter
{
4559 my ($fd, $syntax) = @_;
4560 return $fd unless $fd && !eof($fd) && defined $highlight_bin && defined $syntax;
4562 defined(my $hifd = cmd_pipe
$posix_shell_bin, '-c',
4563 quote_command
(git_cmd
(), "cat-file", "blob", $hash)." | ".
4564 quote_command
($highlight_bin).
4565 " --replace-tabs=8 --fragment --syntax $syntax")
4566 or die_error
(500, "Couldn't open file or run syntax highlighter");
4568 # just in case, should not happen as we tested !eof($fd) above
4569 return $fd if close($hifd);
4572 !$! or die_error
(500, "Couldn't close syntax highighter pipe");
4574 # leaving us with the only possibility a non-zero exit status (possibly a signal);
4575 # instead of dying horribly on this, just skip the highlighting
4576 # but do output a message about it to STDERR that will end up in the log
4577 print STDERR
"warning: skipping failed highlight for --syntax $syntax: ".
4578 sprintf("child exit status 0x%x\n", $?
);
4585 ## ======================================================================
4586 ## functions printing HTML: header, footer, error page
4588 sub get_page_title
{
4589 my $title = to_utf8
($site_name);
4591 unless (defined $project) {
4592 if (defined $project_filter) {
4593 $title .= " - projects in '" . esc_path
($project_filter) . "'";
4597 $title .= " - " . to_utf8
($project);
4599 return $title unless (defined $action);
4600 my $action_print = $action eq 'blame_incremental' ?
'blame' : $action;
4601 $title .= "/$action_print"; # $action is US-ASCII (7bit ASCII)
4603 return $title unless (defined $file_name);
4604 $title .= " - " . esc_path
($file_name);
4605 if ($action eq "tree" && $file_name !~ m
|/$|) {
4612 sub get_content_type_html
{
4613 # We do not ever emit application/xhtml+xml since that gives us
4614 # no benefits and it makes many browsers (e.g. Firefox) exceedingly
4615 # strict, which is troublesome for example when showing user-supplied
4616 # README.html files.
4620 sub print_feed_meta
{
4621 if (defined $project) {
4622 my %href_params = get_feed_info
();
4623 if (!exists $href_params{'-title'}) {
4624 $href_params{'-title'} = 'log';
4627 foreach my $format (qw(RSS Atom)) {
4628 my $type = lc($format);
4630 '-rel' => 'alternate',
4631 '-title' => esc_attr
("$project - $href_params{'-title'} - $format feed"),
4632 '-type' => "application/$type+xml"
4635 $href_params{'extra_options'} = undef;
4636 $href_params{'action'} = $type;
4637 $link_attr{'-href'} = href
(%href_params);
4639 "rel=\"$link_attr{'-rel'}\" ".
4640 "title=\"$link_attr{'-title'}\" ".
4641 "href=\"$link_attr{'-href'}\" ".
4642 "type=\"$link_attr{'-type'}\" ".
4645 $href_params{'extra_options'} = '--no-merges';
4646 $link_attr{'-href'} = href
(%href_params);
4647 $link_attr{'-title'} .= ' (no merges)';
4649 "rel=\"$link_attr{'-rel'}\" ".
4650 "title=\"$link_attr{'-title'}\" ".
4651 "href=\"$link_attr{'-href'}\" ".
4652 "type=\"$link_attr{'-type'}\" ".
4657 printf('<link rel="alternate" title="%s projects list" '.
4658 'href="%s" type="text/plain; charset=utf-8" />'."\n",
4659 esc_attr
($site_name), href
(project
=>undef, action
=>"project_index"));
4660 printf('<link rel="alternate" title="%s projects feeds" '.
4661 'href="%s" type="text/x-opml" />'."\n",
4662 esc_attr
($site_name), href
(project
=>undef, action
=>"opml"));
4666 sub print_header_links
{
4669 # print out each stylesheet that exist, providing backwards capability
4670 # for those people who defined $stylesheet in a config file
4671 if (defined $stylesheet) {
4672 print '<link rel="stylesheet" type="text/css" href="'.esc_url
($stylesheet).'"/>'."\n";
4674 foreach my $stylesheet (@stylesheets) {
4675 next unless $stylesheet;
4676 print '<link rel="stylesheet" type="text/css" href="'.esc_url
($stylesheet).'"/>'."\n";
4680 if ($status eq '200 OK');
4681 if (defined $favicon) {
4682 print qq(<link rel
="shortcut icon" href
=").esc_url($favicon).qq(" type
="image/png" />\n);
4686 sub print_nav_breadcrumbs_path
{
4687 my $dirprefix = undef;
4688 while (my $part = shift) {
4689 $dirprefix .= "/" if defined $dirprefix;
4690 $dirprefix .= $part;
4691 print $cgi->a({-href
=> href
(project
=> undef,
4692 project_filter
=> $dirprefix,
4693 action
=> "project_list")},
4694 esc_html
($part)) . " / ";
4698 sub print_nav_breadcrumbs
{
4701 for my $crumb (@extra_breadcrumbs, [ $home_link_str => $home_link ]) {
4702 print $cgi->a({-href
=> esc_url
($crumb->[1])}, $crumb->[0]) . " / ";
4704 if (defined $project) {
4705 my @dirname = split '/', $project;
4706 my $projectbasename = pop @dirname;
4707 print_nav_breadcrumbs_path
(@dirname);
4708 print $cgi->a({-href
=> href
(action
=>"summary")}, esc_html
($projectbasename));
4709 if (defined $action) {
4710 my $action_print = $action ;
4711 $action_print = 'blame' if $action_print eq 'blame_incremental';
4712 if (defined $opts{-action_extra
}) {
4713 $action_print = $cgi->a({-href
=> href
(action
=>$action)},
4716 print " / $action_print";
4718 if (defined $opts{-action_extra
}) {
4719 print " / $opts{-action_extra}";
4722 } elsif (defined $project_filter) {
4723 print_nav_breadcrumbs_path
(split '/', $project_filter);
4727 sub print_search_form
{
4728 if (!defined $searchtext) {
4732 if (defined $hash_base) {
4733 $search_hash = $hash_base;
4734 } elsif (defined $hash) {
4735 $search_hash = $hash;
4737 $search_hash = "HEAD";
4739 # We can't use href() here because we need to encode the
4740 # URL parameters into the form, not into the action link.
4741 my $action = $my_uri;
4742 my $use_pathinfo = gitweb_check_feature
('pathinfo');
4743 if ($use_pathinfo) {
4744 # See notes about doubled / in href()
4746 $action .= "/".esc_path_info
($project);
4748 print $cgi->start_form(-method
=> "get", -action
=> $action) .
4749 "<div class=\"search\">\n" .
4751 $cgi->input({-name
=>"p", -value
=>$project, -type
=>"hidden"}) . "\n") .
4752 $cgi->input({-name
=>"a", -value
=>"search", -type
=>"hidden"}) . "\n" .
4753 $cgi->input({-name
=>"h", -value
=>$search_hash, -type
=>"hidden"}) . "\n" .
4754 $cgi->popup_menu(-name
=> 'st', -default => 'commit',
4755 -values => ['commit', 'grep', 'author', 'committer', 'pickaxe']) .
4756 " " . $cgi->a({-href
=> href
(action
=>"search_help"),
4757 -title
=> "search help" }, "?") . " search:\n",
4758 $cgi->textfield(-name
=> "s", -value
=> $searchtext, -override
=> 1) . "\n" .
4759 "<span title=\"Extended regular expression\">" .
4760 $cgi->checkbox(-name
=> 'sr', -value
=> 1, -label
=> 're',
4761 -checked
=> $search_use_regexp) .
4764 $cgi->end_form() . "\n";
4767 sub git_header_html
{
4768 my $status = shift || "200 OK";
4769 my $expires = shift;
4772 my $title = get_page_title
();
4773 my $content_type = get_content_type_html
();
4774 print $cgi->header(-type
=>$content_type, -charset
=> 'utf-8',
4775 -status
=> $status, -expires
=> $expires)
4776 unless ($opts{'-no_http_header'});
4777 my $mod_perl_version = $ENV{'MOD_PERL'} ?
" $ENV{'MOD_PERL'}" : '';
4779 <?xml version="1.0" encoding="utf-8"?>
4780 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
4781 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US" lang="en-US">
4782 <!-- git web interface version $version, (C) 2005-2006, Kay Sievers <kay.sievers\@vrfy.org>, Christian Gierke -->
4783 <!-- git core binaries version $git_version -->
4785 <meta http-equiv="content-type" content="$content_type; charset=utf-8"/>
4786 <meta name="generator" content="gitweb/$version git/$git_version$mod_perl_version"/>
4787 <meta name="robots" content="index, nofollow"/>
4788 <title>$title</title>
4789 <script type="text/javascript">/* <![CDATA[ */
4790 function fixBlameLinks() {
4791 var allLinks = document.getElementsByTagName("a");
4792 for (var i = 0; i < allLinks.length; i++) {
4793 var link = allLinks.item(i);
4794 if (link.className == 'blamelink')
4795 link.href = link.href.replace("/blame/", "/blame_incremental/");
4800 # the stylesheet, favicon etc urls won't work correctly with path_info
4801 # unless we set the appropriate base URL
4802 if ($ENV{'PATH_INFO'}) {
4803 print "<base href=\"".esc_url
($base_url)."\" />\n";
4805 print_header_links
($status);
4807 if (defined $site_html_head_string) {
4808 print to_utf8
($site_html_head_string);
4814 if (defined $site_header && -f
$site_header) {
4815 insert_file
($site_header);
4818 print "<div class=\"page_header\">\n";
4819 if (defined $logo) {
4820 print $cgi->a({-href
=> esc_url
($logo_url),
4821 -title
=> $logo_label},
4822 $cgi->img({-src
=> esc_url
($logo),
4823 -width
=> 72, -height
=> 27,
4825 -class => "logo"}));
4827 print_nav_breadcrumbs
(%opts);
4830 my $have_search = gitweb_check_feature
('search');
4831 if (defined $project && $have_search) {
4832 print_search_form
();
4836 sub git_footer_html
{
4837 my $feed_class = 'rss_logo';
4839 print "<div class=\"page_footer\">\n";
4840 if (defined $project) {
4841 my $descr = git_get_project_description
($project);
4842 if (defined $descr) {
4843 print "<div class=\"page_footer_text\">" . esc_html
($descr) . "</div>\n";
4846 my %href_params = get_feed_info
();
4847 if (!%href_params) {
4848 $feed_class .= ' generic';
4850 $href_params{'-title'} ||= 'log';
4852 foreach my $format (qw(RSS Atom)) {
4853 $href_params{'action'} = lc($format);
4854 print $cgi->a({-href
=> href
(%href_params),
4855 -title
=> "$href_params{'-title'} $format feed",
4856 -class => $feed_class}, $format)."\n";
4860 print $cgi->a({-href
=> href
(project
=>undef, action
=>"opml",
4861 project_filter
=> $project_filter),
4862 -class => $feed_class}, "OPML") . " ";
4863 print $cgi->a({-href
=> href
(project
=>undef, action
=>"project_index",
4864 project_filter
=> $project_filter),
4865 -class => $feed_class}, "TXT") . "\n";
4867 print "</div>\n"; # class="page_footer"
4869 if (defined $t0 && gitweb_check_feature
('timed')) {
4870 print "<div id=\"generating_info\">\n";
4871 print 'This page took '.
4872 '<span id="generating_time" class="time_span">'.
4873 tv_interval
($t0, [ gettimeofday
() ]).
4876 '<span id="generating_cmd">'.
4877 $number_of_git_cmds.
4878 '</span> git commands '.
4880 print "</div>\n"; # class="page_footer"
4883 if (defined $site_footer && -f
$site_footer) {
4884 insert_file
($site_footer);
4887 print qq!<script type
="text/javascript" src
="!.esc_url($javascript).qq!"></script
>\n!;
4888 if (defined $action &&
4889 $action eq 'blame_incremental') {
4890 print qq!<script type
="text/javascript">\n!.
4891 qq!startBlame
("!. href(action=>"blame_data
", -replay=>1) .qq!",\n!.
4892 qq! "!. href() .qq!");\n!.
4895 my ($jstimezone, $tz_cookie, $datetime_class) =
4896 gitweb_get_feature
('javascript-timezone');
4898 print qq!<script type
="text/javascript">\n!.
4899 qq!window
.onload
= function
() {\n!;
4900 if (gitweb_check_feature
('blame_incremental')) {
4901 print qq! fixBlameLinks
();\n!;
4903 if (gitweb_check_feature
('javascript-actions')) {
4904 print qq! fixLinks
();\n!;
4906 if ($jstimezone && $tz_cookie && $datetime_class) {
4907 print qq! var tz_cookie
= { name
: '$tz_cookie', expires
: 14, path
: '/' };\n!. # in days
4908 qq! onloadTZSetup
('$jstimezone', tz_cookie
, '$datetime_class');\n!;
4918 # die_error(<http_status_code>, <error_message>[, <detailed_html_description>])
4919 # Example: die_error(404, 'Hash not found')
4920 # By convention, use the following status codes (as defined in RFC 2616):
4921 # 400: Invalid or missing CGI parameters, or
4922 # requested object exists but has wrong type.
4923 # 403: Requested feature (like "pickaxe" or "snapshot") not enabled on
4924 # this server or project.
4925 # 404: Requested object/revision/project doesn't exist.
4926 # 500: The server isn't configured properly, or
4927 # an internal error occurred (e.g. failed assertions caused by bugs), or
4928 # an unknown error occurred (e.g. the git binary died unexpectedly).
4929 # 503: The server is currently unavailable (because it is overloaded,
4930 # or down for maintenance). Generally, this is a temporary state.
4932 my $status = shift || 500;
4933 my $error = esc_html
(shift) || "Internal Server Error";
4937 my %http_responses = (
4938 400 => '400 Bad Request',
4939 403 => '403 Forbidden',
4940 404 => '404 Not Found',
4941 500 => '500 Internal Server Error',
4942 503 => '503 Service Unavailable',
4944 git_header_html
($http_responses{$status}, undef, %opts);
4946 <div class="page_body">
4951 if (defined $extra) {
4959 unless ($opts{'-error_handler'});
4962 ## ----------------------------------------------------------------------
4963 ## functions printing or outputting HTML: navigation
4965 sub git_print_page_nav
{
4966 my ($current, $suppress, $head, $treehead, $treebase, $extra) = @_;
4967 $extra = '' if !defined $extra; # pager or formats
4969 my @navs = qw(summary log commit commitdiff tree refs);
4971 @navs = grep { $_ ne $suppress } @navs;
4974 my %arg = map { $_ => {action
=>$_} } @navs;
4975 if (defined $head) {
4976 for (qw(commit commitdiff)) {
4977 $arg{$_}{'hash'} = $head;
4979 if ($current =~ m/^(tree | log | shortlog | commit | commitdiff | search)$/x) {
4980 $arg{'log'}{'hash'} = $head;
4984 $arg{'log'}{'action'} = 'shortlog';
4985 if ($current eq 'log') {
4986 $current = 'shortlog';
4987 } elsif ($current eq 'shortlog') {
4990 $arg{'tree'}{'hash'} = $treehead if defined $treehead;
4991 $arg{'tree'}{'hash_base'} = $treebase if defined $treebase;
4993 my @actions = gitweb_get_feature
('actions');
4994 my $escname = $project;
4995 $escname =~ s/[+]/%2B/g;
4998 'n' => $project, # project name
4999 'f' => $git_dir, # project path within filesystem
5000 'h' => $treehead || '', # current hash ('h' parameter)
5001 'b' => $treebase || '', # hash base ('hb' parameter)
5002 'e' => $escname, # project name with '+' escaped
5005 my ($label, $link, $pos) = splice(@actions,0,3);
5007 @navs = map { $_ eq $pos ?
($_, $label) : $_ } @navs;
5009 $link =~ s/%([%nfhbe])/$repl{$1}/g;
5010 $arg{$label}{'_href'} = $link;
5013 print "<div class=\"page_nav\">\n" .
5015 map { $_ eq $current ?
5016 $_ : $cgi->a({-href
=> ($arg{$_}{_href
} ?
$arg{$_}{_href
} : href
(%{$arg{$_}}))}, "$_")
5018 print "<br/>\n$extra<br/>\n" .
5022 # returns a submenu for the nagivation of the refs views (tags, heads,
5023 # remotes) with the current view disabled and the remotes view only
5024 # available if the feature is enabled
5025 sub format_ref_views
{
5027 my @ref_views = qw{tags heads
};
5028 push @ref_views, 'remotes' if gitweb_check_feature
('remote_heads');
5029 return join " | ", map {
5030 $_ eq $current ?
$_ :
5031 $cgi->a({-href
=> href
(action
=>$_)}, $_)
5035 sub format_paging_nav
{
5036 my ($action, $page, $has_next_link) = @_;
5042 $cgi->a({-href
=> href
(-replay
=>1, page
=>undef)}, "first") .
5044 $cgi->a({-href
=> href
(-replay
=>1, page
=>$page-1),
5045 -accesskey
=> "p", -title
=> "Alt-p"}, "prev");
5047 $paging_nav .= "first · prev";
5050 if ($has_next_link) {
5051 $paging_nav .= " · " .
5052 $cgi->a({-href
=> href
(-replay
=>1, page
=>$page+1),
5053 -accesskey
=> "n", -title
=> "Alt-n"}, "next");
5055 $paging_nav .= " · next";
5061 sub format_log_nav
{
5062 my ($action, $page, $has_next_link) = @_;
5065 if ($action eq 'shortlog') {
5066 $paging_nav .= 'shortlog';
5068 $paging_nav .= $cgi->a({-href
=> href
(action
=>'shortlog', -replay
=>1)}, 'shortlog');
5070 $paging_nav .= ' | ';
5071 if ($action eq 'log') {
5072 $paging_nav .= 'fulllog';
5074 $paging_nav .= $cgi->a({-href
=> href
(action
=>'log', -replay
=>1)}, 'fulllog');
5077 $paging_nav .= " | " . format_paging_nav
($action, $page, $has_next_link);
5081 ## ......................................................................
5082 ## functions printing or outputting HTML: div
5084 sub git_print_header_div
{
5085 my ($action, $title, $hash, $hash_base, $extra) = @_;
5087 defined $extra or $extra = '';
5089 $args{'action'} = $action;
5090 $args{'hash'} = $hash if $hash;
5091 $args{'hash_base'} = $hash_base if $hash_base;
5093 my $link1 = $cgi->a({-href
=> href
(%args), -class => "title"},
5094 $title ?
$title : $action);
5095 my $link2 = $cgi->a({-href
=> href
(%args), -class => "cover"}, "");
5096 print "<div class=\"header\">\n" . '<span class="title">' .
5097 $link1 . $extra . $link2 . '</span>' . "\n</div>\n";
5100 sub format_repo_url
{
5101 my ($name, $url) = @_;
5102 return "<tr class=\"metadata_url\"><td>$name</td><td>$url</td></tr>\n";
5105 # Group output by placing it in a DIV element and adding a header.
5106 # Options for start_div() can be provided by passing a hash reference as the
5107 # first parameter to the function.
5108 # Options to git_print_header_div() can be provided by passing an array
5109 # reference. This must follow the options to start_div if they are present.
5110 # The content can be a scalar, which is output as-is, a scalar reference, which
5111 # is output after html escaping, an IO handle passed either as *handle or
5112 # *handle{IO}, or a function reference. In the latter case all following
5113 # parameters will be taken as argument to the content function call.
5114 sub git_print_section
{
5115 my ($div_args, $header_args, $content);
5117 if (ref($arg) eq 'HASH') {
5121 if (ref($arg) eq 'ARRAY') {
5122 $header_args = $arg;
5127 print $cgi->start_div($div_args);
5128 git_print_header_div
(@
$header_args);
5130 if (ref($content) eq 'CODE') {
5132 } elsif (ref($content) eq 'SCALAR') {
5133 print esc_html
($$content);
5134 } elsif (ref($content) eq 'GLOB' or ref($content) eq 'IO::Handle') {
5135 while (<$content>) {
5138 } elsif (!ref($content) && defined($content)) {
5142 print $cgi->end_div;
5145 sub format_timestamp_html
{
5147 my $strtime = $date->{'rfc2822'};
5149 my (undef, undef, $datetime_class) =
5150 gitweb_get_feature
('javascript-timezone');
5151 if ($datetime_class) {
5152 $strtime = qq!<span
class="$datetime_class">$strtime</span
>!;
5155 my $localtime_format = '(%d %02d:%02d %s)';
5156 if ($date->{'hour_local'} < 6) {
5157 $localtime_format = '(<span class="atnight">%d %02d:%02d</span> %s)';
5160 sprintf($localtime_format, $date->{'mday_local'},
5161 $date->{'hour_local'}, $date->{'minute_local'}, $date->{'tz_local'});
5166 # Outputs the author name and date in long form
5167 sub git_print_authorship
{
5170 my $tag = $opts{-tag
} || 'div';
5171 my $author = $co->{'author_name'};
5173 my %ad = parse_date
($co->{'author_epoch'}, $co->{'author_tz'});
5174 print "<$tag class=\"author_date\">" .
5175 format_search_author
($author, "author", esc_html
($author)) .
5176 " [".format_timestamp_html
(\
%ad)."]".
5177 git_get_avatar
($co->{'author_email'}, -pad_before
=> 1) .
5181 # Outputs table rows containing the full author or committer information,
5182 # in the format expected for 'commit' view (& similar).
5183 # Parameters are a commit hash reference, followed by the list of people
5184 # to output information for. If the list is empty it defaults to both
5185 # author and committer.
5186 sub git_print_authorship_rows
{
5188 # too bad we can't use @people = @_ || ('author', 'committer')
5190 @people = ('author', 'committer') unless @people;
5191 foreach my $who (@people) {
5192 my %wd = parse_date
($co->{"${who}_epoch"}, $co->{"${who}_tz"});
5193 print "<tr><td>$who</td><td>" .
5194 format_search_author
($co->{"${who}_name"}, $who,
5195 esc_html
($co->{"${who}_name"})) . " " .
5196 format_search_author
($co->{"${who}_email"}, $who,
5197 esc_html
("<" . $co->{"${who}_email"} . ">")) .
5198 "</td><td rowspan=\"2\">" .
5199 git_get_avatar
($co->{"${who}_email"}, -size
=> 'double') .
5203 format_timestamp_html
(\
%wd) .
5209 sub git_print_page_path
{
5215 print "<div class=\"page_path\">";
5216 print $cgi->a({-href
=> href
(action
=>"tree", hash_base
=>$hb),
5217 -title
=> 'tree root'}, to_utf8
("[$project]"));
5219 if (defined $name) {
5220 my @dirname = split '/', $name;
5221 my $basename = pop @dirname;
5224 foreach my $dir (@dirname) {
5225 $fullname .= ($fullname ?
'/' : '') . $dir;
5226 print $cgi->a({-href
=> href
(action
=>"tree", file_name
=>$fullname,
5228 -title
=> $fullname}, esc_path
($dir));
5231 if (defined $type && $type eq 'blob') {
5232 print $cgi->a({-href
=> href
(action
=>"blob_plain", file_name
=>$file_name,
5234 -title
=> $name}, esc_path
($basename));
5235 } elsif (defined $type && $type eq 'tree') {
5236 print $cgi->a({-href
=> href
(action
=>"tree", file_name
=>$file_name,
5238 -title
=> $name}, esc_path
($basename));
5241 print esc_path
($basename);
5244 print "<br/></div>\n";
5251 if ($opts{'-remove_title'}) {
5252 # remove title, i.e. first line of log
5255 # remove leading empty lines
5256 while (defined $log->[0] && $log->[0] eq "") {
5261 my $skip_blank_line = 0;
5262 foreach my $line (@
$log) {
5263 if ($line =~ m/^\s*([A-Z][-A-Za-z]*-[Bb]y|C[Cc]): /) {
5264 if (! $opts{'-remove_signoff'}) {
5265 print "<span class=\"signoff\">" . esc_html
($line) . "</span><br/>\n";
5266 $skip_blank_line = 1;
5271 if ($line =~ m
,\s
*([a
-z
]*link): (https?
://\S
+),i
) {
5272 if (! $opts{'-remove_signoff'}) {
5273 print "<span class=\"signoff\">" . esc_html
($1) . ": " .
5274 "<a href=\"" . esc_html
($2) . "\">" . esc_html
($2) . "</a>" .
5276 $skip_blank_line = 1;
5281 # print only one empty line
5282 # do not print empty line after signoff
5284 next if ($skip_blank_line);
5285 $skip_blank_line = 1;
5287 $skip_blank_line = 0;
5290 print format_log_line_html
($line) . "<br/>\n";
5293 if ($opts{'-final_empty_line'}) {
5294 # end with single empty line
5295 print "<br/>\n" unless $skip_blank_line;
5299 # return link target (what link points to)
5300 sub git_get_link_target
{
5305 defined(my $fd = git_cmd_pipe
"cat-file", "blob", $hash)
5309 $link_target = to_utf8
(scalar <$fd>);
5314 return $link_target;
5317 # given link target, and the directory (basedir) the link is in,
5318 # return target of link relative to top directory (top tree);
5319 # return undef if it is not possible (including absolute links).
5320 sub normalize_link_target
{
5321 my ($link_target, $basedir) = @_;
5323 # absolute symlinks (beginning with '/') cannot be normalized
5324 return if (substr($link_target, 0, 1) eq '/');
5326 # normalize link target to path from top (root) tree (dir)
5329 $path = $basedir . '/' . $link_target;
5331 # we are in top (root) tree (dir)
5332 $path = $link_target;
5335 # remove //, /./, and /../
5337 foreach my $part (split('/', $path)) {
5338 # discard '.' and ''
5339 next if (!$part || $part eq '.');
5341 if ($part eq '..') {
5345 # link leads outside repository (outside top dir)
5349 push @path_parts, $part;
5352 $path = join('/', @path_parts);
5357 # print tree entry (row of git_tree), but without encompassing <tr> element
5358 sub git_print_tree_entry
{
5359 my ($t, $basedir, $hash_base, $have_blame) = @_;
5362 $base_key{'hash_base'} = $hash_base if defined $hash_base;
5364 # The format of a table row is: mode list link. Where mode is
5365 # the mode of the entry, list is the name of the entry, an href,
5366 # and link is the action links of the entry.
5368 print "<td class=\"mode\">" . mode_str
($t->{'mode'}) . "</td>\n";
5369 if (exists $t->{'size'}) {
5370 print "<td class=\"size\">$t->{'size'}</td>\n";
5372 if ($t->{'type'} eq "blob") {
5373 print "<td class=\"list\">" .
5374 $cgi->a({-href
=> href
(action
=>"blob", hash
=>$t->{'hash'},
5375 file_name
=>"$basedir$t->{'name'}", %base_key),
5376 -class => "list"}, esc_path
($t->{'name'}));
5377 if (S_ISLNK
(oct $t->{'mode'})) {
5378 my $link_target = git_get_link_target
($t->{'hash'});
5380 my $norm_target = normalize_link_target
($link_target, $basedir);
5381 if (defined $norm_target) {
5383 $cgi->a({-href
=> href
(action
=>"object", hash_base
=>$hash_base,
5384 file_name
=>$norm_target),
5385 -title
=> $norm_target}, esc_path
($link_target));
5387 print " -> " . esc_path
($link_target);
5392 print "<td class=\"link\">";
5393 print $cgi->a({-href
=> href
(action
=>"blob", hash
=>$t->{'hash'},
5394 file_name
=>"$basedir$t->{'name'}", %base_key)},
5398 $cgi->a({-href
=> href
(action
=>"blame", hash
=>$t->{'hash'},
5399 file_name
=>"$basedir$t->{'name'}", %base_key),
5400 -class => "blamelink"},
5403 if (defined $hash_base) {
5405 $cgi->a({-href
=> href
(action
=>"history", hash_base
=>$hash_base,
5406 hash
=>$t->{'hash'}, file_name
=>"$basedir$t->{'name'}")},
5410 $cgi->a({-href
=> href
(action
=>"blob_plain", hash_base
=>$hash_base,
5411 file_name
=>"$basedir$t->{'name'}")},
5415 } elsif ($t->{'type'} eq "tree") {
5416 print "<td class=\"list\">";
5417 print $cgi->a({-href
=> href
(action
=>"tree", hash
=>$t->{'hash'},
5418 file_name
=>"$basedir$t->{'name'}",
5420 esc_path
($t->{'name'}));
5422 print "<td class=\"link\">";
5423 print $cgi->a({-href
=> href
(action
=>"tree", hash
=>$t->{'hash'},
5424 file_name
=>"$basedir$t->{'name'}",
5427 if (defined $hash_base) {
5429 $cgi->a({-href
=> href
(action
=>"history", hash_base
=>$hash_base,
5430 file_name
=>"$basedir$t->{'name'}")},
5435 # unknown object: we can only present history for it
5436 # (this includes 'commit' object, i.e. submodule support)
5437 print "<td class=\"list\">" .
5438 esc_path
($t->{'name'}) .
5440 print "<td class=\"link\">";
5441 if (defined $hash_base) {
5442 print $cgi->a({-href
=> href
(action
=>"history",
5443 hash_base
=>$hash_base,
5444 file_name
=>"$basedir$t->{'name'}")},
5451 ## ......................................................................
5452 ## functions printing large fragments of HTML
5454 # get pre-image filenames for merge (combined) diff
5455 sub fill_from_file_info
{
5456 my ($diff, @parents) = @_;
5458 $diff->{'from_file'} = [ ];
5459 $diff->{'from_file'}[$diff->{'nparents'} - 1] = undef;
5460 for (my $i = 0; $i < $diff->{'nparents'}; $i++) {
5461 if ($diff->{'status'}[$i] eq 'R' ||
5462 $diff->{'status'}[$i] eq 'C') {
5463 $diff->{'from_file'}[$i] =
5464 git_get_path_by_hash
($parents[$i], $diff->{'from_id'}[$i]);
5471 # is current raw difftree line of file deletion
5473 my $diffinfo = shift;
5475 return $diffinfo->{'to_id'} eq ('0' x
40);
5478 # does patch correspond to [previous] difftree raw line
5479 # $diffinfo - hashref of parsed raw diff format
5480 # $patchinfo - hashref of parsed patch diff format
5481 # (the same keys as in $diffinfo)
5482 sub is_patch_split
{
5483 my ($diffinfo, $patchinfo) = @_;
5485 return defined $diffinfo && defined $patchinfo
5486 && $diffinfo->{'to_file'} eq $patchinfo->{'to_file'};
5490 sub git_difftree_body
{
5491 my ($difftree, $hash, @parents) = @_;
5492 my ($parent) = $parents[0];
5493 my $have_blame = gitweb_check_feature
('blame');
5494 print "<div class=\"list_head\">\n";
5495 if ($#{$difftree} > 10) {
5496 print(($#{$difftree} + 1) . " files changed:\n");
5500 print "<table class=\"" .
5501 (@parents > 1 ?
"combined " : "") .
5504 # header only for combined diff in 'commitdiff' view
5505 my $has_header = @
$difftree && @parents > 1 && $action eq 'commitdiff';
5508 print "<thead><tr>\n" .
5509 "<th></th><th></th>\n"; # filename, patchN link
5510 for (my $i = 0; $i < @parents; $i++) {
5511 my $par = $parents[$i];
5513 $cgi->a({-href
=> href
(action
=>"commitdiff",
5514 hash
=>$hash, hash_parent
=>$par),
5515 -title
=> 'commitdiff to parent number ' .
5516 ($i+1) . ': ' . substr($par,0,7)},
5520 print "</tr></thead>\n<tbody>\n";
5525 foreach my $line (@
{$difftree}) {
5526 my $diff = parsed_difftree_line
($line);
5529 print "<tr class=\"dark\">\n";
5531 print "<tr class=\"light\">\n";
5535 if (exists $diff->{'nparents'}) { # combined diff
5537 fill_from_file_info
($diff, @parents)
5538 unless exists $diff->{'from_file'};
5540 if (!is_deleted
($diff)) {
5541 # file exists in the result (child) commit
5543 $cgi->a({-href
=> href
(action
=>"blob", hash
=>$diff->{'to_id'},
5544 file_name
=>$diff->{'to_file'},
5546 -class => "list"}, esc_path
($diff->{'to_file'})) .
5550 esc_path
($diff->{'to_file'}) .
5554 if ($action eq 'commitdiff') {
5557 print "<td class=\"link\">" .
5558 $cgi->a({-href
=> href
(-anchor
=>"patch$patchno")},
5564 my $has_history = 0;
5565 my $not_deleted = 0;
5566 for (my $i = 0; $i < $diff->{'nparents'}; $i++) {
5567 my $hash_parent = $parents[$i];
5568 my $from_hash = $diff->{'from_id'}[$i];
5569 my $from_path = $diff->{'from_file'}[$i];
5570 my $status = $diff->{'status'}[$i];
5572 $has_history ||= ($status ne 'A');
5573 $not_deleted ||= ($status ne 'D');
5575 if ($status eq 'A') {
5576 print "<td class=\"link\" align=\"right\"> | </td>\n";
5577 } elsif ($status eq 'D') {
5578 print "<td class=\"link\">" .
5579 $cgi->a({-href
=> href
(action
=>"blob",
5582 file_name
=>$from_path)},
5586 if ($diff->{'to_id'} eq $from_hash) {
5587 print "<td class=\"link nochange\">";
5589 print "<td class=\"link\">";
5591 print $cgi->a({-href
=> href
(action
=>"blobdiff",
5592 hash
=>$diff->{'to_id'},
5593 hash_parent
=>$from_hash,
5595 hash_parent_base
=>$hash_parent,
5596 file_name
=>$diff->{'to_file'},
5597 file_parent
=>$from_path)},
5603 print "<td class=\"link\">";
5605 print $cgi->a({-href
=> href
(action
=>"blob",
5606 hash
=>$diff->{'to_id'},
5607 file_name
=>$diff->{'to_file'},
5610 print " | " if ($has_history);
5613 print $cgi->a({-href
=> href
(action
=>"history",
5614 file_name
=>$diff->{'to_file'},
5621 next; # instead of 'else' clause, to avoid extra indent
5623 # else ordinary diff
5625 my ($to_mode_oct, $to_mode_str, $to_file_type);
5626 my ($from_mode_oct, $from_mode_str, $from_file_type);
5627 if ($diff->{'to_mode'} ne ('0' x
6)) {
5628 $to_mode_oct = oct $diff->{'to_mode'};
5629 if (S_ISREG
($to_mode_oct)) { # only for regular file
5630 $to_mode_str = sprintf("%04o", $to_mode_oct & 0777); # permission bits
5632 $to_file_type = file_type
($diff->{'to_mode'});
5634 if ($diff->{'from_mode'} ne ('0' x
6)) {
5635 $from_mode_oct = oct $diff->{'from_mode'};
5636 if (S_ISREG
($from_mode_oct)) { # only for regular file
5637 $from_mode_str = sprintf("%04o", $from_mode_oct & 0777); # permission bits
5639 $from_file_type = file_type
($diff->{'from_mode'});
5642 if ($diff->{'status'} eq "A") { # created
5643 my $mode_chng = "<span class=\"file_status new\">[new $to_file_type";
5644 $mode_chng .= " with mode: $to_mode_str" if $to_mode_str;
5645 $mode_chng .= "]</span>";
5647 print $cgi->a({-href
=> href
(action
=>"blob", hash
=>$diff->{'to_id'},
5648 hash_base
=>$hash, file_name
=>$diff->{'file'}),
5649 -class => "list"}, esc_path
($diff->{'file'}));
5651 print "<td>$mode_chng</td>\n";
5652 print "<td class=\"link\">";
5653 if ($action eq 'commitdiff') {
5656 print $cgi->a({-href
=> href
(-anchor
=>"patch$patchno")},
5660 print $cgi->a({-href
=> href
(action
=>"blob", hash
=>$diff->{'to_id'},
5661 hash_base
=>$hash, file_name
=>$diff->{'file'})},
5665 } elsif ($diff->{'status'} eq "D") { # deleted
5666 my $mode_chng = "<span class=\"file_status deleted\">[deleted $from_file_type]</span>";
5668 print $cgi->a({-href
=> href
(action
=>"blob", hash
=>$diff->{'from_id'},
5669 hash_base
=>$parent, file_name
=>$diff->{'file'}),
5670 -class => "list"}, esc_path
($diff->{'file'}));
5672 print "<td>$mode_chng</td>\n";
5673 print "<td class=\"link\">";
5674 if ($action eq 'commitdiff') {
5677 print $cgi->a({-href
=> href
(-anchor
=>"patch$patchno")},
5681 print $cgi->a({-href
=> href
(action
=>"blob", hash
=>$diff->{'from_id'},
5682 hash_base
=>$parent, file_name
=>$diff->{'file'})},
5685 print $cgi->a({-href
=> href
(action
=>"blame", hash_base
=>$parent,
5686 file_name
=>$diff->{'file'}),
5687 -class => "blamelink"},
5690 print $cgi->a({-href
=> href
(action
=>"history", hash_base
=>$parent,
5691 file_name
=>$diff->{'file'})},
5695 } elsif ($diff->{'status'} eq "M" || $diff->{'status'} eq "T") { # modified, or type changed
5696 my $mode_chnge = "";
5697 if ($diff->{'from_mode'} != $diff->{'to_mode'}) {
5698 $mode_chnge = "<span class=\"file_status mode_chnge\">[changed";
5699 if ($from_file_type ne $to_file_type) {
5700 $mode_chnge .= " from $from_file_type to $to_file_type";
5702 if (($from_mode_oct & 0777) != ($to_mode_oct & 0777)) {
5703 if ($from_mode_str && $to_mode_str) {
5704 $mode_chnge .= " mode: $from_mode_str->$to_mode_str";
5705 } elsif ($to_mode_str) {
5706 $mode_chnge .= " mode: $to_mode_str";
5709 $mode_chnge .= "]</span>\n";
5712 print $cgi->a({-href
=> href
(action
=>"blob", hash
=>$diff->{'to_id'},
5713 hash_base
=>$hash, file_name
=>$diff->{'file'}),
5714 -class => "list"}, esc_path
($diff->{'file'}));
5716 print "<td>$mode_chnge</td>\n";
5717 print "<td class=\"link\">";
5718 if ($action eq 'commitdiff') {
5721 print $cgi->a({-href
=> href
(-anchor
=>"patch$patchno")},
5724 } elsif ($diff->{'to_id'} ne $diff->{'from_id'}) {
5725 # "commit" view and modified file (not onlu mode changed)
5726 print $cgi->a({-href
=> href
(action
=>"blobdiff",
5727 hash
=>$diff->{'to_id'}, hash_parent
=>$diff->{'from_id'},
5728 hash_base
=>$hash, hash_parent_base
=>$parent,
5729 file_name
=>$diff->{'file'})},
5733 print $cgi->a({-href
=> href
(action
=>"blob", hash
=>$diff->{'to_id'},
5734 hash_base
=>$hash, file_name
=>$diff->{'file'})},
5737 print $cgi->a({-href
=> href
(action
=>"blame", hash_base
=>$hash,
5738 file_name
=>$diff->{'file'}),
5739 -class => "blamelink"},
5742 print $cgi->a({-href
=> href
(action
=>"history", hash_base
=>$hash,
5743 file_name
=>$diff->{'file'})},
5747 } elsif ($diff->{'status'} eq "R" || $diff->{'status'} eq "C") { # renamed or copied
5748 my %status_name = ('R' => 'moved', 'C' => 'copied');
5749 my $nstatus = $status_name{$diff->{'status'}};
5751 if ($diff->{'from_mode'} != $diff->{'to_mode'}) {
5752 # mode also for directories, so we cannot use $to_mode_str
5753 $mode_chng = sprintf(", mode: %04o", $to_mode_oct & 0777);
5756 $cgi->a({-href
=> href
(action
=>"blob", hash_base
=>$hash,
5757 hash
=>$diff->{'to_id'}, file_name
=>$diff->{'to_file'}),
5758 -class => "list"}, esc_path
($diff->{'to_file'})) . "</td>\n" .
5759 "<td><span class=\"file_status $nstatus\">[$nstatus from " .
5760 $cgi->a({-href
=> href
(action
=>"blob", hash_base
=>$parent,
5761 hash
=>$diff->{'from_id'}, file_name
=>$diff->{'from_file'}),
5762 -class => "list"}, esc_path
($diff->{'from_file'})) .
5763 " with " . (int $diff->{'similarity'}) . "% similarity$mode_chng]</span></td>\n" .
5764 "<td class=\"link\">";
5765 if ($action eq 'commitdiff') {
5768 print $cgi->a({-href
=> href
(-anchor
=>"patch$patchno")},
5771 } elsif ($diff->{'to_id'} ne $diff->{'from_id'}) {
5772 # "commit" view and modified file (not only pure rename or copy)
5773 print $cgi->a({-href
=> href
(action
=>"blobdiff",
5774 hash
=>$diff->{'to_id'}, hash_parent
=>$diff->{'from_id'},
5775 hash_base
=>$hash, hash_parent_base
=>$parent,
5776 file_name
=>$diff->{'to_file'}, file_parent
=>$diff->{'from_file'})},
5780 print $cgi->a({-href
=> href
(action
=>"blob", hash
=>$diff->{'to_id'},
5781 hash_base
=>$parent, file_name
=>$diff->{'to_file'})},
5784 print $cgi->a({-href
=> href
(action
=>"blame", hash_base
=>$hash,
5785 file_name
=>$diff->{'to_file'}),
5786 -class => "blamelink"},
5789 print $cgi->a({-href
=> href
(action
=>"history", hash_base
=>$hash,
5790 file_name
=>$diff->{'to_file'})},
5794 } # we should not encounter Unmerged (U) or Unknown (X) status
5797 print "</tbody>" if $has_header;
5801 # Print context lines and then rem/add lines in a side-by-side manner.
5802 sub print_sidebyside_diff_lines
{
5803 my ($ctx, $rem, $add) = @_;
5805 # print context block before add/rem block
5808 '<div class="chunk_block ctx">',
5809 '<div class="old">',
5812 '<div class="new">',
5821 '<div class="chunk_block rem">',
5822 '<div class="old">',
5829 '<div class="chunk_block add">',
5830 '<div class="new">',
5836 '<div class="chunk_block chg">',
5837 '<div class="old">',
5840 '<div class="new">',
5847 # Print context lines and then rem/add lines in inline manner.
5848 sub print_inline_diff_lines
{
5849 my ($ctx, $rem, $add) = @_;
5851 print @
$ctx, @
$rem, @
$add;
5854 # Format removed and added line, mark changed part and HTML-format them.
5855 # Implementation is based on contrib/diff-highlight
5856 sub format_rem_add_lines_pair
{
5857 my ($rem, $add, $num_parents) = @_;
5859 # We need to untabify lines before split()'ing them;
5860 # otherwise offsets would be invalid.
5863 $rem = untabify
($rem);
5864 $add = untabify
($add);
5866 my @rem = split(//, $rem);
5867 my @add = split(//, $add);
5868 my ($esc_rem, $esc_add);
5869 # Ignore leading +/- characters for each parent.
5870 my ($prefix_len, $suffix_len) = ($num_parents, 0);
5871 my ($prefix_has_nonspace, $suffix_has_nonspace);
5873 my $shorter = (@rem < @add) ?
@rem : @add;
5874 while ($prefix_len < $shorter) {
5875 last if ($rem[$prefix_len] ne $add[$prefix_len]);
5877 $prefix_has_nonspace = 1 if ($rem[$prefix_len] !~ /\s/);
5881 while ($prefix_len + $suffix_len < $shorter) {
5882 last if ($rem[-1 - $suffix_len] ne $add[-1 - $suffix_len]);
5884 $suffix_has_nonspace = 1 if ($rem[-1 - $suffix_len] !~ /\s/);
5888 # Mark lines that are different from each other, but have some common
5889 # part that isn't whitespace. If lines are completely different, don't
5890 # mark them because that would make output unreadable, especially if
5891 # diff consists of multiple lines.
5892 if ($prefix_has_nonspace || $suffix_has_nonspace) {
5893 $esc_rem = esc_html_hl_regions
($rem, 'marked',
5894 [$prefix_len, @rem - $suffix_len], -nbsp
=>1);
5895 $esc_add = esc_html_hl_regions
($add, 'marked',
5896 [$prefix_len, @add - $suffix_len], -nbsp
=>1);
5898 $esc_rem = esc_html
($rem, -nbsp
=>1);
5899 $esc_add = esc_html
($add, -nbsp
=>1);
5902 return format_diff_line
(\
$esc_rem, 'rem'),
5903 format_diff_line
(\
$esc_add, 'add');
5906 # HTML-format diff context, removed and added lines.
5907 sub format_ctx_rem_add_lines
{
5908 my ($ctx, $rem, $add, $num_parents) = @_;
5909 my (@new_ctx, @new_rem, @new_add);
5910 my $can_highlight = 0;
5911 my $is_combined = ($num_parents > 1);
5913 # Highlight if every removed line has a corresponding added line.
5914 if (@
$add > 0 && @
$add == @
$rem) {
5917 # Highlight lines in combined diff only if the chunk contains
5918 # diff between the same version, e.g.
5925 # Otherwise the highlightling would be confusing.
5927 for (my $i = 0; $i < @
$add; $i++) {
5928 my $prefix_rem = substr($rem->[$i], 0, $num_parents);
5929 my $prefix_add = substr($add->[$i], 0, $num_parents);
5931 $prefix_rem =~ s/-/+/g;
5933 if ($prefix_rem ne $prefix_add) {
5941 if ($can_highlight) {
5942 for (my $i = 0; $i < @
$add; $i++) {
5943 my ($line_rem, $line_add) = format_rem_add_lines_pair
(
5944 $rem->[$i], $add->[$i], $num_parents);
5945 push @new_rem, $line_rem;
5946 push @new_add, $line_add;
5949 @new_rem = map { format_diff_line
($_, 'rem') } @
$rem;
5950 @new_add = map { format_diff_line
($_, 'add') } @
$add;
5953 @new_ctx = map { format_diff_line
($_, 'ctx') } @
$ctx;
5955 return (\
@new_ctx, \
@new_rem, \
@new_add);
5958 # Print context lines and then rem/add lines.
5959 sub print_diff_lines
{
5960 my ($ctx, $rem, $add, $diff_style, $num_parents) = @_;
5961 my $is_combined = $num_parents > 1;
5963 ($ctx, $rem, $add) = format_ctx_rem_add_lines
($ctx, $rem, $add,
5966 if ($diff_style eq 'sidebyside' && !$is_combined) {
5967 print_sidebyside_diff_lines
($ctx, $rem, $add);
5969 # default 'inline' style and unknown styles
5970 print_inline_diff_lines
($ctx, $rem, $add);
5974 sub print_diff_chunk
{
5975 my ($diff_style, $num_parents, $from, $to, @chunk) = @_;
5976 my (@ctx, @rem, @add);
5978 # The class of the previous line.
5979 my $prev_class = '';
5981 return unless @chunk;
5983 # incomplete last line might be among removed or added lines,
5984 # or both, or among context lines: find which
5985 for (my $i = 1; $i < @chunk; $i++) {
5986 if ($chunk[$i][0] eq 'incomplete') {
5987 $chunk[$i][0] = $chunk[$i-1][0];
5992 push @chunk, ["", ""];
5994 foreach my $line_info (@chunk) {
5995 my ($class, $line) = @
$line_info;
5997 # print chunk headers
5998 if ($class && $class eq 'chunk_header') {
5999 print format_diff_line
($line, $class, $from, $to);
6003 ## print from accumulator when have some add/rem lines or end
6004 # of chunk (flush context lines), or when have add and rem
6005 # lines and new block is reached (otherwise add/rem lines could
6007 if (!$class || ((@rem || @add) && $class eq 'ctx') ||
6008 (@rem && @add && $class ne $prev_class)) {
6009 print_diff_lines
(\
@ctx, \
@rem, \
@add,
6010 $diff_style, $num_parents);
6011 @ctx = @rem = @add = ();
6014 ## adding lines to accumulator
6017 # rem, add or change
6018 if ($class eq 'rem') {
6020 } elsif ($class eq 'add') {
6024 if ($class eq 'ctx') {
6028 $prev_class = $class;
6032 sub git_patchset_body
{
6033 my ($fd, $diff_style, $difftree, $hash, @hash_parents) = @_;
6034 my ($hash_parent) = $hash_parents[0];
6036 my $is_combined = (@hash_parents > 1);
6038 my $patch_number = 0;
6043 my @chunk; # for side-by-side diff
6045 print "<div class=\"patchset\">\n";
6047 # skip to first patch
6048 while ($patch_line = to_utf8
(scalar <$fd>)) {
6051 last if ($patch_line =~ m/^diff /);
6055 while ($patch_line) {
6057 # parse "git diff" header line
6058 if ($patch_line =~ m/^diff --git (\"(?:[^\\\"]*(?:\\.[^\\\"]*)*)\"|[^ "]*) (.*)$/) {
6059 # $1 is from_name, which we do not use
6060 $to_name = unquote
($2);
6061 $to_name =~ s!^b/!!;
6062 } elsif ($patch_line =~ m/^diff --(cc|combined) ("?.*"?)$/) {
6063 # $1 is 'cc' or 'combined', which we do not use
6064 $to_name = unquote
($2);
6069 # check if current patch belong to current raw line
6070 # and parse raw git-diff line if needed
6071 if (is_patch_split
($diffinfo, { 'to_file' => $to_name })) {
6072 # this is continuation of a split patch
6073 print "<div class=\"patch cont\">\n";
6075 # advance raw git-diff output if needed
6076 $patch_idx++ if defined $diffinfo;
6078 # read and prepare patch information
6079 $diffinfo = parsed_difftree_line
($difftree->[$patch_idx]);
6081 # compact combined diff output can have some patches skipped
6082 # find which patch (using pathname of result) we are at now;
6084 while ($to_name ne $diffinfo->{'to_file'}) {
6085 print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n" .
6086 format_diff_cc_simplified
($diffinfo, @hash_parents) .
6087 "</div>\n"; # class="patch"
6092 last if $patch_idx > $#$difftree;
6093 $diffinfo = parsed_difftree_line
($difftree->[$patch_idx]);
6097 # modifies %from, %to hashes
6098 parse_from_to_diffinfo
($diffinfo, \
%from, \
%to, @hash_parents);
6100 # this is first patch for raw difftree line with $patch_idx index
6101 # we index @$difftree array from 0, but number patches from 1
6102 print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n";
6106 #assert($patch_line =~ m/^diff /) if DEBUG;
6107 #assert($patch_line !~ m!$/$!) if DEBUG; # is chomp-ed
6109 # print "git diff" header
6110 print format_git_diff_header_line
($patch_line, $diffinfo,
6113 # print extended diff header
6114 print "<div class=\"diff extended_header\">\n";
6116 while ($patch_line = to_utf8
(scalar<$fd>)) {
6119 last EXTENDED_HEADER
if ($patch_line =~ m/^--- |^diff /);
6121 print format_extended_diff_header_line
($patch_line, $diffinfo,
6124 print "</div>\n"; # class="diff extended_header"
6126 # from-file/to-file diff header
6127 if (! $patch_line) {
6128 print "</div>\n"; # class="patch"
6131 next PATCH
if ($patch_line =~ m/^diff /);
6132 #assert($patch_line =~ m/^---/) if DEBUG;
6134 my $last_patch_line = $patch_line;
6135 $patch_line = to_utf8
(scalar <$fd>);
6137 #assert($patch_line =~ m/^\+\+\+/) if DEBUG;
6139 print format_diff_from_to_header
($last_patch_line, $patch_line,
6140 $diffinfo, \
%from, \
%to,
6145 while ($patch_line = to_utf8
(scalar <$fd>)) {
6148 next PATCH
if ($patch_line =~ m/^diff /);
6150 my $class = diff_line_class
($patch_line, \
%from, \
%to);
6152 if ($class eq 'chunk_header') {
6153 print_diff_chunk
($diff_style, scalar @hash_parents, \
%from, \
%to, @chunk);
6157 push @chunk, [ $class, $patch_line ];
6162 print_diff_chunk
($diff_style, scalar @hash_parents, \
%from, \
%to, @chunk);
6165 print "</div>\n"; # class="patch"
6168 # for compact combined (--cc) format, with chunk and patch simplification
6169 # the patchset might be empty, but there might be unprocessed raw lines
6170 for (++$patch_idx if $patch_number > 0;
6171 $patch_idx < @
$difftree;
6173 # read and prepare patch information
6174 $diffinfo = parsed_difftree_line
($difftree->[$patch_idx]);
6176 # generate anchor for "patch" links in difftree / whatchanged part
6177 print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n" .
6178 format_diff_cc_simplified
($diffinfo, @hash_parents) .
6179 "</div>\n"; # class="patch"
6184 if ($patch_number == 0) {
6185 if (@hash_parents > 1) {
6186 print "<div class=\"diff nodifferences\">Trivial merge</div>\n";
6188 print "<div class=\"diff nodifferences\">No differences found</div>\n";
6192 print "</div>\n"; # class="patchset"
6195 # . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
6197 sub git_project_search_form
{
6198 my ($searchtext, $search_use_regexp) = @_;
6201 if ($project_filter) {
6202 $limit = " in '$project_filter'";
6205 print "<div class=\"projsearch\">\n";
6206 print $cgi->start_form(-method
=> 'get', -action
=> $my_uri) .
6207 $cgi->hidden(-name
=> 'a', -value
=> 'project_list') . "\n";
6208 print $cgi->hidden(-name
=> 'pf', -value
=> $project_filter). "\n"
6209 if (defined $project_filter);
6210 print $cgi->textfield(-name
=> 's', -value
=> $searchtext,
6211 -title
=> "Search project by name and description$limit",
6212 -size
=> 60) . "\n" .
6213 "<span title=\"Extended regular expression\">" .
6214 $cgi->checkbox(-name
=> 'sr', -value
=> 1, -label
=> 're',
6215 -checked
=> $search_use_regexp) .
6217 $cgi->submit(-name
=> 'btnS', -value
=> 'Search') .
6218 $cgi->end_form() . "\n" .
6219 "<span class=\"projectlist_link\">" .
6220 $cgi->a({-href
=> href
(project
=> undef, searchtext
=> undef,
6221 action
=> 'project_list',
6222 project_filter
=> $project_filter)},
6223 esc_html
("List all projects$limit")) . "</span><br />\n";
6224 print "<span class=\"projectlist_link\">" .
6225 $cgi->a({-href
=> href
(project
=> undef, searchtext
=> undef,
6226 action
=> 'project_list',
6227 project_filter
=> undef)},
6228 esc_html
("List all projects")) . "</span>\n" if $project_filter;
6232 # entry for given @keys needs filling if at least one of keys in list
6233 # is not present in %$project_info
6234 sub project_info_needs_filling
{
6235 my ($project_info, @keys) = @_;
6237 # return List::MoreUtils::any { !exists $project_info->{$_} } @keys;
6238 foreach my $key (@keys) {
6239 if (!exists $project_info->{$key}) {
6246 sub git_cache_file_format
{
6247 return GITWEB_CACHE_FORMAT
.
6248 (gitweb_check_feature
('forks') ?
" (forks)" : "");
6251 sub git_retrieve_cache_file
{
6252 my $cache_file = shift;
6254 use Storable
qw(retrieve);
6256 if ((my $dump = eval { retrieve
($cache_file) })) {
6258 ref($dump) eq 'ARRAY' &&
6260 ref($$dump[1]) eq 'ARRAY' &&
6261 @
{$$dump[1]} == 2 &&
6262 ref(${$$dump[1]}[0]) eq 'ARRAY' &&
6263 ref(${$$dump[1]}[1]) eq 'HASH' &&
6264 $$dump[0] eq git_cache_file_format
();
6270 sub git_store_cache_file
{
6271 my ($cache_file, $cachedata) = @_;
6273 use File
::Basename
qw(dirname);
6275 use POSIX
qw(:fcntl_h);
6276 use Storable
qw(store_fd);
6279 my $cache_d = dirname
($cache_file);
6281 umask($mask & ~0070) if $cache_grpshared;
6282 if ((-d
$cache_d || mkdir($cache_d, $cache_grpshared ?
0770 : 0700)) &&
6283 sysopen(my $fd, "$cache_file.lock", O_WRONLY
|O_CREAT
|O_EXCL
, $cache_grpshared ?
0660 : 0600)) {
6284 store_fd
([git_cache_file_format
(), $cachedata], $fd);
6286 rename "$cache_file.lock", $cache_file;
6287 $result = stat($cache_file)->mtime;
6289 umask($mask) if $cache_grpshared;
6293 sub git_filter_cached_projects
{
6294 my ($cache, $projlist) = @_;
6296 my $c = ${$$cache[1]}{$_->{'path'}};
6297 defined $c ?
($_ = $c) : ()
6301 # fills project list info (age, description, owner, category, forks, etc.)
6302 # for each project in the list, removing invalid projects from
6303 # returned list, or fill only specified info.
6305 # Invalid projects are removed from the returned list if and only if you
6306 # ask 'age_epoch' to be filled, because they are the only fields
6307 # that run unconditionally git command that requires repository, and
6308 # therefore do always check if project repository is invalid.
6311 # * fill_project_list_info(\@project_list, 'descr_long', 'ctags')
6312 # ensures that 'descr_long' and 'ctags' fields are filled
6313 # * @project_list = fill_project_list_info(\@project_list)
6314 # ensures that all fields are filled (and invalid projects removed)
6316 # NOTE: modifies $projlist, but does not remove entries from it
6317 sub fill_project_list_info
{
6318 my ($projlist, @wanted_keys) = @_;
6322 my $cache_file = "$cache_dir/$projlist_cache_name";
6323 my $cache_lifetime = $projlist_cache_lifetime;
6324 $cache_lifetime = -1
6325 if $cache_lifetime && @wanted_keys && $wanted_keys[0] eq 'rebuild-cache';
6331 if ($cache_lifetime && -f
$cache_file) {
6332 $cache_mtime = stat($cache_file)->mtime;
6333 $cache_dump = undef if $cache_mtime &&
6334 (!$cache_dump_mtime || $cache_dump_mtime != $cache_mtime);
6336 if (defined $cache_mtime && # caching is on and $cache_file exists
6337 $cache_mtime + $cache_lifetime*60 > $now &&
6338 ($cache_dump || ($cache_dump = git_retrieve_cache_file
($cache_file)))) {
6340 $cache_dump_mtime = $cache_mtime;
6341 $stale = $now - $cache_mtime;
6342 @projects = git_filter_cached_projects
($cache_dump, $projlist);
6344 } else { # Cache miss.
6345 if (defined $cache_mtime) {
6346 # Postpone timeout by two minutes so that we get
6347 # enough time to do our job, or to be more exact
6348 # make cache expire after two minutes from now.
6349 my $time = $now - $cache_lifetime*60 + 120;
6350 utime $time, $time, $cache_file;
6352 if ($cache_lifetime) {
6353 my @all_projects = git_get_projects_list
();
6354 my %all_projects_filled = map { ( $_->{'path'} => $_ ) }
6355 fill_project_list_info_uncached
(\
@all_projects);
6356 map { $all_projects_filled{$_->{'path'}} = $_ }
6357 filter_forks_from_projects_list
([values(%all_projects_filled)])
6358 if gitweb_check_feature
('forks');
6359 $cache_dump = [[sort {$a->{'path'} cmp $b->{'path'}} values(%all_projects_filled)],
6360 \
%all_projects_filled];
6361 $cache_dump_mtime = git_store_cache_file
($cache_file, $cache_dump);
6362 @projects = git_filter_cached_projects
($cache_dump, $projlist);
6364 @projects = fill_project_list_info_uncached
($projlist, @wanted_keys);
6368 if ($cache_lifetime && $stale > 0) {
6369 print "<div class=\"stale_info\">Cached version (${stale}s old)</div>\n"
6370 unless $shown_stale_message;
6371 $shown_stale_message = 1;
6377 sub fill_project_list_info_uncached
{
6378 my ($projlist, @wanted_keys) = @_;
6380 my $filter_set = sub { return @_; };
6382 my %wanted_keys = map { $_ => 1 } @wanted_keys;
6383 $filter_set = sub { return grep { $wanted_keys{$_} } @_; };
6386 my $show_ctags = gitweb_check_feature
('ctags');
6388 foreach my $pr (@
$projlist) {
6389 if (project_info_needs_filling
($pr, $filter_set->('age_epoch'))) {
6390 my (@activity) = git_get_last_activity
($pr->{'path'});
6391 unless (@activity) {
6394 ($pr->{'age_epoch'}) = @activity;
6396 if (project_info_needs_filling
($pr, $filter_set->('descr', 'descr_long'))) {
6397 my $descr = git_get_project_description
($pr->{'path'}) || "";
6398 $descr = to_utf8
($descr);
6399 $pr->{'descr_long'} = $descr;
6400 $pr->{'descr'} = chop_str
($descr, $projects_list_description_width, 5);
6402 if (project_info_needs_filling
($pr, $filter_set->('owner'))) {
6403 $pr->{'owner'} = git_get_project_owner
("$pr->{'path'}") || "";
6406 project_info_needs_filling
($pr, $filter_set->('ctags'))) {
6407 $pr->{'ctags'} = git_get_project_ctags
($pr->{'path'});
6409 if ($projects_list_group_categories &&
6410 project_info_needs_filling
($pr, $filter_set->('category'))) {
6411 my $cat = git_get_project_category
($pr->{'path'}) ||
6412 $project_list_default_category;
6413 $pr->{'category'} = to_utf8
($cat);
6416 push @projects, $pr;
6422 sub sort_projects_list
{
6423 my ($projlist, $order) = @_;
6427 return sub { lc($a->{$key}) cmp lc($b->{$key}) };
6430 sub order_reverse_num_then_undef
{
6433 defined $a->{$key} ?
6434 (defined $b->{$key} ?
$b->{$key} <=> $a->{$key} : -1) :
6435 (defined $b->{$key} ?
1 : 0)
6440 project
=> order_str
('path'),
6441 descr
=> order_str
('descr_long'),
6442 owner
=> order_str
('owner'),
6443 age
=> order_reverse_num_then_undef
('age_epoch'),
6446 my $ordering = $orderings{$order};
6447 return defined $ordering ?
sort $ordering @
$projlist : @
$projlist;
6450 # returns a hash of categories, containing the list of project
6451 # belonging to each category
6452 sub build_projlist_by_category
{
6453 my ($projlist, $from, $to) = @_;
6456 $from = 0 unless defined $from;
6457 $to = $#$projlist if (!defined $to || $#$projlist < $to);
6459 for (my $i = $from; $i <= $to; $i++) {
6460 my $pr = $projlist->[$i];
6461 push @
{$categories{ $pr->{'category'} }}, $pr;
6464 return wantarray ?
%categories : \
%categories;
6467 # print 'sort by' <th> element, generating 'sort by $name' replay link
6468 # if that order is not selected
6470 print format_sort_th
(@_);
6473 sub format_sort_th
{
6474 my ($name, $order, $header) = @_;
6476 $header ||= ucfirst($name);
6478 if ($order eq $name) {
6479 $sort_th .= "<th>$header</th>\n";
6481 $sort_th .= "<th>" .
6482 $cgi->a({-href
=> href
(-replay
=>1, order
=>$name),
6483 -class => "header"}, $header) .
6490 sub git_project_list_rows
{
6491 my ($projlist, $from, $to, $check_forks) = @_;
6493 $from = 0 unless defined $from;
6494 $to = $#$projlist if (!defined $to || $#$projlist < $to);
6498 for (my $i = $from; $i <= $to; $i++) {
6499 my $pr = $projlist->[$i];
6502 print "<tr class=\"dark\">\n";
6504 print "<tr class=\"light\">\n";
6510 if ($pr->{'forks'}) {
6511 my $nforks = scalar @
{$pr->{'forks'}};
6512 my $s = $nforks == 1 ?
'' : 's';
6514 print $cgi->a({-href
=> href
(project
=>$pr->{'path'}, action
=>"forks"),
6515 -title
=> "$nforks fork$s"}, "+");
6517 print $cgi->span({-title
=> "$nforks fork$s"}, "+");
6522 my $path = $pr->{'path'};
6523 my $dotgit = $path =~ s/\.git$// ?
'.git' : '';
6524 print "<td>" . $cgi->a({-href
=> href
(project
=>$pr->{'path'}, action
=>"summary"),
6526 esc_html_match_hl
($path, $search_regexp).$dotgit) .
6528 "<td>" . $cgi->a({-href
=> href
(project
=>$pr->{'path'}, action
=>"summary"),
6530 -title
=> $pr->{'descr_long'}},
6532 ? esc_html_match_hl_chopped
($pr->{'descr_long'},
6533 $pr->{'descr'}, $search_regexp)
6534 : esc_html
($pr->{'descr'})) .
6536 unless ($omit_owner) {
6537 print "<td><i>" . ($owner_link_hook
6538 ?
$cgi->a({-href
=> $owner_link_hook->($pr->{'owner'}), -class => "list"},
6539 chop_and_escape_str
($pr->{'owner'}, 15))
6540 : chop_and_escape_str
($pr->{'owner'}, 15)) . "</i></td>\n";
6542 unless ($omit_age_column) {
6543 my ($age, $age_string, $age_epoch);
6544 if (defined($age_epoch = $pr->{'age_epoch'})) {
6545 $age = $now - $age_epoch;
6546 $age_string = age_string
($age);
6548 $age_string = "No commits";
6550 print "<td class=\"". age_class
($age) . "\">" . $age_string . "</td>\n";
6552 print"<td class=\"link\">" .
6553 $cgi->a({-href
=> href
(project
=>$pr->{'path'}, action
=>"summary")}, "summary") . " | " .
6554 $cgi->a({-href
=> href
(project
=>$pr->{'path'}, action
=>"shortlog")}, "log") . " | " .
6555 $cgi->a({-href
=> href
(project
=>$pr->{'path'}, action
=>"tree")}, "tree") .
6556 ($pr->{'forks'} ?
" | " . $cgi->a({-href
=> href
(project
=>$pr->{'path'}, action
=>"forks")}, "forks") : '') .
6562 sub git_project_list_body
{
6563 # actually uses global variable $project
6564 my ($projlist, $order, $from, $to, $extra, $no_header, $ctags_action, $keep_top) = @_;
6565 my @projects = @
$projlist;
6567 my $check_forks = gitweb_check_feature
('forks');
6568 my $show_ctags = gitweb_check_feature
('ctags');
6569 my $tagfilter = $show_ctags ?
$input_params{'ctag_filter'} : undef;
6570 $check_forks = undef
6571 if ($tagfilter || $search_regexp);
6573 # filtering out forks before filling info allows us to do less work
6575 @projects = filter_forks_from_projects_list
(\
@projects);
6576 push @projects, { 'path' => "$project_filter.git" }
6577 if $project_filter && $keep_top && is_valid_project
("$project_filter.git");
6579 # search_projects_list pre-fills required info
6580 @projects = search_projects_list
(\
@projects,
6581 'search_regexp' => $search_regexp,
6582 'tagfilter' => $tagfilter)
6583 if ($tagfilter || $search_regexp);
6585 my @all_fields = ('descr', 'descr_long', 'ctags', 'category');
6586 push @all_fields, 'age_epoch' unless($omit_age_column);
6587 push @all_fields, 'owner' unless($omit_owner);
6588 @projects = fill_project_list_info
(\
@projects, @all_fields);
6590 $order ||= $default_projects_order;
6591 $from = 0 unless defined $from;
6592 $to = $#projects if (!defined $to || $#projects < $to);
6597 "<b>No such projects found</b><br />\n".
6598 "Click ".$cgi->a({-href
=>href
(project
=>undef,action
=>'project_list')},"here")." to view all projects<br />\n".
6599 "</center>\n<br />\n";
6603 @projects = sort_projects_list
(\
@projects, $order);
6606 my $ctags = git_gather_all_ctags
(\
@projects);
6607 my $cloud = git_populate_project_tagcloud
($ctags, $ctags_action||'project_list');
6608 print git_show_project_tagcloud
($cloud, 64);
6611 print "<table class=\"project_list\">\n";
6612 unless ($no_header) {
6615 print "<th></th>\n";
6617 print_sort_th
('project', $order, 'Project');
6618 print_sort_th
('descr', $order, 'Description');
6619 print_sort_th
('owner', $order, 'Owner') unless $omit_owner;
6620 print_sort_th
('age', $order, 'Last Change') unless $omit_age_column;
6621 print "<th></th>\n" . # for links
6625 if ($projects_list_group_categories) {
6626 # only display categories with projects in the $from-$to window
6627 @projects = sort {$a->{'category'} cmp $b->{'category'}} @projects[$from..$to];
6628 my %categories = build_projlist_by_category
(\
@projects, $from, $to);
6629 foreach my $cat (sort keys %categories) {
6630 unless ($cat eq "") {
6633 print "<td></td>\n";
6635 print "<td class=\"category\" colspan=\"5\">".esc_html
($cat)."</td>\n";
6639 git_project_list_rows
($categories{$cat}, undef, undef, $check_forks);
6642 git_project_list_rows
(\
@projects, $from, $to, $check_forks);
6645 if (defined $extra) {
6648 print "<td></td>\n";
6650 print "<td colspan=\"5\">$extra</td>\n" .
6657 # uses global variable $project
6658 my ($commitlist, $from, $to, $refs, $extra) = @_;
6660 $from = 0 unless defined $from;
6661 $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
6663 for (my $i = 0; $i <= $to; $i++) {
6664 my %co = %{$commitlist->[$i]};
6666 my $commit = $co{'id'};
6667 my $ref = format_ref_marker
($refs, $commit);
6668 git_print_header_div
('commit',
6669 "<span class=\"age\">$co{'age_string'}</span>" .
6670 esc_html
($co{'title'}),
6671 $commit, undef, $ref);
6672 print "<div class=\"title_text\">\n" .
6673 "<div class=\"log_link\">\n" .
6674 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$commit)}, "commit") .
6676 $cgi->a({-href
=> href
(action
=>"commitdiff", hash
=>$commit)}, "commitdiff") .
6678 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$commit, hash_base
=>$commit)}, "tree") .
6681 git_print_authorship
(\
%co, -tag
=> 'span');
6682 print "<br/>\n</div>\n";
6684 print "<div class=\"log_body\">\n";
6685 git_print_log
($co{'comment'}, -final_empty_line
=> 1);
6689 print "<div class=\"page_nav\">\n";
6695 sub git_shortlog_body
{
6696 # uses global variable $project
6697 my ($commitlist, $from, $to, $refs, $extra) = @_;
6699 $from = 0 unless defined $from;
6700 $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
6702 print "<table class=\"shortlog\">\n";
6704 for (my $i = $from; $i <= $to; $i++) {
6705 my %co = %{$commitlist->[$i]};
6706 my $commit = $co{'id'};
6707 my $ref = format_ref_marker
($refs, $commit);
6709 print "<tr class=\"dark\">\n";
6711 print "<tr class=\"light\">\n";
6714 # git_summary() used print "<td><i>$co{'age_string'}</i></td>\n" .
6715 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
6716 format_author_html
('td', \
%co, 10) . "<td>";
6717 print format_subject_html
($co{'title'}, $co{'title_short'},
6718 href
(action
=>"commit", hash
=>$commit), $ref);
6720 "<td class=\"link\">" .
6721 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$commit)}, "commit") . " | " .
6722 $cgi->a({-href
=> href
(action
=>"commitdiff", hash
=>$commit)}, "commitdiff") . " | " .
6723 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$commit, hash_base
=>$commit)}, "tree");
6724 my $snapshot_links = format_snapshot_links
($commit);
6725 if (defined $snapshot_links) {
6726 print " | " . $snapshot_links;
6731 if (defined $extra) {
6733 "<td colspan=\"4\">$extra</td>\n" .
6739 sub git_history_body
{
6740 # Warning: assumes constant type (blob or tree) during history
6741 my ($commitlist, $from, $to, $refs, $extra,
6742 $file_name, $file_hash, $ftype) = @_;
6744 $from = 0 unless defined $from;
6745 $to = $#{$commitlist} unless (defined $to && $to <= $#{$commitlist});
6747 print "<table class=\"history\">\n";
6749 for (my $i = $from; $i <= $to; $i++) {
6750 my %co = %{$commitlist->[$i]};
6754 my $commit = $co{'id'};
6756 my $ref = format_ref_marker
($refs, $commit);
6759 print "<tr class=\"dark\">\n";
6761 print "<tr class=\"light\">\n";
6764 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
6765 # shortlog: format_author_html('td', \%co, 10)
6766 format_author_html
('td', \
%co, 15, 3) . "<td>";
6767 # originally git_history used chop_str($co{'title'}, 50)
6768 print format_subject_html
($co{'title'}, $co{'title_short'},
6769 href
(action
=>"commit", hash
=>$commit), $ref);
6771 "<td class=\"link\">" .
6772 $cgi->a({-href
=> href
(action
=>$ftype, hash_base
=>$commit, file_name
=>$file_name)}, $ftype) . " | " .
6773 $cgi->a({-href
=> href
(action
=>"commitdiff", hash
=>$commit)}, "commitdiff");
6775 if ($ftype eq 'blob') {
6776 my $blob_current = $file_hash;
6777 my $blob_parent = git_get_hash_by_path
($commit, $file_name);
6778 if (defined $blob_current && defined $blob_parent &&
6779 $blob_current ne $blob_parent) {
6781 $cgi->a({-href
=> href
(action
=>"blobdiff",
6782 hash
=>$blob_current, hash_parent
=>$blob_parent,
6783 hash_base
=>$hash_base, hash_parent_base
=>$commit,
6784 file_name
=>$file_name)},
6791 if (defined $extra) {
6793 "<td colspan=\"4\">$extra</td>\n" .
6800 # uses global variable $project
6801 my ($taglist, $from, $to, $extra, $head_at, $full, $order) = @_;
6802 $from = 0 unless defined $from;
6803 $to = $#{$taglist} if (!defined $to || $#{$taglist} < $to);
6804 $order ||= $default_refs_order;
6806 print "<table class=\"tags\">\n";
6808 print "<tr class=\"tags_header\">\n";
6809 print_sort_th
('age', $order, 'Last Change');
6810 print_sort_th
('name', $order, 'Name');
6811 print "<th></th>\n" . # for comment
6812 "<th></th>\n" . # for tag
6813 "<th></th>\n" . # for links
6817 for (my $i = $from; $i <= $to; $i++) {
6818 my $entry = $taglist->[$i];
6820 my $comment = $tag{'subject'};
6822 if (defined $comment) {
6823 $comment_short = chop_str
($comment, 30, 5);
6825 my $curr = defined $head_at && $tag{'id'} eq $head_at;
6827 print "<tr class=\"dark\">\n";
6829 print "<tr class=\"light\">\n";
6832 if (defined $tag{'age'}) {
6833 print "<td><i>$tag{'age'}</i></td>\n";
6835 print "<td></td>\n";
6837 print(($curr ?
"<td class=\"current_head\">" : "<td>") .
6838 $cgi->a({-href
=> href
(action
=>$tag{'reftype'}, hash
=>$tag{'refid'}),
6839 -class => "list name"}, esc_html
($tag{'name'})) .
6842 if (defined $comment) {
6843 print format_subject_html
($comment, $comment_short,
6844 href
(action
=>"tag", hash
=>$tag{'id'}));
6847 "<td class=\"selflink\">";
6848 if ($tag{'type'} eq "tag") {
6849 print $cgi->a({-href
=> href
(action
=>"tag", hash
=>$tag{'id'})}, "tag");
6854 "<td class=\"link\">" . " | " .
6855 $cgi->a({-href
=> href
(action
=>$tag{'reftype'}, hash
=>$tag{'refid'})}, $tag{'reftype'});
6856 if ($tag{'reftype'} eq "commit") {
6857 print " | " . $cgi->a({-href
=> href
(action
=>"shortlog", hash
=>$tag{'fullname'})}, "log");
6858 print " | " . $cgi->a({-href
=> href
(action
=>"tree", hash
=>$tag{'fullname'})}, "tree") if $full;
6859 } elsif ($tag{'reftype'} eq "blob") {
6860 print " | " . $cgi->a({-href
=> href
(action
=>"blob_plain", hash
=>$tag{'refid'})}, "raw");
6865 if (defined $extra) {
6867 "<td colspan=\"5\">$extra</td>\n" .
6873 sub git_heads_body
{
6874 # uses global variable $project
6875 my ($headlist, $head_at, $from, $to, $extra) = @_;
6876 $from = 0 unless defined $from;
6877 $to = $#{$headlist} if (!defined $to || $#{$headlist} < $to);
6879 print "<table class=\"heads\">\n";
6881 for (my $i = $from; $i <= $to; $i++) {
6882 my $entry = $headlist->[$i];
6884 my $curr = defined $head_at && $ref{'id'} eq $head_at;
6886 print "<tr class=\"dark\">\n";
6888 print "<tr class=\"light\">\n";
6891 print "<td><i>$ref{'age'}</i></td>\n" .
6892 ($curr ?
"<td class=\"current_head\">" : "<td>") .
6893 $cgi->a({-href
=> href
(action
=>"shortlog", hash
=>$ref{'fullname'}),
6894 -class => "list name"},esc_html
($ref{'name'})) .
6896 "<td class=\"link\">" .
6897 $cgi->a({-href
=> href
(action
=>"shortlog", hash
=>$ref{'fullname'})}, "log") . " | " .
6898 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$ref{'fullname'}, hash_base
=>$ref{'fullname'})}, "tree") .
6902 if (defined $extra) {
6904 "<td colspan=\"3\">$extra</td>\n" .
6910 # Display a single remote block
6911 sub git_remote_block
{
6912 my ($remote, $rdata, $limit, $head) = @_;
6914 my $heads = $rdata->{'heads'};
6915 my $fetch = $rdata->{'fetch'};
6916 my $push = $rdata->{'push'};
6918 my $urls_table = "<table class=\"projects_list\">\n" ;
6920 if (defined $fetch) {
6921 if ($fetch eq $push) {
6922 $urls_table .= format_repo_url
("URL", $fetch);
6924 $urls_table .= format_repo_url
("Fetch URL", $fetch);
6925 $urls_table .= format_repo_url
("Push URL", $push) if defined $push;
6927 } elsif (defined $push) {
6928 $urls_table .= format_repo_url
("Push URL", $push);
6930 $urls_table .= format_repo_url
("", "No remote URL");
6933 $urls_table .= "</table>\n";
6936 if (defined $limit && $limit < @
$heads) {
6937 $dots = $cgi->a({-href
=> href
(action
=>"remotes", hash
=>$remote)}, "...");
6941 git_heads_body
($heads, $head, 0, $limit, $dots);
6944 # Display a list of remote names with the respective fetch and push URLs
6945 sub git_remotes_list
{
6946 my ($remotedata, $limit) = @_;
6947 print "<table class=\"heads\">\n";
6949 my @remotes = sort keys %$remotedata;
6951 my $limited = $limit && $limit < @remotes;
6953 $#remotes = $limit - 1 if $limited;
6955 while (my $remote = shift @remotes) {
6956 my $rdata = $remotedata->{$remote};
6957 my $fetch = $rdata->{'fetch'};
6958 my $push = $rdata->{'push'};
6960 print "<tr class=\"dark\">\n";
6962 print "<tr class=\"light\">\n";
6966 $cgi->a({-href
=> href
(action
=>'remotes', hash
=>$remote),
6967 -class=> "list name"},esc_html
($remote)) .
6969 print "<td class=\"link\">" .
6970 (defined $fetch ?
$cgi->a({-href
=> $fetch}, "fetch") : "fetch") .
6972 (defined $push ?
$cgi->a({-href
=> $push}, "push") : "push") .
6980 "<td colspan=\"3\">" .
6981 $cgi->a({-href
=> href
(action
=>"remotes")}, "...") .
6982 "</td>\n" . "</tr>\n";
6988 # Display remote heads grouped by remote, unless there are too many
6989 # remotes, in which case we only display the remote names
6990 sub git_remotes_body
{
6991 my ($remotedata, $limit, $head) = @_;
6992 if ($limit and $limit < keys %$remotedata) {
6993 git_remotes_list
($remotedata, $limit);
6995 fill_remote_heads
($remotedata);
6996 while (my ($remote, $rdata) = each %$remotedata) {
6997 git_print_section
({-class=>"remote", -id
=>$remote},
6998 ["remotes", $remote, $remote], sub {
6999 git_remote_block
($remote, $rdata, $limit, $head);
7005 sub git_search_message
{
7009 if ($searchtype eq 'commit') {
7010 $greptype = "--grep=";
7011 } elsif ($searchtype eq 'author') {
7012 $greptype = "--author=";
7013 } elsif ($searchtype eq 'committer') {
7014 $greptype = "--committer=";
7016 $greptype .= $searchtext;
7017 my @commitlist = parse_commits
($hash, 101, (100 * $page), undef,
7018 $greptype, '--regexp-ignore-case',
7019 $search_use_regexp ?
'--extended-regexp' : '--fixed-strings');
7021 my $paging_nav = '';
7024 $cgi->a({-href
=> href
(-replay
=>1, page
=>undef)},
7027 $cgi->a({-href
=> href
(-replay
=>1, page
=>$page-1),
7028 -accesskey
=> "p", -title
=> "Alt-p"}, "prev");
7030 $paging_nav .= "first · prev";
7033 if ($#commitlist >= 100) {
7035 $cgi->a({-href
=> href
(-replay
=>1, page
=>$page+1),
7036 -accesskey
=> "n", -title
=> "Alt-n"}, "next");
7037 $paging_nav .= " · $next_link";
7039 $paging_nav .= " · next";
7044 git_print_page_nav
('','', $hash,$co{'tree'},$hash, $paging_nav);
7045 git_print_header_div
('commit', esc_html
($co{'title'}), $hash);
7046 if ($page == 0 && !@commitlist) {
7047 print "<p>No match.</p>\n";
7049 git_search_grep_body
(\
@commitlist, 0, 99, $next_link);
7055 sub git_search_changes
{
7059 defined(my $fd = git_cmd_pipe
'--no-pager', 'log', @diff_opts,
7060 '--pretty=format:%H', '--no-abbrev', '--raw', "-S$searchtext",
7061 ($search_use_regexp ?
'--pickaxe-regex' : ()))
7062 or die_error
(500, "Open git-log failed");
7066 git_print_page_nav
('','', $hash,$co{'tree'},$hash);
7067 git_print_header_div
('commit', esc_html
($co{'title'}), $hash);
7069 print "<table class=\"pickaxe search\">\n";
7073 while (my $line = to_utf8
(scalar <$fd>)) {
7077 my %set = parse_difftree_raw_line
($line);
7078 if (defined $set{'commit'}) {
7079 # finish previous commit
7082 "<td class=\"link\">" .
7083 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$co{'id'})},
7086 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$co{'tree'},
7087 hash_base
=>$co{'id'})},
7094 print "<tr class=\"dark\">\n";
7096 print "<tr class=\"light\">\n";
7099 %co = parse_commit
($set{'commit'});
7100 my $author = chop_and_escape_str
($co{'author_name'}, 15, 5);
7101 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
7102 "<td><i>$author</i></td>\n" .
7104 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$co{'id'}),
7105 -class => "list subject"},
7106 chop_and_escape_str
($co{'title'}, 50) . "<br/>");
7107 } elsif (defined $set{'to_id'}) {
7108 next if ($set{'to_id'} =~ m/^0{40}$/);
7110 print $cgi->a({-href
=> href
(action
=>"blob", hash_base
=>$co{'id'},
7111 hash
=>$set{'to_id'}, file_name
=>$set{'to_file'}),
7113 "<span class=\"match\">" . esc_path
($set{'file'}) . "</span>") .
7119 # finish last commit (warning: repetition!)
7122 "<td class=\"link\">" .
7123 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$co{'id'})},
7126 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$co{'tree'},
7127 hash_base
=>$co{'id'})},
7138 sub git_search_files
{
7142 defined(my $fd = git_cmd_pipe
'grep', '-n', '-z',
7143 $search_use_regexp ?
('-E', '-i') : '-F',
7144 $searchtext, $co{'tree'})
7145 or die_error
(500, "Open git-grep failed");
7149 git_print_page_nav
('','', $hash,$co{'tree'},$hash);
7150 git_print_header_div
('commit', esc_html
($co{'title'}), $hash);
7152 print "<table class=\"grep_search\">\n";
7157 while (my $line = to_utf8
(scalar <$fd>)) {
7159 my ($file, $lno, $ltext, $binary);
7160 last if ($matches++ > 1000);
7161 if ($line =~ /^Binary file (.+) matches$/) {
7165 ($file, $lno, $ltext) = split(/\0/, $line, 3);
7166 $file =~ s/^$co{'tree'}://;
7168 if ($file ne $lastfile) {
7169 $lastfile and print "</td></tr>\n";
7171 print "<tr class=\"dark\">\n";
7173 print "<tr class=\"light\">\n";
7175 $file_href = href
(action
=>"blob", hash_base
=>$co{'id'},
7177 print "<td class=\"list\">".
7178 $cgi->a({-href
=> $file_href, -class => "list"}, esc_path
($file));
7179 print "</td><td>\n";
7183 print "<div class=\"binary\">Binary file</div>\n";
7185 $ltext = untabify
($ltext);
7186 if ($ltext =~ m/^(.*)($search_regexp)(.*)$/i) {
7187 $ltext = esc_html
($1, -nbsp
=>1);
7188 $ltext .= '<span class="match">';
7189 $ltext .= esc_html
($2, -nbsp
=>1);
7190 $ltext .= '</span>';
7191 $ltext .= esc_html
($3, -nbsp
=>1);
7193 $ltext = esc_html
($ltext, -nbsp
=>1);
7195 print "<div class=\"pre\">" .
7196 $cgi->a({-href
=> $file_href.'#l'.$lno,
7197 -class => "linenr"}, sprintf('%4i', $lno)) .
7198 ' ' . $ltext . "</div>\n";
7202 print "</td></tr>\n";
7203 if ($matches > 1000) {
7204 print "<div class=\"diff nodifferences\">Too many matches, listing trimmed</div>\n";
7207 print "<div class=\"diff nodifferences\">No matches found</div>\n";
7216 sub git_search_grep_body
{
7217 my ($commitlist, $from, $to, $extra) = @_;
7218 $from = 0 unless defined $from;
7219 $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
7221 print "<table class=\"commit_search\">\n";
7223 for (my $i = $from; $i <= $to; $i++) {
7224 my %co = %{$commitlist->[$i]};
7228 my $commit = $co{'id'};
7230 print "<tr class=\"dark\">\n";
7232 print "<tr class=\"light\">\n";
7235 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
7236 format_author_html
('td', \
%co, 15, 5) .
7238 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$co{'id'}),
7239 -class => "list subject"},
7240 chop_and_escape_str
($co{'title'}, 50) . "<br/>");
7241 my $comment = $co{'comment'};
7242 foreach my $line (@
$comment) {
7243 if ($line =~ m/^(.*?)($search_regexp)(.*)$/i) {
7244 my ($lead, $match, $trail) = ($1, $2, $3);
7245 $match = chop_str
($match, 70, 5, 'center');
7246 my $contextlen = int((80 - length($match))/2);
7247 $contextlen = 30 if ($contextlen > 30);
7248 $lead = chop_str
($lead, $contextlen, 10, 'left');
7249 $trail = chop_str
($trail, $contextlen, 10, 'right');
7251 $lead = esc_html
($lead);
7252 $match = esc_html
($match);
7253 $trail = esc_html
($trail);
7255 print "$lead<span class=\"match\">$match</span>$trail<br />";
7259 "<td class=\"link\">" .
7260 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$co{'id'})}, "commit") .
7262 $cgi->a({-href
=> href
(action
=>"commitdiff", hash
=>$co{'id'})}, "commitdiff") .
7264 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$co{'tree'}, hash_base
=>$co{'id'})}, "tree");
7268 if (defined $extra) {
7270 "<td colspan=\"3\">$extra</td>\n" .
7276 ## ======================================================================
7277 ## ======================================================================
7280 sub git_project_list_load
{
7281 my $empty_list_ok = shift;
7282 my $order = $input_params{'order'};
7283 if (defined $order && $order !~ m/none|project|descr|owner|age/) {
7284 die_error
(400, "Unknown order parameter");
7287 my @list = git_get_projects_list
($project_filter, $strict_export);
7288 if ($project_filter && (!@list || !gitweb_check_feature
('forks'))) {
7289 push @list, { 'path' => "$project_filter.git" }
7290 if is_valid_project
("$project_filter.git");
7293 die_error
(404, "No projects found") unless $empty_list_ok;
7296 return (\
@list, $order);
7300 my ($projlist, $order);
7302 if ($frontpage_no_project_list) {
7304 $project_filter = undef;
7306 ($projlist, $order) = git_project_list_load
(1);
7309 if (defined $home_text && -f
$home_text) {
7310 print "<div class=\"index_include\">\n";
7311 insert_file
($home_text);
7314 git_project_search_form
($searchtext, $search_use_regexp);
7315 if ($frontpage_no_project_list) {
7316 my $show_ctags = gitweb_check_feature
('ctags');
7317 if ($frontpage_no_project_list == 1 and $show_ctags) {
7318 my @projects = git_get_projects_list
($project_filter, $strict_export);
7319 @projects = filter_forks_from_projects_list
(\
@projects) if gitweb_check_feature
('forks');
7320 @projects = fill_project_list_info
(\
@projects, 'ctags');
7321 my $ctags = git_gather_all_ctags
(\
@projects);
7322 my $cloud = git_populate_project_tagcloud
($ctags, 'project_list');
7323 print git_show_project_tagcloud
($cloud, 64);
7326 git_project_list_body
($projlist, $order, undef, undef, undef, undef, undef, 1);
7331 sub git_project_list
{
7332 my ($projlist, $order) = git_project_list_load
();
7334 if (!$frontpage_no_project_list && defined $home_text && -f
$home_text) {
7335 print "<div class=\"index_include\">\n";
7336 insert_file
($home_text);
7339 git_project_search_form
();
7340 git_project_list_body
($projlist, $order, undef, undef, undef, undef, undef, 1);
7345 my $order = $input_params{'order'};
7346 if (defined $order && $order !~ m/none|project|descr|owner|age/) {
7347 die_error
(400, "Unknown order parameter");
7350 my $filter = $project;
7351 $filter =~ s/\.git$//;
7352 my @list = git_get_projects_list
($filter);
7354 die_error
(404, "No forks found");
7358 git_print_page_nav
('','');
7359 git_print_header_div
('summary', "$project forks");
7360 git_project_list_body
(\
@list, $order, undef, undef, undef, undef, 'forks');
7364 sub git_project_index
{
7365 my @projects = git_get_projects_list
($project_filter, $strict_export);
7367 die_error
(404, "No projects found");
7371 -type
=> 'text/plain',
7372 -charset
=> 'utf-8',
7373 -content_disposition
=> 'inline; filename="index.aux"');
7375 foreach my $pr (@projects) {
7376 if (!exists $pr->{'owner'}) {
7377 $pr->{'owner'} = git_get_project_owner
("$pr->{'path'}");
7380 my ($path, $owner) = ($pr->{'path'}, $pr->{'owner'});
7381 # quote as in CGI::Util::encode, but keep the slash, and use '+' for ' '
7382 $path =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf
("%%%02X", ord($1))/eg
;
7383 $owner =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf
("%%%02X", ord($1))/eg
;
7387 print "$path $owner\n";
7392 my $descr = git_get_project_description
($project) || "none";
7393 my %co = parse_commit
("HEAD");
7394 my %cd = %co ? parse_date
($co{'committer_epoch'}, $co{'committer_tz'}) : ();
7395 my $head = $co{'id'};
7396 my $remote_heads = gitweb_check_feature
('remote_heads');
7398 my $owner = git_get_project_owner
($project);
7399 my $homepage = git_get_project_config
('homepage');
7400 my $base_url = git_get_project_config
('baseurl');
7401 my $last_refresh = git_get_project_config
('lastrefresh');
7403 my $refs = git_get_references
();
7404 # These get_*_list functions return one more to allow us to see if
7405 # there are more ...
7406 my @taglist = git_get_tags_list
(16);
7407 my @headlist = git_get_heads_list
(16);
7408 my %remotedata = $remote_heads ? git_get_remotes_list
() : ();
7410 my $check_forks = gitweb_check_feature
('forks');
7413 # find forks of a project
7414 my $filter = $project;
7415 $filter =~ s/\.git$//;
7416 @forklist = git_get_projects_list
($filter);
7417 # filter out forks of forks
7418 @forklist = filter_forks_from_projects_list
(\
@forklist)
7423 git_print_page_nav
('summary','', $head);
7425 if ($check_forks and $project =~ m
#/#) {
7426 my $xproject = $project; $xproject =~ s
#/[^/]+$#.git#; #
7427 my $r = $cgi->a({-href
=> href
(project
=> $xproject, action
=> 'summary')}, $xproject);
7429 <div class="forkinfo">
7430 This project is a fork of the $r project. If you have that one
7431 already cloned locally, you can use
7432 <pre>git clone --reference /path/to/your/$xproject/incarnation mirror_URL</pre>
7433 to save bandwidth during cloning.
7438 print "<div class=\"title\"> </div>\n";
7439 print "<table class=\"projects_list\">\n" .
7440 "<tr id=\"metadata_desc\"><td>description</td><td>" . esc_html
($descr) . "</td></tr>\n";
7442 print "<tr id=\"metadata_homepage\"><td>homepage URL</td><td>" . $cgi->a({-href
=> $homepage}, $homepage) . "</td></tr>\n";
7445 print "<tr id=\"metadata_baseurl\"><td>repository URL</td><td>" . esc_html
($base_url) . "</td></tr>\n";
7447 if ($owner and not $omit_owner) {
7448 print "<tr id=\"metadata_owner\"><td>owner</td><td>" . ($owner_link_hook
7449 ?
$cgi->a({-href
=> $owner_link_hook->($owner)}, email_obfuscate
($owner))
7450 : email_obfuscate
($owner)) . "</td></tr>\n";
7452 if (defined $cd{'rfc2822'}) {
7453 print "<tr id=\"metadata_lchange\"><td>last change</td>" .
7454 "<td>".format_timestamp_html
(\
%cd)."</td></tr>\n";
7456 if (defined $last_refresh) {
7457 my %rd = parse_date_rfc2822
($last_refresh);
7458 print "<tr id=\"metadata_lrefresh\"><td>last refresh</td>" .
7459 "<td>".format_timestamp_html
(\
%rd)."</td></tr>\n"
7460 if defined $rd{'rfc2822'};
7463 # use per project git URL list in $projectroot/$project/cloneurl
7464 # or make project git URL from git base URL and project name
7465 my $url_tag = $base_url ?
"mirror URL" : "URL";
7466 my @url_list = git_get_project_url_list
($project);
7467 @url_list = map { "$_/$project" } @git_base_url_list unless @url_list;
7468 foreach my $git_url (@url_list) {
7469 next unless $git_url;
7470 print format_repo_url
($url_tag, $git_url);
7473 @url_list = map { "$_/$project" } @git_base_push_urls;
7474 if (-f
"$projectroot/$project/.nofetch") {
7475 $url_tag = "Push URL";
7476 foreach my $git_push_url (@url_list) {
7477 next unless $git_push_url;
7478 my $hint = $https_hint_html && $git_push_url =~ /^https:/i ?
7479 " $https_hint_html" : '';
7480 print "<tr class=\"metadata_pushurl\"><td>$url_tag</td><td>$git_push_url$hint</td></tr>\n";
7486 my $show_ctags = gitweb_check_feature
('ctags');
7488 my $ctags = git_get_project_ctags
($project);
7489 if (%$ctags || $show_ctags !~ /^\d+$/) {
7490 # without ability to add tags, don't show if there are none
7491 my $cloud = git_populate_project_tagcloud
($ctags, 'project_list');
7492 print "<tr id=\"metadata_ctags\">" .
7493 "<td style=\"vertical-align:middle\">content tags<br />";
7494 print "</td>\n<td>" unless %$ctags;
7495 print "<form action=\"$show_ctags\" method=\"post\" style=\"white-space:nowrap\">" .
7496 "<input type=\"hidden\" name=\"p\" value=\"$project\"/>" .
7497 "add: <input type=\"text\" name=\"t\" size=\"8\" /></form>"
7498 unless $show_ctags =~ /^\d+$/;
7499 print "</td>\n<td>" if %$ctags;
7500 print git_show_project_tagcloud
($cloud, 48)."</td>" .
7507 # If XSS prevention is on, we don't include README.html.
7508 # TODO: Allow a readme in some safe format.
7509 if (!$prevent_xss && -s
"$projectroot/$project/README.html") {
7510 print "<div class=\"title\">readme</div>\n" .
7511 "<div class=\"readme\">\n";
7512 insert_file
("$projectroot/$project/README.html");
7513 print "\n</div>\n"; # class="readme"
7516 # we need to request one more than 16 (0..15) to check if
7518 my @commitlist = $head ? parse_commits
($head, 17) : ();
7520 git_print_header_div
('shortlog');
7521 git_shortlog_body
(\
@commitlist, 0, 15, $refs,
7522 $#commitlist <= 15 ?
undef :
7523 $cgi->a({-href
=> href
(action
=>"shortlog")}, "..."));
7527 git_print_header_div
('tags');
7528 git_tags_body
(\
@taglist, 0, 15,
7529 $#taglist <= 15 ?
undef :
7530 $cgi->a({-href
=> href
(action
=>"tags")}, "..."));
7534 git_print_header_div
('heads');
7535 git_heads_body
(\
@headlist, $head, 0, 15,
7536 $#headlist <= 15 ?
undef :
7537 $cgi->a({-href
=> href
(action
=>"heads")}, "..."));
7541 git_print_header_div
('remotes');
7542 git_remotes_body
(\
%remotedata, 15, $head);
7546 git_print_header_div
('forks');
7547 git_project_list_body
(\
@forklist, 'age', 0, 15,
7548 $#forklist <= 15 ?
undef :
7549 $cgi->a({-href
=> href
(action
=>"forks")}, "..."),
7550 'no_header', 'forks');
7557 my %tag = parse_tag
($hash);
7560 die_error
(404, "Unknown tag object");
7564 $fullhash = $hash if $hash =~ m/^[0-9a-fA-F]{40}$/;
7565 $fullhash = git_get_full_hash
($project, $hash) unless $fullhash;
7567 my $head = git_get_head_hash
($project);
7569 git_print_page_nav
('','', $head,undef,$head);
7570 git_print_header_div
('commit', esc_html
($tag{'name'}), $hash);
7571 print "<div class=\"title_text\">\n" .
7572 "<table class=\"object_header\">\n" .
7573 "<tr><td>tag</td><td class=\"sha1\">$fullhash</td></tr>\n" .
7575 "<td>object</td>\n" .
7576 "<td>" . $cgi->a({-class => "list", -href
=> href
(action
=>$tag{'type'}, hash
=>$tag{'object'})},
7577 $tag{'object'}) . "</td>\n" .
7578 "<td class=\"link\">" . $cgi->a({-href
=> href
(action
=>$tag{'type'}, hash
=>$tag{'object'})},
7579 $tag{'type'}) . "</td>\n" .
7581 if (defined($tag{'author'})) {
7582 git_print_authorship_rows
(\
%tag, 'author');
7584 print "</table>\n\n" .
7586 print "<div class=\"page_body\">";
7587 my $comment = $tag{'comment'};
7588 foreach my $line (@
$comment) {
7590 print esc_html
($line, -nbsp
=>1) . "<br/>\n";
7596 sub git_blame_common
{
7597 my $format = shift || 'porcelain';
7598 if ($format eq 'porcelain' && $input_params{'javascript'}) {
7599 $format = 'incremental';
7600 $action = 'blame_incremental'; # for page title etc
7604 gitweb_check_feature
('blame')
7605 or die_error
(403, "Blame view not allowed");
7608 die_error
(400, "No file name given") unless $file_name;
7609 $hash_base ||= git_get_head_hash
($project);
7610 die_error
(404, "Couldn't find base commit") unless $hash_base;
7611 my %co = parse_commit
($hash_base)
7612 or die_error
(404, "Commit not found");
7614 if (!defined $hash) {
7615 $hash = git_get_hash_by_path
($hash_base, $file_name, "blob")
7616 or die_error
(404, "Error looking up file");
7618 $ftype = git_get_type
($hash);
7619 if ($ftype !~ "blob") {
7620 die_error
(400, "Object is not a blob");
7625 if ($format eq 'incremental') {
7626 # get file contents (as base)
7627 defined($fd = git_cmd_pipe
'cat-file', 'blob', $hash)
7628 or die_error
(500, "Open git-cat-file failed");
7629 } elsif ($format eq 'data') {
7630 # run git-blame --incremental
7631 defined($fd = git_cmd_pipe
"blame", "--incremental",
7632 $hash_base, "--", $file_name)
7633 or die_error
(500, "Open git-blame --incremental failed");
7635 # run git-blame --porcelain
7636 defined($fd = git_cmd_pipe
"blame", '-p',
7637 $hash_base, '--', $file_name)
7638 or die_error
(500, "Open git-blame --porcelain failed");
7641 # incremental blame data returns early
7642 if ($format eq 'data') {
7644 -type
=>"text/plain", -charset
=> "utf-8",
7645 -status
=> "200 OK");
7646 local $| = 1; # output autoflush
7651 or print "ERROR $!\n";
7654 if (defined $t0 && gitweb_check_feature
('timed')) {
7656 tv_interval
($t0, [ gettimeofday
() ]).
7657 ' '.$number_of_git_cmds;
7667 $cgi->a({-href
=> href
(action
=>"blob", -replay
=>1)},
7671 $cgi->a({-href
=> href
(action
=>"history", -replay
=>1)},
7674 $cgi->a({-href
=> href
(action
=>$action, file_name
=>$file_name)},
7676 git_print_page_nav
('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
7677 git_print_header_div
('commit', esc_html
($co{'title'}), $hash_base);
7678 git_print_page_path
($file_name, $ftype, $hash_base);
7681 if ($format eq 'incremental') {
7682 print "<noscript>\n<div class=\"error\"><center><b>\n".
7683 "This page requires JavaScript to run.\n Use ".
7684 $cgi->a({-href
=> href
(action
=>'blame',javascript
=>0,-replay
=>1)},
7687 "</b></center></div>\n</noscript>\n";
7689 print qq!<div id
="progress_bar" style
="width: 100%; background-color: yellow"></div
>\n!;
7692 print qq!<div
class="page_body">\n!;
7693 print qq!<div id
="progress_info">... / ...</div
>\n!
7694 if ($format eq 'incremental');
7695 print qq!<table id
="blame_table" class="blame" width
="100%">\n!.
7696 #qq!<col width="5.5em" /><col width="2.5em" /><col width="*" />\n!.
7698 qq!<tr
><th nowrap
="nowrap" style
="white-space:nowrap">!.
7699 qq!Commit
 <a href="javascript:extra_blame_columns()" id="columns_expander" !.
7700 qq!title
="toggles blame author information display">[+]</a></th
>!.
7701 qq!<th
class="extra_column">Author
</th><th class="extra_column">Date</th
>!.
7702 qq!<th
>Line
</th><th width="100%">Data</th
></tr
>\n!.
7706 my @rev_color = qw(light dark);
7707 my $num_colors = scalar(@rev_color);
7708 my $current_color = 0;
7710 if ($format eq 'incremental') {
7711 my $color_class = $rev_color[$current_color];
7716 while (my $line = to_utf8
(scalar <$fd>)) {
7720 print qq!<tr id
="l$linenr" class="$color_class">!.
7721 qq!<td
class="sha1"><a href
=""> </a></td
>!.
7722 qq!<td
class="extra_column" nowrap
="nowrap"></td
>!.
7723 qq!<td
class="extra_column" nowrap
="nowrap"></td
>!.
7724 qq!<td
class="linenr">!.
7725 qq!<a
class="linenr" href
="">$linenr</a></td
>!;
7726 print qq!<td
class="pre">! . esc_html
($line) . "</td>\n";
7730 } else { # porcelain, i.e. ordinary blame
7731 my %metainfo = (); # saves information about commits
7735 while (my $line = to_utf8
(scalar <$fd>)) {
7737 # the header: <SHA-1> <src lineno> <dst lineno> [<lines in group>]
7738 # no <lines in group> for subsequent lines in group of lines
7739 my ($full_rev, $orig_lineno, $lineno, $group_size) =
7740 ($line =~ /^([0-9a-f]{40}) (\d+) (\d+)(?: (\d+))?$/);
7741 if (!exists $metainfo{$full_rev}) {
7742 $metainfo{$full_rev} = { 'nprevious' => 0 };
7744 my $meta = $metainfo{$full_rev};
7746 while ($data = to_utf8
(scalar <$fd>)) {
7748 last if ($data =~ s/^\t//); # contents of line
7749 if ($data =~ /^(\S+)(?: (.*))?$/) {
7750 $meta->{$1} = $2 unless exists $meta->{$1};
7752 if ($data =~ /^previous /) {
7753 $meta->{'nprevious'}++;
7756 my $short_rev = substr($full_rev, 0, 8);
7757 my $author = $meta->{'author'};
7759 parse_date
($meta->{'author-time'}, $meta->{'author-tz'});
7760 my $date = $date{'iso-tz'};
7762 $current_color = ($current_color + 1) % $num_colors;
7764 my $tr_class = $rev_color[$current_color];
7765 $tr_class .= ' boundary' if (exists $meta->{'boundary'});
7766 $tr_class .= ' no-previous' if ($meta->{'nprevious'} == 0);
7767 $tr_class .= ' multiple-previous' if ($meta->{'nprevious'} > 1);
7768 print "<tr id=\"l$lineno\" class=\"$tr_class\">\n";
7770 my $rowspan = $group_size > 1 ?
" rowspan=\"$group_size\"" : "";
7771 print "<td class=\"sha1\"";
7772 print " title=\"". esc_html
($author) . ", $date\"";
7774 print $cgi->a({-href
=> href
(action
=>"commit",
7776 file_name
=>$file_name)},
7777 esc_html
($short_rev));
7778 if ($group_size >= 2) {
7779 my @author_initials = ($author =~ /\b([[:upper:]])\B/g);
7780 if (@author_initials) {
7782 esc_html
(join('', @author_initials));
7787 print "<td class=\"extra_column\" nowrap=\"nowrap\"$rowspan>". esc_html
($author) . "</td>";
7788 print "<td class=\"extra_column\" nowrap=\"nowrap\"$rowspan>". $date . "</td>";
7790 # 'previous' <sha1 of parent commit> <filename at commit>
7791 if (exists $meta->{'previous'} &&
7792 $meta->{'previous'} =~ /^([a-fA-F0-9]{40}) (.*)$/) {
7793 $meta->{'parent'} = $1;
7794 $meta->{'file_parent'} = unquote
($2);
7797 exists($meta->{'parent'}) ?
7798 $meta->{'parent'} : $full_rev;
7799 my $linenr_filename =
7800 exists($meta->{'file_parent'}) ?
7801 $meta->{'file_parent'} : unquote
($meta->{'filename'});
7802 my $blamed = href
(action
=> 'blame',
7803 file_name
=> $linenr_filename,
7804 hash_base
=> $linenr_commit);
7805 print "<td class=\"linenr\">";
7806 print $cgi->a({ -href
=> "$blamed#l$orig_lineno",
7807 -class => "linenr" },
7810 print "<td class=\"pre\">" . esc_html
($data) . "</td>\n";
7818 "</table>\n"; # class="blame"
7819 print "</div>\n"; # class="blame_body"
7821 or print "Reading blob failed\n";
7830 sub git_blame_incremental
{
7831 git_blame_common
('incremental');
7834 sub git_blame_data
{
7835 git_blame_common
('data');
7839 my $head = git_get_head_hash
($project);
7841 git_print_page_nav
('','', $head,undef,$head,format_ref_views
('tags'));
7842 git_print_header_div
('summary', $project);
7844 my @tagslist = git_get_tags_list
();
7846 git_tags_body
(\
@tagslist);
7852 my $order = $input_params{'order'};
7853 if (defined $order && $order !~ m/age|name/) {
7854 die_error
(400, "Unknown order parameter");
7857 my $head = git_get_head_hash
($project);
7859 git_print_page_nav
('','', $head,undef,$head,format_ref_views
('refs'));
7860 git_print_header_div
('summary', $project);
7862 my @refslist = git_get_tags_list
(undef, 1, $order);
7864 git_tags_body
(\
@refslist, undef, undef, undef, $head, 1, $order);
7870 my $head = git_get_head_hash
($project);
7872 git_print_page_nav
('','', $head,undef,$head,format_ref_views
('heads'));
7873 git_print_header_div
('summary', $project);
7875 my @headslist = git_get_heads_list
();
7877 git_heads_body
(\
@headslist, $head);
7882 # used both for single remote view and for list of all the remotes
7884 gitweb_check_feature
('remote_heads')
7885 or die_error
(403, "Remote heads view is disabled");
7887 my $head = git_get_head_hash
($project);
7888 my $remote = $input_params{'hash'};
7890 my $remotedata = git_get_remotes_list
($remote);
7891 die_error
(500, "Unable to get remote information") unless defined $remotedata;
7893 unless (%$remotedata) {
7894 die_error
(404, defined $remote ?
7895 "Remote $remote not found" :
7896 "No remotes found");
7899 git_header_html
(undef, undef, -action_extra
=> $remote);
7900 git_print_page_nav
('', '', $head, undef, $head,
7901 format_ref_views
($remote ?
'' : 'remotes'));
7903 fill_remote_heads
($remotedata);
7904 if (defined $remote) {
7905 git_print_header_div
('remotes', "$remote remote for $project");
7906 git_remote_block
($remote, $remotedata->{$remote}, undef, $head);
7908 git_print_header_div
('summary', "$project remotes");
7909 git_remotes_body
($remotedata, undef, $head);
7915 sub git_blob_plain
{
7919 if (!defined $hash) {
7920 if (defined $file_name) {
7921 my $base = $hash_base || git_get_head_hash
($project);
7922 $hash = git_get_hash_by_path
($base, $file_name, "blob")
7923 or die_error
(404, "Cannot find file");
7925 die_error
(400, "No file name defined");
7927 } elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) {
7928 # blobs defined by non-textual hash id's can be cached
7932 defined(my $fd = git_cmd_pipe
"cat-file", "blob", $hash)
7933 or die_error
(500, "Open git-cat-file blob '$hash' failed");
7936 # content-type (can include charset)
7938 ($type, $leader) = blob_contenttype
($fd, $file_name, $type);
7940 # "save as" filename, even when no $file_name is given
7941 my $save_as = "$hash";
7942 if (defined $file_name) {
7943 $save_as = $file_name;
7944 } elsif ($type =~ m/^text\//) {
7948 # With XSS prevention on, blobs of all types except a few known safe
7949 # ones are served with "Content-Disposition: attachment" to make sure
7950 # they don't run in our security domain. For certain image types,
7951 # blob view writes an <img> tag referring to blob_plain view, and we
7952 # want to be sure not to break that by serving the image as an
7953 # attachment (though Firefox 3 doesn't seem to care).
7954 my $sandbox = $prevent_xss &&
7955 $type !~ m!^(?:text/[a-z]+|image/(?:gif|png|jpeg))(?:[ ;]|$)!;
7957 # serve text/* as text/plain
7959 ($type =~ m!^text/[a-z]+\b(.*)$! ||
7960 ($type =~ m!^[a-z]+/[a-z]\+xml\b(.*)$! && -T
$fd))) {
7962 $rest = defined $rest ?
$rest : '';
7963 $type = "text/plain$rest";
7968 -expires
=> $expires,
7969 -content_disposition
=>
7970 ($sandbox ?
'attachment' : 'inline')
7971 . '; filename="' . $save_as . '"');
7972 binmode STDOUT
, ':raw';
7974 print $leader if defined $leader;
7976 while (read($fd, $buf, 32768)) {
7979 binmode STDOUT
, ':utf8'; # as set at the beginning of gitweb.cgi
7988 if (!defined $hash) {
7989 if (defined $file_name) {
7990 my $base = $hash_base || git_get_head_hash
($project);
7991 $hash = git_get_hash_by_path
($base, $file_name, "blob")
7992 or die_error
(404, "Cannot find file");
7995 die_error
(400, "No file name defined");
7997 } elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) {
7998 # blobs defined by non-textual hash id's can be cached
8002 $fullhash = git_get_full_hash
($project, $hash) unless $fullhash;
8004 my $have_blame = gitweb_check_feature
('blame');
8005 defined(my $fd = git_cmd_pipe
"cat-file", "blob", $hash)
8006 or die_error
(500, "Couldn't cat $file_name, $hash");
8008 my $mimetype = blob_mimetype
($fd, $file_name);
8009 # use 'blob_plain' (aka 'raw') view for files that cannot be displayed
8010 if ($mimetype !~ m!^(?:text/|image/(?:gif|png|jpeg)$)! && -B
$fd) {
8012 return git_blob_plain
($mimetype);
8014 # we can have blame only for text/* mimetype
8015 $have_blame &&= ($mimetype =~ m!^text/!);
8017 my $highlight = gitweb_check_feature
('highlight') && defined $highlight_bin;
8018 my $syntax = guess_file_syntax
($fd, $mimetype, $file_name) if $highlight;
8019 my $highlight_mode_active;
8020 ($fd, $highlight_mode_active) = run_highlighter
($fd, $syntax) if $syntax;
8022 git_header_html
(undef, $expires);
8023 my $formats_nav = '';
8024 if (defined $hash_base && (my %co = parse_commit
($hash_base))) {
8025 if (defined $file_name) {
8028 $cgi->a({-href
=> href
(action
=>"blame", -replay
=>1),
8029 -class => "blamelink"},
8034 $cgi->a({-href
=> href
(action
=>"history", -replay
=>1)},
8037 $cgi->a({-href
=> href
(action
=>"blob_plain", -replay
=>1)},
8040 $cgi->a({-href
=> href
(action
=>"blob",
8041 hash_base
=>"HEAD", file_name
=>$file_name)},
8045 $cgi->a({-href
=> href
(action
=>"blob_plain", -replay
=>1)},
8048 git_print_page_nav
('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
8049 git_print_header_div
('commit', esc_html
($co{'title'}), $hash_base);
8051 print "<div class=\"page_nav\">\n" .
8052 "<br/><br/></div>\n" .
8053 "<div class=\"title\">".esc_html
($hash)."</div>\n";
8055 git_print_page_path
($file_name, "blob", $hash_base);
8056 print "<div class=\"title_text\">\n" .
8057 "<table class=\"object_header\">\n";
8058 print "<tr><td>blob</td><td class=\"sha1\">$fullhash</td></tr>\n";
8061 print "<div class=\"page_body\">\n";
8062 if ($mimetype =~ m!^image/!) {
8063 print qq!<img
class="blob" type
="!.esc_attr($mimetype).qq!"!;
8065 print qq! alt
="!.esc_attr($file_name).qq!" title
="!.esc_attr($file_name).qq!"!;
8068 href(action=>"blob_plain
", hash=>$hash,
8069 hash_base=>$hash_base, file_name=>$file_name) .
8073 while (my $line = to_utf8
(scalar <$fd>)) {
8076 $line = untabify
($line);
8077 printf qq!<div
class="pre"><a id
="l%i" href
="%s#l%i" class="linenr">%4i</a> %s</div
>\n!,
8078 $nr, esc_attr
(href
(-replay
=> 1)), $nr, $nr,
8079 $highlight_mode_active ? sanitize
($line) : esc_html
($line, -nbsp
=>1);
8083 or print "Reading blob failed.\n";
8090 if (!defined $hash_base) {
8091 $hash_base = "HEAD";
8093 if (!defined $hash) {
8094 if (defined $file_name) {
8095 $hash = git_get_hash_by_path
($hash_base, $file_name, "tree");
8101 die_error
(404, "No such tree") unless defined($hash);
8102 $fullhash = $hash if !$fullhash && $hash =~ m/^[0-9a-fA-F]{40}$/;
8103 $fullhash = git_get_full_hash
($project, $hash) unless $fullhash;
8105 my $show_sizes = gitweb_check_feature
('show-sizes');
8106 my $have_blame = gitweb_check_feature
('blame');
8111 defined(my $fd = git_cmd_pipe
"ls-tree", '-z',
8112 ($show_sizes ?
'-l' : ()), @extra_options, $hash)
8113 or die_error
(500, "Open git-ls-tree failed");
8114 @entries = map { chomp; to_utf8
($_) } <$fd>;
8116 or die_error
(404, "Reading tree failed");
8121 if (defined $hash_base && (my %co = parse_commit
($hash_base))) {
8122 my $refs = git_get_references
();
8123 my $ref = format_ref_marker
($refs, $co{'id'});
8125 if (defined $file_name) {
8127 $cgi->a({-href
=> href
(action
=>"history", -replay
=>1)},
8129 $cgi->a({-href
=> href
(action
=>"tree",
8130 hash_base
=>"HEAD", file_name
=>$file_name)},
8133 my $snapshot_links = format_snapshot_links
($hash);
8134 if (defined $snapshot_links) {
8135 # FIXME: Should be available when we have no hash base as well.
8136 push @views_nav, $snapshot_links;
8138 git_print_page_nav
('tree','', $hash_base, undef, undef,
8139 join(' | ', @views_nav));
8140 git_print_header_div
('commit', esc_html
($co{'title'}), $hash_base, undef, $ref);
8143 print "<div class=\"page_nav\">\n";
8144 print "<br/><br/></div>\n";
8145 print "<div class=\"title\">".esc_html
($hash)."</div>\n";
8147 if (defined $file_name) {
8148 $basedir = $file_name;
8149 if ($basedir ne '' && substr($basedir, -1) ne '/') {
8152 git_print_page_path
($file_name, 'tree', $hash_base);
8154 print "<div class=\"title_text\">\n" .
8155 "<table class=\"object_header\">\n";
8156 print "<tr><td>tree</td><td class=\"sha1\">$fullhash</td></tr>\n";
8159 print "<div class=\"page_body\">\n";
8160 print "<table class=\"tree\">\n";
8162 # '..' (top directory) link if possible
8163 if (defined $hash_base &&
8164 defined $file_name && $file_name =~ m![^/]+$!) {
8166 print "<tr class=\"dark\">\n";
8168 print "<tr class=\"light\">\n";
8172 my $up = $file_name;
8173 $up =~ s!/?[^/]+$!!;
8174 undef $up unless $up;
8175 # based on git_print_tree_entry
8176 print '<td class="mode">' . mode_str
('040000') . "</td>\n";
8177 print '<td class="size"> </td>'."\n" if $show_sizes;
8178 print '<td class="list">';
8179 print $cgi->a({-href
=> href
(action
=>"tree",
8180 hash_base
=>$hash_base,
8184 print "<td class=\"link\"></td>\n";
8188 foreach my $line (@entries) {
8189 my %t = parse_ls_tree_line
($line, -z
=> 1, -l
=> $show_sizes);
8192 print "<tr class=\"dark\">\n";
8194 print "<tr class=\"light\">\n";
8198 git_print_tree_entry
(\
%t, $basedir, $hash_base, $have_blame);
8202 print "</table>\n" .
8207 sub sanitize_for_filename
{
8211 $name =~ s/[^[:alnum:]_.-]//g;
8217 my ($project, $hash) = @_;
8219 # path/to/project.git -> project
8220 # path/to/project/.git -> project
8221 my $name = to_utf8
($project);
8222 $name =~ s
,([^/])/*\
.git
$,$1,;
8223 $name = sanitize_for_filename
(basename
($name));
8226 if ($hash =~ /^[0-9a-fA-F]+$/) {
8227 # shorten SHA-1 hash
8228 my $full_hash = git_get_full_hash
($project, $hash);
8229 if ($full_hash =~ /^$hash/ && length($hash) > 7) {
8230 $ver = git_get_short_hash
($project, $hash);
8232 } elsif ($hash =~ m!^refs/tags/(.*)$!) {
8233 # tags don't need shortened SHA-1 hash
8236 # branches and other need shortened SHA-1 hash
8237 my $strip_refs = join '|', map { quotemeta } get_branch_refs
();
8238 if ($hash =~ m!^refs/($strip_refs|remotes)/(.*)$!) {
8239 my $ref_dir = (defined $1) ?
$1 : '';
8242 $ref_dir = sanitize_for_filename
($ref_dir);
8243 # for refs neither in heads nor remotes we want to
8244 # add a ref dir to archive name
8245 if ($ref_dir ne '' and $ref_dir ne 'heads' and $ref_dir ne 'remotes') {
8246 $ver = $ref_dir . '-' . $ver;
8249 $ver .= '-' . git_get_short_hash
($project, $hash);
8251 # special case of sanitization for filename - we change
8252 # slashes to dots instead of dashes
8253 # in case of hierarchical branch names
8255 $ver =~ s/[^[:alnum:]_.-]//g;
8257 # name = project-version_string
8258 $name = "$name-$ver";
8260 return wantarray ?
($name, $name) : $name;
8263 sub exit_if_unmodified_since
{
8264 my ($latest_epoch) = @_;
8267 my $if_modified = $cgi->http('IF_MODIFIED_SINCE');
8268 if (defined $if_modified) {
8270 if (eval { require HTTP
::Date
; 1; }) {
8271 $since = HTTP
::Date
::str2time
($if_modified);
8272 } elsif (eval { require Time
::ParseDate
; 1; }) {
8273 $since = Time
::ParseDate
::parsedate
($if_modified, GMT
=> 1);
8275 if (defined $since && $latest_epoch <= $since) {
8276 my %latest_date = parse_date
($latest_epoch);
8278 -last_modified
=> $latest_date{'rfc2822'},
8279 -status
=> '304 Not Modified');
8286 my $format = $input_params{'snapshot_format'};
8287 if (!@snapshot_fmts) {
8288 die_error
(403, "Snapshots not allowed");
8290 # default to first supported snapshot format
8291 $format ||= $snapshot_fmts[0];
8292 if ($format !~ m/^[a-z0-9]+$/) {
8293 die_error
(400, "Invalid snapshot format parameter");
8294 } elsif (!exists($known_snapshot_formats{$format})) {
8295 die_error
(400, "Unknown snapshot format");
8296 } elsif ($known_snapshot_formats{$format}{'disabled'}) {
8297 die_error
(403, "Snapshot format not allowed");
8298 } elsif (!grep($_ eq $format, @snapshot_fmts)) {
8299 die_error
(403, "Unsupported snapshot format");
8302 my $type = git_get_type
("$hash^{}");
8304 die_error
(404, 'Object does not exist');
8305 } elsif ($type eq 'blob') {
8306 die_error
(400, 'Object is not a tree-ish');
8309 my ($name, $prefix) = snapshot_name
($project, $hash);
8310 my $filename = "$name$known_snapshot_formats{$format}{'suffix'}";
8312 my %co = parse_commit
($hash);
8313 exit_if_unmodified_since
($co{'committer_epoch'}) if %co;
8316 git_cmd
(), 'archive',
8317 "--format=$known_snapshot_formats{$format}{'format'}",
8318 "--prefix=$prefix/", $hash);
8319 if (exists $known_snapshot_formats{$format}{'compressor'}) {
8320 @cmd = ($posix_shell_bin, '-c', quote_command
(@cmd) .
8321 ' | ' . quote_command
(@
{$known_snapshot_formats{$format}{'compressor'}}));
8324 $filename =~ s/(["\\])/\\$1/g;
8327 %latest_date = parse_date
($co{'committer_epoch'}, $co{'committer_tz'});
8331 -type
=> $known_snapshot_formats{$format}{'type'},
8332 -content_disposition
=> 'inline; filename="' . $filename . '"',
8333 %co ?
(-last_modified
=> $latest_date{'rfc2822'}) : (),
8334 -status
=> '200 OK');
8336 defined(my $fd = cmd_pipe
@cmd)
8337 or die_error
(500, "Execute git-archive failed");
8339 binmode STDOUT
, ':raw';
8342 while (read($fd, $buf, 32768)) {
8345 binmode STDOUT
, ':utf8'; # as set at the beginning of gitweb.cgi
8350 sub git_log_generic
{
8351 my ($fmt_name, $body_subr, $base, $parent, $file_name, $file_hash) = @_;
8353 my $head = git_get_head_hash
($project);
8354 if (!defined $base) {
8357 if (!defined $page) {
8360 my $refs = git_get_references
();
8362 my $commit_hash = $base;
8363 if (defined $parent) {
8364 $commit_hash = "$parent..$base";
8367 parse_commits
($commit_hash, 101, (100 * $page),
8368 defined $file_name ?
($file_name, "--full-history") : ());
8371 if (!defined $file_hash && defined $file_name) {
8372 # some commits could have deleted file in question,
8373 # and not have it in tree, but one of them has to have it
8374 for (my $i = 0; $i < @commitlist; $i++) {
8375 $file_hash = git_get_hash_by_path
($commitlist[$i]{'id'}, $file_name);
8376 last if defined $file_hash;
8379 if (defined $file_hash) {
8380 $ftype = git_get_type
($file_hash);
8382 if (defined $file_name && !defined $ftype) {
8383 die_error
(500, "Unknown type of object");
8386 if (defined $file_name) {
8387 %co = parse_commit
($base)
8388 or die_error
(404, "Unknown commit object");
8392 my $paging_nav = format_log_nav
($fmt_name, $page, $#commitlist >= 100);
8394 if ($#commitlist >= 100) {
8396 $cgi->a({-href
=> href
(-replay
=>1, page
=>$page+1),
8397 -accesskey
=> "n", -title
=> "Alt-n"}, "next");
8399 my ($patch_max) = gitweb_get_feature
('patches');
8400 if ($patch_max && !defined $file_name) {
8401 if ($patch_max < 0 || @commitlist <= $patch_max) {
8402 $paging_nav .= " · " .
8403 $cgi->a({-href
=> href
(action
=>"patches", -replay
=>1)},
8409 local $action = 'fulllog';
8412 git_print_page_nav
($fmt_name,'', $hash,$hash,$hash, $paging_nav);
8413 if (defined $file_name) {
8414 git_print_header_div
('commit', esc_html
($co{'title'}), $base);
8416 git_print_header_div
('summary', $project)
8418 git_print_page_path
($file_name, $ftype, $hash_base)
8419 if (defined $file_name);
8421 $body_subr->(\
@commitlist, 0, 99, $refs, $next_link,
8422 $file_name, $file_hash, $ftype);
8428 git_log_generic
('log', \
&git_log_body
,
8429 $hash, $hash_parent);
8433 $hash ||= $hash_base || "HEAD";
8434 my %co = parse_commit
($hash)
8435 or die_error
(404, "Unknown commit object");
8437 my $parent = $co{'parent'};
8438 my $parents = $co{'parents'}; # listref
8440 # we need to prepare $formats_nav before any parameter munging
8442 if (!defined $parent) {
8444 $formats_nav .= '(initial)';
8445 } elsif (@
$parents == 1) {
8446 # single parent commit
8449 $cgi->a({-href
=> href
(action
=>"commit",
8451 esc_html
(substr($parent, 0, 7))) .
8458 $cgi->a({-href
=> href
(action
=>"commit",
8460 esc_html
(substr($_, 0, 7)));
8464 if (gitweb_check_feature
('patches') && @
$parents <= 1) {
8465 $formats_nav .= " | " .
8466 $cgi->a({-href
=> href
(action
=>"patch", -replay
=>1)},
8470 if (!defined $parent) {
8474 defined(my $fd = git_cmd_pipe
"diff-tree", '-r', "--no-commit-id",
8476 (@
$parents <= 1 ?
$parent : '-c'),
8478 or die_error
(500, "Open git-diff-tree failed");
8479 @difftree = map { chomp; to_utf8
($_) } <$fd>;
8480 close $fd or die_error
(404, "Reading git-diff-tree failed");
8482 # non-textual hash id's can be cached
8484 if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
8487 my $refs = git_get_references
();
8488 my $ref = format_ref_marker
($refs, $co{'id'});
8490 git_header_html
(undef, $expires);
8491 git_print_page_nav
('commit', '',
8492 $hash, $co{'tree'}, $hash,
8495 if (defined $co{'parent'}) {
8496 git_print_header_div
('commitdiff', esc_html
($co{'title'}), $hash, undef, $ref);
8498 git_print_header_div
('tree', esc_html
($co{'title'}), $co{'tree'}, $hash, $ref);
8500 print "<div class=\"title_text\">\n" .
8501 "<table class=\"object_header\">\n";
8502 print "<tr><td>commit</td><td class=\"sha1\">$co{'id'}</td></tr>\n";
8503 git_print_authorship_rows
(\
%co);
8506 "<td class=\"sha1\">" .
8507 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$co{'tree'}, hash_base
=>$hash),
8508 class => "list"}, $co{'tree'}) .
8510 "<td class=\"link\">" .
8511 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$co{'tree'}, hash_base
=>$hash)},
8513 my $snapshot_links = format_snapshot_links
($hash);
8514 if (defined $snapshot_links) {
8515 print " | " . $snapshot_links;
8520 foreach my $par (@
$parents) {
8523 "<td class=\"sha1\">" .
8524 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$par),
8525 class => "list"}, $par) .
8527 "<td class=\"link\">" .
8528 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$par)}, "commit") .
8530 $cgi->a({-href
=> href
(action
=>"commitdiff", hash
=>$hash, hash_parent
=>$par)}, "diff") .
8537 print "<div class=\"page_body\">\n";
8538 git_print_log
($co{'comment'});
8541 git_difftree_body
(\
@difftree, $hash, @
$parents);
8547 # object is defined by:
8548 # - hash or hash_base alone
8549 # - hash_base and file_name
8552 # - hash or hash_base alone
8553 if ($hash || ($hash_base && !defined $file_name)) {
8554 my $object_id = $hash || $hash_base;
8556 defined(my $fd = git_cmd_pipe
'cat-file', '-t', $object_id)
8557 or die_error
(404, "Object does not exist");
8561 or die_error
(404, "Object does not exist");
8563 # - hash_base and file_name
8564 } elsif ($hash_base && defined $file_name) {
8565 $file_name =~ s
,/+$,,;
8567 system(git_cmd
(), "cat-file", '-e', $hash_base) == 0
8568 or die_error
(404, "Base object does not exist");
8570 # here errors should not happen
8571 defined(my $fd = git_cmd_pipe
"ls-tree", $hash_base, "--", $file_name)
8572 or die_error
(500, "Open git-ls-tree failed");
8573 my $line = to_utf8
(scalar <$fd>);
8576 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'
8577 unless ($line && $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t/) {
8578 die_error
(404, "File or directory for given base does not exist");
8583 die_error
(400, "Not enough information to find object");
8586 print $cgi->redirect(-uri
=> href
(action
=>$type, -full
=>1,
8587 hash
=>$hash, hash_base
=>$hash_base,
8588 file_name
=>$file_name),
8589 -status
=> '302 Found');
8593 my $format = shift || 'html';
8594 my $diff_style = $input_params{'diff_style'} || 'inline';
8601 # preparing $fd and %diffinfo for git_patchset_body
8603 if (defined $hash_base && defined $hash_parent_base) {
8604 if (defined $file_name) {
8606 defined($fd = git_cmd_pipe
"diff-tree", '-r', @diff_opts,
8607 $hash_parent_base, $hash_base,
8608 "--", (defined $file_parent ?
$file_parent : ()), $file_name)
8609 or die_error
(500, "Open git-diff-tree failed");
8610 @difftree = map { chomp; to_utf8
($_) } <$fd>;
8612 or die_error
(404, "Reading git-diff-tree failed");
8614 or die_error
(404, "Blob diff not found");
8616 } elsif (defined $hash &&
8617 $hash =~ /[0-9a-fA-F]{40}/) {
8618 # try to find filename from $hash
8620 # read filtered raw output
8621 defined($fd = git_cmd_pipe
"diff-tree", '-r', @diff_opts,
8622 $hash_parent_base, $hash_base, "--")
8623 or die_error
(500, "Open git-diff-tree failed");
8625 # ':100644 100644 03b21826... 3b93d5e7... M ls-files.c'
8627 grep { /^:[0-7]{6} [0-7]{6} [0-9a-fA-F]{40} $hash/ }
8628 map { chomp; to_utf8
($_) } <$fd>;
8630 or die_error
(404, "Reading git-diff-tree failed");
8632 or die_error
(404, "Blob diff not found");
8635 die_error
(400, "Missing one of the blob diff parameters");
8638 if (@difftree > 1) {
8639 die_error
(400, "Ambiguous blob diff specification");
8642 %diffinfo = parse_difftree_raw_line
($difftree[0]);
8643 $file_parent ||= $diffinfo{'from_file'} || $file_name;
8644 $file_name ||= $diffinfo{'to_file'};
8646 $hash_parent ||= $diffinfo{'from_id'};
8647 $hash ||= $diffinfo{'to_id'};
8649 # non-textual hash id's can be cached
8650 if ($hash_base =~ m/^[0-9a-fA-F]{40}$/ &&
8651 $hash_parent_base =~ m/^[0-9a-fA-F]{40}$/) {
8656 defined($fd = git_cmd_pipe
"diff-tree", '-r', @diff_opts,
8657 '-p', ($format eq 'html' ?
"--full-index" : ()),
8658 $hash_parent_base, $hash_base,
8659 "--", (defined $file_parent ?
$file_parent : ()), $file_name)
8660 or die_error
(500, "Open git-diff-tree failed");
8663 # old/legacy style URI -- not generated anymore since 1.4.3.
8665 die_error
('404 Not Found', "Missing one of the blob diff parameters")
8669 if ($format eq 'html') {
8671 $cgi->a({-href
=> href
(action
=>"blobdiff_plain", -replay
=>1)},
8673 $formats_nav .= diff_style_nav
($diff_style);
8674 git_header_html
(undef, $expires);
8675 if (defined $hash_base && (my %co = parse_commit
($hash_base))) {
8676 git_print_page_nav
('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
8677 git_print_header_div
('commit', esc_html
($co{'title'}), $hash_base);
8679 print "<div class=\"page_nav\"><br/>$formats_nav<br/></div>\n";
8680 print "<div class=\"title\">".esc_html
("$hash vs $hash_parent")."</div>\n";
8682 if (defined $file_name) {
8683 git_print_page_path
($file_name, "blob", $hash_base);
8685 print "<div class=\"page_path\"></div>\n";
8688 } elsif ($format eq 'plain') {
8690 -type
=> 'text/plain',
8691 -charset
=> 'utf-8',
8692 -expires
=> $expires,
8693 -content_disposition
=> 'inline; filename="' . "$file_name" . '.patch"');
8695 print "X-Git-Url: " . $cgi->self_url() . "\n\n";
8698 die_error
(400, "Unknown blobdiff format");
8702 if ($format eq 'html') {
8703 print "<div class=\"page_body\">\n";
8705 git_patchset_body
($fd, $diff_style,
8706 [ \
%diffinfo ], $hash_base, $hash_parent_base);
8709 print "</div>\n"; # class="page_body"
8713 while (my $line = to_utf8
(scalar <$fd>)) {
8714 $line =~ s!a/($hash|$hash_parent)!'a/'.esc_path($diffinfo{'from_file'})!eg;
8715 $line =~ s!b/($hash|$hash_parent)!'b/'.esc_path($diffinfo{'to_file'})!eg;
8719 last if $line =~ m!^\+\+\+!;
8728 sub git_blobdiff_plain
{
8729 git_blobdiff
('plain');
8732 # assumes that it is added as later part of already existing navigation,
8733 # so it returns "| foo | bar" rather than just "foo | bar"
8734 sub diff_style_nav
{
8735 my ($diff_style, $is_combined) = @_;
8736 $diff_style ||= 'inline';
8738 return "" if ($is_combined);
8740 my @styles = (inline
=> 'inline', 'sidebyside' => 'side by side');
8741 my %styles = @styles;
8743 @styles[ map { $_ * 2 } 0..$#styles/2 ];
8748 $_ eq $diff_style ?
$styles{$_} :
8749 $cgi->a({-href
=> href
(-replay
=>1, diff_style
=> $_)}, $styles{$_})
8753 sub git_commitdiff
{
8755 my $format = $params{-format
} || 'html';
8756 my $diff_style = $input_params{'diff_style'} || 'inline';
8758 my ($patch_max) = gitweb_get_feature
('patches');
8759 if ($format eq 'patch') {
8760 die_error
(403, "Patch view not allowed") unless $patch_max;
8763 $hash ||= $hash_base || "HEAD";
8764 my %co = parse_commit
($hash)
8765 or die_error
(404, "Unknown commit object");
8767 # choose format for commitdiff for merge
8768 if (! defined $hash_parent && @
{$co{'parents'}} > 1) {
8769 $hash_parent = '--cc';
8771 # we need to prepare $formats_nav before almost any parameter munging
8773 if ($format eq 'html') {
8775 $cgi->a({-href
=> href
(action
=>"commitdiff_plain", -replay
=>1)},
8777 if ($patch_max && @
{$co{'parents'}} <= 1) {
8778 $formats_nav .= " | " .
8779 $cgi->a({-href
=> href
(action
=>"patch", -replay
=>1)},
8782 $formats_nav .= diff_style_nav
($diff_style, @
{$co{'parents'}} > 1);
8784 if (defined $hash_parent &&
8785 $hash_parent ne '-c' && $hash_parent ne '--cc') {
8786 # commitdiff with two commits given
8787 my $hash_parent_short = $hash_parent;
8788 if ($hash_parent =~ m/^[0-9a-fA-F]{40}$/) {
8789 $hash_parent_short = substr($hash_parent, 0, 7);
8793 for (my $i = 0; $i < @
{$co{'parents'}}; $i++) {
8794 if ($co{'parents'}[$i] eq $hash_parent) {
8795 $formats_nav .= ' parent ' . ($i+1);
8799 $formats_nav .= ': ' .
8800 $cgi->a({-href
=> href
(-replay
=>1,
8801 hash
=>$hash_parent, hash_base
=>undef)},
8802 esc_html
($hash_parent_short)) .
8804 } elsif (!$co{'parent'}) {
8806 $formats_nav .= ' (initial)';
8807 } elsif (scalar @
{$co{'parents'}} == 1) {
8808 # single parent commit
8811 $cgi->a({-href
=> href
(-replay
=>1,
8812 hash
=>$co{'parent'}, hash_base
=>undef)},
8813 esc_html
(substr($co{'parent'}, 0, 7))) .
8817 if ($hash_parent eq '--cc') {
8818 $formats_nav .= ' | ' .
8819 $cgi->a({-href
=> href
(-replay
=>1,
8820 hash
=>$hash, hash_parent
=>'-c')},
8822 } else { # $hash_parent eq '-c'
8823 $formats_nav .= ' | ' .
8824 $cgi->a({-href
=> href
(-replay
=>1,
8825 hash
=>$hash, hash_parent
=>'--cc')},
8831 $cgi->a({-href
=> href
(-replay
=>1,
8832 hash
=>$_, hash_base
=>undef)},
8833 esc_html
(substr($_, 0, 7)));
8834 } @
{$co{'parents'}} ) .
8839 my $hash_parent_param = $hash_parent;
8840 if (!defined $hash_parent_param) {
8841 # --cc for multiple parents, --root for parentless
8842 $hash_parent_param =
8843 @
{$co{'parents'}} > 1 ?
'--cc' : $co{'parent'} || '--root';
8849 if ($format eq 'html') {
8850 defined($fd = git_cmd_pipe
"diff-tree", '-r', @diff_opts,
8851 "--no-commit-id", "--patch-with-raw", "--full-index",
8852 $hash_parent_param, $hash, "--")
8853 or die_error
(500, "Open git-diff-tree failed");
8855 while (my $line = to_utf8
(scalar <$fd>)) {
8857 # empty line ends raw part of diff-tree output
8859 push @difftree, scalar parse_difftree_raw_line
($line);
8862 } elsif ($format eq 'plain') {
8863 defined($fd = git_cmd_pipe
"diff-tree", '-r', @diff_opts,
8864 '-p', $hash_parent_param, $hash, "--")
8865 or die_error
(500, "Open git-diff-tree failed");
8866 } elsif ($format eq 'patch') {
8867 # For commit ranges, we limit the output to the number of
8868 # patches specified in the 'patches' feature.
8869 # For single commits, we limit the output to a single patch,
8870 # diverging from the git-format-patch default.
8871 my @commit_spec = ();
8873 if ($patch_max > 0) {
8874 push @commit_spec, "-$patch_max";
8876 push @commit_spec, '-n', "$hash_parent..$hash";
8878 if ($params{-single
}) {
8879 push @commit_spec, '-1';
8881 if ($patch_max > 0) {
8882 push @commit_spec, "-$patch_max";
8884 push @commit_spec, "-n";
8886 push @commit_spec, '--root', $hash;
8888 defined($fd = git_cmd_pipe
"format-patch", @diff_opts,
8889 '--encoding=utf8', '--stdout', @commit_spec)
8890 or die_error
(500, "Open git-format-patch failed");
8892 die_error
(400, "Unknown commitdiff format");
8895 # non-textual hash id's can be cached
8897 if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
8901 # write commit message
8902 if ($format eq 'html') {
8903 my $refs = git_get_references
();
8904 my $ref = format_ref_marker
($refs, $co{'id'});
8906 git_header_html
(undef, $expires);
8907 git_print_page_nav
('commitdiff','', $hash,$co{'tree'},$hash, $formats_nav);
8908 git_print_header_div
('commit', esc_html
($co{'title'}), $hash, undef, $ref);
8909 print "<div class=\"title_text\">\n" .
8910 "<table class=\"object_header\">\n";
8911 git_print_authorship_rows
(\
%co);
8914 print "<div class=\"page_body\">\n";
8915 if (@
{$co{'comment'}} > 1) {
8916 print "<div class=\"log\">\n";
8917 git_print_log
($co{'comment'}, -final_empty_line
=> 1, -remove_title
=> 1);
8918 print "</div>\n"; # class="log"
8921 } elsif ($format eq 'plain') {
8922 my $refs = git_get_references
("tags");
8923 my $tagname = git_get_rev_name_tags
($hash);
8924 my $filename = basename
($project) . "-$hash.patch";
8927 -type
=> 'text/plain',
8928 -charset
=> 'utf-8',
8929 -expires
=> $expires,
8930 -content_disposition
=> 'inline; filename="' . "$filename" . '"');
8931 my %ad = parse_date
($co{'author_epoch'}, $co{'author_tz'});
8932 print "From: " . to_utf8
($co{'author'}) . "\n";
8933 print "Date: $ad{'rfc2822'} ($ad{'tz_local'})\n";
8934 print "Subject: " . to_utf8
($co{'title'}) . "\n";
8936 print "X-Git-Tag: $tagname\n" if $tagname;
8937 print "X-Git-Url: " . $cgi->self_url() . "\n\n";
8939 foreach my $line (@
{$co{'comment'}}) {
8940 print to_utf8
($line) . "\n";
8943 } elsif ($format eq 'patch') {
8944 my $filename = basename
($project) . "-$hash.patch";
8947 -type
=> 'text/plain',
8948 -charset
=> 'utf-8',
8949 -expires
=> $expires,
8950 -content_disposition
=> 'inline; filename="' . "$filename" . '"');
8954 if ($format eq 'html') {
8955 my $use_parents = !defined $hash_parent ||
8956 $hash_parent eq '-c' || $hash_parent eq '--cc';
8957 git_difftree_body
(\
@difftree, $hash,
8958 $use_parents ? @
{$co{'parents'}} : $hash_parent);
8961 git_patchset_body
($fd, $diff_style,
8963 $use_parents ? @
{$co{'parents'}} : $hash_parent);
8965 print "</div>\n"; # class="page_body"
8968 } elsif ($format eq 'plain') {
8973 or print "Reading git-diff-tree failed\n";
8974 } elsif ($format eq 'patch') {
8979 or print "Reading git-format-patch failed\n";
8983 sub git_commitdiff_plain
{
8984 git_commitdiff
(-format
=> 'plain');
8987 # format-patch-style patches
8989 git_commitdiff
(-format
=> 'patch', -single
=> 1);
8993 git_commitdiff
(-format
=> 'patch');
8997 git_log_generic
('history', \
&git_history_body
,
8998 $hash_base, $hash_parent_base,
9003 $searchtype ||= 'commit';
9005 # check if appropriate features are enabled
9006 gitweb_check_feature
('search')
9007 or die_error
(403, "Search is disabled");
9008 if ($searchtype eq 'pickaxe') {
9009 # pickaxe may take all resources of your box and run for several minutes
9010 # with every query - so decide by yourself how public you make this feature
9011 gitweb_check_feature
('pickaxe')
9012 or die_error
(403, "Pickaxe search is disabled");
9014 if ($searchtype eq 'grep') {
9015 # grep search might be potentially CPU-intensive, too
9016 gitweb_check_feature
('grep')
9017 or die_error
(403, "Grep search is disabled");
9020 if (!defined $searchtext) {
9021 die_error
(400, "Text field is empty");
9023 if (!defined $hash) {
9024 $hash = git_get_head_hash
($project);
9026 my %co = parse_commit
($hash);
9028 die_error
(404, "Unknown commit object");
9030 if (!defined $page) {
9034 if ($searchtype eq 'commit' ||
9035 $searchtype eq 'author' ||
9036 $searchtype eq 'committer') {
9037 git_search_message
(%co);
9038 } elsif ($searchtype eq 'pickaxe') {
9039 git_search_changes
(%co);
9040 } elsif ($searchtype eq 'grep') {
9041 git_search_files
(%co);
9043 die_error
(400, "Unknown search type");
9047 sub git_search_help
{
9049 git_print_page_nav
('','', $hash,$hash,$hash);
9051 <p><strong>Pattern</strong> is by default a normal string that is matched precisely (but without
9052 regard to case, except in the case of pickaxe). However, when you check the <em>re</em> checkbox,
9053 the pattern entered is recognized as the POSIX extended
9054 <a href="http://en.wikipedia.org/wiki/Regular_expression">regular expression</a> (also case
9057 <dt><b>commit</b></dt>
9058 <dd>The commit messages and authorship information will be scanned for the given pattern.</dd>
9060 my $have_grep = gitweb_check_feature
('grep');
9063 <dt><b>grep</b></dt>
9064 <dd>All files in the currently selected tree (HEAD unless you are explicitly browsing
9065 a different one) are searched for the given pattern. On large trees, this search can take
9066 a while and put some strain on the server, so please use it with some consideration. Note that
9067 due to git-grep peculiarity, currently if regexp mode is turned off, the matches are
9068 case-sensitive.</dd>
9072 <dt><b>author</b></dt>
9073 <dd>Name and e-mail of the change author and date of birth of the patch will be scanned for the given pattern.</dd>
9074 <dt><b>committer</b></dt>
9075 <dd>Name and e-mail of the committer and date of commit will be scanned for the given pattern.</dd>
9077 my $have_pickaxe = gitweb_check_feature
('pickaxe');
9078 if ($have_pickaxe) {
9080 <dt><b>pickaxe</b></dt>
9081 <dd>All commits that caused the string to appear or disappear from any file (changes that
9082 added, removed or "modified" the string) will be listed. This search can take a while and
9083 takes a lot of strain on the server, so please use it wisely. Note that since you may be
9084 interested even in changes just changing the case as well, this search is case sensitive.</dd>
9092 git_log_generic
('shortlog', \
&git_shortlog_body
,
9093 $hash, $hash_parent);
9096 ## ......................................................................
9097 ## feeds (RSS, Atom; OPML)
9100 my $format = shift || 'atom';
9101 my $have_blame = gitweb_check_feature
('blame');
9103 # Atom: http://www.atomenabled.org/developers/syndication/
9104 # RSS: http://www.notestips.com/80256B3A007F2692/1/NAMO5P9UPQ
9105 if ($format ne 'rss' && $format ne 'atom') {
9106 die_error
(400, "Unknown web feed format");
9109 # log/feed of current (HEAD) branch, log of given branch, history of file/directory
9110 my $head = $hash || 'HEAD';
9111 my @commitlist = parse_commits
($head, 150, 0, $file_name);
9115 my $content_type = "application/$format+xml";
9116 if (defined $cgi->http('HTTP_ACCEPT') &&
9117 $cgi->Accept('text/xml') > $cgi->Accept($content_type)) {
9118 # browser (feed reader) prefers text/xml
9119 $content_type = 'text/xml';
9121 if (defined($commitlist[0])) {
9122 %latest_commit = %{$commitlist[0]};
9123 my $latest_epoch = $latest_commit{'committer_epoch'};
9124 exit_if_unmodified_since
($latest_epoch);
9125 %latest_date = parse_date
($latest_epoch, $latest_commit{'committer_tz'});
9128 -type
=> $content_type,
9129 -charset
=> 'utf-8',
9130 %latest_date ?
(-last_modified
=> $latest_date{'rfc2822'}) : (),
9131 -status
=> '200 OK');
9133 # Optimization: skip generating the body if client asks only
9134 # for Last-Modified date.
9135 return if ($cgi->request_method() eq 'HEAD');
9138 my $title = "$site_name - $project/$action";
9139 my $feed_type = 'log';
9140 if (defined $hash) {
9141 $title .= " - '$hash'";
9142 $feed_type = 'branch log';
9143 if (defined $file_name) {
9144 $title .= " :: $file_name";
9145 $feed_type = 'history';
9147 } elsif (defined $file_name) {
9148 $title .= " - $file_name";
9149 $feed_type = 'history';
9151 $title .= " $feed_type";
9152 $title = esc_html
($title);
9153 my $descr = git_get_project_description
($project);
9154 if (defined $descr) {
9155 $descr = esc_html
($descr);
9157 $descr = "$project " .
9158 ($format eq 'rss' ?
'RSS' : 'Atom') .
9161 my $owner = git_get_project_owner
($project);
9162 $owner = esc_html
($owner);
9166 if (defined $file_name) {
9167 $alt_url = href
(-full
=>1, action
=>"history", hash
=>$hash, file_name
=>$file_name);
9168 } elsif (defined $hash) {
9169 $alt_url = href
(-full
=>1, action
=>"log", hash
=>$hash);
9171 $alt_url = href
(-full
=>1, action
=>"summary");
9173 print qq!<?xml version
="1.0" encoding
="utf-8"?
>\n!;
9174 if ($format eq 'rss') {
9176 <rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
9179 print "<title>$title</title>\n" .
9180 "<link>$alt_url</link>\n" .
9181 "<description>$descr</description>\n" .
9182 "<language>en</language>\n" .
9183 # project owner is responsible for 'editorial' content
9184 "<managingEditor>$owner</managingEditor>\n";
9185 if (defined $logo || defined $favicon) {
9186 # prefer the logo to the favicon, since RSS
9187 # doesn't allow both
9188 my $img = esc_url
($logo || $favicon);
9190 "<url>$img</url>\n" .
9191 "<title>$title</title>\n" .
9192 "<link>$alt_url</link>\n" .
9196 print "<pubDate>$latest_date{'rfc2822'}</pubDate>\n";
9197 print "<lastBuildDate>$latest_date{'rfc2822'}</lastBuildDate>\n";
9199 print "<generator>gitweb v.$version/$git_version</generator>\n";
9200 } elsif ($format eq 'atom') {
9202 <feed xmlns="http://www.w3.org/2005/Atom">
9204 print "<title>$title</title>\n" .
9205 "<subtitle>$descr</subtitle>\n" .
9206 '<link rel="alternate" type="text/html" href="' .
9207 $alt_url . '" />' . "\n" .
9208 '<link rel="self" type="' . $content_type . '" href="' .
9209 $cgi->self_url() . '" />' . "\n" .
9210 "<id>" . href
(-full
=>1) . "</id>\n" .
9211 # use project owner for feed author
9212 '<author><name>'. email_obfuscate
($owner) . '</name></author>\n';
9213 if (defined $favicon) {
9214 print "<icon>" . esc_url
($favicon) . "</icon>\n";
9216 if (defined $logo) {
9217 # not twice as wide as tall: 72 x 27 pixels
9218 print "<logo>" . esc_url
($logo) . "</logo>\n";
9220 if (! %latest_date) {
9221 # dummy date to keep the feed valid until commits trickle in:
9222 print "<updated>1970-01-01T00:00:00Z</updated>\n";
9224 print "<updated>$latest_date{'iso-8601'}</updated>\n";
9226 print "<generator version='$version/$git_version'>gitweb</generator>\n";
9230 for (my $i = 0; $i <= $#commitlist; $i++) {
9231 my %co = %{$commitlist[$i]};
9232 my $commit = $co{'id'};
9233 # we read 150, we always show 30 and the ones more recent than 48 hours
9234 if (($i >= 20) && ((time - $co{'author_epoch'}) > 48*60*60)) {
9237 my %cd = parse_date
($co{'author_epoch'}, $co{'author_tz'});
9239 # get list of changed files
9240 defined(my $fd = git_cmd_pipe
"diff-tree", '-r', @diff_opts,
9241 $co{'parent'} || "--root",
9242 $co{'id'}, "--", (defined $file_name ?
$file_name : ()))
9244 my @difftree = map { chomp; to_utf8
($_) } <$fd>;
9248 # print element (entry, item)
9249 my $co_url = href
(-full
=>1, action
=>"commitdiff", hash
=>$commit);
9250 if ($format eq 'rss') {
9252 "<title>" . esc_html
($co{'title'}) . "</title>\n" .
9253 "<author>" . esc_html
($co{'author'}) . "</author>\n" .
9254 "<pubDate>$cd{'rfc2822'}</pubDate>\n" .
9255 "<guid isPermaLink=\"true\">$co_url</guid>\n" .
9256 "<link>$co_url</link>\n" .
9257 "<description>" . esc_html
($co{'title'}) . "</description>\n" .
9258 "<content:encoded>" .
9260 } elsif ($format eq 'atom') {
9262 "<title type=\"html\">" . esc_html
($co{'title'}) . "</title>\n" .
9263 "<updated>$cd{'iso-8601'}</updated>\n" .
9265 " <name>" . esc_html
($co{'author_name'}) . "</name>\n";
9266 if ($co{'author_email'}) {
9267 print " <email>" . esc_html
($co{'author_email'}) . "</email>\n";
9269 print "</author>\n" .
9270 # use committer for contributor
9272 " <name>" . esc_html
($co{'committer_name'}) . "</name>\n";
9273 if ($co{'committer_email'}) {
9274 print " <email>" . esc_html
($co{'committer_email'}) . "</email>\n";
9276 print "</contributor>\n" .
9277 "<published>$cd{'iso-8601'}</published>\n" .
9278 "<link rel=\"alternate\" type=\"text/html\" href=\"$co_url\" />\n" .
9279 "<id>$co_url</id>\n" .
9280 "<content type=\"xhtml\" xml:base=\"" . esc_url
($my_url) . "\">\n" .
9281 "<div xmlns=\"http://www.w3.org/1999/xhtml\">\n";
9283 my $comment = $co{'comment'};
9285 foreach my $line (@
$comment) {
9286 $line = esc_html
($line);
9289 print "</pre><ul>\n";
9290 foreach my $difftree_line (@difftree) {
9291 my %difftree = parse_difftree_raw_line
($difftree_line);
9292 next if !$difftree{'from_id'};
9294 my $file = $difftree{'file'} || $difftree{'to_file'};
9298 $cgi->a({-href
=> href
(-full
=>1, action
=>"blobdiff",
9299 hash
=>$difftree{'to_id'}, hash_parent
=>$difftree{'from_id'},
9300 hash_base
=>$co{'id'}, hash_parent_base
=>$co{'parent'},
9301 file_name
=>$file, file_parent
=>$difftree{'from_file'}),
9302 -title
=> "diff"}, 'D');
9304 print $cgi->a({-href
=> href
(-full
=>1, action
=>"blame",
9305 file_name
=>$file, hash_base
=>$commit),
9306 -class => "blamelink",
9307 -title
=> "blame"}, 'B');
9309 # if this is not a feed of a file history
9310 if (!defined $file_name || $file_name ne $file) {
9311 print $cgi->a({-href
=> href
(-full
=>1, action
=>"history",
9312 file_name
=>$file, hash
=>$commit),
9313 -title
=> "history"}, 'H');
9315 $file = esc_path
($file);
9319 if ($format eq 'rss') {
9320 print "</ul>]]>\n" .
9321 "</content:encoded>\n" .
9323 } elsif ($format eq 'atom') {
9324 print "</ul>\n</div>\n" .
9331 if ($format eq 'rss') {
9332 print "</channel>\n</rss>\n";
9333 } elsif ($format eq 'atom') {
9347 my @list = git_get_projects_list
($project_filter, $strict_export);
9349 die_error
(404, "No projects found");
9353 -type
=> 'text/xml',
9354 -charset
=> 'utf-8',
9355 -content_disposition
=> 'inline; filename="opml.xml"');
9357 my $title = esc_html
($site_name);
9358 my $filter = " within subdirectory ";
9359 if (defined $project_filter) {
9360 $filter .= esc_html
($project_filter);
9365 <?xml version="1.0" encoding="utf-8"?>
9366 <opml version="1.0">
9368 <title>$title OPML Export$filter</title>
9371 <outline text="git RSS feeds">
9374 foreach my $pr (@list) {
9376 my $head = git_get_head_hash
($proj{'path'});
9377 if (!defined $head) {
9380 $git_dir = "$projectroot/$proj{'path'}";
9381 my %co = parse_commit
($head);
9386 my $path = esc_html
(chop_str
($proj{'path'}, 25, 5));
9387 my $rss = href
('project' => $proj{'path'}, 'action' => 'rss', -full
=> 1);
9388 my $html = href
('project' => $proj{'path'}, 'action' => 'summary', -full
=> 1);
9389 print "<outline type=\"rss\" text=\"$path\" title=\"$path\" xmlUrl=\"$rss\" htmlUrl=\"$html\"/>\n";