Merge branch 't/config/highlight-exts' into refs/top-bases/t/misc/posix-shell
[git/gitweb.git] / gitweb / gitweb.perl
blob5dadd961896b88e6c80c2fca8f007676fa8c7523
1 #!/usr/bin/perl
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
10 use 5.008;
11 use strict;
12 use warnings;
13 use CGI qw(:standard :escapeHTML -nosticky);
14 use CGI::Util qw(unescape);
15 use CGI::Carp qw(fatalsToBrowser set_message);
16 use Encode;
17 use Fcntl ':mode';
18 use File::Find qw();
19 use File::Basename qw(basename);
20 use File::Spec;
21 use Time::HiRes qw(gettimeofday tv_interval);
22 binmode STDOUT, ':utf8';
24 if (!defined($CGI::VERSION) || $CGI::VERSION < 4.08) {
25 eval 'sub CGI::multi_param { CGI::param(@_) }'
28 our $t0 = [ gettimeofday() ];
29 our $number_of_git_cmds = 0;
31 BEGIN {
32 CGI->compile() if $ENV{'MOD_PERL'};
35 our $version = "++GIT_VERSION++";
37 our ($my_url, $my_uri, $base_url, $path_info, $home_link);
38 sub evaluate_uri {
39 our $cgi;
41 our $my_url = $cgi->url();
42 our $my_uri = $cgi->url(-absolute => 1);
44 # Base URL for relative URLs in gitweb ($logo, $favicon, ...),
45 # needed and used only for URLs with nonempty PATH_INFO
46 our $base_url = $my_url;
48 # When the script is used as DirectoryIndex, the URL does not contain the name
49 # of the script file itself, and $cgi->url() fails to strip PATH_INFO, so we
50 # have to do it ourselves. We make $path_info global because it's also used
51 # later on.
53 # Another issue with the script being the DirectoryIndex is that the resulting
54 # $my_url data is not the full script URL: this is good, because we want
55 # generated links to keep implying the script name if it wasn't explicitly
56 # indicated in the URL we're handling, but it means that $my_url cannot be used
57 # as base URL.
58 # Therefore, if we needed to strip PATH_INFO, then we know that we have
59 # to build the base URL ourselves:
60 our $path_info = decode_utf8($ENV{"PATH_INFO"});
61 if ($path_info) {
62 # $path_info has already been URL-decoded by the web server, but
63 # $my_url and $my_uri have not. URL-decode them so we can properly
64 # strip $path_info.
65 $my_url = unescape($my_url);
66 $my_uri = unescape($my_uri);
67 if ($my_url =~ s,\Q$path_info\E$,, &&
68 $my_uri =~ s,\Q$path_info\E$,, &&
69 defined $ENV{'SCRIPT_NAME'}) {
70 $base_url = $cgi->url(-base => 1) . $ENV{'SCRIPT_NAME'};
74 # target of the home link on top of all pages
75 our $home_link = $my_uri || "/";
78 # core git executable to use
79 # this can just be "git" if your webserver has a sensible PATH
80 our $GIT = "++GIT_BINDIR++/git";
82 # absolute fs-path which will be prepended to the project path
83 #our $projectroot = "/pub/scm";
84 our $projectroot = "++GITWEB_PROJECTROOT++";
86 # fs traversing limit for getting project list
87 # the number is relative to the projectroot
88 our $project_maxdepth = "++GITWEB_PROJECT_MAXDEPTH++";
90 # string of the home link on top of all pages
91 our $home_link_str = "++GITWEB_HOME_LINK_STR++";
93 # extra breadcrumbs preceding the home link
94 our @extra_breadcrumbs = ();
96 # name of your site or organization to appear in page titles
97 # replace this with something more descriptive for clearer bookmarks
98 our $site_name = "++GITWEB_SITENAME++"
99 || ($ENV{'SERVER_NAME'} || "Untitled") . " Git";
101 # html snippet to include in the <head> section of each page
102 our $site_html_head_string = "++GITWEB_SITE_HTML_HEAD_STRING++";
103 # filename of html text to include at top of each page
104 our $site_header = "++GITWEB_SITE_HEADER++";
105 # html text to include at home page
106 our $home_text = "++GITWEB_HOMETEXT++";
107 # filename of html text to include at bottom of each page
108 our $site_footer = "++GITWEB_SITE_FOOTER++";
110 # URI of stylesheets
111 our @stylesheets = ("++GITWEB_CSS++");
112 # URI of a single stylesheet, which can be overridden in GITWEB_CONFIG.
113 our $stylesheet = undef;
114 # URI of GIT logo (72x27 size)
115 our $logo = "++GITWEB_LOGO++";
116 # URI of GIT favicon, assumed to be image/png type
117 our $favicon = "++GITWEB_FAVICON++";
118 # URI of gitweb.js (JavaScript code for gitweb)
119 our $javascript = "++GITWEB_JS++";
121 # URI and label (title) of GIT logo link
122 #our $logo_url = "http://www.kernel.org/pub/software/scm/git/docs/";
123 #our $logo_label = "git documentation";
124 our $logo_url = "http://git-scm.com/";
125 our $logo_label = "git homepage";
127 # source of projects list
128 our $projects_list = "++GITWEB_LIST++";
130 # the width (in characters) of the projects list "Description" column
131 our $projects_list_description_width = 25;
133 # group projects by category on the projects list
134 # (enabled if this variable evaluates to true)
135 our $projects_list_group_categories = 0;
137 # default category if none specified
138 # (leave the empty string for no category)
139 our $project_list_default_category = "";
141 # default order of projects list
142 # valid values are none, project, descr, owner, and age
143 our $default_projects_order = "project";
145 # show repository only if this file exists
146 # (only effective if this variable evaluates to true)
147 our $export_ok = "++GITWEB_EXPORT_OK++";
149 # don't generate age column on the projects list page
150 our $omit_age_column = 0;
152 # don't generate information about owners of repositories
153 our $omit_owner=0;
155 # show repository only if this subroutine returns true
156 # when given the path to the project, for example:
157 # sub { return -e "$_[0]/git-daemon-export-ok"; }
158 our $export_auth_hook = undef;
160 # only allow viewing of repositories also shown on the overview page
161 our $strict_export = "++GITWEB_STRICT_EXPORT++";
163 # list of git base URLs used for URL to where fetch project from,
164 # i.e. full URL is "$git_base_url/$project"
165 our @git_base_url_list = grep { $_ ne '' } ("++GITWEB_BASE_URL++");
167 # default blob_plain mimetype and default charset for text/plain blob
168 our $default_blob_plain_mimetype = 'text/plain';
169 our $default_text_plain_charset = undef;
171 # file to use for guessing MIME types before trying /etc/mime.types
172 # (relative to the current git repository)
173 our $mimetypes_file = undef;
175 # assume this charset if line contains non-UTF-8 characters;
176 # it should be valid encoding (see Encoding::Supported(3pm) for list),
177 # for which encoding all byte sequences are valid, for example
178 # 'iso-8859-1' aka 'latin1' (it is decoded without checking, so it
179 # could be even 'utf-8' for the old behavior)
180 our $fallback_encoding = 'latin1';
182 # rename detection options for git-diff and git-diff-tree
183 # - default is '-M', with the cost proportional to
184 # (number of removed files) * (number of new files).
185 # - more costly is '-C' (which implies '-M'), with the cost proportional to
186 # (number of changed files + number of removed files) * (number of new files)
187 # - even more costly is '-C', '--find-copies-harder' with cost
188 # (number of files in the original tree) * (number of new files)
189 # - one might want to include '-B' option, e.g. '-B', '-M'
190 our @diff_opts = ('-M'); # taken from git_commit
192 # Disables features that would allow repository owners to inject script into
193 # the gitweb domain.
194 our $prevent_xss = 0;
196 # Path to the highlight executable to use (must be the one from
197 # http://www.andre-simon.de due to assumptions about parameters and output).
198 # Useful if highlight is not installed on your webserver's PATH.
199 # [Default: highlight]
200 our $highlight_bin = "++HIGHLIGHT_BIN++";
202 # information about snapshot formats that gitweb is capable of serving
203 our %known_snapshot_formats = (
204 # name => {
205 # 'display' => display name,
206 # 'type' => mime type,
207 # 'suffix' => filename suffix,
208 # 'format' => --format for git-archive,
209 # 'compressor' => [compressor command and arguments]
210 # (array reference, optional)
211 # 'disabled' => boolean (optional)}
213 'tgz' => {
214 'display' => 'tar.gz',
215 'type' => 'application/x-gzip',
216 'suffix' => '.tar.gz',
217 'format' => 'tar',
218 'compressor' => ['gzip', '-n']},
220 'tbz2' => {
221 'display' => 'tar.bz2',
222 'type' => 'application/x-bzip2',
223 'suffix' => '.tar.bz2',
224 'format' => 'tar',
225 'compressor' => ['bzip2']},
227 'txz' => {
228 'display' => 'tar.xz',
229 'type' => 'application/x-xz',
230 'suffix' => '.tar.xz',
231 'format' => 'tar',
232 'compressor' => ['xz'],
233 'disabled' => 1},
235 'zip' => {
236 'display' => 'zip',
237 'type' => 'application/x-zip',
238 'suffix' => '.zip',
239 'format' => 'zip'},
242 # Aliases so we understand old gitweb.snapshot values in repository
243 # configuration.
244 our %known_snapshot_format_aliases = (
245 'gzip' => 'tgz',
246 'bzip2' => 'tbz2',
247 'xz' => 'txz',
249 # backward compatibility: legacy gitweb config support
250 'x-gzip' => undef, 'gz' => undef,
251 'x-bzip2' => undef, 'bz2' => undef,
252 'x-zip' => undef, '' => undef,
255 # Pixel sizes for icons and avatars. If the default font sizes or lineheights
256 # are changed, it may be appropriate to change these values too via
257 # $GITWEB_CONFIG.
258 our %avatar_size = (
259 'default' => 16,
260 'double' => 32
263 # Used to set the maximum load that we will still respond to gitweb queries.
264 # If server load exceed this value then return "503 server busy" error.
265 # If gitweb cannot determined server load, it is taken to be 0.
266 # Leave it undefined (or set to 'undef') to turn off load checking.
267 our $maxload = 300;
269 # configuration for 'highlight' (http://www.andre-simon.de/)
270 # match by basename
271 our %highlight_basename = (
272 #'Program' => 'py',
273 #'Library' => 'py',
274 'SConstruct' => 'py', # SCons equivalent of Makefile
275 'Makefile' => 'make',
276 'makefile' => 'make',
277 'GNUmakefile' => 'make',
278 'BSDmakefile' => 'make',
280 # match by shebang regex
281 our %highlight_shebang = (
282 # Each entry has a key which is the syntax to use and
283 # a value which is either a qr regex or an array of qr regexs to match
284 # against the first 128 (less if the blob is shorter) BYTES of the blob.
285 # We match /usr/bin/env items separately to require "/usr/bin/env" and
286 # allow a limited subset of NAME=value items to appear.
287 'awk' => [ qr,^#!\s*/(?:\w+/)*(?:[gnm]?awk)(?:\s|$),mo,
288 qr,^#!\s*/usr/bin/env\s+(?:\w+=\w*\s+)*(?:[gnm]?awk)(?:\s|$),mo ],
289 'make' => [ qr,^#!\s*/(?:\w+/)*(?:g?make)(?:\s|$),mo,
290 qr,^#!\s*/usr/bin/env\s+(?:\w+=\w*\s+)*(?:g?make)(?:\s|$),mo ],
291 'php' => [ qr,^#!\s*/(?:\w+/)*(?:php)(?:\s|$),mo,
292 qr,^#!\s*/usr/bin/env\s+(?:\w+=\w*\s+)*(?:php)(?:\s|$),mo ],
293 'pl' => [ qr,^#!\s*/(?:\w+/)*(?:perl)(?:\s|$),mo,
294 qr,^#!\s*/usr/bin/env\s+(?:\w+=\w*\s+)*(?:perl)(?:\s|$),mo ],
295 'py' => [ qr,^#!\s*/(?:\w+/)*(?:python)(?:\s|$),mo,
296 qr,^#!\s*/usr/bin/env\s+(?:\w+=\w*\s+)*(?:python)(?:\s|$),mo ],
297 'sh' => [ qr,^#!\s*/(?:\w+/)*(?:[bd]ash|t?csh|[akz]?sh)(?:\s|$),mo,
298 qr,^#!\s*/usr/bin/env\s+(?:\w+=\w*\s+)*(?:[bd]ash|t?csh|[akz]?sh)(?:\s|$),mo ],
299 'rb' => [ qr,^#!\s*/(?:\w+/)*(?:perl)(?:\s|$),mo,
300 qr,^#!\s*/usr/bin/env\s+(?:\w+=\w*\s+)*(?:perl)(?:\s|$),mo ],
302 # match by extension
303 our %highlight_ext = (
304 # main extensions, defining name of syntax;
305 # see files in /usr/share/highlight/langDefs/ directory
306 (map { $_ => $_ } qw(
307 4gl a4c abnf abp ada agda ahk ampl amtrix applescript arc
308 arm as asm asp aspect ats au3 avenue awk bat bb bbcode bib
309 bms bnf boo c cb cfc chl clipper clojure clp cob cs css d
310 diff dot dylan e ebnf erl euphoria exp f90 flx for frink fs
311 go haskell hcl html httpd hx icl icn idl idlang ili
312 inc_luatex ini inp io iss j java js jsp lbn ldif lgt lhs
313 lisp lotos ls lsl lua ly make mel mercury mib miranda ml mo
314 mod2 mod3 mpl ms mssql n nas nbc nice nrx nsi nut nxc oberon
315 objc octave oorexx os oz pas php pike pl pl1 pov pro
316 progress ps ps1 psl pure py pyx q qmake qu r rb rebol rexx
317 rnc s sas sc scala scilab sh sma smalltalk sml sno spec spn
318 sql sybase tcl tcsh tex ttcn3 vala vb verilog vhd xml xpp y
319 yaiff znn)),
320 # alternate extensions, see /etc/highlight/filetypes.conf
321 (map { $_ => '4gl' } qw(informix)),
322 (map { $_ => 'a4c' } qw(ascend)),
323 (map { $_ => 'abp' } qw(abp4)),
324 (map { $_ => 'ada' } qw(a adb ads gnad)),
325 (map { $_ => 'ahk' } qw(autohotkey)),
326 (map { $_ => 'ampl' } qw(dat run)),
327 (map { $_ => 'amtrix' } qw(hnd s4 s4h s4t t4)),
328 (map { $_ => 'as' } qw(actionscript)),
329 (map { $_ => 'asm' } qw(29k 68s 68x a51 assembler x68 x86)),
330 (map { $_ => 'asp' } qw(asa)),
331 (map { $_ => 'aspect' } qw(was wud)),
332 (map { $_ => 'ats' } qw(dats)),
333 (map { $_ => 'au3' } qw(autoit)),
334 (map { $_ => 'bat' } qw(cmd)),
335 (map { $_ => 'bb' } qw(blitzbasic)),
336 (map { $_ => 'bib' } qw(bibtex)),
337 (map { $_ => 'c' } qw(c++ cc cpp cu cxx h hh hpp hxx)),
338 (map { $_ => 'cb' } qw(clearbasic)),
339 (map { $_ => 'cfc' } qw(cfm coldfusion)),
340 (map { $_ => 'chl' } qw(chill)),
341 (map { $_ => 'cob' } qw(cbl cobol)),
342 (map { $_ => 'cs' } qw(csharp)),
343 (map { $_ => 'diff' } qw(patch)),
344 (map { $_ => 'dot' } qw(graphviz)),
345 (map { $_ => 'e' } qw(eiffel se)),
346 (map { $_ => 'erl' } qw(erlang hrl)),
347 (map { $_ => 'euphoria' } qw(eu ew ex exu exw wxu)),
348 (map { $_ => 'exp' } qw(express)),
349 (map { $_ => 'f90' } qw(f95)),
350 (map { $_ => 'flx' } qw(felix)),
351 (map { $_ => 'for' } qw(f f77 ftn)),
352 (map { $_ => 'fs' } qw(fsharp fsx)),
353 (map { $_ => 'haskell' } qw(hs)),
354 (map { $_ => 'html' } qw(htm xhtml)),
355 (map { $_ => 'hx' } qw(haxe)),
356 (map { $_ => 'icl' } qw(clean)),
357 (map { $_ => 'icn' } qw(icon)),
358 (map { $_ => 'ili' } qw(interlis)),
359 (map { $_ => 'inp' } qw(fame)),
360 (map { $_ => 'iss' } qw(innosetup)),
361 (map { $_ => 'j' } qw(jasmin)),
362 (map { $_ => 'java' } qw(groovy grv)),
363 (map { $_ => 'lbn' } qw(luban)),
364 (map { $_ => 'lgt' } qw(logtalk)),
365 (map { $_ => 'lisp' } qw(cl clisp el lsp sbcl scom)),
366 (map { $_ => 'ls' } qw(lotus)),
367 (map { $_ => 'lsl' } qw(lindenscript)),
368 (map { $_ => 'ly' } qw(lilypond)),
369 (map { $_ => 'make' } qw(mak mk kmk)),
370 (map { $_ => 'mel' } qw(maya)),
371 (map { $_ => 'mib' } qw(smi snmp)),
372 (map { $_ => 'ml' } qw(mli ocaml)),
373 (map { $_ => 'mo' } qw(modelica)),
374 (map { $_ => 'mod2' } qw(def mod)),
375 (map { $_ => 'mod3' } qw(i3 m3)),
376 (map { $_ => 'mpl' } qw(maple)),
377 (map { $_ => 'n' } qw(nemerle)),
378 (map { $_ => 'nas' } qw(nasal)),
379 (map { $_ => 'nrx' } qw(netrexx)),
380 (map { $_ => 'nsi' } qw(nsis)),
381 (map { $_ => 'nut' } qw(squirrel)),
382 (map { $_ => 'oberon' } qw(ooc)),
383 (map { $_ => 'objc' } qw(M m mm)),
384 (map { $_ => 'php' } qw(php3 php4 php5 php6)),
385 (map { $_ => 'pike' } qw(pmod)),
386 (map { $_ => 'pl' } qw(perl plex plx pm)),
387 (map { $_ => 'pl1' } qw(bdy ff fp fpp rpp sf sp spb spe spp sps wf wp wpb wpp wps)),
388 (map { $_ => 'progress' } qw(i p w)),
389 (map { $_ => 'py' } qw(python)),
390 (map { $_ => 'pyx' } qw(pyrex)),
391 (map { $_ => 'rb' } qw(pp rjs ruby)),
392 (map { $_ => 'rexx' } qw(rex rx the)),
393 (map { $_ => 'sc' } qw(paradox)),
394 (map { $_ => 'scilab' } qw(sce sci)),
395 (map { $_ => 'sh' } qw(bash ebuild eclass ksh zsh)),
396 (map { $_ => 'sma' } qw(small)),
397 (map { $_ => 'smalltalk' } qw(gst sq st)),
398 (map { $_ => 'sno' } qw(snobal)),
399 (map { $_ => 'sybase' } qw(sp)),
400 (map { $_ => 'tcl' } qw(itcl wish)),
401 (map { $_ => 'tex' } qw(cls sty)),
402 (map { $_ => 'vb' } qw(bas basic bi vbs)),
403 (map { $_ => 'verilog' } qw(v)),
404 (map { $_ => 'xml' } qw(dtd ecf ent hdr hub jnlp nrm plist resx sgm sgml svg tld vxml wml xsd xsl)),
405 (map { $_ => 'y' } qw(bison)),
408 # You define site-wide feature defaults here; override them with
409 # $GITWEB_CONFIG as necessary.
410 our %feature = (
411 # feature => {
412 # 'sub' => feature-sub (subroutine),
413 # 'override' => allow-override (boolean),
414 # 'default' => [ default options...] (array reference)}
416 # if feature is overridable (it means that allow-override has true value),
417 # then feature-sub will be called with default options as parameters;
418 # return value of feature-sub indicates if to enable specified feature
420 # if there is no 'sub' key (no feature-sub), then feature cannot be
421 # overridden
423 # use gitweb_get_feature(<feature>) to retrieve the <feature> value
424 # (an array) or gitweb_check_feature(<feature>) to check if <feature>
425 # is enabled
427 # Enable the 'blame' blob view, showing the last commit that modified
428 # each line in the file. This can be very CPU-intensive.
430 # To enable system wide have in $GITWEB_CONFIG
431 # $feature{'blame'}{'default'} = [1];
432 # To have project specific config enable override in $GITWEB_CONFIG
433 # $feature{'blame'}{'override'} = 1;
434 # and in project config gitweb.blame = 0|1;
435 'blame' => {
436 'sub' => sub { feature_bool('blame', @_) },
437 'override' => 0,
438 'default' => [0]},
440 # Enable the 'snapshot' link, providing a compressed archive of any
441 # tree. This can potentially generate high traffic if you have large
442 # project.
444 # Value is a list of formats defined in %known_snapshot_formats that
445 # you wish to offer.
446 # To disable system wide have in $GITWEB_CONFIG
447 # $feature{'snapshot'}{'default'} = [];
448 # To have project specific config enable override in $GITWEB_CONFIG
449 # $feature{'snapshot'}{'override'} = 1;
450 # and in project config, a comma-separated list of formats or "none"
451 # to disable. Example: gitweb.snapshot = tbz2,zip;
452 'snapshot' => {
453 'sub' => \&feature_snapshot,
454 'override' => 0,
455 'default' => ['tgz']},
457 # Enable text search, which will list the commits which match author,
458 # committer or commit text to a given string. Enabled by default.
459 # Project specific override is not supported.
461 # Note that this controls all search features, which means that if
462 # it is disabled, then 'grep' and 'pickaxe' search would also be
463 # disabled.
464 'search' => {
465 'override' => 0,
466 'default' => [1]},
468 # Enable grep search, which will list the files in currently selected
469 # tree containing the given string. Enabled by default. This can be
470 # potentially CPU-intensive, of course.
471 # Note that you need to have 'search' feature enabled too.
473 # To enable system wide have in $GITWEB_CONFIG
474 # $feature{'grep'}{'default'} = [1];
475 # To have project specific config enable override in $GITWEB_CONFIG
476 # $feature{'grep'}{'override'} = 1;
477 # and in project config gitweb.grep = 0|1;
478 'grep' => {
479 'sub' => sub { feature_bool('grep', @_) },
480 'override' => 0,
481 'default' => [1]},
483 # Enable the pickaxe search, which will list the commits that modified
484 # a given string in a file. This can be practical and quite faster
485 # alternative to 'blame', but still potentially CPU-intensive.
486 # Note that you need to have 'search' feature enabled too.
488 # To enable system wide have in $GITWEB_CONFIG
489 # $feature{'pickaxe'}{'default'} = [1];
490 # To have project specific config enable override in $GITWEB_CONFIG
491 # $feature{'pickaxe'}{'override'} = 1;
492 # and in project config gitweb.pickaxe = 0|1;
493 'pickaxe' => {
494 'sub' => sub { feature_bool('pickaxe', @_) },
495 'override' => 0,
496 'default' => [1]},
498 # Enable showing size of blobs in a 'tree' view, in a separate
499 # column, similar to what 'ls -l' does. This cost a bit of IO.
501 # To disable system wide have in $GITWEB_CONFIG
502 # $feature{'show-sizes'}{'default'} = [0];
503 # To have project specific config enable override in $GITWEB_CONFIG
504 # $feature{'show-sizes'}{'override'} = 1;
505 # and in project config gitweb.showsizes = 0|1;
506 'show-sizes' => {
507 'sub' => sub { feature_bool('showsizes', @_) },
508 'override' => 0,
509 'default' => [1]},
511 # Make gitweb use an alternative format of the URLs which can be
512 # more readable and natural-looking: project name is embedded
513 # directly in the path and the query string contains other
514 # auxiliary information. All gitweb installations recognize
515 # URL in either format; this configures in which formats gitweb
516 # generates links.
518 # To enable system wide have in $GITWEB_CONFIG
519 # $feature{'pathinfo'}{'default'} = [1];
520 # Project specific override is not supported.
522 # Note that you will need to change the default location of CSS,
523 # favicon, logo and possibly other files to an absolute URL. Also,
524 # if gitweb.cgi serves as your indexfile, you will need to force
525 # $my_uri to contain the script name in your $GITWEB_CONFIG.
526 'pathinfo' => {
527 'override' => 0,
528 'default' => [0]},
530 # Make gitweb consider projects in project root subdirectories
531 # to be forks of existing projects. Given project $projname.git,
532 # projects matching $projname/*.git will not be shown in the main
533 # projects list, instead a '+' mark will be added to $projname
534 # there and a 'forks' view will be enabled for the project, listing
535 # all the forks. If project list is taken from a file, forks have
536 # to be listed after the main project.
538 # To enable system wide have in $GITWEB_CONFIG
539 # $feature{'forks'}{'default'} = [1];
540 # Project specific override is not supported.
541 'forks' => {
542 'override' => 0,
543 'default' => [0]},
545 # Insert custom links to the action bar of all project pages.
546 # This enables you mainly to link to third-party scripts integrating
547 # into gitweb; e.g. git-browser for graphical history representation
548 # or custom web-based repository administration interface.
550 # The 'default' value consists of a list of triplets in the form
551 # (label, link, position) where position is the label after which
552 # to insert the link and link is a format string where %n expands
553 # to the project name, %f to the project path within the filesystem,
554 # %h to the current hash (h gitweb parameter) and %b to the current
555 # hash base (hb gitweb parameter); %% expands to %.
557 # To enable system wide have in $GITWEB_CONFIG e.g.
558 # $feature{'actions'}{'default'} = [('graphiclog',
559 # '/git-browser/by-commit.html?r=%n', 'summary')];
560 # Project specific override is not supported.
561 'actions' => {
562 'override' => 0,
563 'default' => []},
565 # Allow gitweb scan project content tags of project repository,
566 # and display the popular Web 2.0-ish "tag cloud" near the projects
567 # list. Note that this is something COMPLETELY different from the
568 # normal Git tags.
570 # gitweb by itself can show existing tags, but it does not handle
571 # tagging itself; you need to do it externally, outside gitweb.
572 # The format is described in git_get_project_ctags() subroutine.
573 # You may want to install the HTML::TagCloud Perl module to get
574 # a pretty tag cloud instead of just a list of tags.
576 # To enable system wide have in $GITWEB_CONFIG
577 # $feature{'ctags'}{'default'} = [1];
578 # Project specific override is not supported.
580 # In the future whether ctags editing is enabled might depend
581 # on the value, but using 1 should always mean no editing of ctags.
582 'ctags' => {
583 'override' => 0,
584 'default' => [0]},
586 # The maximum number of patches in a patchset generated in patch
587 # view. Set this to 0 or undef to disable patch view, or to a
588 # negative number to remove any limit.
590 # To disable system wide have in $GITWEB_CONFIG
591 # $feature{'patches'}{'default'} = [0];
592 # To have project specific config enable override in $GITWEB_CONFIG
593 # $feature{'patches'}{'override'} = 1;
594 # and in project config gitweb.patches = 0|n;
595 # where n is the maximum number of patches allowed in a patchset.
596 'patches' => {
597 'sub' => \&feature_patches,
598 'override' => 0,
599 'default' => [16]},
601 # Avatar support. When this feature is enabled, views such as
602 # shortlog or commit will display an avatar associated with
603 # the email of the committer(s) and/or author(s).
605 # Currently available providers are gravatar and picon.
606 # If an unknown provider is specified, the feature is disabled.
608 # Gravatar depends on Digest::MD5.
609 # Picon currently relies on the indiana.edu database.
611 # To enable system wide have in $GITWEB_CONFIG
612 # $feature{'avatar'}{'default'} = ['<provider>'];
613 # where <provider> is either gravatar or picon.
614 # To have project specific config enable override in $GITWEB_CONFIG
615 # $feature{'avatar'}{'override'} = 1;
616 # and in project config gitweb.avatar = <provider>;
617 'avatar' => {
618 'sub' => \&feature_avatar,
619 'override' => 0,
620 'default' => ['']},
622 # Enable displaying how much time and how many git commands
623 # it took to generate and display page. Disabled by default.
624 # Project specific override is not supported.
625 'timed' => {
626 'override' => 0,
627 'default' => [0]},
629 # Enable turning some links into links to actions which require
630 # JavaScript to run (like 'blame_incremental'). Not enabled by
631 # default. Project specific override is currently not supported.
632 'javascript-actions' => {
633 'override' => 0,
634 'default' => [0]},
636 # Enable and configure ability to change common timezone for dates
637 # in gitweb output via JavaScript. Enabled by default.
638 # Project specific override is not supported.
639 'javascript-timezone' => {
640 'override' => 0,
641 'default' => [
642 'local', # default timezone: 'utc', 'local', or '(-|+)HHMM' format,
643 # or undef to turn off this feature
644 'gitweb_tz', # name of cookie where to store selected timezone
645 'datetime', # CSS class used to mark up dates for manipulation
648 # Syntax highlighting support. This is based on Daniel Svensson's
649 # and Sham Chukoury's work in gitweb-xmms2.git.
650 # It requires the 'highlight' program present in $PATH,
651 # and therefore is disabled by default.
653 # To enable system wide have in $GITWEB_CONFIG
654 # $feature{'highlight'}{'default'} = [1];
656 'highlight' => {
657 'sub' => sub { feature_bool('highlight', @_) },
658 'override' => 0,
659 'default' => [0]},
661 # Enable displaying of remote heads in the heads list
663 # To enable system wide have in $GITWEB_CONFIG
664 # $feature{'remote_heads'}{'default'} = [1];
665 # To have project specific config enable override in $GITWEB_CONFIG
666 # $feature{'remote_heads'}{'override'} = 1;
667 # and in project config gitweb.remoteheads = 0|1;
668 'remote_heads' => {
669 'sub' => sub { feature_bool('remote_heads', @_) },
670 'override' => 0,
671 'default' => [0]},
673 # Enable showing branches under other refs in addition to heads
675 # To set system wide extra branch refs have in $GITWEB_CONFIG
676 # $feature{'extra-branch-refs'}{'default'} = ['dirs', 'of', 'choice'];
677 # To have project specific config enable override in $GITWEB_CONFIG
678 # $feature{'extra-branch-refs'}{'override'} = 1;
679 # and in project config gitweb.extrabranchrefs = dirs of choice
680 # Every directory is separated with whitespace.
682 'extra-branch-refs' => {
683 'sub' => \&feature_extra_branch_refs,
684 'override' => 0,
685 'default' => []},
688 sub gitweb_get_feature {
689 my ($name) = @_;
690 return unless exists $feature{$name};
691 my ($sub, $override, @defaults) = (
692 $feature{$name}{'sub'},
693 $feature{$name}{'override'},
694 @{$feature{$name}{'default'}});
695 # project specific override is possible only if we have project
696 our $git_dir; # global variable, declared later
697 if (!$override || !defined $git_dir) {
698 return @defaults;
700 if (!defined $sub) {
701 warn "feature $name is not overridable";
702 return @defaults;
704 return $sub->(@defaults);
707 # A wrapper to check if a given feature is enabled.
708 # With this, you can say
710 # my $bool_feat = gitweb_check_feature('bool_feat');
711 # gitweb_check_feature('bool_feat') or somecode;
713 # instead of
715 # my ($bool_feat) = gitweb_get_feature('bool_feat');
716 # (gitweb_get_feature('bool_feat'))[0] or somecode;
718 sub gitweb_check_feature {
719 return (gitweb_get_feature(@_))[0];
723 sub feature_bool {
724 my $key = shift;
725 my ($val) = git_get_project_config($key, '--bool');
727 if (!defined $val) {
728 return ($_[0]);
729 } elsif ($val eq 'true') {
730 return (1);
731 } elsif ($val eq 'false') {
732 return (0);
736 sub feature_snapshot {
737 my (@fmts) = @_;
739 my ($val) = git_get_project_config('snapshot');
741 if ($val) {
742 @fmts = ($val eq 'none' ? () : split /\s*[,\s]\s*/, $val);
745 return @fmts;
748 sub feature_patches {
749 my @val = (git_get_project_config('patches', '--int'));
751 if (@val) {
752 return @val;
755 return ($_[0]);
758 sub feature_avatar {
759 my @val = (git_get_project_config('avatar'));
761 return @val ? @val : @_;
764 sub feature_extra_branch_refs {
765 my (@branch_refs) = @_;
766 my $values = git_get_project_config('extrabranchrefs');
768 if ($values) {
769 $values = config_to_multi ($values);
770 @branch_refs = ();
771 foreach my $value (@{$values}) {
772 push @branch_refs, split /\s+/, $value;
776 return @branch_refs;
779 # checking HEAD file with -e is fragile if the repository was
780 # initialized long time ago (i.e. symlink HEAD) and was pack-ref'ed
781 # and then pruned.
782 sub check_head_link {
783 my ($dir) = @_;
784 my $headfile = "$dir/HEAD";
785 return ((-e $headfile) ||
786 (-l $headfile && readlink($headfile) =~ /^refs\/heads\//));
789 sub check_export_ok {
790 my ($dir) = @_;
791 return (check_head_link($dir) &&
792 (!$export_ok || -e "$dir/$export_ok") &&
793 (!$export_auth_hook || $export_auth_hook->($dir)));
796 # process alternate names for backward compatibility
797 # filter out unsupported (unknown) snapshot formats
798 sub filter_snapshot_fmts {
799 my @fmts = @_;
801 @fmts = map {
802 exists $known_snapshot_format_aliases{$_} ?
803 $known_snapshot_format_aliases{$_} : $_} @fmts;
804 @fmts = grep {
805 exists $known_snapshot_formats{$_} &&
806 !$known_snapshot_formats{$_}{'disabled'}} @fmts;
809 sub filter_and_validate_refs {
810 my @refs = @_;
811 my %unique_refs = ();
813 foreach my $ref (@refs) {
814 die_error(500, "Invalid ref '$ref' in 'extra-branch-refs' feature") unless (is_valid_ref_format($ref));
815 # 'heads' are added implicitly in get_branch_refs().
816 $unique_refs{$ref} = 1 if ($ref ne 'heads');
818 return sort keys %unique_refs;
821 # If it is set to code reference, it is code that it is to be run once per
822 # request, allowing updating configurations that change with each request,
823 # while running other code in config file only once.
825 # Otherwise, if it is false then gitweb would process config file only once;
826 # if it is true then gitweb config would be run for each request.
827 our $per_request_config = 1;
829 # read and parse gitweb config file given by its parameter.
830 # returns true on success, false on recoverable error, allowing
831 # to chain this subroutine, using first file that exists.
832 # dies on errors during parsing config file, as it is unrecoverable.
833 sub read_config_file {
834 my $filename = shift;
835 return unless defined $filename;
836 # die if there are errors parsing config file
837 if (-e $filename) {
838 do $filename;
839 die $@ if $@;
840 return 1;
842 return;
845 our ($GITWEB_CONFIG, $GITWEB_CONFIG_SYSTEM, $GITWEB_CONFIG_COMMON);
846 sub evaluate_gitweb_config {
847 our $GITWEB_CONFIG = $ENV{'GITWEB_CONFIG'} || "++GITWEB_CONFIG++";
848 our $GITWEB_CONFIG_SYSTEM = $ENV{'GITWEB_CONFIG_SYSTEM'} || "++GITWEB_CONFIG_SYSTEM++";
849 our $GITWEB_CONFIG_COMMON = $ENV{'GITWEB_CONFIG_COMMON'} || "++GITWEB_CONFIG_COMMON++";
851 # Protect against duplications of file names, to not read config twice.
852 # Only one of $GITWEB_CONFIG and $GITWEB_CONFIG_SYSTEM is used, so
853 # there possibility of duplication of filename there doesn't matter.
854 $GITWEB_CONFIG = "" if ($GITWEB_CONFIG eq $GITWEB_CONFIG_COMMON);
855 $GITWEB_CONFIG_SYSTEM = "" if ($GITWEB_CONFIG_SYSTEM eq $GITWEB_CONFIG_COMMON);
857 # Common system-wide settings for convenience.
858 # Those settings can be ovverriden by GITWEB_CONFIG or GITWEB_CONFIG_SYSTEM.
859 read_config_file($GITWEB_CONFIG_COMMON);
861 # Use first config file that exists. This means use the per-instance
862 # GITWEB_CONFIG if exists, otherwise use GITWEB_SYSTEM_CONFIG.
863 read_config_file($GITWEB_CONFIG) and return;
864 read_config_file($GITWEB_CONFIG_SYSTEM);
867 # Get loadavg of system, to compare against $maxload.
868 # Currently it requires '/proc/loadavg' present to get loadavg;
869 # if it is not present it returns 0, which means no load checking.
870 sub get_loadavg {
871 if( -e '/proc/loadavg' ){
872 open my $fd, '<', '/proc/loadavg'
873 or return 0;
874 my @load = split(/\s+/, scalar <$fd>);
875 close $fd;
877 # The first three columns measure CPU and IO utilization of the last one,
878 # five, and 10 minute periods. The fourth column shows the number of
879 # currently running processes and the total number of processes in the m/n
880 # format. The last column displays the last process ID used.
881 return $load[0] || 0;
883 # additional checks for load average should go here for things that don't export
884 # /proc/loadavg
886 return 0;
889 # version of the core git binary
890 our $git_version;
891 sub evaluate_git_version {
892 our $git_version = qx("$GIT" --version) =~ m/git version (.*)$/ ? $1 : "unknown";
893 $number_of_git_cmds++;
896 sub check_loadavg {
897 if (defined $maxload && get_loadavg() > $maxload) {
898 die_error(503, "The load average on the server is too high");
902 # ======================================================================
903 # input validation and dispatch
905 # input parameters can be collected from a variety of sources (presently, CGI
906 # and PATH_INFO), so we define an %input_params hash that collects them all
907 # together during validation: this allows subsequent uses (e.g. href()) to be
908 # agnostic of the parameter origin
910 our %input_params = ();
912 # input parameters are stored with the long parameter name as key. This will
913 # also be used in the href subroutine to convert parameters to their CGI
914 # equivalent, and since the href() usage is the most frequent one, we store
915 # the name -> CGI key mapping here, instead of the reverse.
917 # XXX: Warning: If you touch this, check the search form for updating,
918 # too.
920 our @cgi_param_mapping = (
921 project => "p",
922 action => "a",
923 file_name => "f",
924 file_parent => "fp",
925 hash => "h",
926 hash_parent => "hp",
927 hash_base => "hb",
928 hash_parent_base => "hpb",
929 page => "pg",
930 order => "o",
931 searchtext => "s",
932 searchtype => "st",
933 snapshot_format => "sf",
934 extra_options => "opt",
935 search_use_regexp => "sr",
936 ctag => "by_tag",
937 diff_style => "ds",
938 project_filter => "pf",
939 # this must be last entry (for manipulation from JavaScript)
940 javascript => "js"
942 our %cgi_param_mapping = @cgi_param_mapping;
944 # we will also need to know the possible actions, for validation
945 our %actions = (
946 "blame" => \&git_blame,
947 "blame_incremental" => \&git_blame_incremental,
948 "blame_data" => \&git_blame_data,
949 "blobdiff" => \&git_blobdiff,
950 "blobdiff_plain" => \&git_blobdiff_plain,
951 "blob" => \&git_blob,
952 "blob_plain" => \&git_blob_plain,
953 "commitdiff" => \&git_commitdiff,
954 "commitdiff_plain" => \&git_commitdiff_plain,
955 "commit" => \&git_commit,
956 "forks" => \&git_forks,
957 "heads" => \&git_heads,
958 "history" => \&git_history,
959 "log" => \&git_log,
960 "patch" => \&git_patch,
961 "patches" => \&git_patches,
962 "remotes" => \&git_remotes,
963 "rss" => \&git_rss,
964 "atom" => \&git_atom,
965 "search" => \&git_search,
966 "search_help" => \&git_search_help,
967 "shortlog" => \&git_shortlog,
968 "summary" => \&git_summary,
969 "tag" => \&git_tag,
970 "tags" => \&git_tags,
971 "tree" => \&git_tree,
972 "snapshot" => \&git_snapshot,
973 "object" => \&git_object,
974 # those below don't need $project
975 "opml" => \&git_opml,
976 "project_list" => \&git_project_list,
977 "project_index" => \&git_project_index,
980 # finally, we have the hash of allowed extra_options for the commands that
981 # allow them
982 our %allowed_options = (
983 "--no-merges" => [ qw(rss atom log shortlog history) ],
986 # fill %input_params with the CGI parameters. All values except for 'opt'
987 # should be single values, but opt can be an array. We should probably
988 # build an array of parameters that can be multi-valued, but since for the time
989 # being it's only this one, we just single it out
990 sub evaluate_query_params {
991 our $cgi;
993 while (my ($name, $symbol) = each %cgi_param_mapping) {
994 if ($symbol eq 'opt') {
995 $input_params{$name} = [ map { decode_utf8($_) } $cgi->multi_param($symbol) ];
996 } else {
997 $input_params{$name} = decode_utf8($cgi->param($symbol));
1002 # now read PATH_INFO and update the parameter list for missing parameters
1003 sub evaluate_path_info {
1004 return if defined $input_params{'project'};
1005 return if !$path_info;
1006 $path_info =~ s,^/+,,;
1007 return if !$path_info;
1009 # find which part of PATH_INFO is project
1010 my $project = $path_info;
1011 $project =~ s,/+$,,;
1012 while ($project && !check_head_link("$projectroot/$project")) {
1013 $project =~ s,/*[^/]*$,,;
1015 return unless $project;
1016 $input_params{'project'} = $project;
1018 # do not change any parameters if an action is given using the query string
1019 return if $input_params{'action'};
1020 $path_info =~ s,^\Q$project\E/*,,;
1022 # next, check if we have an action
1023 my $action = $path_info;
1024 $action =~ s,/.*$,,;
1025 if (exists $actions{$action}) {
1026 $path_info =~ s,^$action/*,,;
1027 $input_params{'action'} = $action;
1030 # list of actions that want hash_base instead of hash, but can have no
1031 # pathname (f) parameter
1032 my @wants_base = (
1033 'tree',
1034 'history',
1037 # we want to catch, among others
1038 # [$hash_parent_base[:$file_parent]..]$hash_parent[:$file_name]
1039 my ($parentrefname, $parentpathname, $refname, $pathname) =
1040 ($path_info =~ /^(?:(.+?)(?::(.+))?\.\.)?([^:]+?)?(?::(.+))?$/);
1042 # first, analyze the 'current' part
1043 if (defined $pathname) {
1044 # we got "branch:filename" or "branch:dir/"
1045 # we could use git_get_type(branch:pathname), but:
1046 # - it needs $git_dir
1047 # - it does a git() call
1048 # - the convention of terminating directories with a slash
1049 # makes it superfluous
1050 # - embedding the action in the PATH_INFO would make it even
1051 # more superfluous
1052 $pathname =~ s,^/+,,;
1053 if (!$pathname || substr($pathname, -1) eq "/") {
1054 $input_params{'action'} ||= "tree";
1055 $pathname =~ s,/$,,;
1056 } else {
1057 # the default action depends on whether we had parent info
1058 # or not
1059 if ($parentrefname) {
1060 $input_params{'action'} ||= "blobdiff_plain";
1061 } else {
1062 $input_params{'action'} ||= "blob_plain";
1065 $input_params{'hash_base'} ||= $refname;
1066 $input_params{'file_name'} ||= $pathname;
1067 } elsif (defined $refname) {
1068 # we got "branch". In this case we have to choose if we have to
1069 # set hash or hash_base.
1071 # Most of the actions without a pathname only want hash to be
1072 # set, except for the ones specified in @wants_base that want
1073 # hash_base instead. It should also be noted that hand-crafted
1074 # links having 'history' as an action and no pathname or hash
1075 # set will fail, but that happens regardless of PATH_INFO.
1076 if (defined $parentrefname) {
1077 # if there is parent let the default be 'shortlog' action
1078 # (for http://git.example.com/repo.git/A..B links); if there
1079 # is no parent, dispatch will detect type of object and set
1080 # action appropriately if required (if action is not set)
1081 $input_params{'action'} ||= "shortlog";
1083 if ($input_params{'action'} &&
1084 grep { $_ eq $input_params{'action'} } @wants_base) {
1085 $input_params{'hash_base'} ||= $refname;
1086 } else {
1087 $input_params{'hash'} ||= $refname;
1091 # next, handle the 'parent' part, if present
1092 if (defined $parentrefname) {
1093 # a missing pathspec defaults to the 'current' filename, allowing e.g.
1094 # someproject/blobdiff/oldrev..newrev:/filename
1095 if ($parentpathname) {
1096 $parentpathname =~ s,^/+,,;
1097 $parentpathname =~ s,/$,,;
1098 $input_params{'file_parent'} ||= $parentpathname;
1099 } else {
1100 $input_params{'file_parent'} ||= $input_params{'file_name'};
1102 # we assume that hash_parent_base is wanted if a path was specified,
1103 # or if the action wants hash_base instead of hash
1104 if (defined $input_params{'file_parent'} ||
1105 grep { $_ eq $input_params{'action'} } @wants_base) {
1106 $input_params{'hash_parent_base'} ||= $parentrefname;
1107 } else {
1108 $input_params{'hash_parent'} ||= $parentrefname;
1112 # for the snapshot action, we allow URLs in the form
1113 # $project/snapshot/$hash.ext
1114 # where .ext determines the snapshot and gets removed from the
1115 # passed $refname to provide the $hash.
1117 # To be able to tell that $refname includes the format extension, we
1118 # require the following two conditions to be satisfied:
1119 # - the hash input parameter MUST have been set from the $refname part
1120 # of the URL (i.e. they must be equal)
1121 # - the snapshot format MUST NOT have been defined already (e.g. from
1122 # CGI parameter sf)
1123 # It's also useless to try any matching unless $refname has a dot,
1124 # so we check for that too
1125 if (defined $input_params{'action'} &&
1126 $input_params{'action'} eq 'snapshot' &&
1127 defined $refname && index($refname, '.') != -1 &&
1128 $refname eq $input_params{'hash'} &&
1129 !defined $input_params{'snapshot_format'}) {
1130 # We loop over the known snapshot formats, checking for
1131 # extensions. Allowed extensions are both the defined suffix
1132 # (which includes the initial dot already) and the snapshot
1133 # format key itself, with a prepended dot
1134 while (my ($fmt, $opt) = each %known_snapshot_formats) {
1135 my $hash = $refname;
1136 unless ($hash =~ s/(\Q$opt->{'suffix'}\E|\Q.$fmt\E)$//) {
1137 next;
1139 my $sfx = $1;
1140 # a valid suffix was found, so set the snapshot format
1141 # and reset the hash parameter
1142 $input_params{'snapshot_format'} = $fmt;
1143 $input_params{'hash'} = $hash;
1144 # we also set the format suffix to the one requested
1145 # in the URL: this way a request for e.g. .tgz returns
1146 # a .tgz instead of a .tar.gz
1147 $known_snapshot_formats{$fmt}{'suffix'} = $sfx;
1148 last;
1153 our ($action, $project, $file_name, $file_parent, $hash, $hash_parent, $hash_base,
1154 $hash_parent_base, @extra_options, $page, $searchtype, $search_use_regexp,
1155 $searchtext, $search_regexp, $project_filter);
1156 sub evaluate_and_validate_params {
1157 our $action = $input_params{'action'};
1158 if (defined $action) {
1159 if (!is_valid_action($action)) {
1160 die_error(400, "Invalid action parameter");
1164 # parameters which are pathnames
1165 our $project = $input_params{'project'};
1166 if (defined $project) {
1167 if (!is_valid_project($project)) {
1168 undef $project;
1169 die_error(404, "No such project");
1173 our $project_filter = $input_params{'project_filter'};
1174 if (defined $project_filter) {
1175 if (!is_valid_pathname($project_filter)) {
1176 die_error(404, "Invalid project_filter parameter");
1180 our $file_name = $input_params{'file_name'};
1181 if (defined $file_name) {
1182 if (!is_valid_pathname($file_name)) {
1183 die_error(400, "Invalid file parameter");
1187 our $file_parent = $input_params{'file_parent'};
1188 if (defined $file_parent) {
1189 if (!is_valid_pathname($file_parent)) {
1190 die_error(400, "Invalid file parent parameter");
1194 # parameters which are refnames
1195 our $hash = $input_params{'hash'};
1196 if (defined $hash) {
1197 if (!is_valid_refname($hash)) {
1198 die_error(400, "Invalid hash parameter");
1202 our $hash_parent = $input_params{'hash_parent'};
1203 if (defined $hash_parent) {
1204 if (!is_valid_refname($hash_parent)) {
1205 die_error(400, "Invalid hash parent parameter");
1209 our $hash_base = $input_params{'hash_base'};
1210 if (defined $hash_base) {
1211 if (!is_valid_refname($hash_base)) {
1212 die_error(400, "Invalid hash base parameter");
1216 our @extra_options = @{$input_params{'extra_options'}};
1217 # @extra_options is always defined, since it can only be (currently) set from
1218 # CGI, and $cgi->param() returns the empty array in array context if the param
1219 # is not set
1220 foreach my $opt (@extra_options) {
1221 if (not exists $allowed_options{$opt}) {
1222 die_error(400, "Invalid option parameter");
1224 if (not grep(/^$action$/, @{$allowed_options{$opt}})) {
1225 die_error(400, "Invalid option parameter for this action");
1229 our $hash_parent_base = $input_params{'hash_parent_base'};
1230 if (defined $hash_parent_base) {
1231 if (!is_valid_refname($hash_parent_base)) {
1232 die_error(400, "Invalid hash parent base parameter");
1236 # other parameters
1237 our $page = $input_params{'page'};
1238 if (defined $page) {
1239 if ($page =~ m/[^0-9]/) {
1240 die_error(400, "Invalid page parameter");
1244 our $searchtype = $input_params{'searchtype'};
1245 if (defined $searchtype) {
1246 if ($searchtype =~ m/[^a-z]/) {
1247 die_error(400, "Invalid searchtype parameter");
1251 our $search_use_regexp = $input_params{'search_use_regexp'};
1253 our $searchtext = $input_params{'searchtext'};
1254 our $search_regexp = undef;
1255 if (defined $searchtext) {
1256 if (length($searchtext) < 2) {
1257 die_error(403, "At least two characters are required for search parameter");
1259 if ($search_use_regexp) {
1260 $search_regexp = $searchtext;
1261 if (!eval { qr/$search_regexp/; 1; }) {
1262 (my $error = $@) =~ s/ at \S+ line \d+.*\n?//;
1263 die_error(400, "Invalid search regexp '$search_regexp'",
1264 esc_html($error));
1266 } else {
1267 $search_regexp = quotemeta $searchtext;
1272 # path to the current git repository
1273 our $git_dir;
1274 sub evaluate_git_dir {
1275 our $git_dir = "$projectroot/$project" if $project;
1278 our (@snapshot_fmts, $git_avatar, @extra_branch_refs);
1279 sub configure_gitweb_features {
1280 # list of supported snapshot formats
1281 our @snapshot_fmts = gitweb_get_feature('snapshot');
1282 @snapshot_fmts = filter_snapshot_fmts(@snapshot_fmts);
1284 # check that the avatar feature is set to a known provider name,
1285 # and for each provider check if the dependencies are satisfied.
1286 # if the provider name is invalid or the dependencies are not met,
1287 # reset $git_avatar to the empty string.
1288 our ($git_avatar) = gitweb_get_feature('avatar');
1289 if ($git_avatar eq 'gravatar') {
1290 $git_avatar = '' unless (eval { require Digest::MD5; 1; });
1291 } elsif ($git_avatar eq 'picon') {
1292 # no dependencies
1293 } else {
1294 $git_avatar = '';
1297 our @extra_branch_refs = gitweb_get_feature('extra-branch-refs');
1298 @extra_branch_refs = filter_and_validate_refs (@extra_branch_refs);
1301 sub get_branch_refs {
1302 return ('heads', @extra_branch_refs);
1305 # custom error handler: 'die <message>' is Internal Server Error
1306 sub handle_errors_html {
1307 my $msg = shift; # it is already HTML escaped
1309 # to avoid infinite loop where error occurs in die_error,
1310 # change handler to default handler, disabling handle_errors_html
1311 set_message("Error occurred when inside die_error:\n$msg");
1313 # you cannot jump out of die_error when called as error handler;
1314 # the subroutine set via CGI::Carp::set_message is called _after_
1315 # HTTP headers are already written, so it cannot write them itself
1316 die_error(undef, undef, $msg, -error_handler => 1, -no_http_header => 1);
1318 set_message(\&handle_errors_html);
1320 # dispatch
1321 sub dispatch {
1322 if (!defined $action) {
1323 if (defined $hash) {
1324 $action = git_get_type($hash);
1325 $action or die_error(404, "Object does not exist");
1326 } elsif (defined $hash_base && defined $file_name) {
1327 $action = git_get_type("$hash_base:$file_name");
1328 $action or die_error(404, "File or directory does not exist");
1329 } elsif (defined $project) {
1330 $action = 'summary';
1331 } else {
1332 $action = 'project_list';
1335 if (!defined($actions{$action})) {
1336 die_error(400, "Unknown action");
1338 if ($action !~ m/^(?:opml|project_list|project_index)$/ &&
1339 !$project) {
1340 die_error(400, "Project needed");
1342 $actions{$action}->();
1345 sub reset_timer {
1346 our $t0 = [ gettimeofday() ]
1347 if defined $t0;
1348 our $number_of_git_cmds = 0;
1351 our $first_request = 1;
1352 sub run_request {
1353 reset_timer();
1355 evaluate_uri();
1356 if ($first_request) {
1357 evaluate_gitweb_config();
1358 evaluate_git_version();
1360 if ($per_request_config) {
1361 if (ref($per_request_config) eq 'CODE') {
1362 $per_request_config->();
1363 } elsif (!$first_request) {
1364 evaluate_gitweb_config();
1367 check_loadavg();
1369 # $projectroot and $projects_list might be set in gitweb config file
1370 $projects_list ||= $projectroot;
1372 evaluate_query_params();
1373 evaluate_path_info();
1374 evaluate_and_validate_params();
1375 evaluate_git_dir();
1377 configure_gitweb_features();
1379 dispatch();
1382 our $is_last_request = sub { 1 };
1383 our ($pre_dispatch_hook, $post_dispatch_hook, $pre_listen_hook);
1384 our $CGI = 'CGI';
1385 our $cgi;
1386 sub configure_as_fcgi {
1387 require CGI::Fast;
1388 our $CGI = 'CGI::Fast';
1390 my $request_number = 0;
1391 # let each child service 100 requests
1392 our $is_last_request = sub { ++$request_number > 100 };
1394 sub evaluate_argv {
1395 my $script_name = $ENV{'SCRIPT_NAME'} || $ENV{'SCRIPT_FILENAME'} || __FILE__;
1396 configure_as_fcgi()
1397 if $script_name =~ /\.fcgi$/;
1399 return unless (@ARGV);
1401 require Getopt::Long;
1402 Getopt::Long::GetOptions(
1403 'fastcgi|fcgi|f' => \&configure_as_fcgi,
1404 'nproc|n=i' => sub {
1405 my ($arg, $val) = @_;
1406 return unless eval { require FCGI::ProcManager; 1; };
1407 my $proc_manager = FCGI::ProcManager->new({
1408 n_processes => $val,
1410 our $pre_listen_hook = sub { $proc_manager->pm_manage() };
1411 our $pre_dispatch_hook = sub { $proc_manager->pm_pre_dispatch() };
1412 our $post_dispatch_hook = sub { $proc_manager->pm_post_dispatch() };
1417 sub run {
1418 evaluate_argv();
1420 $first_request = 1;
1421 $pre_listen_hook->()
1422 if $pre_listen_hook;
1424 REQUEST:
1425 while ($cgi = $CGI->new()) {
1426 $pre_dispatch_hook->()
1427 if $pre_dispatch_hook;
1429 run_request();
1431 $post_dispatch_hook->()
1432 if $post_dispatch_hook;
1433 $first_request = 0;
1435 last REQUEST if ($is_last_request->());
1438 DONE_GITWEB:
1442 run();
1444 if (defined caller) {
1445 # wrapped in a subroutine processing requests,
1446 # e.g. mod_perl with ModPerl::Registry, or PSGI with Plack::App::WrapCGI
1447 return;
1448 } else {
1449 # pure CGI script, serving single request
1450 exit;
1453 ## ======================================================================
1454 ## action links
1456 # possible values of extra options
1457 # -full => 0|1 - use absolute/full URL ($my_uri/$my_url as base)
1458 # -replay => 1 - start from a current view (replay with modifications)
1459 # -path_info => 0|1 - don't use/use path_info URL (if possible)
1460 # -anchor => ANCHOR - add #ANCHOR to end of URL, implies -replay if used alone
1461 sub href {
1462 my %params = @_;
1463 # default is to use -absolute url() i.e. $my_uri
1464 my $href = $params{-full} ? $my_url : $my_uri;
1466 # implicit -replay, must be first of implicit params
1467 $params{-replay} = 1 if (keys %params == 1 && $params{-anchor});
1469 $params{'project'} = $project unless exists $params{'project'};
1471 if ($params{-replay}) {
1472 while (my ($name, $symbol) = each %cgi_param_mapping) {
1473 if (!exists $params{$name}) {
1474 $params{$name} = $input_params{$name};
1479 my $use_pathinfo = gitweb_check_feature('pathinfo');
1480 if (defined $params{'project'} &&
1481 (exists $params{-path_info} ? $params{-path_info} : $use_pathinfo)) {
1482 # try to put as many parameters as possible in PATH_INFO:
1483 # - project name
1484 # - action
1485 # - hash_parent or hash_parent_base:/file_parent
1486 # - hash or hash_base:/filename
1487 # - the snapshot_format as an appropriate suffix
1489 # When the script is the root DirectoryIndex for the domain,
1490 # $href here would be something like http://gitweb.example.com/
1491 # Thus, we strip any trailing / from $href, to spare us double
1492 # slashes in the final URL
1493 $href =~ s,/$,,;
1495 # Then add the project name, if present
1496 $href .= "/".esc_path_info($params{'project'});
1497 delete $params{'project'};
1499 # since we destructively absorb parameters, we keep this
1500 # boolean that remembers if we're handling a snapshot
1501 my $is_snapshot = $params{'action'} eq 'snapshot';
1503 # Summary just uses the project path URL, any other action is
1504 # added to the URL
1505 if (defined $params{'action'}) {
1506 $href .= "/".esc_path_info($params{'action'})
1507 unless $params{'action'} eq 'summary';
1508 delete $params{'action'};
1511 # Next, we put hash_parent_base:/file_parent..hash_base:/file_name,
1512 # stripping nonexistent or useless pieces
1513 $href .= "/" if ($params{'hash_base'} || $params{'hash_parent_base'}
1514 || $params{'hash_parent'} || $params{'hash'});
1515 if (defined $params{'hash_base'}) {
1516 if (defined $params{'hash_parent_base'}) {
1517 $href .= esc_path_info($params{'hash_parent_base'});
1518 # skip the file_parent if it's the same as the file_name
1519 if (defined $params{'file_parent'}) {
1520 if (defined $params{'file_name'} && $params{'file_parent'} eq $params{'file_name'}) {
1521 delete $params{'file_parent'};
1522 } elsif ($params{'file_parent'} !~ /\.\./) {
1523 $href .= ":/".esc_path_info($params{'file_parent'});
1524 delete $params{'file_parent'};
1527 $href .= "..";
1528 delete $params{'hash_parent'};
1529 delete $params{'hash_parent_base'};
1530 } elsif (defined $params{'hash_parent'}) {
1531 $href .= esc_path_info($params{'hash_parent'}). "..";
1532 delete $params{'hash_parent'};
1535 $href .= esc_path_info($params{'hash_base'});
1536 if (defined $params{'file_name'} && $params{'file_name'} !~ /\.\./) {
1537 $href .= ":/".esc_path_info($params{'file_name'});
1538 delete $params{'file_name'};
1540 delete $params{'hash'};
1541 delete $params{'hash_base'};
1542 } elsif (defined $params{'hash'}) {
1543 $href .= esc_path_info($params{'hash'});
1544 delete $params{'hash'};
1547 # If the action was a snapshot, we can absorb the
1548 # snapshot_format parameter too
1549 if ($is_snapshot) {
1550 my $fmt = $params{'snapshot_format'};
1551 # snapshot_format should always be defined when href()
1552 # is called, but just in case some code forgets, we
1553 # fall back to the default
1554 $fmt ||= $snapshot_fmts[0];
1555 $href .= $known_snapshot_formats{$fmt}{'suffix'};
1556 delete $params{'snapshot_format'};
1560 # now encode the parameters explicitly
1561 my @result = ();
1562 for (my $i = 0; $i < @cgi_param_mapping; $i += 2) {
1563 my ($name, $symbol) = ($cgi_param_mapping[$i], $cgi_param_mapping[$i+1]);
1564 if (defined $params{$name}) {
1565 if (ref($params{$name}) eq "ARRAY") {
1566 foreach my $par (@{$params{$name}}) {
1567 push @result, $symbol . "=" . esc_param($par);
1569 } else {
1570 push @result, $symbol . "=" . esc_param($params{$name});
1574 $href .= "?" . join(';', @result) if scalar @result;
1576 # final transformation: trailing spaces must be escaped (URI-encoded)
1577 $href =~ s/(\s+)$/CGI::escape($1)/e;
1579 if ($params{-anchor}) {
1580 $href .= "#".esc_param($params{-anchor});
1583 return $href;
1587 ## ======================================================================
1588 ## validation, quoting/unquoting and escaping
1590 sub is_valid_action {
1591 my $input = shift;
1592 return undef unless exists $actions{$input};
1593 return 1;
1596 sub is_valid_project {
1597 my $input = shift;
1599 return unless defined $input;
1600 if (!is_valid_pathname($input) ||
1601 !(-d "$projectroot/$input") ||
1602 !check_export_ok("$projectroot/$input") ||
1603 ($strict_export && !project_in_list($input))) {
1604 return undef;
1605 } else {
1606 return 1;
1610 sub is_valid_pathname {
1611 my $input = shift;
1613 return undef unless defined $input;
1614 # no '.' or '..' as elements of path, i.e. no '.' or '..'
1615 # at the beginning, at the end, and between slashes.
1616 # also this catches doubled slashes
1617 if ($input =~ m!(^|/)(|\.|\.\.)(/|$)!) {
1618 return undef;
1620 # no null characters
1621 if ($input =~ m!\0!) {
1622 return undef;
1624 return 1;
1627 sub is_valid_ref_format {
1628 my $input = shift;
1630 return undef unless defined $input;
1631 # restrictions on ref name according to git-check-ref-format
1632 if ($input =~ m!(/\.|\.\.|[\000-\040\177 ~^:?*\[]|/$)!) {
1633 return undef;
1635 return 1;
1638 sub is_valid_refname {
1639 my $input = shift;
1641 return undef unless defined $input;
1642 # textual hashes are O.K.
1643 if ($input =~ m/^[0-9a-fA-F]{40}$/) {
1644 return 1;
1646 # it must be correct pathname
1647 is_valid_pathname($input) or return undef;
1648 # check git-check-ref-format restrictions
1649 is_valid_ref_format($input) or return undef;
1650 return 1;
1653 # decode sequences of octets in utf8 into Perl's internal form,
1654 # which is utf-8 with utf8 flag set if needed. gitweb writes out
1655 # in utf-8 thanks to "binmode STDOUT, ':utf8'" at beginning
1656 sub to_utf8 {
1657 my $str = shift;
1658 return undef unless defined $str;
1660 if (utf8::is_utf8($str) || utf8::decode($str)) {
1661 return $str;
1662 } else {
1663 return decode($fallback_encoding, $str, Encode::FB_DEFAULT);
1667 # quote unsafe chars, but keep the slash, even when it's not
1668 # correct, but quoted slashes look too horrible in bookmarks
1669 sub esc_param {
1670 my $str = shift;
1671 return undef unless defined $str;
1672 $str =~ s/([^A-Za-z0-9\-_.~()\/:@ ]+)/CGI::escape($1)/eg;
1673 $str =~ s/ /\+/g;
1674 return $str;
1677 # the quoting rules for path_info fragment are slightly different
1678 sub esc_path_info {
1679 my $str = shift;
1680 return undef unless defined $str;
1682 # path_info doesn't treat '+' as space (specially), but '?' must be escaped
1683 $str =~ s/([^A-Za-z0-9\-_.~();\/;:@&= +]+)/CGI::escape($1)/eg;
1685 return $str;
1688 # quote unsafe chars in whole URL, so some characters cannot be quoted
1689 sub esc_url {
1690 my $str = shift;
1691 return undef unless defined $str;
1692 $str =~ s/([^A-Za-z0-9\-_.~();\/;?:@&= ]+)/CGI::escape($1)/eg;
1693 $str =~ s/ /\+/g;
1694 return $str;
1697 # quote unsafe characters in HTML attributes
1698 sub esc_attr {
1700 # for XHTML conformance escaping '"' to '&quot;' is not enough
1701 return esc_html(@_);
1704 # replace invalid utf8 character with SUBSTITUTION sequence
1705 sub esc_html {
1706 my $str = shift;
1707 my %opts = @_;
1709 return undef unless defined $str;
1711 $str = to_utf8($str);
1712 $str = $cgi->escapeHTML($str);
1713 if ($opts{'-nbsp'}) {
1714 $str =~ s/ /&nbsp;/g;
1716 $str =~ s|([[:cntrl:]])|(($1 ne "\t") ? quot_cec($1) : $1)|eg;
1717 return $str;
1720 # quote control characters and escape filename to HTML
1721 sub esc_path {
1722 my $str = shift;
1723 my %opts = @_;
1725 return undef unless defined $str;
1727 $str = to_utf8($str);
1728 $str = $cgi->escapeHTML($str);
1729 if ($opts{'-nbsp'}) {
1730 $str =~ s/ /&nbsp;/g;
1732 $str =~ s|([[:cntrl:]])|quot_cec($1)|eg;
1733 return $str;
1736 # Sanitize for use in XHTML + application/xml+xhtm (valid XML 1.0)
1737 sub sanitize {
1738 my $str = shift;
1740 return undef unless defined $str;
1742 $str = to_utf8($str);
1743 $str =~ s|([[:cntrl:]])|(index("\t\n\r", $1) != -1 ? $1 : quot_cec($1))|eg;
1744 return $str;
1747 # Make control characters "printable", using character escape codes (CEC)
1748 sub quot_cec {
1749 my $cntrl = shift;
1750 my %opts = @_;
1751 my %es = ( # character escape codes, aka escape sequences
1752 "\t" => '\t', # tab (HT)
1753 "\n" => '\n', # line feed (LF)
1754 "\r" => '\r', # carrige return (CR)
1755 "\f" => '\f', # form feed (FF)
1756 "\b" => '\b', # backspace (BS)
1757 "\a" => '\a', # alarm (bell) (BEL)
1758 "\e" => '\e', # escape (ESC)
1759 "\013" => '\v', # vertical tab (VT)
1760 "\000" => '\0', # nul character (NUL)
1762 my $chr = ( (exists $es{$cntrl})
1763 ? $es{$cntrl}
1764 : sprintf('\%2x', ord($cntrl)) );
1765 if ($opts{-nohtml}) {
1766 return $chr;
1767 } else {
1768 return "<span class=\"cntrl\">$chr</span>";
1772 # Alternatively use unicode control pictures codepoints,
1773 # Unicode "printable representation" (PR)
1774 sub quot_upr {
1775 my $cntrl = shift;
1776 my %opts = @_;
1778 my $chr = sprintf('&#%04d;', 0x2400+ord($cntrl));
1779 if ($opts{-nohtml}) {
1780 return $chr;
1781 } else {
1782 return "<span class=\"cntrl\">$chr</span>";
1786 # git may return quoted and escaped filenames
1787 sub unquote {
1788 my $str = shift;
1790 sub unq {
1791 my $seq = shift;
1792 my %es = ( # character escape codes, aka escape sequences
1793 't' => "\t", # tab (HT, TAB)
1794 'n' => "\n", # newline (NL)
1795 'r' => "\r", # return (CR)
1796 'f' => "\f", # form feed (FF)
1797 'b' => "\b", # backspace (BS)
1798 'a' => "\a", # alarm (bell) (BEL)
1799 'e' => "\e", # escape (ESC)
1800 'v' => "\013", # vertical tab (VT)
1803 if ($seq =~ m/^[0-7]{1,3}$/) {
1804 # octal char sequence
1805 return chr(oct($seq));
1806 } elsif (exists $es{$seq}) {
1807 # C escape sequence, aka character escape code
1808 return $es{$seq};
1810 # quoted ordinary character
1811 return $seq;
1814 if ($str =~ m/^"(.*)"$/) {
1815 # needs unquoting
1816 $str = $1;
1817 $str =~ s/\\([^0-7]|[0-7]{1,3})/unq($1)/eg;
1819 return $str;
1822 # escape tabs (convert tabs to spaces)
1823 sub untabify {
1824 my $line = shift;
1826 while ((my $pos = index($line, "\t")) != -1) {
1827 if (my $count = (8 - ($pos % 8))) {
1828 my $spaces = ' ' x $count;
1829 $line =~ s/\t/$spaces/;
1833 return $line;
1836 sub project_in_list {
1837 my $project = shift;
1838 my @list = git_get_projects_list();
1839 return @list && scalar(grep { $_->{'path'} eq $project } @list);
1842 ## ----------------------------------------------------------------------
1843 ## HTML aware string manipulation
1845 # Try to chop given string on a word boundary between position
1846 # $len and $len+$add_len. If there is no word boundary there,
1847 # chop at $len+$add_len. Do not chop if chopped part plus ellipsis
1848 # (marking chopped part) would be longer than given string.
1849 sub chop_str {
1850 my $str = shift;
1851 my $len = shift;
1852 my $add_len = shift || 10;
1853 my $where = shift || 'right'; # 'left' | 'center' | 'right'
1855 # Make sure perl knows it is utf8 encoded so we don't
1856 # cut in the middle of a utf8 multibyte char.
1857 $str = to_utf8($str);
1859 # allow only $len chars, but don't cut a word if it would fit in $add_len
1860 # if it doesn't fit, cut it if it's still longer than the dots we would add
1861 # remove chopped character entities entirely
1863 # when chopping in the middle, distribute $len into left and right part
1864 # return early if chopping wouldn't make string shorter
1865 if ($where eq 'center') {
1866 return $str if ($len + 5 >= length($str)); # filler is length 5
1867 $len = int($len/2);
1868 } else {
1869 return $str if ($len + 4 >= length($str)); # filler is length 4
1872 # regexps: ending and beginning with word part up to $add_len
1873 my $endre = qr/.{$len}\w{0,$add_len}/;
1874 my $begre = qr/\w{0,$add_len}.{$len}/;
1876 if ($where eq 'left') {
1877 $str =~ m/^(.*?)($begre)$/;
1878 my ($lead, $body) = ($1, $2);
1879 if (length($lead) > 4) {
1880 $lead = " ...";
1882 return "$lead$body";
1884 } elsif ($where eq 'center') {
1885 $str =~ m/^($endre)(.*)$/;
1886 my ($left, $str) = ($1, $2);
1887 $str =~ m/^(.*?)($begre)$/;
1888 my ($mid, $right) = ($1, $2);
1889 if (length($mid) > 5) {
1890 $mid = " ... ";
1892 return "$left$mid$right";
1894 } else {
1895 $str =~ m/^($endre)(.*)$/;
1896 my $body = $1;
1897 my $tail = $2;
1898 if (length($tail) > 4) {
1899 $tail = "... ";
1901 return "$body$tail";
1905 # takes the same arguments as chop_str, but also wraps a <span> around the
1906 # result with a title attribute if it does get chopped. Additionally, the
1907 # string is HTML-escaped.
1908 sub chop_and_escape_str {
1909 my ($str) = @_;
1911 my $chopped = chop_str(@_);
1912 $str = to_utf8($str);
1913 if ($chopped eq $str) {
1914 return esc_html($chopped);
1915 } else {
1916 $str =~ s/[[:cntrl:]]/?/g;
1917 return $cgi->span({-title=>$str}, esc_html($chopped));
1921 # Highlight selected fragments of string, using given CSS class,
1922 # and escape HTML. It is assumed that fragments do not overlap.
1923 # Regions are passed as list of pairs (array references).
1925 # Example: esc_html_hl_regions("foobar", "mark", [ 0, 3 ]) returns
1926 # '<span class="mark">foo</span>bar'
1927 sub esc_html_hl_regions {
1928 my ($str, $css_class, @sel) = @_;
1929 my %opts = grep { ref($_) ne 'ARRAY' } @sel;
1930 @sel = grep { ref($_) eq 'ARRAY' } @sel;
1931 return esc_html($str, %opts) unless @sel;
1933 my $out = '';
1934 my $pos = 0;
1936 for my $s (@sel) {
1937 my ($begin, $end) = @$s;
1939 # Don't create empty <span> elements.
1940 next if $end <= $begin;
1942 my $escaped = esc_html(substr($str, $begin, $end - $begin),
1943 %opts);
1945 $out .= esc_html(substr($str, $pos, $begin - $pos), %opts)
1946 if ($begin - $pos > 0);
1947 $out .= $cgi->span({-class => $css_class}, $escaped);
1949 $pos = $end;
1951 $out .= esc_html(substr($str, $pos), %opts)
1952 if ($pos < length($str));
1954 return $out;
1957 # return positions of beginning and end of each match
1958 sub matchpos_list {
1959 my ($str, $regexp) = @_;
1960 return unless (defined $str && defined $regexp);
1962 my @matches;
1963 while ($str =~ /$regexp/g) {
1964 push @matches, [$-[0], $+[0]];
1966 return @matches;
1969 # highlight match (if any), and escape HTML
1970 sub esc_html_match_hl {
1971 my ($str, $regexp) = @_;
1972 return esc_html($str) unless defined $regexp;
1974 my @matches = matchpos_list($str, $regexp);
1975 return esc_html($str) unless @matches;
1977 return esc_html_hl_regions($str, 'match', @matches);
1981 # highlight match (if any) of shortened string, and escape HTML
1982 sub esc_html_match_hl_chopped {
1983 my ($str, $chopped, $regexp) = @_;
1984 return esc_html_match_hl($str, $regexp) unless defined $chopped;
1986 my @matches = matchpos_list($str, $regexp);
1987 return esc_html($chopped) unless @matches;
1989 # filter matches so that we mark chopped string
1990 my $tail = "... "; # see chop_str
1991 unless ($chopped =~ s/\Q$tail\E$//) {
1992 $tail = '';
1994 my $chop_len = length($chopped);
1995 my $tail_len = length($tail);
1996 my @filtered;
1998 for my $m (@matches) {
1999 if ($m->[0] > $chop_len) {
2000 push @filtered, [ $chop_len, $chop_len + $tail_len ] if ($tail_len > 0);
2001 last;
2002 } elsif ($m->[1] > $chop_len) {
2003 push @filtered, [ $m->[0], $chop_len + $tail_len ];
2004 last;
2006 push @filtered, $m;
2009 return esc_html_hl_regions($chopped . $tail, 'match', @filtered);
2012 ## ----------------------------------------------------------------------
2013 ## functions returning short strings
2015 # CSS class for given age value (in seconds)
2016 sub age_class {
2017 my $age = shift;
2019 if (!defined $age) {
2020 return "noage";
2021 } elsif ($age < 60*60*2) {
2022 return "age0";
2023 } elsif ($age < 60*60*24*2) {
2024 return "age1";
2025 } else {
2026 return "age2";
2030 # convert age in seconds to "nn units ago" string
2031 sub age_string {
2032 my $age = shift;
2033 my $age_str;
2035 if ($age > 60*60*24*365*2) {
2036 $age_str = (int $age/60/60/24/365);
2037 $age_str .= " years ago";
2038 } elsif ($age > 60*60*24*(365/12)*2) {
2039 $age_str = int $age/60/60/24/(365/12);
2040 $age_str .= " months ago";
2041 } elsif ($age > 60*60*24*7*2) {
2042 $age_str = int $age/60/60/24/7;
2043 $age_str .= " weeks ago";
2044 } elsif ($age > 60*60*24*2) {
2045 $age_str = int $age/60/60/24;
2046 $age_str .= " days ago";
2047 } elsif ($age > 60*60*2) {
2048 $age_str = int $age/60/60;
2049 $age_str .= " hours ago";
2050 } elsif ($age > 60*2) {
2051 $age_str = int $age/60;
2052 $age_str .= " min ago";
2053 } elsif ($age > 2) {
2054 $age_str = int $age;
2055 $age_str .= " sec ago";
2056 } else {
2057 $age_str .= " right now";
2059 return $age_str;
2062 use constant {
2063 S_IFINVALID => 0030000,
2064 S_IFGITLINK => 0160000,
2067 # submodule/subproject, a commit object reference
2068 sub S_ISGITLINK {
2069 my $mode = shift;
2071 return (($mode & S_IFMT) == S_IFGITLINK)
2074 # convert file mode in octal to symbolic file mode string
2075 sub mode_str {
2076 my $mode = oct shift;
2078 if (S_ISGITLINK($mode)) {
2079 return 'm---------';
2080 } elsif (S_ISDIR($mode & S_IFMT)) {
2081 return 'drwxr-xr-x';
2082 } elsif (S_ISLNK($mode)) {
2083 return 'lrwxrwxrwx';
2084 } elsif (S_ISREG($mode)) {
2085 # git cares only about the executable bit
2086 if ($mode & S_IXUSR) {
2087 return '-rwxr-xr-x';
2088 } else {
2089 return '-rw-r--r--';
2091 } else {
2092 return '----------';
2096 # convert file mode in octal to file type string
2097 sub file_type {
2098 my $mode = shift;
2100 if ($mode !~ m/^[0-7]+$/) {
2101 return $mode;
2102 } else {
2103 $mode = oct $mode;
2106 if (S_ISGITLINK($mode)) {
2107 return "submodule";
2108 } elsif (S_ISDIR($mode & S_IFMT)) {
2109 return "directory";
2110 } elsif (S_ISLNK($mode)) {
2111 return "symlink";
2112 } elsif (S_ISREG($mode)) {
2113 return "file";
2114 } else {
2115 return "unknown";
2119 # convert file mode in octal to file type description string
2120 sub file_type_long {
2121 my $mode = shift;
2123 if ($mode !~ m/^[0-7]+$/) {
2124 return $mode;
2125 } else {
2126 $mode = oct $mode;
2129 if (S_ISGITLINK($mode)) {
2130 return "submodule";
2131 } elsif (S_ISDIR($mode & S_IFMT)) {
2132 return "directory";
2133 } elsif (S_ISLNK($mode)) {
2134 return "symlink";
2135 } elsif (S_ISREG($mode)) {
2136 if ($mode & S_IXUSR) {
2137 return "executable";
2138 } else {
2139 return "file";
2141 } else {
2142 return "unknown";
2147 ## ----------------------------------------------------------------------
2148 ## functions returning short HTML fragments, or transforming HTML fragments
2149 ## which don't belong to other sections
2151 # format line of commit message.
2152 sub format_log_line_html {
2153 my $line = shift;
2155 $line = esc_html($line, -nbsp=>1);
2156 $line =~ s{\b([0-9a-fA-F]{8,40})\b}{
2157 $cgi->a({-href => href(action=>"object", hash=>$1),
2158 -class => "text"}, $1);
2159 }eg;
2161 return $line;
2164 # format marker of refs pointing to given object
2166 # the destination action is chosen based on object type and current context:
2167 # - for annotated tags, we choose the tag view unless it's the current view
2168 # already, in which case we go to shortlog view
2169 # - for other refs, we keep the current view if we're in history, shortlog or
2170 # log view, and select shortlog otherwise
2171 sub format_ref_marker {
2172 my ($refs, $id) = @_;
2173 my $markers = '';
2175 if (defined $refs->{$id}) {
2176 foreach my $ref (@{$refs->{$id}}) {
2177 # this code exploits the fact that non-lightweight tags are the
2178 # only indirect objects, and that they are the only objects for which
2179 # we want to use tag instead of shortlog as action
2180 my ($type, $name) = qw();
2181 my $indirect = ($ref =~ s/\^\{\}$//);
2182 # e.g. tags/v2.6.11 or heads/next
2183 if ($ref =~ m!^(.*?)s?/(.*)$!) {
2184 $type = $1;
2185 $name = $2;
2186 } else {
2187 $type = "ref";
2188 $name = $ref;
2191 my $class = $type;
2192 $class .= " indirect" if $indirect;
2194 my $dest_action = "shortlog";
2196 if ($indirect) {
2197 $dest_action = "tag" unless $action eq "tag";
2198 } elsif ($action =~ /^(history|(short)?log)$/) {
2199 $dest_action = $action;
2202 my $dest = "";
2203 $dest .= "refs/" unless $ref =~ m!^refs/!;
2204 $dest .= $ref;
2206 my $link = $cgi->a({
2207 -href => href(
2208 action=>$dest_action,
2209 hash=>$dest
2210 )}, $name);
2212 $markers .= " <span class=\"".esc_attr($class)."\" title=\"".esc_attr($ref)."\">" .
2213 $link . "</span>";
2217 if ($markers) {
2218 return ' <span class="refs">'. $markers . '</span>';
2219 } else {
2220 return "";
2224 # format, perhaps shortened and with markers, title line
2225 sub format_subject_html {
2226 my ($long, $short, $href, $extra) = @_;
2227 $extra = '' unless defined($extra);
2229 if (length($short) < length($long)) {
2230 $long =~ s/[[:cntrl:]]/?/g;
2231 return $cgi->a({-href => $href, -class => "list subject",
2232 -title => to_utf8($long)},
2233 esc_html($short)) . $extra;
2234 } else {
2235 return $cgi->a({-href => $href, -class => "list subject"},
2236 esc_html($long)) . $extra;
2240 # Rather than recomputing the url for an email multiple times, we cache it
2241 # after the first hit. This gives a visible benefit in views where the avatar
2242 # for the same email is used repeatedly (e.g. shortlog).
2243 # The cache is shared by all avatar engines (currently gravatar only), which
2244 # are free to use it as preferred. Since only one avatar engine is used for any
2245 # given page, there's no risk for cache conflicts.
2246 our %avatar_cache = ();
2248 # Compute the picon url for a given email, by using the picon search service over at
2249 # http://www.cs.indiana.edu/picons/search.html
2250 sub picon_url {
2251 my $email = lc shift;
2252 if (!$avatar_cache{$email}) {
2253 my ($user, $domain) = split('@', $email);
2254 $avatar_cache{$email} =
2255 "//www.cs.indiana.edu/cgi-pub/kinzler/piconsearch.cgi/" .
2256 "$domain/$user/" .
2257 "users+domains+unknown/up/single";
2259 return $avatar_cache{$email};
2262 # Compute the gravatar url for a given email, if it's not in the cache already.
2263 # Gravatar stores only the part of the URL before the size, since that's the
2264 # one computationally more expensive. This also allows reuse of the cache for
2265 # different sizes (for this particular engine).
2266 sub gravatar_url {
2267 my $email = lc shift;
2268 my $size = shift;
2269 $avatar_cache{$email} ||=
2270 "//www.gravatar.com/avatar/" .
2271 Digest::MD5::md5_hex($email) . "?s=";
2272 return $avatar_cache{$email} . $size;
2275 # Insert an avatar for the given $email at the given $size if the feature
2276 # is enabled.
2277 sub git_get_avatar {
2278 my ($email, %opts) = @_;
2279 my $pre_white = ($opts{-pad_before} ? "&nbsp;" : "");
2280 my $post_white = ($opts{-pad_after} ? "&nbsp;" : "");
2281 $opts{-size} ||= 'default';
2282 my $size = $avatar_size{$opts{-size}} || $avatar_size{'default'};
2283 my $url = "";
2284 if ($git_avatar eq 'gravatar') {
2285 $url = gravatar_url($email, $size);
2286 } elsif ($git_avatar eq 'picon') {
2287 $url = picon_url($email);
2289 # Other providers can be added by extending the if chain, defining $url
2290 # as needed. If no variant puts something in $url, we assume avatars
2291 # are completely disabled/unavailable.
2292 if ($url) {
2293 return $pre_white .
2294 "<img width=\"$size\" " .
2295 "class=\"avatar\" " .
2296 "src=\"".esc_url($url)."\" " .
2297 "alt=\"\" " .
2298 "/>" . $post_white;
2299 } else {
2300 return "";
2304 sub format_search_author {
2305 my ($author, $searchtype, $displaytext) = @_;
2306 my $have_search = gitweb_check_feature('search');
2308 if ($have_search) {
2309 my $performed = "";
2310 if ($searchtype eq 'author') {
2311 $performed = "authored";
2312 } elsif ($searchtype eq 'committer') {
2313 $performed = "committed";
2316 return $cgi->a({-href => href(action=>"search", hash=>$hash,
2317 searchtext=>$author,
2318 searchtype=>$searchtype), class=>"list",
2319 title=>"Search for commits $performed by $author"},
2320 $displaytext);
2322 } else {
2323 return $displaytext;
2327 # format the author name of the given commit with the given tag
2328 # the author name is chopped and escaped according to the other
2329 # optional parameters (see chop_str).
2330 sub format_author_html {
2331 my $tag = shift;
2332 my $co = shift;
2333 my $author = chop_and_escape_str($co->{'author_name'}, @_);
2334 return "<$tag class=\"author\">" .
2335 format_search_author($co->{'author_name'}, "author",
2336 git_get_avatar($co->{'author_email'}, -pad_after => 1) .
2337 $author) .
2338 "</$tag>";
2341 # format git diff header line, i.e. "diff --(git|combined|cc) ..."
2342 sub format_git_diff_header_line {
2343 my $line = shift;
2344 my $diffinfo = shift;
2345 my ($from, $to) = @_;
2347 if ($diffinfo->{'nparents'}) {
2348 # combined diff
2349 $line =~ s!^(diff (.*?) )"?.*$!$1!;
2350 if ($to->{'href'}) {
2351 $line .= $cgi->a({-href => $to->{'href'}, -class => "path"},
2352 esc_path($to->{'file'}));
2353 } else { # file was deleted (no href)
2354 $line .= esc_path($to->{'file'});
2356 } else {
2357 # "ordinary" diff
2358 $line =~ s!^(diff (.*?) )"?a/.*$!$1!;
2359 if ($from->{'href'}) {
2360 $line .= $cgi->a({-href => $from->{'href'}, -class => "path"},
2361 'a/' . esc_path($from->{'file'}));
2362 } else { # file was added (no href)
2363 $line .= 'a/' . esc_path($from->{'file'});
2365 $line .= ' ';
2366 if ($to->{'href'}) {
2367 $line .= $cgi->a({-href => $to->{'href'}, -class => "path"},
2368 'b/' . esc_path($to->{'file'}));
2369 } else { # file was deleted
2370 $line .= 'b/' . esc_path($to->{'file'});
2374 return "<div class=\"diff header\">$line</div>\n";
2377 # format extended diff header line, before patch itself
2378 sub format_extended_diff_header_line {
2379 my $line = shift;
2380 my $diffinfo = shift;
2381 my ($from, $to) = @_;
2383 # match <path>
2384 if ($line =~ s!^((copy|rename) from ).*$!$1! && $from->{'href'}) {
2385 $line .= $cgi->a({-href=>$from->{'href'}, -class=>"path"},
2386 esc_path($from->{'file'}));
2388 if ($line =~ s!^((copy|rename) to ).*$!$1! && $to->{'href'}) {
2389 $line .= $cgi->a({-href=>$to->{'href'}, -class=>"path"},
2390 esc_path($to->{'file'}));
2392 # match single <mode>
2393 if ($line =~ m/\s(\d{6})$/) {
2394 $line .= '<span class="info"> (' .
2395 file_type_long($1) .
2396 ')</span>';
2398 # match <hash>
2399 if ($line =~ m/^index [0-9a-fA-F]{40},[0-9a-fA-F]{40}/) {
2400 # can match only for combined diff
2401 $line = 'index ';
2402 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
2403 if ($from->{'href'}[$i]) {
2404 $line .= $cgi->a({-href=>$from->{'href'}[$i],
2405 -class=>"hash"},
2406 substr($diffinfo->{'from_id'}[$i],0,7));
2407 } else {
2408 $line .= '0' x 7;
2410 # separator
2411 $line .= ',' if ($i < $diffinfo->{'nparents'} - 1);
2413 $line .= '..';
2414 if ($to->{'href'}) {
2415 $line .= $cgi->a({-href=>$to->{'href'}, -class=>"hash"},
2416 substr($diffinfo->{'to_id'},0,7));
2417 } else {
2418 $line .= '0' x 7;
2421 } elsif ($line =~ m/^index [0-9a-fA-F]{40}..[0-9a-fA-F]{40}/) {
2422 # can match only for ordinary diff
2423 my ($from_link, $to_link);
2424 if ($from->{'href'}) {
2425 $from_link = $cgi->a({-href=>$from->{'href'}, -class=>"hash"},
2426 substr($diffinfo->{'from_id'},0,7));
2427 } else {
2428 $from_link = '0' x 7;
2430 if ($to->{'href'}) {
2431 $to_link = $cgi->a({-href=>$to->{'href'}, -class=>"hash"},
2432 substr($diffinfo->{'to_id'},0,7));
2433 } else {
2434 $to_link = '0' x 7;
2436 my ($from_id, $to_id) = ($diffinfo->{'from_id'}, $diffinfo->{'to_id'});
2437 $line =~ s!$from_id\.\.$to_id!$from_link..$to_link!;
2440 return $line . "<br/>\n";
2443 # format from-file/to-file diff header
2444 sub format_diff_from_to_header {
2445 my ($from_line, $to_line, $diffinfo, $from, $to, @parents) = @_;
2446 my $line;
2447 my $result = '';
2449 $line = $from_line;
2450 #assert($line =~ m/^---/) if DEBUG;
2451 # no extra formatting for "^--- /dev/null"
2452 if (! $diffinfo->{'nparents'}) {
2453 # ordinary (single parent) diff
2454 if ($line =~ m!^--- "?a/!) {
2455 if ($from->{'href'}) {
2456 $line = '--- a/' .
2457 $cgi->a({-href=>$from->{'href'}, -class=>"path"},
2458 esc_path($from->{'file'}));
2459 } else {
2460 $line = '--- a/' .
2461 esc_path($from->{'file'});
2464 $result .= qq!<div class="diff from_file">$line</div>\n!;
2466 } else {
2467 # combined diff (merge commit)
2468 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
2469 if ($from->{'href'}[$i]) {
2470 $line = '--- ' .
2471 $cgi->a({-href=>href(action=>"blobdiff",
2472 hash_parent=>$diffinfo->{'from_id'}[$i],
2473 hash_parent_base=>$parents[$i],
2474 file_parent=>$from->{'file'}[$i],
2475 hash=>$diffinfo->{'to_id'},
2476 hash_base=>$hash,
2477 file_name=>$to->{'file'}),
2478 -class=>"path",
2479 -title=>"diff" . ($i+1)},
2480 $i+1) .
2481 '/' .
2482 $cgi->a({-href=>$from->{'href'}[$i], -class=>"path"},
2483 esc_path($from->{'file'}[$i]));
2484 } else {
2485 $line = '--- /dev/null';
2487 $result .= qq!<div class="diff from_file">$line</div>\n!;
2491 $line = $to_line;
2492 #assert($line =~ m/^\+\+\+/) if DEBUG;
2493 # no extra formatting for "^+++ /dev/null"
2494 if ($line =~ m!^\+\+\+ "?b/!) {
2495 if ($to->{'href'}) {
2496 $line = '+++ b/' .
2497 $cgi->a({-href=>$to->{'href'}, -class=>"path"},
2498 esc_path($to->{'file'}));
2499 } else {
2500 $line = '+++ b/' .
2501 esc_path($to->{'file'});
2504 $result .= qq!<div class="diff to_file">$line</div>\n!;
2506 return $result;
2509 # create note for patch simplified by combined diff
2510 sub format_diff_cc_simplified {
2511 my ($diffinfo, @parents) = @_;
2512 my $result = '';
2514 $result .= "<div class=\"diff header\">" .
2515 "diff --cc ";
2516 if (!is_deleted($diffinfo)) {
2517 $result .= $cgi->a({-href => href(action=>"blob",
2518 hash_base=>$hash,
2519 hash=>$diffinfo->{'to_id'},
2520 file_name=>$diffinfo->{'to_file'}),
2521 -class => "path"},
2522 esc_path($diffinfo->{'to_file'}));
2523 } else {
2524 $result .= esc_path($diffinfo->{'to_file'});
2526 $result .= "</div>\n" . # class="diff header"
2527 "<div class=\"diff nodifferences\">" .
2528 "Simple merge" .
2529 "</div>\n"; # class="diff nodifferences"
2531 return $result;
2534 sub diff_line_class {
2535 my ($line, $from, $to) = @_;
2537 # ordinary diff
2538 my $num_sign = 1;
2539 # combined diff
2540 if ($from && $to && ref($from->{'href'}) eq "ARRAY") {
2541 $num_sign = scalar @{$from->{'href'}};
2544 my @diff_line_classifier = (
2545 { regexp => qr/^\@\@{$num_sign} /, class => "chunk_header"},
2546 { regexp => qr/^\\/, class => "incomplete" },
2547 { regexp => qr/^ {$num_sign}/, class => "ctx" },
2548 # classifier for context must come before classifier add/rem,
2549 # or we would have to use more complicated regexp, for example
2550 # qr/(?= {0,$m}\+)[+ ]{$num_sign}/, where $m = $num_sign - 1;
2551 { regexp => qr/^[+ ]{$num_sign}/, class => "add" },
2552 { regexp => qr/^[- ]{$num_sign}/, class => "rem" },
2554 for my $clsfy (@diff_line_classifier) {
2555 return $clsfy->{'class'}
2556 if ($line =~ $clsfy->{'regexp'});
2559 # fallback
2560 return "";
2563 # assumes that $from and $to are defined and correctly filled,
2564 # and that $line holds a line of chunk header for unified diff
2565 sub format_unidiff_chunk_header {
2566 my ($line, $from, $to) = @_;
2568 my ($from_text, $from_start, $from_lines, $to_text, $to_start, $to_lines, $section) =
2569 $line =~ m/^\@{2} (-(\d+)(?:,(\d+))?) (\+(\d+)(?:,(\d+))?) \@{2}(.*)$/;
2571 $from_lines = 0 unless defined $from_lines;
2572 $to_lines = 0 unless defined $to_lines;
2574 if ($from->{'href'}) {
2575 $from_text = $cgi->a({-href=>"$from->{'href'}#l$from_start",
2576 -class=>"list"}, $from_text);
2578 if ($to->{'href'}) {
2579 $to_text = $cgi->a({-href=>"$to->{'href'}#l$to_start",
2580 -class=>"list"}, $to_text);
2582 $line = "<span class=\"chunk_info\">@@ $from_text $to_text @@</span>" .
2583 "<span class=\"section\">" . esc_html($section, -nbsp=>1) . "</span>";
2584 return $line;
2587 # assumes that $from and $to are defined and correctly filled,
2588 # and that $line holds a line of chunk header for combined diff
2589 sub format_cc_diff_chunk_header {
2590 my ($line, $from, $to) = @_;
2592 my ($prefix, $ranges, $section) = $line =~ m/^(\@+) (.*?) \@+(.*)$/;
2593 my (@from_text, @from_start, @from_nlines, $to_text, $to_start, $to_nlines);
2595 @from_text = split(' ', $ranges);
2596 for (my $i = 0; $i < @from_text; ++$i) {
2597 ($from_start[$i], $from_nlines[$i]) =
2598 (split(',', substr($from_text[$i], 1)), 0);
2601 $to_text = pop @from_text;
2602 $to_start = pop @from_start;
2603 $to_nlines = pop @from_nlines;
2605 $line = "<span class=\"chunk_info\">$prefix ";
2606 for (my $i = 0; $i < @from_text; ++$i) {
2607 if ($from->{'href'}[$i]) {
2608 $line .= $cgi->a({-href=>"$from->{'href'}[$i]#l$from_start[$i]",
2609 -class=>"list"}, $from_text[$i]);
2610 } else {
2611 $line .= $from_text[$i];
2613 $line .= " ";
2615 if ($to->{'href'}) {
2616 $line .= $cgi->a({-href=>"$to->{'href'}#l$to_start",
2617 -class=>"list"}, $to_text);
2618 } else {
2619 $line .= $to_text;
2621 $line .= " $prefix</span>" .
2622 "<span class=\"section\">" . esc_html($section, -nbsp=>1) . "</span>";
2623 return $line;
2626 # process patch (diff) line (not to be used for diff headers),
2627 # returning HTML-formatted (but not wrapped) line.
2628 # If the line is passed as a reference, it is treated as HTML and not
2629 # esc_html()'ed.
2630 sub format_diff_line {
2631 my ($line, $diff_class, $from, $to) = @_;
2633 if (ref($line)) {
2634 $line = $$line;
2635 } else {
2636 chomp $line;
2637 $line = untabify($line);
2639 if ($from && $to && $line =~ m/^\@{2} /) {
2640 $line = format_unidiff_chunk_header($line, $from, $to);
2641 } elsif ($from && $to && $line =~ m/^\@{3}/) {
2642 $line = format_cc_diff_chunk_header($line, $from, $to);
2643 } else {
2644 $line = esc_html($line, -nbsp=>1);
2648 my $diff_classes = "diff";
2649 $diff_classes .= " $diff_class" if ($diff_class);
2650 $line = "<div class=\"$diff_classes\">$line</div>\n";
2652 return $line;
2655 # Generates undef or something like "_snapshot_" or "snapshot (_tbz2_ _zip_)",
2656 # linked. Pass the hash of the tree/commit to snapshot.
2657 sub format_snapshot_links {
2658 my ($hash) = @_;
2659 my $num_fmts = @snapshot_fmts;
2660 if ($num_fmts > 1) {
2661 # A parenthesized list of links bearing format names.
2662 # e.g. "snapshot (_tar.gz_ _zip_)"
2663 return "snapshot (" . join(' ', map
2664 $cgi->a({
2665 -href => href(
2666 action=>"snapshot",
2667 hash=>$hash,
2668 snapshot_format=>$_
2670 }, $known_snapshot_formats{$_}{'display'})
2671 , @snapshot_fmts) . ")";
2672 } elsif ($num_fmts == 1) {
2673 # A single "snapshot" link whose tooltip bears the format name.
2674 # i.e. "_snapshot_"
2675 my ($fmt) = @snapshot_fmts;
2676 return
2677 $cgi->a({
2678 -href => href(
2679 action=>"snapshot",
2680 hash=>$hash,
2681 snapshot_format=>$fmt
2683 -title => "in format: $known_snapshot_formats{$fmt}{'display'}"
2684 }, "snapshot");
2685 } else { # $num_fmts == 0
2686 return undef;
2690 ## ......................................................................
2691 ## functions returning values to be passed, perhaps after some
2692 ## transformation, to other functions; e.g. returning arguments to href()
2694 # returns hash to be passed to href to generate gitweb URL
2695 # in -title key it returns description of link
2696 sub get_feed_info {
2697 my $format = shift || 'Atom';
2698 my %res = (action => lc($format));
2699 my $matched_ref = 0;
2701 # feed links are possible only for project views
2702 return unless (defined $project);
2703 # some views should link to OPML, or to generic project feed,
2704 # or don't have specific feed yet (so they should use generic)
2705 return if (!$action || $action =~ /^(?:tags|heads|forks|tag|search)$/x);
2707 my $branch = undef;
2708 # branches refs uses 'refs/' + $get_branch_refs()[x] + '/' prefix
2709 # (fullname) to differentiate from tag links; this also makes
2710 # possible to detect branch links
2711 for my $ref (get_branch_refs()) {
2712 if ((defined $hash_base && $hash_base =~ m!^refs/\Q$ref\E/(.*)$!) ||
2713 (defined $hash && $hash =~ m!^refs/\Q$ref\E/(.*)$!)) {
2714 $branch = $1;
2715 $matched_ref = $ref;
2716 last;
2719 # find log type for feed description (title)
2720 my $type = 'log';
2721 if (defined $file_name) {
2722 $type = "history of $file_name";
2723 $type .= "/" if ($action eq 'tree');
2724 $type .= " on '$branch'" if (defined $branch);
2725 } else {
2726 $type = "log of $branch" if (defined $branch);
2729 $res{-title} = $type;
2730 $res{'hash'} = (defined $branch ? "refs/$matched_ref/$branch" : undef);
2731 $res{'file_name'} = $file_name;
2733 return %res;
2736 ## ----------------------------------------------------------------------
2737 ## git utility subroutines, invoking git commands
2739 # returns path to the core git executable and the --git-dir parameter as list
2740 sub git_cmd {
2741 $number_of_git_cmds++;
2742 return $GIT, '--git-dir='.$git_dir;
2745 # opens a "-|" cmd pipe handle with 2>/dev/null and returns it
2746 sub cmd_pipe {
2748 # In order to be compatible with FCGI mode we must use POSIX
2749 # and access the STDERR_FILENO file descriptor directly
2751 use POSIX qw(STDERR_FILENO dup dup2);
2753 open(my $null, '>', File::Spec->devnull) or die "couldn't open devnull: $!";
2754 (my $saveerr = dup(STDERR_FILENO)) or die "couldn't dup STDERR: $!";
2755 my $dup2ok = dup2(fileno($null), STDERR_FILENO);
2756 close($null) or !$dup2ok or die "couldn't close NULL: $!";
2757 $dup2ok or POSIX::close($saveerr), die "couldn't dup NULL to STDERR: $!";
2758 my $result = open(my $fd, "-|", @_);
2759 $dup2ok = dup2($saveerr, STDERR_FILENO);
2760 POSIX::close($saveerr) or !$dup2ok or die "couldn't close SAVEERR: $!";
2761 $dup2ok or die "couldn't dup SAVERR to STDERR: $!";
2763 return $result ? $fd : undef;
2766 # opens a "-|" git_cmd pipe handle with 2>/dev/null and returns it
2767 sub git_cmd_pipe {
2768 return cmd_pipe git_cmd(), @_;
2771 # quote the given arguments for passing them to the shell
2772 # quote_command("command", "arg 1", "arg with ' and ! characters")
2773 # => "'command' 'arg 1' 'arg with '\'' and '\!' characters'"
2774 # Try to avoid using this function wherever possible.
2775 sub quote_command {
2776 return join(' ',
2777 map { my $a = $_; $a =~ s/(['!])/'\\$1'/g; "'$a'" } @_ );
2780 # get HEAD ref of given project as hash
2781 sub git_get_head_hash {
2782 return git_get_full_hash(shift, 'HEAD');
2785 sub git_get_full_hash {
2786 return git_get_hash(@_);
2789 sub git_get_short_hash {
2790 return git_get_hash(@_, '--short=7');
2793 sub git_get_hash {
2794 my ($project, $hash, @options) = @_;
2795 my $o_git_dir = $git_dir;
2796 my $retval = undef;
2797 $git_dir = "$projectroot/$project";
2798 if (defined(my $fd = git_cmd_pipe 'rev-parse',
2799 '--verify', '-q', @options, $hash)) {
2800 $retval = <$fd>;
2801 chomp $retval if defined $retval;
2802 close $fd;
2804 if (defined $o_git_dir) {
2805 $git_dir = $o_git_dir;
2807 return $retval;
2810 # get type of given object
2811 sub git_get_type {
2812 my $hash = shift;
2814 defined(my $fd = git_cmd_pipe "cat-file", '-t', $hash) or return;
2815 my $type = <$fd>;
2816 close $fd or return;
2817 chomp $type;
2818 return $type;
2821 # repository configuration
2822 our $config_file = '';
2823 our %config;
2825 # store multiple values for single key as anonymous array reference
2826 # single values stored directly in the hash, not as [ <value> ]
2827 sub hash_set_multi {
2828 my ($hash, $key, $value) = @_;
2830 if (!exists $hash->{$key}) {
2831 $hash->{$key} = $value;
2832 } elsif (!ref $hash->{$key}) {
2833 $hash->{$key} = [ $hash->{$key}, $value ];
2834 } else {
2835 push @{$hash->{$key}}, $value;
2839 # return hash of git project configuration
2840 # optionally limited to some section, e.g. 'gitweb'
2841 sub git_parse_project_config {
2842 my $section_regexp = shift;
2843 my %config;
2845 local $/ = "\0";
2847 defined(my $fh = git_cmd_pipe "config", '-z', '-l')
2848 or return;
2850 while (my $keyval = <$fh>) {
2851 chomp $keyval;
2852 my ($key, $value) = split(/\n/, $keyval, 2);
2854 hash_set_multi(\%config, $key, $value)
2855 if (!defined $section_regexp || $key =~ /^(?:$section_regexp)\./o);
2857 close $fh;
2859 return %config;
2862 # convert config value to boolean: 'true' or 'false'
2863 # no value, number > 0, 'true' and 'yes' values are true
2864 # rest of values are treated as false (never as error)
2865 sub config_to_bool {
2866 my $val = shift;
2868 return 1 if !defined $val; # section.key
2870 # strip leading and trailing whitespace
2871 $val =~ s/^\s+//;
2872 $val =~ s/\s+$//;
2874 return (($val =~ /^\d+$/ && $val) || # section.key = 1
2875 ($val =~ /^(?:true|yes)$/i)); # section.key = true
2878 # convert config value to simple decimal number
2879 # an optional value suffix of 'k', 'm', or 'g' will cause the value
2880 # to be multiplied by 1024, 1048576, or 1073741824
2881 sub config_to_int {
2882 my $val = shift;
2884 # strip leading and trailing whitespace
2885 $val =~ s/^\s+//;
2886 $val =~ s/\s+$//;
2888 if (my ($num, $unit) = ($val =~ /^([0-9]*)([kmg])$/i)) {
2889 $unit = lc($unit);
2890 # unknown unit is treated as 1
2891 return $num * ($unit eq 'g' ? 1073741824 :
2892 $unit eq 'm' ? 1048576 :
2893 $unit eq 'k' ? 1024 : 1);
2895 return $val;
2898 # convert config value to array reference, if needed
2899 sub config_to_multi {
2900 my $val = shift;
2902 return ref($val) ? $val : (defined($val) ? [ $val ] : []);
2905 sub git_get_project_config {
2906 my ($key, $type) = @_;
2908 return unless defined $git_dir;
2910 # key sanity check
2911 return unless ($key);
2912 # only subsection, if exists, is case sensitive,
2913 # and not lowercased by 'git config -z -l'
2914 if (my ($hi, $mi, $lo) = ($key =~ /^([^.]*)\.(.*)\.([^.]*)$/)) {
2915 $lo =~ s/_//g;
2916 $key = join(".", lc($hi), $mi, lc($lo));
2917 return if ($lo =~ /\W/ || $hi =~ /\W/);
2918 } else {
2919 $key = lc($key);
2920 $key =~ s/_//g;
2921 return if ($key =~ /\W/);
2923 $key =~ s/^gitweb\.//;
2925 # type sanity check
2926 if (defined $type) {
2927 $type =~ s/^--//;
2928 $type = undef
2929 unless ($type eq 'bool' || $type eq 'int');
2932 # get config
2933 if (!defined $config_file ||
2934 $config_file ne "$git_dir/config") {
2935 %config = git_parse_project_config('gitweb');
2936 $config_file = "$git_dir/config";
2939 # check if config variable (key) exists
2940 return unless exists $config{"gitweb.$key"};
2942 # ensure given type
2943 if (!defined $type) {
2944 return $config{"gitweb.$key"};
2945 } elsif ($type eq 'bool') {
2946 # backward compatibility: 'git config --bool' returns true/false
2947 return config_to_bool($config{"gitweb.$key"}) ? 'true' : 'false';
2948 } elsif ($type eq 'int') {
2949 return config_to_int($config{"gitweb.$key"});
2951 return $config{"gitweb.$key"};
2954 # get hash of given path at given ref
2955 sub git_get_hash_by_path {
2956 my $base = shift;
2957 my $path = shift || return undef;
2958 my $type = shift;
2960 $path =~ s,/+$,,;
2962 defined(my $fd = git_cmd_pipe "ls-tree", $base, "--", $path)
2963 or die_error(500, "Open git-ls-tree failed");
2964 my $line = <$fd>;
2965 close $fd or return undef;
2967 if (!defined $line) {
2968 # there is no tree or hash given by $path at $base
2969 return undef;
2972 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'
2973 $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t/;
2974 if (defined $type && $type ne $2) {
2975 # type doesn't match
2976 return undef;
2978 return $3;
2981 # get path of entry with given hash at given tree-ish (ref)
2982 # used to get 'from' filename for combined diff (merge commit) for renames
2983 sub git_get_path_by_hash {
2984 my $base = shift || return;
2985 my $hash = shift || return;
2987 local $/ = "\0";
2989 defined(my $fd = git_cmd_pipe "ls-tree", '-r', '-t', '-z', $base)
2990 or return undef;
2991 while (my $line = <$fd>) {
2992 chomp $line;
2994 #'040000 tree 595596a6a9117ddba9fe379b6b012b558bac8423 gitweb'
2995 #'100644 blob e02e90f0429be0d2a69b76571101f20b8f75530f gitweb/README'
2996 if ($line =~ m/(?:[0-9]+) (?:.+) $hash\t(.+)$/) {
2997 close $fd;
2998 return $1;
3001 close $fd;
3002 return undef;
3005 ## ......................................................................
3006 ## git utility functions, directly accessing git repository
3008 # get the value of config variable either from file named as the variable
3009 # itself in the repository ($GIT_DIR/$name file), or from gitweb.$name
3010 # configuration variable in the repository config file.
3011 sub git_get_file_or_project_config {
3012 my ($path, $name) = @_;
3014 $git_dir = "$projectroot/$path";
3015 open my $fd, '<', "$git_dir/$name"
3016 or return git_get_project_config($name);
3017 my $conf = <$fd>;
3018 close $fd;
3019 if (defined $conf) {
3020 chomp $conf;
3022 return $conf;
3025 sub git_get_project_description {
3026 my $path = shift;
3027 return git_get_file_or_project_config($path, 'description');
3030 sub git_get_project_category {
3031 my $path = shift;
3032 return git_get_file_or_project_config($path, 'category');
3036 # supported formats:
3037 # * $GIT_DIR/ctags/<tagname> file (in 'ctags' subdirectory)
3038 # - if its contents is a number, use it as tag weight,
3039 # - otherwise add a tag with weight 1
3040 # * $GIT_DIR/ctags file, each line is a tag (with weight 1)
3041 # the same value multiple times increases tag weight
3042 # * `gitweb.ctag' multi-valued repo config variable
3043 sub git_get_project_ctags {
3044 my $project = shift;
3045 my $ctags = {};
3047 $git_dir = "$projectroot/$project";
3048 if (opendir my $dh, "$git_dir/ctags") {
3049 my @files = grep { -f $_ } map { "$git_dir/ctags/$_" } readdir($dh);
3050 foreach my $tagfile (@files) {
3051 open my $ct, '<', $tagfile
3052 or next;
3053 my $val = <$ct>;
3054 chomp $val if $val;
3055 close $ct;
3057 (my $ctag = $tagfile) =~ s#.*/##;
3058 if ($val =~ /^\d+$/) {
3059 $ctags->{$ctag} = $val;
3060 } else {
3061 $ctags->{$ctag} = 1;
3064 closedir $dh;
3066 } elsif (open my $fh, '<', "$git_dir/ctags") {
3067 while (my $line = <$fh>) {
3068 chomp $line;
3069 $ctags->{$line}++ if $line;
3071 close $fh;
3073 } else {
3074 my $taglist = config_to_multi(git_get_project_config('ctag'));
3075 foreach my $tag (@$taglist) {
3076 $ctags->{$tag}++;
3080 return $ctags;
3083 # return hash, where keys are content tags ('ctags'),
3084 # and values are sum of weights of given tag in every project
3085 sub git_gather_all_ctags {
3086 my $projects = shift;
3087 my $ctags = {};
3089 foreach my $p (@$projects) {
3090 foreach my $ct (keys %{$p->{'ctags'}}) {
3091 $ctags->{$ct} += $p->{'ctags'}->{$ct};
3095 return $ctags;
3098 sub git_populate_project_tagcloud {
3099 my $ctags = shift;
3101 # First, merge different-cased tags; tags vote on casing
3102 my %ctags_lc;
3103 foreach (keys %$ctags) {
3104 $ctags_lc{lc $_}->{count} += $ctags->{$_};
3105 if (not $ctags_lc{lc $_}->{topcount}
3106 or $ctags_lc{lc $_}->{topcount} < $ctags->{$_}) {
3107 $ctags_lc{lc $_}->{topcount} = $ctags->{$_};
3108 $ctags_lc{lc $_}->{topname} = $_;
3112 my $cloud;
3113 my $matched = $input_params{'ctag'};
3114 if (eval { require HTML::TagCloud; 1; }) {
3115 $cloud = HTML::TagCloud->new;
3116 foreach my $ctag (sort keys %ctags_lc) {
3117 # Pad the title with spaces so that the cloud looks
3118 # less crammed.
3119 my $title = esc_html($ctags_lc{$ctag}->{topname});
3120 $title =~ s/ /&nbsp;/g;
3121 $title =~ s/^/&nbsp;/g;
3122 $title =~ s/$/&nbsp;/g;
3123 if (defined $matched && $matched eq $ctag) {
3124 $title = qq(<span class="match">$title</span>);
3126 $cloud->add($title, href(project=>undef, ctag=>$ctag),
3127 $ctags_lc{$ctag}->{count});
3129 } else {
3130 $cloud = {};
3131 foreach my $ctag (keys %ctags_lc) {
3132 my $title = esc_html($ctags_lc{$ctag}->{topname}, -nbsp=>1);
3133 if (defined $matched && $matched eq $ctag) {
3134 $title = qq(<span class="match">$title</span>);
3136 $cloud->{$ctag}{count} = $ctags_lc{$ctag}->{count};
3137 $cloud->{$ctag}{ctag} =
3138 $cgi->a({-href=>href(project=>undef, ctag=>$ctag)}, $title);
3141 return $cloud;
3144 sub git_show_project_tagcloud {
3145 my ($cloud, $count) = @_;
3146 if (ref $cloud eq 'HTML::TagCloud') {
3147 return $cloud->html_and_css($count);
3148 } else {
3149 my @tags = sort { $cloud->{$a}->{'count'} <=> $cloud->{$b}->{'count'} } keys %$cloud;
3150 return
3151 '<div id="htmltagcloud"'.($project ? '' : ' align="center"').'>' .
3152 join (', ', map {
3153 $cloud->{$_}->{'ctag'}
3154 } splice(@tags, 0, $count)) .
3155 '</div>';
3159 sub git_get_project_url_list {
3160 my $path = shift;
3162 $git_dir = "$projectroot/$path";
3163 open my $fd, '<', "$git_dir/cloneurl"
3164 or return wantarray ?
3165 @{ config_to_multi(git_get_project_config('url')) } :
3166 config_to_multi(git_get_project_config('url'));
3167 my @git_project_url_list = map { chomp; $_ } <$fd>;
3168 close $fd;
3170 return wantarray ? @git_project_url_list : \@git_project_url_list;
3173 sub git_get_projects_list {
3174 my $filter = shift || '';
3175 my $paranoid = shift;
3176 my @list;
3178 if (-d $projects_list) {
3179 # search in directory
3180 my $dir = $projects_list;
3181 # remove the trailing "/"
3182 $dir =~ s!/+$!!;
3183 my $pfxlen = length("$dir");
3184 my $pfxdepth = ($dir =~ tr!/!!);
3185 # when filtering, search only given subdirectory
3186 if ($filter && !$paranoid) {
3187 $dir .= "/$filter";
3188 $dir =~ s!/+$!!;
3191 File::Find::find({
3192 follow_fast => 1, # follow symbolic links
3193 follow_skip => 2, # ignore duplicates
3194 dangling_symlinks => 0, # ignore dangling symlinks, silently
3195 wanted => sub {
3196 # global variables
3197 our $project_maxdepth;
3198 our $projectroot;
3199 # skip project-list toplevel, if we get it.
3200 return if (m!^[/.]$!);
3201 # only directories can be git repositories
3202 return unless (-d $_);
3203 # don't traverse too deep (Find is super slow on os x)
3204 # $project_maxdepth excludes depth of $projectroot
3205 if (($File::Find::name =~ tr!/!!) - $pfxdepth > $project_maxdepth) {
3206 $File::Find::prune = 1;
3207 return;
3210 my $path = substr($File::Find::name, $pfxlen + 1);
3211 # paranoidly only filter here
3212 if ($paranoid && $filter && $path !~ m!^\Q$filter\E/!) {
3213 next;
3215 # we check related file in $projectroot
3216 if (check_export_ok("$projectroot/$path")) {
3217 push @list, { path => $path };
3218 $File::Find::prune = 1;
3221 }, "$dir");
3223 } elsif (-f $projects_list) {
3224 # read from file(url-encoded):
3225 # 'git%2Fgit.git Linus+Torvalds'
3226 # 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin'
3227 # 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman'
3228 open my $fd, '<', $projects_list or return;
3229 PROJECT:
3230 while (my $line = <$fd>) {
3231 chomp $line;
3232 my ($path, $owner) = split ' ', $line;
3233 $path = unescape($path);
3234 $owner = unescape($owner);
3235 if (!defined $path) {
3236 next;
3238 # if $filter is rpovided, check if $path begins with $filter
3239 if ($filter && $path !~ m!^\Q$filter\E/!) {
3240 next;
3242 if (check_export_ok("$projectroot/$path")) {
3243 my $pr = {
3244 path => $path
3246 if ($owner) {
3247 $pr->{'owner'} = to_utf8($owner);
3249 push @list, $pr;
3252 close $fd;
3254 return @list;
3257 # written with help of Tree::Trie module (Perl Artistic License, GPL compatibile)
3258 # as side effects it sets 'forks' field to list of forks for forked projects
3259 sub filter_forks_from_projects_list {
3260 my $projects = shift;
3262 my %trie; # prefix tree of directories (path components)
3263 # generate trie out of those directories that might contain forks
3264 foreach my $pr (@$projects) {
3265 my $path = $pr->{'path'};
3266 $path =~ s/\.git$//; # forks of 'repo.git' are in 'repo/' directory
3267 next if ($path =~ m!/$!); # skip non-bare repositories, e.g. 'repo/.git'
3268 next unless ($path); # skip '.git' repository: tests, git-instaweb
3269 next unless (-d "$projectroot/$path"); # containing directory exists
3270 $pr->{'forks'} = []; # there can be 0 or more forks of project
3272 # add to trie
3273 my @dirs = split('/', $path);
3274 # walk the trie, until either runs out of components or out of trie
3275 my $ref = \%trie;
3276 while (scalar @dirs &&
3277 exists($ref->{$dirs[0]})) {
3278 $ref = $ref->{shift @dirs};
3280 # create rest of trie structure from rest of components
3281 foreach my $dir (@dirs) {
3282 $ref = $ref->{$dir} = {};
3284 # create end marker, store $pr as a data
3285 $ref->{''} = $pr if (!exists $ref->{''});
3288 # filter out forks, by finding shortest prefix match for paths
3289 my @filtered;
3290 PROJECT:
3291 foreach my $pr (@$projects) {
3292 # trie lookup
3293 my $ref = \%trie;
3294 DIR:
3295 foreach my $dir (split('/', $pr->{'path'})) {
3296 if (exists $ref->{''}) {
3297 # found [shortest] prefix, is a fork - skip it
3298 push @{$ref->{''}{'forks'}}, $pr;
3299 next PROJECT;
3301 if (!exists $ref->{$dir}) {
3302 # not in trie, cannot have prefix, not a fork
3303 push @filtered, $pr;
3304 next PROJECT;
3306 # If the dir is there, we just walk one step down the trie.
3307 $ref = $ref->{$dir};
3309 # we ran out of trie
3310 # (shouldn't happen: it's either no match, or end marker)
3311 push @filtered, $pr;
3314 return @filtered;
3317 # note: fill_project_list_info must be run first,
3318 # for 'descr_long' and 'ctags' to be filled
3319 sub search_projects_list {
3320 my ($projlist, %opts) = @_;
3321 my $tagfilter = $opts{'tagfilter'};
3322 my $search_re = $opts{'search_regexp'};
3324 return @$projlist
3325 unless ($tagfilter || $search_re);
3327 # searching projects require filling to be run before it;
3328 fill_project_list_info($projlist,
3329 $tagfilter ? 'ctags' : (),
3330 $search_re ? ('path', 'descr') : ());
3331 my @projects;
3332 PROJECT:
3333 foreach my $pr (@$projlist) {
3335 if ($tagfilter) {
3336 next unless ref($pr->{'ctags'}) eq 'HASH';
3337 next unless
3338 grep { lc($_) eq lc($tagfilter) } keys %{$pr->{'ctags'}};
3341 if ($search_re) {
3342 next unless
3343 $pr->{'path'} =~ /$search_re/ ||
3344 $pr->{'descr_long'} =~ /$search_re/;
3347 push @projects, $pr;
3350 return @projects;
3353 our $gitweb_project_owner = undef;
3354 sub git_get_project_list_from_file {
3356 return if (defined $gitweb_project_owner);
3358 $gitweb_project_owner = {};
3359 # read from file (url-encoded):
3360 # 'git%2Fgit.git Linus+Torvalds'
3361 # 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin'
3362 # 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman'
3363 if (-f $projects_list) {
3364 open(my $fd, '<', $projects_list);
3365 while (my $line = <$fd>) {
3366 chomp $line;
3367 my ($pr, $ow) = split ' ', $line;
3368 $pr = unescape($pr);
3369 $ow = unescape($ow);
3370 $gitweb_project_owner->{$pr} = to_utf8($ow);
3372 close $fd;
3376 sub git_get_project_owner {
3377 my $project = shift;
3378 my $owner;
3380 return undef unless $project;
3381 $git_dir = "$projectroot/$project";
3383 if (!defined $gitweb_project_owner) {
3384 git_get_project_list_from_file();
3387 if (exists $gitweb_project_owner->{$project}) {
3388 $owner = $gitweb_project_owner->{$project};
3390 if (!defined $owner){
3391 $owner = git_get_project_config('owner');
3393 if (!defined $owner) {
3394 $owner = get_file_owner("$git_dir");
3397 return $owner;
3400 sub git_get_last_activity {
3401 my ($path) = @_;
3402 my $fd;
3404 $git_dir = "$projectroot/$path";
3405 defined($fd = git_cmd_pipe 'for-each-ref',
3406 '--format=%(committer)',
3407 '--sort=-committerdate',
3408 '--count=1',
3409 map { "refs/$_" } get_branch_refs ()) or return;
3410 my $most_recent = <$fd>;
3411 close $fd or return;
3412 if (defined $most_recent &&
3413 $most_recent =~ / (\d+) [-+][01]\d\d\d$/) {
3414 my $timestamp = $1;
3415 my $age = time - $timestamp;
3416 return ($age, age_string($age));
3418 return (undef, undef);
3421 # Implementation note: when a single remote is wanted, we cannot use 'git
3422 # remote show -n' because that command always work (assuming it's a remote URL
3423 # if it's not defined), and we cannot use 'git remote show' because that would
3424 # try to make a network roundtrip. So the only way to find if that particular
3425 # remote is defined is to walk the list provided by 'git remote -v' and stop if
3426 # and when we find what we want.
3427 sub git_get_remotes_list {
3428 my $wanted = shift;
3429 my %remotes = ();
3431 my $fd = git_cmd_pipe 'remote', '-v';
3432 return unless $fd;
3433 while (my $remote = <$fd>) {
3434 chomp $remote;
3435 $remote =~ s!\t(.*?)\s+\((\w+)\)$!!;
3436 next if $wanted and not $remote eq $wanted;
3437 my ($url, $key) = ($1, $2);
3439 $remotes{$remote} ||= { 'heads' => () };
3440 $remotes{$remote}{$key} = $url;
3442 close $fd or return;
3443 return wantarray ? %remotes : \%remotes;
3446 # Takes a hash of remotes as first parameter and fills it by adding the
3447 # available remote heads for each of the indicated remotes.
3448 sub fill_remote_heads {
3449 my $remotes = shift;
3450 my @heads = map { "remotes/$_" } keys %$remotes;
3451 my @remoteheads = git_get_heads_list(undef, @heads);
3452 foreach my $remote (keys %$remotes) {
3453 $remotes->{$remote}{'heads'} = [ grep {
3454 $_->{'name'} =~ s!^$remote/!!
3455 } @remoteheads ];
3459 sub git_get_references {
3460 my $type = shift || "";
3461 my %refs;
3462 # 5dc01c595e6c6ec9ccda4f6f69c131c0dd945f8c refs/tags/v2.6.11
3463 # c39ae07f393806ccf406ef966e9a15afc43cc36a refs/tags/v2.6.11^{}
3464 defined(my $fd = git_cmd_pipe "show-ref", "--dereference",
3465 ($type ? ("--", "refs/$type") : ())) # use -- <pattern> if $type
3466 or return;
3468 while (my $line = <$fd>) {
3469 chomp $line;
3470 if ($line =~ m!^([0-9a-fA-F]{40})\srefs/($type.*)$!) {
3471 if (defined $refs{$1}) {
3472 push @{$refs{$1}}, $2;
3473 } else {
3474 $refs{$1} = [ $2 ];
3478 close $fd or return;
3479 return \%refs;
3482 sub git_get_rev_name_tags {
3483 my $hash = shift || return undef;
3485 defined(my $fd = git_cmd_pipe "name-rev", "--tags", $hash)
3486 or return;
3487 my $name_rev = <$fd>;
3488 close $fd;
3490 if ($name_rev =~ m|^$hash tags/(.*)$|) {
3491 return $1;
3492 } else {
3493 # catches also '$hash undefined' output
3494 return undef;
3498 ## ----------------------------------------------------------------------
3499 ## parse to hash functions
3501 sub parse_date {
3502 my $epoch = shift;
3503 my $tz = shift || "-0000";
3505 my %date;
3506 my @months = ("Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec");
3507 my @days = ("Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat");
3508 my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($epoch);
3509 $date{'hour'} = $hour;
3510 $date{'minute'} = $min;
3511 $date{'mday'} = $mday;
3512 $date{'day'} = $days[$wday];
3513 $date{'month'} = $months[$mon];
3514 $date{'rfc2822'} = sprintf "%s, %d %s %4d %02d:%02d:%02d +0000",
3515 $days[$wday], $mday, $months[$mon], 1900+$year, $hour ,$min, $sec;
3516 $date{'mday-time'} = sprintf "%d %s %02d:%02d",
3517 $mday, $months[$mon], $hour ,$min;
3518 $date{'iso-8601'} = sprintf "%04d-%02d-%02dT%02d:%02d:%02dZ",
3519 1900+$year, 1+$mon, $mday, $hour ,$min, $sec;
3521 my ($tz_sign, $tz_hour, $tz_min) =
3522 ($tz =~ m/^([-+])(\d\d)(\d\d)$/);
3523 $tz_sign = ($tz_sign eq '-' ? -1 : +1);
3524 my $local = $epoch + $tz_sign*((($tz_hour*60) + $tz_min)*60);
3525 ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($local);
3526 $date{'hour_local'} = $hour;
3527 $date{'minute_local'} = $min;
3528 $date{'tz_local'} = $tz;
3529 $date{'iso-tz'} = sprintf("%04d-%02d-%02d %02d:%02d:%02d %s",
3530 1900+$year, $mon+1, $mday,
3531 $hour, $min, $sec, $tz);
3532 return %date;
3535 sub parse_tag {
3536 my $tag_id = shift;
3537 my %tag;
3538 my @comment;
3540 defined(my $fd = git_cmd_pipe "cat-file", "tag", $tag_id) or return;
3541 $tag{'id'} = $tag_id;
3542 while (my $line = <$fd>) {
3543 chomp $line;
3544 if ($line =~ m/^object ([0-9a-fA-F]{40})$/) {
3545 $tag{'object'} = $1;
3546 } elsif ($line =~ m/^type (.+)$/) {
3547 $tag{'type'} = $1;
3548 } elsif ($line =~ m/^tag (.+)$/) {
3549 $tag{'name'} = $1;
3550 } elsif ($line =~ m/^tagger (.*) ([0-9]+) (.*)$/) {
3551 $tag{'author'} = $1;
3552 $tag{'author_epoch'} = $2;
3553 $tag{'author_tz'} = $3;
3554 if ($tag{'author'} =~ m/^([^<]+) <([^>]*)>/) {
3555 $tag{'author_name'} = $1;
3556 $tag{'author_email'} = $2;
3557 } else {
3558 $tag{'author_name'} = $tag{'author'};
3560 } elsif ($line =~ m/--BEGIN/) {
3561 push @comment, $line;
3562 last;
3563 } elsif ($line eq "") {
3564 last;
3567 push @comment, <$fd>;
3568 $tag{'comment'} = \@comment;
3569 close $fd or return;
3570 if (!defined $tag{'name'}) {
3571 return
3573 return %tag
3576 sub parse_commit_text {
3577 my ($commit_text, $withparents) = @_;
3578 my @commit_lines = split '\n', $commit_text;
3579 my %co;
3581 pop @commit_lines; # Remove '\0'
3583 if (! @commit_lines) {
3584 return;
3587 my $header = shift @commit_lines;
3588 if ($header !~ m/^[0-9a-fA-F]{40}/) {
3589 return;
3591 ($co{'id'}, my @parents) = split ' ', $header;
3592 while (my $line = shift @commit_lines) {
3593 last if $line eq "\n";
3594 if ($line =~ m/^tree ([0-9a-fA-F]{40})$/) {
3595 $co{'tree'} = $1;
3596 } elsif ((!defined $withparents) && ($line =~ m/^parent ([0-9a-fA-F]{40})$/)) {
3597 push @parents, $1;
3598 } elsif ($line =~ m/^author (.*) ([0-9]+) (.*)$/) {
3599 $co{'author'} = to_utf8($1);
3600 $co{'author_epoch'} = $2;
3601 $co{'author_tz'} = $3;
3602 if ($co{'author'} =~ m/^([^<]+) <([^>]*)>/) {
3603 $co{'author_name'} = $1;
3604 $co{'author_email'} = $2;
3605 } else {
3606 $co{'author_name'} = $co{'author'};
3608 } elsif ($line =~ m/^committer (.*) ([0-9]+) (.*)$/) {
3609 $co{'committer'} = to_utf8($1);
3610 $co{'committer_epoch'} = $2;
3611 $co{'committer_tz'} = $3;
3612 if ($co{'committer'} =~ m/^([^<]+) <([^>]*)>/) {
3613 $co{'committer_name'} = $1;
3614 $co{'committer_email'} = $2;
3615 } else {
3616 $co{'committer_name'} = $co{'committer'};
3620 if (!defined $co{'tree'}) {
3621 return;
3623 $co{'parents'} = \@parents;
3624 $co{'parent'} = $parents[0];
3626 foreach my $title (@commit_lines) {
3627 $title =~ s/^ //;
3628 if ($title ne "") {
3629 $co{'title'} = chop_str($title, 80, 5);
3630 # remove leading stuff of merges to make the interesting part visible
3631 if (length($title) > 50) {
3632 $title =~ s/^Automatic //;
3633 $title =~ s/^merge (of|with) /Merge ... /i;
3634 if (length($title) > 50) {
3635 $title =~ s/(http|rsync):\/\///;
3637 if (length($title) > 50) {
3638 $title =~ s/(master|www|rsync)\.//;
3640 if (length($title) > 50) {
3641 $title =~ s/kernel.org:?//;
3643 if (length($title) > 50) {
3644 $title =~ s/\/pub\/scm//;
3647 $co{'title_short'} = chop_str($title, 50, 5);
3648 last;
3651 if (! defined $co{'title'} || $co{'title'} eq "") {
3652 $co{'title'} = $co{'title_short'} = '(no commit message)';
3654 # remove added spaces
3655 foreach my $line (@commit_lines) {
3656 $line =~ s/^ //;
3658 $co{'comment'} = \@commit_lines;
3660 my $age = time - $co{'committer_epoch'};
3661 $co{'age'} = $age;
3662 $co{'age_string'} = age_string($age);
3663 my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($co{'committer_epoch'});
3664 if ($age > 60*60*24*7*2) {
3665 $co{'age_string_date'} = sprintf "%4i-%02u-%02i", 1900 + $year, $mon+1, $mday;
3666 $co{'age_string_age'} = $co{'age_string'};
3667 } else {
3668 $co{'age_string_date'} = $co{'age_string'};
3669 $co{'age_string_age'} = sprintf "%4i-%02u-%02i", 1900 + $year, $mon+1, $mday;
3671 return %co;
3674 sub parse_commit {
3675 my ($commit_id) = @_;
3676 my %co;
3678 local $/ = "\0";
3680 defined(my $fd = git_cmd_pipe "rev-list",
3681 "--parents",
3682 "--header",
3683 "--max-count=1",
3684 $commit_id,
3685 "--")
3686 or die_error(500, "Open git-rev-list failed");
3687 %co = parse_commit_text(<$fd>, 1);
3688 close $fd;
3690 return %co;
3693 sub parse_commits {
3694 my ($commit_id, $maxcount, $skip, $filename, @args) = @_;
3695 my @cos;
3697 $maxcount ||= 1;
3698 $skip ||= 0;
3700 local $/ = "\0";
3702 defined(my $fd = git_cmd_pipe "rev-list",
3703 "--header",
3704 @args,
3705 ("--max-count=" . $maxcount),
3706 ("--skip=" . $skip),
3707 @extra_options,
3708 $commit_id,
3709 "--",
3710 ($filename ? ($filename) : ()))
3711 or die_error(500, "Open git-rev-list failed");
3712 while (my $line = <$fd>) {
3713 my %co = parse_commit_text($line);
3714 push @cos, \%co;
3716 close $fd;
3718 return wantarray ? @cos : \@cos;
3721 # parse line of git-diff-tree "raw" output
3722 sub parse_difftree_raw_line {
3723 my $line = shift;
3724 my %res;
3726 # ':100644 100644 03b218260e99b78c6df0ed378e59ed9205ccc96d 3b93d5e7cc7f7dd4ebed13a5cc1a4ad976fc94d8 M ls-files.c'
3727 # ':100644 100644 7f9281985086971d3877aca27704f2aaf9c448ce bc190ebc71bbd923f2b728e505408f5e54bd073a M rev-tree.c'
3728 if ($line =~ m/^:([0-7]{6}) ([0-7]{6}) ([0-9a-fA-F]{40}) ([0-9a-fA-F]{40}) (.)([0-9]{0,3})\t(.*)$/) {
3729 $res{'from_mode'} = $1;
3730 $res{'to_mode'} = $2;
3731 $res{'from_id'} = $3;
3732 $res{'to_id'} = $4;
3733 $res{'status'} = $5;
3734 $res{'similarity'} = $6;
3735 if ($res{'status'} eq 'R' || $res{'status'} eq 'C') { # renamed or copied
3736 ($res{'from_file'}, $res{'to_file'}) = map { unquote($_) } split("\t", $7);
3737 } else {
3738 $res{'from_file'} = $res{'to_file'} = $res{'file'} = unquote($7);
3741 # '::100755 100755 100755 60e79ca1b01bc8b057abe17ddab484699a7f5fdb 94067cc5f73388f33722d52ae02f44692bc07490 94067cc5f73388f33722d52ae02f44692bc07490 MR git-gui/git-gui.sh'
3742 # combined diff (for merge commit)
3743 elsif ($line =~ s/^(::+)((?:[0-7]{6} )+)((?:[0-9a-fA-F]{40} )+)([a-zA-Z]+)\t(.*)$//) {
3744 $res{'nparents'} = length($1);
3745 $res{'from_mode'} = [ split(' ', $2) ];
3746 $res{'to_mode'} = pop @{$res{'from_mode'}};
3747 $res{'from_id'} = [ split(' ', $3) ];
3748 $res{'to_id'} = pop @{$res{'from_id'}};
3749 $res{'status'} = [ split('', $4) ];
3750 $res{'to_file'} = unquote($5);
3752 # 'c512b523472485aef4fff9e57b229d9d243c967f'
3753 elsif ($line =~ m/^([0-9a-fA-F]{40})$/) {
3754 $res{'commit'} = $1;
3757 return wantarray ? %res : \%res;
3760 # wrapper: return parsed line of git-diff-tree "raw" output
3761 # (the argument might be raw line, or parsed info)
3762 sub parsed_difftree_line {
3763 my $line_or_ref = shift;
3765 if (ref($line_or_ref) eq "HASH") {
3766 # pre-parsed (or generated by hand)
3767 return $line_or_ref;
3768 } else {
3769 return parse_difftree_raw_line($line_or_ref);
3773 # parse line of git-ls-tree output
3774 sub parse_ls_tree_line {
3775 my $line = shift;
3776 my %opts = @_;
3777 my %res;
3779 if ($opts{'-l'}) {
3780 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa 16717 panic.c'
3781 $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40}) +(-|[0-9]+)\t(.+)$/s;
3783 $res{'mode'} = $1;
3784 $res{'type'} = $2;
3785 $res{'hash'} = $3;
3786 $res{'size'} = $4;
3787 if ($opts{'-z'}) {
3788 $res{'name'} = $5;
3789 } else {
3790 $res{'name'} = unquote($5);
3792 } else {
3793 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'
3794 $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t(.+)$/s;
3796 $res{'mode'} = $1;
3797 $res{'type'} = $2;
3798 $res{'hash'} = $3;
3799 if ($opts{'-z'}) {
3800 $res{'name'} = $4;
3801 } else {
3802 $res{'name'} = unquote($4);
3806 return wantarray ? %res : \%res;
3809 # generates _two_ hashes, references to which are passed as 2 and 3 argument
3810 sub parse_from_to_diffinfo {
3811 my ($diffinfo, $from, $to, @parents) = @_;
3813 if ($diffinfo->{'nparents'}) {
3814 # combined diff
3815 $from->{'file'} = [];
3816 $from->{'href'} = [];
3817 fill_from_file_info($diffinfo, @parents)
3818 unless exists $diffinfo->{'from_file'};
3819 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
3820 $from->{'file'}[$i] =
3821 defined $diffinfo->{'from_file'}[$i] ?
3822 $diffinfo->{'from_file'}[$i] :
3823 $diffinfo->{'to_file'};
3824 if ($diffinfo->{'status'}[$i] ne "A") { # not new (added) file
3825 $from->{'href'}[$i] = href(action=>"blob",
3826 hash_base=>$parents[$i],
3827 hash=>$diffinfo->{'from_id'}[$i],
3828 file_name=>$from->{'file'}[$i]);
3829 } else {
3830 $from->{'href'}[$i] = undef;
3833 } else {
3834 # ordinary (not combined) diff
3835 $from->{'file'} = $diffinfo->{'from_file'};
3836 if ($diffinfo->{'status'} ne "A") { # not new (added) file
3837 $from->{'href'} = href(action=>"blob", hash_base=>$hash_parent,
3838 hash=>$diffinfo->{'from_id'},
3839 file_name=>$from->{'file'});
3840 } else {
3841 delete $from->{'href'};
3845 $to->{'file'} = $diffinfo->{'to_file'};
3846 if (!is_deleted($diffinfo)) { # file exists in result
3847 $to->{'href'} = href(action=>"blob", hash_base=>$hash,
3848 hash=>$diffinfo->{'to_id'},
3849 file_name=>$to->{'file'});
3850 } else {
3851 delete $to->{'href'};
3855 ## ......................................................................
3856 ## parse to array of hashes functions
3858 sub git_get_heads_list {
3859 my ($limit, @classes) = @_;
3860 @classes = get_branch_refs() unless @classes;
3861 my @patterns = map { "refs/$_" } @classes;
3862 my @headslist;
3864 defined(my $fd = git_cmd_pipe 'for-each-ref',
3865 ($limit ? '--count='.($limit+1) : ()), '--sort=-committerdate',
3866 '--format=%(objectname) %(refname) %(subject)%00%(committer)',
3867 @patterns)
3868 or return;
3869 while (my $line = <$fd>) {
3870 my %ref_item;
3872 chomp $line;
3873 my ($refinfo, $committerinfo) = split(/\0/, $line);
3874 my ($hash, $name, $title) = split(' ', $refinfo, 3);
3875 my ($committer, $epoch, $tz) =
3876 ($committerinfo =~ /^(.*) ([0-9]+) (.*)$/);
3877 $ref_item{'fullname'} = $name;
3878 my $strip_refs = join '|', map { quotemeta } get_branch_refs();
3879 $name =~ s!^refs/($strip_refs|remotes)/!!;
3880 $ref_item{'name'} = $name;
3881 # for refs neither in 'heads' nor 'remotes' we want to
3882 # show their ref dir
3883 my $ref_dir = (defined $1) ? $1 : '';
3884 if ($ref_dir ne '' and $ref_dir ne 'heads' and $ref_dir ne 'remotes') {
3885 $ref_item{'name'} .= ' (' . $ref_dir . ')';
3888 $ref_item{'id'} = $hash;
3889 $ref_item{'title'} = $title || '(no commit message)';
3890 $ref_item{'epoch'} = $epoch;
3891 if ($epoch) {
3892 $ref_item{'age'} = age_string(time - $ref_item{'epoch'});
3893 } else {
3894 $ref_item{'age'} = "unknown";
3897 push @headslist, \%ref_item;
3899 close $fd;
3901 return wantarray ? @headslist : \@headslist;
3904 sub git_get_tags_list {
3905 my $limit = shift;
3906 my @tagslist;
3908 defined(my $fd = git_cmd_pipe 'for-each-ref',
3909 ($limit ? '--count='.($limit+1) : ()), '--sort=-creatordate',
3910 '--format=%(objectname) %(objecttype) %(refname) '.
3911 '%(*objectname) %(*objecttype) %(subject)%00%(creator)',
3912 'refs/tags')
3913 or return;
3914 while (my $line = <$fd>) {
3915 my %ref_item;
3917 chomp $line;
3918 my ($refinfo, $creatorinfo) = split(/\0/, $line);
3919 my ($id, $type, $name, $refid, $reftype, $title) = split(' ', $refinfo, 6);
3920 my ($creator, $epoch, $tz) =
3921 ($creatorinfo =~ /^(.*) ([0-9]+) (.*)$/);
3922 $ref_item{'fullname'} = $name;
3923 $name =~ s!^refs/tags/!!;
3925 $ref_item{'type'} = $type;
3926 $ref_item{'id'} = $id;
3927 $ref_item{'name'} = $name;
3928 if ($type eq "tag") {
3929 $ref_item{'subject'} = $title;
3930 $ref_item{'reftype'} = $reftype;
3931 $ref_item{'refid'} = $refid;
3932 } else {
3933 $ref_item{'reftype'} = $type;
3934 $ref_item{'refid'} = $id;
3937 if ($type eq "tag" || $type eq "commit") {
3938 $ref_item{'epoch'} = $epoch;
3939 if ($epoch) {
3940 $ref_item{'age'} = age_string(time - $ref_item{'epoch'});
3941 } else {
3942 $ref_item{'age'} = "unknown";
3946 push @tagslist, \%ref_item;
3948 close $fd;
3950 return wantarray ? @tagslist : \@tagslist;
3953 ## ----------------------------------------------------------------------
3954 ## filesystem-related functions
3956 sub get_file_owner {
3957 my $path = shift;
3959 my ($dev, $ino, $mode, $nlink, $st_uid, $st_gid, $rdev, $size) = stat($path);
3960 my ($name, $passwd, $uid, $gid, $quota, $comment, $gcos, $dir, $shell) = getpwuid($st_uid);
3961 if (!defined $gcos) {
3962 return undef;
3964 my $owner = $gcos;
3965 $owner =~ s/[,;].*$//;
3966 return to_utf8($owner);
3969 # assume that file exists
3970 sub insert_file {
3971 my $filename = shift;
3973 open my $fd, '<', $filename;
3974 print map { to_utf8($_) } <$fd>;
3975 close $fd;
3978 ## ......................................................................
3979 ## mimetype related functions
3981 sub mimetype_guess_file {
3982 my $filename = shift;
3983 my $mimemap = shift;
3984 -r $mimemap or return undef;
3986 my %mimemap;
3987 open(my $mh, '<', $mimemap) or return undef;
3988 while (<$mh>) {
3989 next if m/^#/; # skip comments
3990 my ($mimetype, @exts) = split(/\s+/);
3991 foreach my $ext (@exts) {
3992 $mimemap{$ext} = $mimetype;
3995 close($mh);
3997 $filename =~ /\.([^.]*)$/;
3998 return $mimemap{$1};
4001 sub mimetype_guess {
4002 my $filename = shift;
4003 my $mime;
4004 $filename =~ /\./ or return undef;
4006 if ($mimetypes_file) {
4007 my $file = $mimetypes_file;
4008 if ($file !~ m!^/!) { # if it is relative path
4009 # it is relative to project
4010 $file = "$projectroot/$project/$file";
4012 $mime = mimetype_guess_file($filename, $file);
4014 $mime ||= mimetype_guess_file($filename, '/etc/mime.types');
4015 return $mime;
4018 sub blob_mimetype {
4019 my $fd = shift;
4020 my $filename = shift;
4022 if ($filename) {
4023 my $mime = mimetype_guess($filename);
4024 $mime and return $mime;
4027 # just in case
4028 return $default_blob_plain_mimetype unless $fd;
4030 if (-T $fd) {
4031 return 'text/plain';
4032 } elsif (! $filename) {
4033 return 'application/octet-stream';
4034 } elsif ($filename =~ m/\.png$/i) {
4035 return 'image/png';
4036 } elsif ($filename =~ m/\.gif$/i) {
4037 return 'image/gif';
4038 } elsif ($filename =~ m/\.jpe?g$/i) {
4039 return 'image/jpeg';
4040 } else {
4041 return 'application/octet-stream';
4045 sub blob_contenttype {
4046 my ($fd, $file_name, $type) = @_;
4048 $type ||= blob_mimetype($fd, $file_name);
4049 if ($type eq 'text/plain' && defined $default_text_plain_charset) {
4050 $type .= "; charset=$default_text_plain_charset";
4053 return $type;
4056 # peek the first upto 128 bytes off a file handle
4057 sub peek128bytes {
4058 my $fd = shift;
4060 use IO::Handle;
4061 use bytes;
4063 my $prefix128;
4064 return '' unless $fd && read($fd, $prefix128, 128);
4066 # In the general case, we're guaranteed only to be able to ungetc one
4067 # character (provided, of course, we actually got a character first).
4069 # However, we know:
4071 # 1) we are dealing with a :perlio layer since blob_mimetype will have
4072 # already been called at least once on the file handle before us
4074 # 2) we have an $fd positioned at the start of the input stream and
4075 # therefore know we were positioned at a buffer boundary before
4076 # reading the initial upto 128 bytes
4078 # 3) the buffer size is at least 512 bytes
4080 # 4) we are careful to only unget raw bytes
4082 # 5) we are attempting to unget exactly the same number of bytes we got
4084 # Given the above conditions we will ALWAYS be able to safely unget
4085 # the $prefix128 value we just got.
4087 # In fact, we could read up to 511 bytes and still be sure.
4088 # (Reading 512 might pop us into the next internal buffer, but probably
4089 # not since that could break the always able to unget at least the one
4090 # you just got guarantee.)
4092 map {$fd->ungetc(ord($_))} reverse(split //, $prefix128);
4094 return $prefix128;
4097 # guess file syntax for syntax highlighting; return undef if no highlighting
4098 # the name of syntax can (in the future) depend on syntax highlighter used
4099 sub guess_file_syntax {
4100 my ($fd, $mimetype, $file_name) = @_;
4101 return undef unless $fd && defined $file_name &&
4102 defined $mimetype && $mimetype =~ m!^text/.+!i;
4103 my $basename = basename($file_name, '.in');
4104 return $highlight_basename{$basename}
4105 if exists $highlight_basename{$basename};
4107 # Peek to see if there's a shebang or xml line.
4108 # We always operate on bytes when testing this.
4110 use bytes;
4111 my $shebang = peek128bytes($fd);
4112 if (length($shebang) >= 4 && $shebang =~ /^#!/) { # 4 would be '#!/x'
4113 foreach my $key (keys %highlight_shebang) {
4114 my $ar = ref($highlight_shebang{$key}) ?
4115 $highlight_shebang{$key} :
4116 [$highlight_shebang{key}];
4117 map {return $key if $shebang =~ /$_/} @$ar;
4120 return 'xml' if $shebang =~ m!^\s*<\?xml\s!; # "xml" must be lowercase
4123 $basename =~ /\.([^.]*)$/;
4124 my $ext = $1 or return undef;
4125 return $highlight_ext{$ext}
4126 if exists $highlight_ext{$ext};
4128 return undef;
4131 # run highlighter and return FD of its output,
4132 # or return original FD if no highlighting
4133 sub run_highlighter {
4134 my ($fd, $syntax) = @_;
4135 return $fd unless $fd && !eof($fd) && defined $highlight_bin && defined $syntax;
4137 defined(open my $hifd, quote_command(git_cmd(), "cat-file", "blob", $hash)." | ".
4138 quote_command($highlight_bin).
4139 " --replace-tabs=8 --fragment --syntax $syntax |")
4140 or die_error(500, "Couldn't open file or run syntax highlighter");
4141 if (eof $hifd) {
4142 # just in case, should not happen as we tested !eof($fd) above
4143 return $fd if close($hifd);
4145 # should not happen
4146 !$! or die_error(500, "Couldn't close syntax highighter pipe");
4148 # leaving us with the only possibility a non-zero exit status (possibly a signal);
4149 # instead of dying horribly on this, just skip the highlighting
4150 # but do output a message about it to STDERR that will end up in the log
4151 print STDERR "warning: skipping failed highlight for --syntax $syntax: ".
4152 sprintf("child exit status 0x%x\n", $?);
4153 return $fd
4155 close $fd;
4156 return ($hifd, 1);
4159 ## ======================================================================
4160 ## functions printing HTML: header, footer, error page
4162 sub get_page_title {
4163 my $title = to_utf8($site_name);
4165 unless (defined $project) {
4166 if (defined $project_filter) {
4167 $title .= " - projects in '" . esc_path($project_filter) . "'";
4169 return $title;
4171 $title .= " - " . to_utf8($project);
4173 return $title unless (defined $action);
4174 $title .= "/$action"; # $action is US-ASCII (7bit ASCII)
4176 return $title unless (defined $file_name);
4177 $title .= " - " . esc_path($file_name);
4178 if ($action eq "tree" && $file_name !~ m|/$|) {
4179 $title .= "/";
4182 return $title;
4185 sub get_content_type_html {
4186 # require explicit support from the UA if we are to send the page as
4187 # 'application/xhtml+xml', otherwise send it as plain old 'text/html'.
4188 # we have to do this because MSIE sometimes globs '*/*', pretending to
4189 # support xhtml+xml but choking when it gets what it asked for.
4190 if (defined $cgi->http('HTTP_ACCEPT') &&
4191 $cgi->http('HTTP_ACCEPT') =~ m/(,|;|\s|^)application\/xhtml\+xml(,|;|\s|$)/ &&
4192 $cgi->Accept('application/xhtml+xml') != 0) {
4193 return 'application/xhtml+xml';
4194 } else {
4195 return 'text/html';
4199 sub print_feed_meta {
4200 if (defined $project) {
4201 my %href_params = get_feed_info();
4202 if (!exists $href_params{'-title'}) {
4203 $href_params{'-title'} = 'log';
4206 foreach my $format (qw(RSS Atom)) {
4207 my $type = lc($format);
4208 my %link_attr = (
4209 '-rel' => 'alternate',
4210 '-title' => esc_attr("$project - $href_params{'-title'} - $format feed"),
4211 '-type' => "application/$type+xml"
4214 $href_params{'extra_options'} = undef;
4215 $href_params{'action'} = $type;
4216 $link_attr{'-href'} = href(%href_params);
4217 print "<link ".
4218 "rel=\"$link_attr{'-rel'}\" ".
4219 "title=\"$link_attr{'-title'}\" ".
4220 "href=\"$link_attr{'-href'}\" ".
4221 "type=\"$link_attr{'-type'}\" ".
4222 "/>\n";
4224 $href_params{'extra_options'} = '--no-merges';
4225 $link_attr{'-href'} = href(%href_params);
4226 $link_attr{'-title'} .= ' (no merges)';
4227 print "<link ".
4228 "rel=\"$link_attr{'-rel'}\" ".
4229 "title=\"$link_attr{'-title'}\" ".
4230 "href=\"$link_attr{'-href'}\" ".
4231 "type=\"$link_attr{'-type'}\" ".
4232 "/>\n";
4235 } else {
4236 printf('<link rel="alternate" title="%s projects list" '.
4237 'href="%s" type="text/plain; charset=utf-8" />'."\n",
4238 esc_attr($site_name), href(project=>undef, action=>"project_index"));
4239 printf('<link rel="alternate" title="%s projects feeds" '.
4240 'href="%s" type="text/x-opml" />'."\n",
4241 esc_attr($site_name), href(project=>undef, action=>"opml"));
4245 sub print_header_links {
4246 my $status = shift;
4248 # print out each stylesheet that exist, providing backwards capability
4249 # for those people who defined $stylesheet in a config file
4250 if (defined $stylesheet) {
4251 print '<link rel="stylesheet" type="text/css" href="'.esc_url($stylesheet).'"/>'."\n";
4252 } else {
4253 foreach my $stylesheet (@stylesheets) {
4254 next unless $stylesheet;
4255 print '<link rel="stylesheet" type="text/css" href="'.esc_url($stylesheet).'"/>'."\n";
4258 print_feed_meta()
4259 if ($status eq '200 OK');
4260 if (defined $favicon) {
4261 print qq(<link rel="shortcut icon" href=").esc_url($favicon).qq(" type="image/png" />\n);
4265 sub print_nav_breadcrumbs_path {
4266 my $dirprefix = undef;
4267 while (my $part = shift) {
4268 $dirprefix .= "/" if defined $dirprefix;
4269 $dirprefix .= $part;
4270 print $cgi->a({-href => href(project => undef,
4271 project_filter => $dirprefix,
4272 action => "project_list")},
4273 esc_html($part)) . " / ";
4277 sub print_nav_breadcrumbs {
4278 my %opts = @_;
4280 for my $crumb (@extra_breadcrumbs, [ $home_link_str => $home_link ]) {
4281 print $cgi->a({-href => esc_url($crumb->[1])}, $crumb->[0]) . " / ";
4283 if (defined $project) {
4284 my @dirname = split '/', $project;
4285 my $projectbasename = pop @dirname;
4286 print_nav_breadcrumbs_path(@dirname);
4287 print $cgi->a({-href => href(action=>"summary")}, esc_html($projectbasename));
4288 if (defined $action) {
4289 my $action_print = $action ;
4290 if (defined $opts{-action_extra}) {
4291 $action_print = $cgi->a({-href => href(action=>$action)},
4292 $action);
4294 print " / $action_print";
4296 if (defined $opts{-action_extra}) {
4297 print " / $opts{-action_extra}";
4299 print "\n";
4300 } elsif (defined $project_filter) {
4301 print_nav_breadcrumbs_path(split '/', $project_filter);
4305 sub print_search_form {
4306 if (!defined $searchtext) {
4307 $searchtext = "";
4309 my $search_hash;
4310 if (defined $hash_base) {
4311 $search_hash = $hash_base;
4312 } elsif (defined $hash) {
4313 $search_hash = $hash;
4314 } else {
4315 $search_hash = "HEAD";
4317 my $action = $my_uri;
4318 my $use_pathinfo = gitweb_check_feature('pathinfo');
4319 if ($use_pathinfo) {
4320 $action .= "/".esc_url($project);
4322 print $cgi->start_form(-method => "get", -action => $action) .
4323 "<div class=\"search\">\n" .
4324 (!$use_pathinfo &&
4325 $cgi->input({-name=>"p", -value=>$project, -type=>"hidden"}) . "\n") .
4326 $cgi->input({-name=>"a", -value=>"search", -type=>"hidden"}) . "\n" .
4327 $cgi->input({-name=>"h", -value=>$search_hash, -type=>"hidden"}) . "\n" .
4328 $cgi->popup_menu(-name => 'st', -default => 'commit',
4329 -values => ['commit', 'grep', 'author', 'committer', 'pickaxe']) .
4330 " " . $cgi->a({-href => href(action=>"search_help"),
4331 -title => "search help" }, "?") . " search:\n",
4332 $cgi->textfield(-name => "s", -value => $searchtext, -override => 1) . "\n" .
4333 "<span title=\"Extended regular expression\">" .
4334 $cgi->checkbox(-name => 'sr', -value => 1, -label => 're',
4335 -checked => $search_use_regexp) .
4336 "</span>" .
4337 "</div>" .
4338 $cgi->end_form() . "\n";
4341 sub git_header_html {
4342 my $status = shift || "200 OK";
4343 my $expires = shift;
4344 my %opts = @_;
4346 my $title = get_page_title();
4347 my $content_type = get_content_type_html();
4348 print $cgi->header(-type=>$content_type, -charset => 'utf-8',
4349 -status=> $status, -expires => $expires)
4350 unless ($opts{'-no_http_header'});
4351 my $mod_perl_version = $ENV{'MOD_PERL'} ? " $ENV{'MOD_PERL'}" : '';
4352 print <<EOF;
4353 <?xml version="1.0" encoding="utf-8"?>
4354 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
4355 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US" lang="en-US">
4356 <!-- git web interface version $version, (C) 2005-2006, Kay Sievers <kay.sievers\@vrfy.org>, Christian Gierke -->
4357 <!-- git core binaries version $git_version -->
4358 <head>
4359 <meta http-equiv="content-type" content="$content_type; charset=utf-8"/>
4360 <meta name="generator" content="gitweb/$version git/$git_version$mod_perl_version"/>
4361 <meta name="robots" content="index, nofollow"/>
4362 <title>$title</title>
4364 # the stylesheet, favicon etc urls won't work correctly with path_info
4365 # unless we set the appropriate base URL
4366 if ($ENV{'PATH_INFO'}) {
4367 print "<base href=\"".esc_url($base_url)."\" />\n";
4369 print_header_links($status);
4371 if (defined $site_html_head_string) {
4372 print to_utf8($site_html_head_string);
4375 print "</head>\n" .
4376 "<body>\n";
4378 if (defined $site_header && -f $site_header) {
4379 insert_file($site_header);
4382 print "<div class=\"page_header\">\n";
4383 if (defined $logo) {
4384 print $cgi->a({-href => esc_url($logo_url),
4385 -title => $logo_label},
4386 $cgi->img({-src => esc_url($logo),
4387 -width => 72, -height => 27,
4388 -alt => "git",
4389 -class => "logo"}));
4391 print_nav_breadcrumbs(%opts);
4392 print "</div>\n";
4394 my $have_search = gitweb_check_feature('search');
4395 if (defined $project && $have_search) {
4396 print_search_form();
4400 sub git_footer_html {
4401 my $feed_class = 'rss_logo';
4403 print "<div class=\"page_footer\">\n";
4404 if (defined $project) {
4405 my $descr = git_get_project_description($project);
4406 if (defined $descr) {
4407 print "<div class=\"page_footer_text\">" . esc_html($descr) . "</div>\n";
4410 my %href_params = get_feed_info();
4411 if (!%href_params) {
4412 $feed_class .= ' generic';
4414 $href_params{'-title'} ||= 'log';
4416 foreach my $format (qw(RSS Atom)) {
4417 $href_params{'action'} = lc($format);
4418 print $cgi->a({-href => href(%href_params),
4419 -title => "$href_params{'-title'} $format feed",
4420 -class => $feed_class}, $format)."\n";
4423 } else {
4424 print $cgi->a({-href => href(project=>undef, action=>"opml",
4425 project_filter => $project_filter),
4426 -class => $feed_class}, "OPML") . " ";
4427 print $cgi->a({-href => href(project=>undef, action=>"project_index",
4428 project_filter => $project_filter),
4429 -class => $feed_class}, "TXT") . "\n";
4431 print "</div>\n"; # class="page_footer"
4433 if (defined $t0 && gitweb_check_feature('timed')) {
4434 print "<div id=\"generating_info\">\n";
4435 print 'This page took '.
4436 '<span id="generating_time" class="time_span">'.
4437 tv_interval($t0, [ gettimeofday() ]).
4438 ' seconds </span>'.
4439 ' and '.
4440 '<span id="generating_cmd">'.
4441 $number_of_git_cmds.
4442 '</span> git commands '.
4443 " to generate.\n";
4444 print "</div>\n"; # class="page_footer"
4447 if (defined $site_footer && -f $site_footer) {
4448 insert_file($site_footer);
4451 print qq!<script type="text/javascript" src="!.esc_url($javascript).qq!"></script>\n!;
4452 if (defined $action &&
4453 $action eq 'blame_incremental') {
4454 print qq!<script type="text/javascript">\n!.
4455 qq!startBlame("!. href(action=>"blame_data", -replay=>1) .qq!",\n!.
4456 qq! "!. href() .qq!");\n!.
4457 qq!</script>\n!;
4458 } else {
4459 my ($jstimezone, $tz_cookie, $datetime_class) =
4460 gitweb_get_feature('javascript-timezone');
4462 print qq!<script type="text/javascript">\n!.
4463 qq!window.onload = function () {\n!;
4464 if (gitweb_check_feature('javascript-actions')) {
4465 print qq! fixLinks();\n!;
4467 if ($jstimezone && $tz_cookie && $datetime_class) {
4468 print qq! var tz_cookie = { name: '$tz_cookie', expires: 14, path: '/' };\n!. # in days
4469 qq! onloadTZSetup('$jstimezone', tz_cookie, '$datetime_class');\n!;
4471 print qq!};\n!.
4472 qq!</script>\n!;
4475 print "</body>\n" .
4476 "</html>";
4479 # die_error(<http_status_code>, <error_message>[, <detailed_html_description>])
4480 # Example: die_error(404, 'Hash not found')
4481 # By convention, use the following status codes (as defined in RFC 2616):
4482 # 400: Invalid or missing CGI parameters, or
4483 # requested object exists but has wrong type.
4484 # 403: Requested feature (like "pickaxe" or "snapshot") not enabled on
4485 # this server or project.
4486 # 404: Requested object/revision/project doesn't exist.
4487 # 500: The server isn't configured properly, or
4488 # an internal error occurred (e.g. failed assertions caused by bugs), or
4489 # an unknown error occurred (e.g. the git binary died unexpectedly).
4490 # 503: The server is currently unavailable (because it is overloaded,
4491 # or down for maintenance). Generally, this is a temporary state.
4492 sub die_error {
4493 my $status = shift || 500;
4494 my $error = esc_html(shift) || "Internal Server Error";
4495 my $extra = shift;
4496 my %opts = @_;
4498 my %http_responses = (
4499 400 => '400 Bad Request',
4500 403 => '403 Forbidden',
4501 404 => '404 Not Found',
4502 500 => '500 Internal Server Error',
4503 503 => '503 Service Unavailable',
4505 git_header_html($http_responses{$status}, undef, %opts);
4506 print <<EOF;
4507 <div class="page_body">
4508 <br /><br />
4509 $status - $error
4510 <br />
4512 if (defined $extra) {
4513 print "<hr />\n" .
4514 "$extra\n";
4516 print "</div>\n";
4518 git_footer_html();
4519 goto DONE_GITWEB
4520 unless ($opts{'-error_handler'});
4523 ## ----------------------------------------------------------------------
4524 ## functions printing or outputting HTML: navigation
4526 sub git_print_page_nav {
4527 my ($current, $suppress, $head, $treehead, $treebase, $extra) = @_;
4528 $extra = '' if !defined $extra; # pager or formats
4530 my @navs = qw(summary shortlog log commit commitdiff tree);
4531 if ($suppress) {
4532 @navs = grep { $_ ne $suppress } @navs;
4535 my %arg = map { $_ => {action=>$_} } @navs;
4536 if (defined $head) {
4537 for (qw(commit commitdiff)) {
4538 $arg{$_}{'hash'} = $head;
4540 if ($current =~ m/^(tree | log | shortlog | commit | commitdiff | search)$/x) {
4541 for (qw(shortlog log)) {
4542 $arg{$_}{'hash'} = $head;
4547 $arg{'tree'}{'hash'} = $treehead if defined $treehead;
4548 $arg{'tree'}{'hash_base'} = $treebase if defined $treebase;
4550 my @actions = gitweb_get_feature('actions');
4551 my %repl = (
4552 '%' => '%',
4553 'n' => $project, # project name
4554 'f' => $git_dir, # project path within filesystem
4555 'h' => $treehead || '', # current hash ('h' parameter)
4556 'b' => $treebase || '', # hash base ('hb' parameter)
4558 while (@actions) {
4559 my ($label, $link, $pos) = splice(@actions,0,3);
4560 # insert
4561 @navs = map { $_ eq $pos ? ($_, $label) : $_ } @navs;
4562 # munch munch
4563 $link =~ s/%([%nfhb])/$repl{$1}/g;
4564 $arg{$label}{'_href'} = $link;
4567 print "<div class=\"page_nav\">\n" .
4568 (join " | ",
4569 map { $_ eq $current ?
4570 $_ : $cgi->a({-href => ($arg{$_}{_href} ? $arg{$_}{_href} : href(%{$arg{$_}}))}, "$_")
4571 } @navs);
4572 print "<br/>\n$extra<br/>\n" .
4573 "</div>\n";
4576 # returns a submenu for the nagivation of the refs views (tags, heads,
4577 # remotes) with the current view disabled and the remotes view only
4578 # available if the feature is enabled
4579 sub format_ref_views {
4580 my ($current) = @_;
4581 my @ref_views = qw{tags heads};
4582 push @ref_views, 'remotes' if gitweb_check_feature('remote_heads');
4583 return join " | ", map {
4584 $_ eq $current ? $_ :
4585 $cgi->a({-href => href(action=>$_)}, $_)
4586 } @ref_views
4589 sub format_paging_nav {
4590 my ($action, $page, $has_next_link) = @_;
4591 my $paging_nav;
4594 if ($page > 0) {
4595 $paging_nav .=
4596 $cgi->a({-href => href(-replay=>1, page=>undef)}, "first") .
4597 " &sdot; " .
4598 $cgi->a({-href => href(-replay=>1, page=>$page-1),
4599 -accesskey => "p", -title => "Alt-p"}, "prev");
4600 } else {
4601 $paging_nav .= "first &sdot; prev";
4604 if ($has_next_link) {
4605 $paging_nav .= " &sdot; " .
4606 $cgi->a({-href => href(-replay=>1, page=>$page+1),
4607 -accesskey => "n", -title => "Alt-n"}, "next");
4608 } else {
4609 $paging_nav .= " &sdot; next";
4612 return $paging_nav;
4615 ## ......................................................................
4616 ## functions printing or outputting HTML: div
4618 sub git_print_header_div {
4619 my ($action, $title, $hash, $hash_base) = @_;
4620 my %args = ();
4622 $args{'action'} = $action;
4623 $args{'hash'} = $hash if $hash;
4624 $args{'hash_base'} = $hash_base if $hash_base;
4626 print "<div class=\"header\">\n" .
4627 $cgi->a({-href => href(%args), -class => "title"},
4628 $title ? $title : $action) .
4629 "\n</div>\n";
4632 sub format_repo_url {
4633 my ($name, $url) = @_;
4634 return "<tr class=\"metadata_url\"><td>$name</td><td>$url</td></tr>\n";
4637 # Group output by placing it in a DIV element and adding a header.
4638 # Options for start_div() can be provided by passing a hash reference as the
4639 # first parameter to the function.
4640 # Options to git_print_header_div() can be provided by passing an array
4641 # reference. This must follow the options to start_div if they are present.
4642 # The content can be a scalar, which is output as-is, a scalar reference, which
4643 # is output after html escaping, an IO handle passed either as *handle or
4644 # *handle{IO}, or a function reference. In the latter case all following
4645 # parameters will be taken as argument to the content function call.
4646 sub git_print_section {
4647 my ($div_args, $header_args, $content);
4648 my $arg = shift;
4649 if (ref($arg) eq 'HASH') {
4650 $div_args = $arg;
4651 $arg = shift;
4653 if (ref($arg) eq 'ARRAY') {
4654 $header_args = $arg;
4655 $arg = shift;
4657 $content = $arg;
4659 print $cgi->start_div($div_args);
4660 git_print_header_div(@$header_args);
4662 if (ref($content) eq 'CODE') {
4663 $content->(@_);
4664 } elsif (ref($content) eq 'SCALAR') {
4665 print esc_html($$content);
4666 } elsif (ref($content) eq 'GLOB' or ref($content) eq 'IO::Handle') {
4667 print <$content>;
4668 } elsif (!ref($content) && defined($content)) {
4669 print $content;
4672 print $cgi->end_div;
4675 sub format_timestamp_html {
4676 my $date = shift;
4677 my $strtime = $date->{'rfc2822'};
4679 my (undef, undef, $datetime_class) =
4680 gitweb_get_feature('javascript-timezone');
4681 if ($datetime_class) {
4682 $strtime = qq!<span class="$datetime_class">$strtime</span>!;
4685 my $localtime_format = '(%02d:%02d %s)';
4686 if ($date->{'hour_local'} < 6) {
4687 $localtime_format = '(<span class="atnight">%02d:%02d</span> %s)';
4689 $strtime .= ' ' .
4690 sprintf($localtime_format,
4691 $date->{'hour_local'}, $date->{'minute_local'}, $date->{'tz_local'});
4693 return $strtime;
4696 # Outputs the author name and date in long form
4697 sub git_print_authorship {
4698 my $co = shift;
4699 my %opts = @_;
4700 my $tag = $opts{-tag} || 'div';
4701 my $author = $co->{'author_name'};
4703 my %ad = parse_date($co->{'author_epoch'}, $co->{'author_tz'});
4704 print "<$tag class=\"author_date\">" .
4705 format_search_author($author, "author", esc_html($author)) .
4706 " [".format_timestamp_html(\%ad)."]".
4707 git_get_avatar($co->{'author_email'}, -pad_before => 1) .
4708 "</$tag>\n";
4711 # Outputs table rows containing the full author or committer information,
4712 # in the format expected for 'commit' view (& similar).
4713 # Parameters are a commit hash reference, followed by the list of people
4714 # to output information for. If the list is empty it defaults to both
4715 # author and committer.
4716 sub git_print_authorship_rows {
4717 my $co = shift;
4718 # too bad we can't use @people = @_ || ('author', 'committer')
4719 my @people = @_;
4720 @people = ('author', 'committer') unless @people;
4721 foreach my $who (@people) {
4722 my %wd = parse_date($co->{"${who}_epoch"}, $co->{"${who}_tz"});
4723 print "<tr><td>$who</td><td>" .
4724 format_search_author($co->{"${who}_name"}, $who,
4725 esc_html($co->{"${who}_name"})) . " " .
4726 format_search_author($co->{"${who}_email"}, $who,
4727 esc_html("<" . $co->{"${who}_email"} . ">")) .
4728 "</td><td rowspan=\"2\">" .
4729 git_get_avatar($co->{"${who}_email"}, -size => 'double') .
4730 "</td></tr>\n" .
4731 "<tr>" .
4732 "<td></td><td>" .
4733 format_timestamp_html(\%wd) .
4734 "</td>" .
4735 "</tr>\n";
4739 sub git_print_page_path {
4740 my $name = shift;
4741 my $type = shift;
4742 my $hb = shift;
4745 print "<div class=\"page_path\">";
4746 print $cgi->a({-href => href(action=>"tree", hash_base=>$hb),
4747 -title => 'tree root'}, to_utf8("[$project]"));
4748 print " / ";
4749 if (defined $name) {
4750 my @dirname = split '/', $name;
4751 my $basename = pop @dirname;
4752 my $fullname = '';
4754 foreach my $dir (@dirname) {
4755 $fullname .= ($fullname ? '/' : '') . $dir;
4756 print $cgi->a({-href => href(action=>"tree", file_name=>$fullname,
4757 hash_base=>$hb),
4758 -title => $fullname}, esc_path($dir));
4759 print " / ";
4761 if (defined $type && $type eq 'blob') {
4762 print $cgi->a({-href => href(action=>"blob_plain", file_name=>$file_name,
4763 hash_base=>$hb),
4764 -title => $name}, esc_path($basename));
4765 } elsif (defined $type && $type eq 'tree') {
4766 print $cgi->a({-href => href(action=>"tree", file_name=>$file_name,
4767 hash_base=>$hb),
4768 -title => $name}, esc_path($basename));
4769 print " / ";
4770 } else {
4771 print esc_path($basename);
4774 print "<br/></div>\n";
4777 sub git_print_log {
4778 my $log = shift;
4779 my %opts = @_;
4781 if ($opts{'-remove_title'}) {
4782 # remove title, i.e. first line of log
4783 shift @$log;
4785 # remove leading empty lines
4786 while (defined $log->[0] && $log->[0] eq "") {
4787 shift @$log;
4790 # print log
4791 my $skip_blank_line = 0;
4792 foreach my $line (@$log) {
4793 if ($line =~ m/^\s*([A-Z][-A-Za-z]*-[Bb]y|C[Cc]): /) {
4794 if (! $opts{'-remove_signoff'}) {
4795 print "<span class=\"signoff\">" . esc_html($line) . "</span><br/>\n";
4796 $skip_blank_line = 1;
4798 next;
4801 if ($line =~ m,\s*([a-z]*link): (https?://\S+),i) {
4802 if (! $opts{'-remove_signoff'}) {
4803 print "<span class=\"signoff\">" . esc_html($1) . ": " .
4804 "<a href=\"" . esc_html($2) . "\">" . esc_html($2) . "</a>" .
4805 "</span><br/>\n";
4806 $skip_blank_line = 1;
4808 next;
4811 # print only one empty line
4812 # do not print empty line after signoff
4813 if ($line eq "") {
4814 next if ($skip_blank_line);
4815 $skip_blank_line = 1;
4816 } else {
4817 $skip_blank_line = 0;
4820 print format_log_line_html($line) . "<br/>\n";
4823 if ($opts{'-final_empty_line'}) {
4824 # end with single empty line
4825 print "<br/>\n" unless $skip_blank_line;
4829 # return link target (what link points to)
4830 sub git_get_link_target {
4831 my $hash = shift;
4832 my $link_target;
4834 # read link
4835 defined(my $fd = git_cmd_pipe "cat-file", "blob", $hash)
4836 or return;
4838 local $/ = undef;
4839 $link_target = <$fd>;
4841 close $fd
4842 or return;
4844 return $link_target;
4847 # given link target, and the directory (basedir) the link is in,
4848 # return target of link relative to top directory (top tree);
4849 # return undef if it is not possible (including absolute links).
4850 sub normalize_link_target {
4851 my ($link_target, $basedir) = @_;
4853 # absolute symlinks (beginning with '/') cannot be normalized
4854 return if (substr($link_target, 0, 1) eq '/');
4856 # normalize link target to path from top (root) tree (dir)
4857 my $path;
4858 if ($basedir) {
4859 $path = $basedir . '/' . $link_target;
4860 } else {
4861 # we are in top (root) tree (dir)
4862 $path = $link_target;
4865 # remove //, /./, and /../
4866 my @path_parts;
4867 foreach my $part (split('/', $path)) {
4868 # discard '.' and ''
4869 next if (!$part || $part eq '.');
4870 # handle '..'
4871 if ($part eq '..') {
4872 if (@path_parts) {
4873 pop @path_parts;
4874 } else {
4875 # link leads outside repository (outside top dir)
4876 return;
4878 } else {
4879 push @path_parts, $part;
4882 $path = join('/', @path_parts);
4884 return $path;
4887 # print tree entry (row of git_tree), but without encompassing <tr> element
4888 sub git_print_tree_entry {
4889 my ($t, $basedir, $hash_base, $have_blame) = @_;
4891 my %base_key = ();
4892 $base_key{'hash_base'} = $hash_base if defined $hash_base;
4894 # The format of a table row is: mode list link. Where mode is
4895 # the mode of the entry, list is the name of the entry, an href,
4896 # and link is the action links of the entry.
4898 print "<td class=\"mode\">" . mode_str($t->{'mode'}) . "</td>\n";
4899 if (exists $t->{'size'}) {
4900 print "<td class=\"size\">$t->{'size'}</td>\n";
4902 if ($t->{'type'} eq "blob") {
4903 print "<td class=\"list\">" .
4904 $cgi->a({-href => href(action=>"blob", hash=>$t->{'hash'},
4905 file_name=>"$basedir$t->{'name'}", %base_key),
4906 -class => "list"}, esc_path($t->{'name'}));
4907 if (S_ISLNK(oct $t->{'mode'})) {
4908 my $link_target = git_get_link_target($t->{'hash'});
4909 if ($link_target) {
4910 my $norm_target = normalize_link_target($link_target, $basedir);
4911 if (defined $norm_target) {
4912 print " -> " .
4913 $cgi->a({-href => href(action=>"object", hash_base=>$hash_base,
4914 file_name=>$norm_target),
4915 -title => $norm_target}, esc_path($link_target));
4916 } else {
4917 print " -> " . esc_path($link_target);
4921 print "</td>\n";
4922 print "<td class=\"link\">";
4923 print $cgi->a({-href => href(action=>"blob", hash=>$t->{'hash'},
4924 file_name=>"$basedir$t->{'name'}", %base_key)},
4925 "blob");
4926 if ($have_blame) {
4927 print " | " .
4928 $cgi->a({-href => href(action=>"blame", hash=>$t->{'hash'},
4929 file_name=>"$basedir$t->{'name'}", %base_key)},
4930 "blame");
4932 if (defined $hash_base) {
4933 print " | " .
4934 $cgi->a({-href => href(action=>"history", hash_base=>$hash_base,
4935 hash=>$t->{'hash'}, file_name=>"$basedir$t->{'name'}")},
4936 "history");
4938 print " | " .
4939 $cgi->a({-href => href(action=>"blob_plain", hash_base=>$hash_base,
4940 file_name=>"$basedir$t->{'name'}")},
4941 "raw");
4942 print "</td>\n";
4944 } elsif ($t->{'type'} eq "tree") {
4945 print "<td class=\"list\">";
4946 print $cgi->a({-href => href(action=>"tree", hash=>$t->{'hash'},
4947 file_name=>"$basedir$t->{'name'}",
4948 %base_key)},
4949 esc_path($t->{'name'}));
4950 print "</td>\n";
4951 print "<td class=\"link\">";
4952 print $cgi->a({-href => href(action=>"tree", hash=>$t->{'hash'},
4953 file_name=>"$basedir$t->{'name'}",
4954 %base_key)},
4955 "tree");
4956 if (defined $hash_base) {
4957 print " | " .
4958 $cgi->a({-href => href(action=>"history", hash_base=>$hash_base,
4959 file_name=>"$basedir$t->{'name'}")},
4960 "history");
4962 print "</td>\n";
4963 } else {
4964 # unknown object: we can only present history for it
4965 # (this includes 'commit' object, i.e. submodule support)
4966 print "<td class=\"list\">" .
4967 esc_path($t->{'name'}) .
4968 "</td>\n";
4969 print "<td class=\"link\">";
4970 if (defined $hash_base) {
4971 print $cgi->a({-href => href(action=>"history",
4972 hash_base=>$hash_base,
4973 file_name=>"$basedir$t->{'name'}")},
4974 "history");
4976 print "</td>\n";
4980 ## ......................................................................
4981 ## functions printing large fragments of HTML
4983 # get pre-image filenames for merge (combined) diff
4984 sub fill_from_file_info {
4985 my ($diff, @parents) = @_;
4987 $diff->{'from_file'} = [ ];
4988 $diff->{'from_file'}[$diff->{'nparents'} - 1] = undef;
4989 for (my $i = 0; $i < $diff->{'nparents'}; $i++) {
4990 if ($diff->{'status'}[$i] eq 'R' ||
4991 $diff->{'status'}[$i] eq 'C') {
4992 $diff->{'from_file'}[$i] =
4993 git_get_path_by_hash($parents[$i], $diff->{'from_id'}[$i]);
4997 return $diff;
5000 # is current raw difftree line of file deletion
5001 sub is_deleted {
5002 my $diffinfo = shift;
5004 return $diffinfo->{'to_id'} eq ('0' x 40);
5007 # does patch correspond to [previous] difftree raw line
5008 # $diffinfo - hashref of parsed raw diff format
5009 # $patchinfo - hashref of parsed patch diff format
5010 # (the same keys as in $diffinfo)
5011 sub is_patch_split {
5012 my ($diffinfo, $patchinfo) = @_;
5014 return defined $diffinfo && defined $patchinfo
5015 && $diffinfo->{'to_file'} eq $patchinfo->{'to_file'};
5019 sub git_difftree_body {
5020 my ($difftree, $hash, @parents) = @_;
5021 my ($parent) = $parents[0];
5022 my $have_blame = gitweb_check_feature('blame');
5023 print "<div class=\"list_head\">\n";
5024 if ($#{$difftree} > 10) {
5025 print(($#{$difftree} + 1) . " files changed:\n");
5027 print "</div>\n";
5029 print "<table class=\"" .
5030 (@parents > 1 ? "combined " : "") .
5031 "diff_tree\">\n";
5033 # header only for combined diff in 'commitdiff' view
5034 my $has_header = @$difftree && @parents > 1 && $action eq 'commitdiff';
5035 if ($has_header) {
5036 # table header
5037 print "<thead><tr>\n" .
5038 "<th></th><th></th>\n"; # filename, patchN link
5039 for (my $i = 0; $i < @parents; $i++) {
5040 my $par = $parents[$i];
5041 print "<th>" .
5042 $cgi->a({-href => href(action=>"commitdiff",
5043 hash=>$hash, hash_parent=>$par),
5044 -title => 'commitdiff to parent number ' .
5045 ($i+1) . ': ' . substr($par,0,7)},
5046 $i+1) .
5047 "&nbsp;</th>\n";
5049 print "</tr></thead>\n<tbody>\n";
5052 my $alternate = 1;
5053 my $patchno = 0;
5054 foreach my $line (@{$difftree}) {
5055 my $diff = parsed_difftree_line($line);
5057 if ($alternate) {
5058 print "<tr class=\"dark\">\n";
5059 } else {
5060 print "<tr class=\"light\">\n";
5062 $alternate ^= 1;
5064 if (exists $diff->{'nparents'}) { # combined diff
5066 fill_from_file_info($diff, @parents)
5067 unless exists $diff->{'from_file'};
5069 if (!is_deleted($diff)) {
5070 # file exists in the result (child) commit
5071 print "<td>" .
5072 $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
5073 file_name=>$diff->{'to_file'},
5074 hash_base=>$hash),
5075 -class => "list"}, esc_path($diff->{'to_file'})) .
5076 "</td>\n";
5077 } else {
5078 print "<td>" .
5079 esc_path($diff->{'to_file'}) .
5080 "</td>\n";
5083 if ($action eq 'commitdiff') {
5084 # link to patch
5085 $patchno++;
5086 print "<td class=\"link\">" .
5087 $cgi->a({-href => href(-anchor=>"patch$patchno")},
5088 "patch") .
5089 " | " .
5090 "</td>\n";
5093 my $has_history = 0;
5094 my $not_deleted = 0;
5095 for (my $i = 0; $i < $diff->{'nparents'}; $i++) {
5096 my $hash_parent = $parents[$i];
5097 my $from_hash = $diff->{'from_id'}[$i];
5098 my $from_path = $diff->{'from_file'}[$i];
5099 my $status = $diff->{'status'}[$i];
5101 $has_history ||= ($status ne 'A');
5102 $not_deleted ||= ($status ne 'D');
5104 if ($status eq 'A') {
5105 print "<td class=\"link\" align=\"right\"> | </td>\n";
5106 } elsif ($status eq 'D') {
5107 print "<td class=\"link\">" .
5108 $cgi->a({-href => href(action=>"blob",
5109 hash_base=>$hash,
5110 hash=>$from_hash,
5111 file_name=>$from_path)},
5112 "blob" . ($i+1)) .
5113 " | </td>\n";
5114 } else {
5115 if ($diff->{'to_id'} eq $from_hash) {
5116 print "<td class=\"link nochange\">";
5117 } else {
5118 print "<td class=\"link\">";
5120 print $cgi->a({-href => href(action=>"blobdiff",
5121 hash=>$diff->{'to_id'},
5122 hash_parent=>$from_hash,
5123 hash_base=>$hash,
5124 hash_parent_base=>$hash_parent,
5125 file_name=>$diff->{'to_file'},
5126 file_parent=>$from_path)},
5127 "diff" . ($i+1)) .
5128 " | </td>\n";
5132 print "<td class=\"link\">";
5133 if ($not_deleted) {
5134 print $cgi->a({-href => href(action=>"blob",
5135 hash=>$diff->{'to_id'},
5136 file_name=>$diff->{'to_file'},
5137 hash_base=>$hash)},
5138 "blob");
5139 print " | " if ($has_history);
5141 if ($has_history) {
5142 print $cgi->a({-href => href(action=>"history",
5143 file_name=>$diff->{'to_file'},
5144 hash_base=>$hash)},
5145 "history");
5147 print "</td>\n";
5149 print "</tr>\n";
5150 next; # instead of 'else' clause, to avoid extra indent
5152 # else ordinary diff
5154 my ($to_mode_oct, $to_mode_str, $to_file_type);
5155 my ($from_mode_oct, $from_mode_str, $from_file_type);
5156 if ($diff->{'to_mode'} ne ('0' x 6)) {
5157 $to_mode_oct = oct $diff->{'to_mode'};
5158 if (S_ISREG($to_mode_oct)) { # only for regular file
5159 $to_mode_str = sprintf("%04o", $to_mode_oct & 0777); # permission bits
5161 $to_file_type = file_type($diff->{'to_mode'});
5163 if ($diff->{'from_mode'} ne ('0' x 6)) {
5164 $from_mode_oct = oct $diff->{'from_mode'};
5165 if (S_ISREG($from_mode_oct)) { # only for regular file
5166 $from_mode_str = sprintf("%04o", $from_mode_oct & 0777); # permission bits
5168 $from_file_type = file_type($diff->{'from_mode'});
5171 if ($diff->{'status'} eq "A") { # created
5172 my $mode_chng = "<span class=\"file_status new\">[new $to_file_type";
5173 $mode_chng .= " with mode: $to_mode_str" if $to_mode_str;
5174 $mode_chng .= "]</span>";
5175 print "<td>";
5176 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
5177 hash_base=>$hash, file_name=>$diff->{'file'}),
5178 -class => "list"}, esc_path($diff->{'file'}));
5179 print "</td>\n";
5180 print "<td>$mode_chng</td>\n";
5181 print "<td class=\"link\">";
5182 if ($action eq 'commitdiff') {
5183 # link to patch
5184 $patchno++;
5185 print $cgi->a({-href => href(-anchor=>"patch$patchno")},
5186 "patch") .
5187 " | ";
5189 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
5190 hash_base=>$hash, file_name=>$diff->{'file'})},
5191 "blob");
5192 print "</td>\n";
5194 } elsif ($diff->{'status'} eq "D") { # deleted
5195 my $mode_chng = "<span class=\"file_status deleted\">[deleted $from_file_type]</span>";
5196 print "<td>";
5197 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'from_id'},
5198 hash_base=>$parent, file_name=>$diff->{'file'}),
5199 -class => "list"}, esc_path($diff->{'file'}));
5200 print "</td>\n";
5201 print "<td>$mode_chng</td>\n";
5202 print "<td class=\"link\">";
5203 if ($action eq 'commitdiff') {
5204 # link to patch
5205 $patchno++;
5206 print $cgi->a({-href => href(-anchor=>"patch$patchno")},
5207 "patch") .
5208 " | ";
5210 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'from_id'},
5211 hash_base=>$parent, file_name=>$diff->{'file'})},
5212 "blob") . " | ";
5213 if ($have_blame) {
5214 print $cgi->a({-href => href(action=>"blame", hash_base=>$parent,
5215 file_name=>$diff->{'file'})},
5216 "blame") . " | ";
5218 print $cgi->a({-href => href(action=>"history", hash_base=>$parent,
5219 file_name=>$diff->{'file'})},
5220 "history");
5221 print "</td>\n";
5223 } elsif ($diff->{'status'} eq "M" || $diff->{'status'} eq "T") { # modified, or type changed
5224 my $mode_chnge = "";
5225 if ($diff->{'from_mode'} != $diff->{'to_mode'}) {
5226 $mode_chnge = "<span class=\"file_status mode_chnge\">[changed";
5227 if ($from_file_type ne $to_file_type) {
5228 $mode_chnge .= " from $from_file_type to $to_file_type";
5230 if (($from_mode_oct & 0777) != ($to_mode_oct & 0777)) {
5231 if ($from_mode_str && $to_mode_str) {
5232 $mode_chnge .= " mode: $from_mode_str->$to_mode_str";
5233 } elsif ($to_mode_str) {
5234 $mode_chnge .= " mode: $to_mode_str";
5237 $mode_chnge .= "]</span>\n";
5239 print "<td>";
5240 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
5241 hash_base=>$hash, file_name=>$diff->{'file'}),
5242 -class => "list"}, esc_path($diff->{'file'}));
5243 print "</td>\n";
5244 print "<td>$mode_chnge</td>\n";
5245 print "<td class=\"link\">";
5246 if ($action eq 'commitdiff') {
5247 # link to patch
5248 $patchno++;
5249 print $cgi->a({-href => href(-anchor=>"patch$patchno")},
5250 "patch") .
5251 " | ";
5252 } elsif ($diff->{'to_id'} ne $diff->{'from_id'}) {
5253 # "commit" view and modified file (not onlu mode changed)
5254 print $cgi->a({-href => href(action=>"blobdiff",
5255 hash=>$diff->{'to_id'}, hash_parent=>$diff->{'from_id'},
5256 hash_base=>$hash, hash_parent_base=>$parent,
5257 file_name=>$diff->{'file'})},
5258 "diff") .
5259 " | ";
5261 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
5262 hash_base=>$hash, file_name=>$diff->{'file'})},
5263 "blob") . " | ";
5264 if ($have_blame) {
5265 print $cgi->a({-href => href(action=>"blame", hash_base=>$hash,
5266 file_name=>$diff->{'file'})},
5267 "blame") . " | ";
5269 print $cgi->a({-href => href(action=>"history", hash_base=>$hash,
5270 file_name=>$diff->{'file'})},
5271 "history");
5272 print "</td>\n";
5274 } elsif ($diff->{'status'} eq "R" || $diff->{'status'} eq "C") { # renamed or copied
5275 my %status_name = ('R' => 'moved', 'C' => 'copied');
5276 my $nstatus = $status_name{$diff->{'status'}};
5277 my $mode_chng = "";
5278 if ($diff->{'from_mode'} != $diff->{'to_mode'}) {
5279 # mode also for directories, so we cannot use $to_mode_str
5280 $mode_chng = sprintf(", mode: %04o", $to_mode_oct & 0777);
5282 print "<td>" .
5283 $cgi->a({-href => href(action=>"blob", hash_base=>$hash,
5284 hash=>$diff->{'to_id'}, file_name=>$diff->{'to_file'}),
5285 -class => "list"}, esc_path($diff->{'to_file'})) . "</td>\n" .
5286 "<td><span class=\"file_status $nstatus\">[$nstatus from " .
5287 $cgi->a({-href => href(action=>"blob", hash_base=>$parent,
5288 hash=>$diff->{'from_id'}, file_name=>$diff->{'from_file'}),
5289 -class => "list"}, esc_path($diff->{'from_file'})) .
5290 " with " . (int $diff->{'similarity'}) . "% similarity$mode_chng]</span></td>\n" .
5291 "<td class=\"link\">";
5292 if ($action eq 'commitdiff') {
5293 # link to patch
5294 $patchno++;
5295 print $cgi->a({-href => href(-anchor=>"patch$patchno")},
5296 "patch") .
5297 " | ";
5298 } elsif ($diff->{'to_id'} ne $diff->{'from_id'}) {
5299 # "commit" view and modified file (not only pure rename or copy)
5300 print $cgi->a({-href => href(action=>"blobdiff",
5301 hash=>$diff->{'to_id'}, hash_parent=>$diff->{'from_id'},
5302 hash_base=>$hash, hash_parent_base=>$parent,
5303 file_name=>$diff->{'to_file'}, file_parent=>$diff->{'from_file'})},
5304 "diff") .
5305 " | ";
5307 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
5308 hash_base=>$parent, file_name=>$diff->{'to_file'})},
5309 "blob") . " | ";
5310 if ($have_blame) {
5311 print $cgi->a({-href => href(action=>"blame", hash_base=>$hash,
5312 file_name=>$diff->{'to_file'})},
5313 "blame") . " | ";
5315 print $cgi->a({-href => href(action=>"history", hash_base=>$hash,
5316 file_name=>$diff->{'to_file'})},
5317 "history");
5318 print "</td>\n";
5320 } # we should not encounter Unmerged (U) or Unknown (X) status
5321 print "</tr>\n";
5323 print "</tbody>" if $has_header;
5324 print "</table>\n";
5327 # Print context lines and then rem/add lines in a side-by-side manner.
5328 sub print_sidebyside_diff_lines {
5329 my ($ctx, $rem, $add) = @_;
5331 # print context block before add/rem block
5332 if (@$ctx) {
5333 print join '',
5334 '<div class="chunk_block ctx">',
5335 '<div class="old">',
5336 @$ctx,
5337 '</div>',
5338 '<div class="new">',
5339 @$ctx,
5340 '</div>',
5341 '</div>';
5344 if (!@$add) {
5345 # pure removal
5346 print join '',
5347 '<div class="chunk_block rem">',
5348 '<div class="old">',
5349 @$rem,
5350 '</div>',
5351 '</div>';
5352 } elsif (!@$rem) {
5353 # pure addition
5354 print join '',
5355 '<div class="chunk_block add">',
5356 '<div class="new">',
5357 @$add,
5358 '</div>',
5359 '</div>';
5360 } else {
5361 print join '',
5362 '<div class="chunk_block chg">',
5363 '<div class="old">',
5364 @$rem,
5365 '</div>',
5366 '<div class="new">',
5367 @$add,
5368 '</div>',
5369 '</div>';
5373 # Print context lines and then rem/add lines in inline manner.
5374 sub print_inline_diff_lines {
5375 my ($ctx, $rem, $add) = @_;
5377 print @$ctx, @$rem, @$add;
5380 # Format removed and added line, mark changed part and HTML-format them.
5381 # Implementation is based on contrib/diff-highlight
5382 sub format_rem_add_lines_pair {
5383 my ($rem, $add, $num_parents) = @_;
5385 # We need to untabify lines before split()'ing them;
5386 # otherwise offsets would be invalid.
5387 chomp $rem;
5388 chomp $add;
5389 $rem = untabify($rem);
5390 $add = untabify($add);
5392 my @rem = split(//, $rem);
5393 my @add = split(//, $add);
5394 my ($esc_rem, $esc_add);
5395 # Ignore leading +/- characters for each parent.
5396 my ($prefix_len, $suffix_len) = ($num_parents, 0);
5397 my ($prefix_has_nonspace, $suffix_has_nonspace);
5399 my $shorter = (@rem < @add) ? @rem : @add;
5400 while ($prefix_len < $shorter) {
5401 last if ($rem[$prefix_len] ne $add[$prefix_len]);
5403 $prefix_has_nonspace = 1 if ($rem[$prefix_len] !~ /\s/);
5404 $prefix_len++;
5407 while ($prefix_len + $suffix_len < $shorter) {
5408 last if ($rem[-1 - $suffix_len] ne $add[-1 - $suffix_len]);
5410 $suffix_has_nonspace = 1 if ($rem[-1 - $suffix_len] !~ /\s/);
5411 $suffix_len++;
5414 # Mark lines that are different from each other, but have some common
5415 # part that isn't whitespace. If lines are completely different, don't
5416 # mark them because that would make output unreadable, especially if
5417 # diff consists of multiple lines.
5418 if ($prefix_has_nonspace || $suffix_has_nonspace) {
5419 $esc_rem = esc_html_hl_regions($rem, 'marked',
5420 [$prefix_len, @rem - $suffix_len], -nbsp=>1);
5421 $esc_add = esc_html_hl_regions($add, 'marked',
5422 [$prefix_len, @add - $suffix_len], -nbsp=>1);
5423 } else {
5424 $esc_rem = esc_html($rem, -nbsp=>1);
5425 $esc_add = esc_html($add, -nbsp=>1);
5428 return format_diff_line(\$esc_rem, 'rem'),
5429 format_diff_line(\$esc_add, 'add');
5432 # HTML-format diff context, removed and added lines.
5433 sub format_ctx_rem_add_lines {
5434 my ($ctx, $rem, $add, $num_parents) = @_;
5435 my (@new_ctx, @new_rem, @new_add);
5436 my $can_highlight = 0;
5437 my $is_combined = ($num_parents > 1);
5439 # Highlight if every removed line has a corresponding added line.
5440 if (@$add > 0 && @$add == @$rem) {
5441 $can_highlight = 1;
5443 # Highlight lines in combined diff only if the chunk contains
5444 # diff between the same version, e.g.
5446 # - a
5447 # - b
5448 # + c
5449 # + d
5451 # Otherwise the highlightling would be confusing.
5452 if ($is_combined) {
5453 for (my $i = 0; $i < @$add; $i++) {
5454 my $prefix_rem = substr($rem->[$i], 0, $num_parents);
5455 my $prefix_add = substr($add->[$i], 0, $num_parents);
5457 $prefix_rem =~ s/-/+/g;
5459 if ($prefix_rem ne $prefix_add) {
5460 $can_highlight = 0;
5461 last;
5467 if ($can_highlight) {
5468 for (my $i = 0; $i < @$add; $i++) {
5469 my ($line_rem, $line_add) = format_rem_add_lines_pair(
5470 $rem->[$i], $add->[$i], $num_parents);
5471 push @new_rem, $line_rem;
5472 push @new_add, $line_add;
5474 } else {
5475 @new_rem = map { format_diff_line($_, 'rem') } @$rem;
5476 @new_add = map { format_diff_line($_, 'add') } @$add;
5479 @new_ctx = map { format_diff_line($_, 'ctx') } @$ctx;
5481 return (\@new_ctx, \@new_rem, \@new_add);
5484 # Print context lines and then rem/add lines.
5485 sub print_diff_lines {
5486 my ($ctx, $rem, $add, $diff_style, $num_parents) = @_;
5487 my $is_combined = $num_parents > 1;
5489 ($ctx, $rem, $add) = format_ctx_rem_add_lines($ctx, $rem, $add,
5490 $num_parents);
5492 if ($diff_style eq 'sidebyside' && !$is_combined) {
5493 print_sidebyside_diff_lines($ctx, $rem, $add);
5494 } else {
5495 # default 'inline' style and unknown styles
5496 print_inline_diff_lines($ctx, $rem, $add);
5500 sub print_diff_chunk {
5501 my ($diff_style, $num_parents, $from, $to, @chunk) = @_;
5502 my (@ctx, @rem, @add);
5504 # The class of the previous line.
5505 my $prev_class = '';
5507 return unless @chunk;
5509 # incomplete last line might be among removed or added lines,
5510 # or both, or among context lines: find which
5511 for (my $i = 1; $i < @chunk; $i++) {
5512 if ($chunk[$i][0] eq 'incomplete') {
5513 $chunk[$i][0] = $chunk[$i-1][0];
5517 # guardian
5518 push @chunk, ["", ""];
5520 foreach my $line_info (@chunk) {
5521 my ($class, $line) = @$line_info;
5523 # print chunk headers
5524 if ($class && $class eq 'chunk_header') {
5525 print format_diff_line($line, $class, $from, $to);
5526 next;
5529 ## print from accumulator when have some add/rem lines or end
5530 # of chunk (flush context lines), or when have add and rem
5531 # lines and new block is reached (otherwise add/rem lines could
5532 # be reordered)
5533 if (!$class || ((@rem || @add) && $class eq 'ctx') ||
5534 (@rem && @add && $class ne $prev_class)) {
5535 print_diff_lines(\@ctx, \@rem, \@add,
5536 $diff_style, $num_parents);
5537 @ctx = @rem = @add = ();
5540 ## adding lines to accumulator
5541 # guardian value
5542 last unless $line;
5543 # rem, add or change
5544 if ($class eq 'rem') {
5545 push @rem, $line;
5546 } elsif ($class eq 'add') {
5547 push @add, $line;
5549 # context line
5550 if ($class eq 'ctx') {
5551 push @ctx, $line;
5554 $prev_class = $class;
5558 sub git_patchset_body {
5559 my ($fd, $diff_style, $difftree, $hash, @hash_parents) = @_;
5560 my ($hash_parent) = $hash_parents[0];
5562 my $is_combined = (@hash_parents > 1);
5563 my $patch_idx = 0;
5564 my $patch_number = 0;
5565 my $patch_line;
5566 my $diffinfo;
5567 my $to_name;
5568 my (%from, %to);
5569 my @chunk; # for side-by-side diff
5571 print "<div class=\"patchset\">\n";
5573 # skip to first patch
5574 while ($patch_line = <$fd>) {
5575 chomp $patch_line;
5577 last if ($patch_line =~ m/^diff /);
5580 PATCH:
5581 while ($patch_line) {
5583 # parse "git diff" header line
5584 if ($patch_line =~ m/^diff --git (\"(?:[^\\\"]*(?:\\.[^\\\"]*)*)\"|[^ "]*) (.*)$/) {
5585 # $1 is from_name, which we do not use
5586 $to_name = unquote($2);
5587 $to_name =~ s!^b/!!;
5588 } elsif ($patch_line =~ m/^diff --(cc|combined) ("?.*"?)$/) {
5589 # $1 is 'cc' or 'combined', which we do not use
5590 $to_name = unquote($2);
5591 } else {
5592 $to_name = undef;
5595 # check if current patch belong to current raw line
5596 # and parse raw git-diff line if needed
5597 if (is_patch_split($diffinfo, { 'to_file' => $to_name })) {
5598 # this is continuation of a split patch
5599 print "<div class=\"patch cont\">\n";
5600 } else {
5601 # advance raw git-diff output if needed
5602 $patch_idx++ if defined $diffinfo;
5604 # read and prepare patch information
5605 $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
5607 # compact combined diff output can have some patches skipped
5608 # find which patch (using pathname of result) we are at now;
5609 if ($is_combined) {
5610 while ($to_name ne $diffinfo->{'to_file'}) {
5611 print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n" .
5612 format_diff_cc_simplified($diffinfo, @hash_parents) .
5613 "</div>\n"; # class="patch"
5615 $patch_idx++;
5616 $patch_number++;
5618 last if $patch_idx > $#$difftree;
5619 $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
5623 # modifies %from, %to hashes
5624 parse_from_to_diffinfo($diffinfo, \%from, \%to, @hash_parents);
5626 # this is first patch for raw difftree line with $patch_idx index
5627 # we index @$difftree array from 0, but number patches from 1
5628 print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n";
5631 # git diff header
5632 #assert($patch_line =~ m/^diff /) if DEBUG;
5633 #assert($patch_line !~ m!$/$!) if DEBUG; # is chomp-ed
5634 $patch_number++;
5635 # print "git diff" header
5636 print format_git_diff_header_line($patch_line, $diffinfo,
5637 \%from, \%to);
5639 # print extended diff header
5640 print "<div class=\"diff extended_header\">\n";
5641 EXTENDED_HEADER:
5642 while ($patch_line = <$fd>) {
5643 chomp $patch_line;
5645 last EXTENDED_HEADER if ($patch_line =~ m/^--- |^diff /);
5647 print format_extended_diff_header_line($patch_line, $diffinfo,
5648 \%from, \%to);
5650 print "</div>\n"; # class="diff extended_header"
5652 # from-file/to-file diff header
5653 if (! $patch_line) {
5654 print "</div>\n"; # class="patch"
5655 last PATCH;
5657 next PATCH if ($patch_line =~ m/^diff /);
5658 #assert($patch_line =~ m/^---/) if DEBUG;
5660 my $last_patch_line = $patch_line;
5661 $patch_line = <$fd>;
5662 chomp $patch_line;
5663 #assert($patch_line =~ m/^\+\+\+/) if DEBUG;
5665 print format_diff_from_to_header($last_patch_line, $patch_line,
5666 $diffinfo, \%from, \%to,
5667 @hash_parents);
5669 # the patch itself
5670 LINE:
5671 while ($patch_line = <$fd>) {
5672 chomp $patch_line;
5674 next PATCH if ($patch_line =~ m/^diff /);
5676 my $class = diff_line_class($patch_line, \%from, \%to);
5678 if ($class eq 'chunk_header') {
5679 print_diff_chunk($diff_style, scalar @hash_parents, \%from, \%to, @chunk);
5680 @chunk = ();
5683 push @chunk, [ $class, $patch_line ];
5686 } continue {
5687 if (@chunk) {
5688 print_diff_chunk($diff_style, scalar @hash_parents, \%from, \%to, @chunk);
5689 @chunk = ();
5691 print "</div>\n"; # class="patch"
5694 # for compact combined (--cc) format, with chunk and patch simplification
5695 # the patchset might be empty, but there might be unprocessed raw lines
5696 for (++$patch_idx if $patch_number > 0;
5697 $patch_idx < @$difftree;
5698 ++$patch_idx) {
5699 # read and prepare patch information
5700 $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
5702 # generate anchor for "patch" links in difftree / whatchanged part
5703 print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n" .
5704 format_diff_cc_simplified($diffinfo, @hash_parents) .
5705 "</div>\n"; # class="patch"
5707 $patch_number++;
5710 if ($patch_number == 0) {
5711 if (@hash_parents > 1) {
5712 print "<div class=\"diff nodifferences\">Trivial merge</div>\n";
5713 } else {
5714 print "<div class=\"diff nodifferences\">No differences found</div>\n";
5718 print "</div>\n"; # class="patchset"
5721 # . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
5723 sub git_project_search_form {
5724 my ($searchtext, $search_use_regexp) = @_;
5726 my $limit = '';
5727 if ($project_filter) {
5728 $limit = " in '$project_filter/'";
5731 print "<div class=\"projsearch\">\n";
5732 print $cgi->start_form(-method => 'get', -action => $my_uri) .
5733 $cgi->hidden(-name => 'a', -value => 'project_list') . "\n";
5734 print $cgi->hidden(-name => 'pf', -value => $project_filter). "\n"
5735 if (defined $project_filter);
5736 print $cgi->textfield(-name => 's', -value => $searchtext,
5737 -title => "Search project by name and description$limit",
5738 -size => 60) . "\n" .
5739 "<span title=\"Extended regular expression\">" .
5740 $cgi->checkbox(-name => 'sr', -value => 1, -label => 're',
5741 -checked => $search_use_regexp) .
5742 "</span>\n" .
5743 $cgi->submit(-name => 'btnS', -value => 'Search') .
5744 $cgi->end_form() . "\n" .
5745 $cgi->a({-href => href(project => undef, searchtext => undef,
5746 project_filter => $project_filter)},
5747 esc_html("List all projects$limit")) . "<br />\n";
5748 print "</div>\n";
5751 # entry for given @keys needs filling if at least one of keys in list
5752 # is not present in %$project_info
5753 sub project_info_needs_filling {
5754 my ($project_info, @keys) = @_;
5756 # return List::MoreUtils::any { !exists $project_info->{$_} } @keys;
5757 foreach my $key (@keys) {
5758 if (!exists $project_info->{$key}) {
5759 return 1;
5762 return;
5765 # fills project list info (age, description, owner, category, forks, etc.)
5766 # for each project in the list, removing invalid projects from
5767 # returned list, or fill only specified info.
5769 # Invalid projects are removed from the returned list if and only if you
5770 # ask 'age' or 'age_string' to be filled, because they are the only fields
5771 # that run unconditionally git command that requires repository, and
5772 # therefore do always check if project repository is invalid.
5774 # USAGE:
5775 # * fill_project_list_info(\@project_list, 'descr_long', 'ctags')
5776 # ensures that 'descr_long' and 'ctags' fields are filled
5777 # * @project_list = fill_project_list_info(\@project_list)
5778 # ensures that all fields are filled (and invalid projects removed)
5780 # NOTE: modifies $projlist, but does not remove entries from it
5781 sub fill_project_list_info {
5782 my ($projlist, @wanted_keys) = @_;
5783 my @projects;
5784 my $filter_set = sub { return @_; };
5785 if (@wanted_keys) {
5786 my %wanted_keys = map { $_ => 1 } @wanted_keys;
5787 $filter_set = sub { return grep { $wanted_keys{$_} } @_; };
5790 my $show_ctags = gitweb_check_feature('ctags');
5791 PROJECT:
5792 foreach my $pr (@$projlist) {
5793 if (project_info_needs_filling($pr, $filter_set->('age', 'age_string'))) {
5794 my (@activity) = git_get_last_activity($pr->{'path'});
5795 unless (@activity) {
5796 next PROJECT;
5798 ($pr->{'age'}, $pr->{'age_string'}) = @activity;
5800 if (project_info_needs_filling($pr, $filter_set->('descr', 'descr_long'))) {
5801 my $descr = git_get_project_description($pr->{'path'}) || "";
5802 $descr = to_utf8($descr);
5803 $pr->{'descr_long'} = $descr;
5804 $pr->{'descr'} = chop_str($descr, $projects_list_description_width, 5);
5806 if (project_info_needs_filling($pr, $filter_set->('owner'))) {
5807 $pr->{'owner'} = git_get_project_owner("$pr->{'path'}") || "";
5809 if ($show_ctags &&
5810 project_info_needs_filling($pr, $filter_set->('ctags'))) {
5811 $pr->{'ctags'} = git_get_project_ctags($pr->{'path'});
5813 if ($projects_list_group_categories &&
5814 project_info_needs_filling($pr, $filter_set->('category'))) {
5815 my $cat = git_get_project_category($pr->{'path'}) ||
5816 $project_list_default_category;
5817 $pr->{'category'} = to_utf8($cat);
5820 push @projects, $pr;
5823 return @projects;
5826 sub sort_projects_list {
5827 my ($projlist, $order) = @_;
5829 sub order_str {
5830 my $key = shift;
5831 return sub { $a->{$key} cmp $b->{$key} };
5834 sub order_num_then_undef {
5835 my $key = shift;
5836 return sub {
5837 defined $a->{$key} ?
5838 (defined $b->{$key} ? $a->{$key} <=> $b->{$key} : -1) :
5839 (defined $b->{$key} ? 1 : 0)
5843 my %orderings = (
5844 project => order_str('path'),
5845 descr => order_str('descr_long'),
5846 owner => order_str('owner'),
5847 age => order_num_then_undef('age'),
5850 my $ordering = $orderings{$order};
5851 return defined $ordering ? sort $ordering @$projlist : @$projlist;
5854 # returns a hash of categories, containing the list of project
5855 # belonging to each category
5856 sub build_projlist_by_category {
5857 my ($projlist, $from, $to) = @_;
5858 my %categories;
5860 $from = 0 unless defined $from;
5861 $to = $#$projlist if (!defined $to || $#$projlist < $to);
5863 for (my $i = $from; $i <= $to; $i++) {
5864 my $pr = $projlist->[$i];
5865 push @{$categories{ $pr->{'category'} }}, $pr;
5868 return wantarray ? %categories : \%categories;
5871 # print 'sort by' <th> element, generating 'sort by $name' replay link
5872 # if that order is not selected
5873 sub print_sort_th {
5874 print format_sort_th(@_);
5877 sub format_sort_th {
5878 my ($name, $order, $header) = @_;
5879 my $sort_th = "";
5880 $header ||= ucfirst($name);
5882 if ($order eq $name) {
5883 $sort_th .= "<th>$header</th>\n";
5884 } else {
5885 $sort_th .= "<th>" .
5886 $cgi->a({-href => href(-replay=>1, order=>$name),
5887 -class => "header"}, $header) .
5888 "</th>\n";
5891 return $sort_th;
5894 sub git_project_list_rows {
5895 my ($projlist, $from, $to, $check_forks) = @_;
5897 $from = 0 unless defined $from;
5898 $to = $#$projlist if (!defined $to || $#$projlist < $to);
5900 my $alternate = 1;
5901 for (my $i = $from; $i <= $to; $i++) {
5902 my $pr = $projlist->[$i];
5904 if ($alternate) {
5905 print "<tr class=\"dark\">\n";
5906 } else {
5907 print "<tr class=\"light\">\n";
5909 $alternate ^= 1;
5911 if ($check_forks) {
5912 print "<td>";
5913 if ($pr->{'forks'}) {
5914 my $nforks = scalar @{$pr->{'forks'}};
5915 if ($nforks > 0) {
5916 print $cgi->a({-href => href(project=>$pr->{'path'}, action=>"forks"),
5917 -title => "$nforks forks"}, "+");
5918 } else {
5919 print $cgi->span({-title => "$nforks forks"}, "+");
5922 print "</td>\n";
5924 print "<td>" . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary"),
5925 -class => "list"},
5926 esc_html_match_hl($pr->{'path'}, $search_regexp)) .
5927 "</td>\n" .
5928 "<td>" . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary"),
5929 -class => "list",
5930 -title => $pr->{'descr_long'}},
5931 $search_regexp
5932 ? esc_html_match_hl_chopped($pr->{'descr_long'},
5933 $pr->{'descr'}, $search_regexp)
5934 : esc_html($pr->{'descr'})) .
5935 "</td>\n";
5936 unless ($omit_owner) {
5937 print "<td><i>" . chop_and_escape_str($pr->{'owner'}, 15) . "</i></td>\n";
5939 unless ($omit_age_column) {
5940 print "<td class=\"". age_class($pr->{'age'}) . "\">" .
5941 (defined $pr->{'age_string'} ? $pr->{'age_string'} : "No commits") . "</td>\n";
5943 print"<td class=\"link\">" .
5944 $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary")}, "summary") . " | " .
5945 $cgi->a({-href => href(project=>$pr->{'path'}, action=>"shortlog")}, "shortlog") . " | " .
5946 $cgi->a({-href => href(project=>$pr->{'path'}, action=>"log")}, "log") . " | " .
5947 $cgi->a({-href => href(project=>$pr->{'path'}, action=>"tree")}, "tree") .
5948 ($pr->{'forks'} ? " | " . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"forks")}, "forks") : '') .
5949 "</td>\n" .
5950 "</tr>\n";
5954 sub git_project_list_body {
5955 # actually uses global variable $project
5956 my ($projlist, $order, $from, $to, $extra, $no_header) = @_;
5957 my @projects = @$projlist;
5959 my $check_forks = gitweb_check_feature('forks');
5960 my $show_ctags = gitweb_check_feature('ctags');
5961 my $tagfilter = $show_ctags ? $input_params{'ctag'} : undef;
5962 $check_forks = undef
5963 if ($tagfilter || $search_regexp);
5965 # filtering out forks before filling info allows to do less work
5966 @projects = filter_forks_from_projects_list(\@projects)
5967 if ($check_forks);
5968 # search_projects_list pre-fills required info
5969 @projects = search_projects_list(\@projects,
5970 'search_regexp' => $search_regexp,
5971 'tagfilter' => $tagfilter)
5972 if ($tagfilter || $search_regexp);
5973 # fill the rest
5974 my @all_fields = ('descr', 'descr_long', 'ctags', 'category');
5975 push @all_fields, ('age', 'age_string') unless($omit_age_column);
5976 push @all_fields, 'owner' unless($omit_owner);
5977 @projects = fill_project_list_info(\@projects, @all_fields);
5979 $order ||= $default_projects_order;
5980 $from = 0 unless defined $from;
5981 $to = $#projects if (!defined $to || $#projects < $to);
5983 # short circuit
5984 if ($from > $to) {
5985 print "<center>\n".
5986 "<b>No such projects found</b><br />\n".
5987 "Click ".$cgi->a({-href=>href(project=>undef)},"here")." to view all projects<br />\n".
5988 "</center>\n<br />\n";
5989 return;
5992 @projects = sort_projects_list(\@projects, $order);
5994 if ($show_ctags) {
5995 my $ctags = git_gather_all_ctags(\@projects);
5996 my $cloud = git_populate_project_tagcloud($ctags);
5997 print git_show_project_tagcloud($cloud, 64);
6000 print "<table class=\"project_list\">\n";
6001 unless ($no_header) {
6002 print "<tr>\n";
6003 if ($check_forks) {
6004 print "<th></th>\n";
6006 print_sort_th('project', $order, 'Project');
6007 print_sort_th('descr', $order, 'Description');
6008 print_sort_th('owner', $order, 'Owner') unless $omit_owner;
6009 print_sort_th('age', $order, 'Last Change') unless $omit_age_column;
6010 print "<th></th>\n" . # for links
6011 "</tr>\n";
6014 if ($projects_list_group_categories) {
6015 # only display categories with projects in the $from-$to window
6016 @projects = sort {$a->{'category'} cmp $b->{'category'}} @projects[$from..$to];
6017 my %categories = build_projlist_by_category(\@projects, $from, $to);
6018 foreach my $cat (sort keys %categories) {
6019 unless ($cat eq "") {
6020 print "<tr>\n";
6021 if ($check_forks) {
6022 print "<td></td>\n";
6024 print "<td class=\"category\" colspan=\"5\">".esc_html($cat)."</td>\n";
6025 print "</tr>\n";
6028 git_project_list_rows($categories{$cat}, undef, undef, $check_forks);
6030 } else {
6031 git_project_list_rows(\@projects, $from, $to, $check_forks);
6034 if (defined $extra) {
6035 print "<tr>\n";
6036 if ($check_forks) {
6037 print "<td></td>\n";
6039 print "<td colspan=\"5\">$extra</td>\n" .
6040 "</tr>\n";
6042 print "</table>\n";
6045 sub git_log_body {
6046 # uses global variable $project
6047 my ($commitlist, $from, $to, $refs, $extra) = @_;
6049 $from = 0 unless defined $from;
6050 $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
6052 for (my $i = 0; $i <= $to; $i++) {
6053 my %co = %{$commitlist->[$i]};
6054 next if !%co;
6055 my $commit = $co{'id'};
6056 my $ref = format_ref_marker($refs, $commit);
6057 git_print_header_div('commit',
6058 "<span class=\"age\">$co{'age_string'}</span>" .
6059 esc_html($co{'title'}) . $ref,
6060 $commit);
6061 print "<div class=\"title_text\">\n" .
6062 "<div class=\"log_link\">\n" .
6063 $cgi->a({-href => href(action=>"commit", hash=>$commit)}, "commit") .
6064 " | " .
6065 $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff") .
6066 " | " .
6067 $cgi->a({-href => href(action=>"tree", hash=>$commit, hash_base=>$commit)}, "tree") .
6068 "<br/>\n" .
6069 "</div>\n";
6070 git_print_authorship(\%co, -tag => 'span');
6071 print "<br/>\n</div>\n";
6073 print "<div class=\"log_body\">\n";
6074 git_print_log($co{'comment'}, -final_empty_line=> 1);
6075 print "</div>\n";
6077 if ($extra) {
6078 print "<div class=\"page_nav\">\n";
6079 print "$extra\n";
6080 print "</div>\n";
6084 sub git_shortlog_body {
6085 # uses global variable $project
6086 my ($commitlist, $from, $to, $refs, $extra) = @_;
6088 $from = 0 unless defined $from;
6089 $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
6091 print "<table class=\"shortlog\">\n";
6092 my $alternate = 1;
6093 for (my $i = $from; $i <= $to; $i++) {
6094 my %co = %{$commitlist->[$i]};
6095 my $commit = $co{'id'};
6096 my $ref = format_ref_marker($refs, $commit);
6097 if ($alternate) {
6098 print "<tr class=\"dark\">\n";
6099 } else {
6100 print "<tr class=\"light\">\n";
6102 $alternate ^= 1;
6103 # git_summary() used print "<td><i>$co{'age_string'}</i></td>\n" .
6104 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
6105 format_author_html('td', \%co, 10) . "<td>";
6106 print format_subject_html($co{'title'}, $co{'title_short'},
6107 href(action=>"commit", hash=>$commit), $ref);
6108 print "</td>\n" .
6109 "<td class=\"link\">" .
6110 $cgi->a({-href => href(action=>"commit", hash=>$commit)}, "commit") . " | " .
6111 $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff") . " | " .
6112 $cgi->a({-href => href(action=>"tree", hash=>$commit, hash_base=>$commit)}, "tree");
6113 my $snapshot_links = format_snapshot_links($commit);
6114 if (defined $snapshot_links) {
6115 print " | " . $snapshot_links;
6117 print "</td>\n" .
6118 "</tr>\n";
6120 if (defined $extra) {
6121 print "<tr>\n" .
6122 "<td colspan=\"4\">$extra</td>\n" .
6123 "</tr>\n";
6125 print "</table>\n";
6128 sub git_history_body {
6129 # Warning: assumes constant type (blob or tree) during history
6130 my ($commitlist, $from, $to, $refs, $extra,
6131 $file_name, $file_hash, $ftype) = @_;
6133 $from = 0 unless defined $from;
6134 $to = $#{$commitlist} unless (defined $to && $to <= $#{$commitlist});
6136 print "<table class=\"history\">\n";
6137 my $alternate = 1;
6138 for (my $i = $from; $i <= $to; $i++) {
6139 my %co = %{$commitlist->[$i]};
6140 if (!%co) {
6141 next;
6143 my $commit = $co{'id'};
6145 my $ref = format_ref_marker($refs, $commit);
6147 if ($alternate) {
6148 print "<tr class=\"dark\">\n";
6149 } else {
6150 print "<tr class=\"light\">\n";
6152 $alternate ^= 1;
6153 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
6154 # shortlog: format_author_html('td', \%co, 10)
6155 format_author_html('td', \%co, 15, 3) . "<td>";
6156 # originally git_history used chop_str($co{'title'}, 50)
6157 print format_subject_html($co{'title'}, $co{'title_short'},
6158 href(action=>"commit", hash=>$commit), $ref);
6159 print "</td>\n" .
6160 "<td class=\"link\">" .
6161 $cgi->a({-href => href(action=>$ftype, hash_base=>$commit, file_name=>$file_name)}, $ftype) . " | " .
6162 $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff");
6164 if ($ftype eq 'blob') {
6165 my $blob_current = $file_hash;
6166 my $blob_parent = git_get_hash_by_path($commit, $file_name);
6167 if (defined $blob_current && defined $blob_parent &&
6168 $blob_current ne $blob_parent) {
6169 print " | " .
6170 $cgi->a({-href => href(action=>"blobdiff",
6171 hash=>$blob_current, hash_parent=>$blob_parent,
6172 hash_base=>$hash_base, hash_parent_base=>$commit,
6173 file_name=>$file_name)},
6174 "diff to current");
6177 print "</td>\n" .
6178 "</tr>\n";
6180 if (defined $extra) {
6181 print "<tr>\n" .
6182 "<td colspan=\"4\">$extra</td>\n" .
6183 "</tr>\n";
6185 print "</table>\n";
6188 sub git_tags_body {
6189 # uses global variable $project
6190 my ($taglist, $from, $to, $extra) = @_;
6191 $from = 0 unless defined $from;
6192 $to = $#{$taglist} if (!defined $to || $#{$taglist} < $to);
6194 print "<table class=\"tags\">\n";
6195 my $alternate = 1;
6196 for (my $i = $from; $i <= $to; $i++) {
6197 my $entry = $taglist->[$i];
6198 my %tag = %$entry;
6199 my $comment = $tag{'subject'};
6200 my $comment_short;
6201 if (defined $comment) {
6202 $comment_short = chop_str($comment, 30, 5);
6204 if ($alternate) {
6205 print "<tr class=\"dark\">\n";
6206 } else {
6207 print "<tr class=\"light\">\n";
6209 $alternate ^= 1;
6210 if (defined $tag{'age'}) {
6211 print "<td><i>$tag{'age'}</i></td>\n";
6212 } else {
6213 print "<td></td>\n";
6215 print "<td>" .
6216 $cgi->a({-href => href(action=>$tag{'reftype'}, hash=>$tag{'refid'}),
6217 -class => "list name"}, esc_html($tag{'name'})) .
6218 "</td>\n" .
6219 "<td>";
6220 if (defined $comment) {
6221 print format_subject_html($comment, $comment_short,
6222 href(action=>"tag", hash=>$tag{'id'}));
6224 print "</td>\n" .
6225 "<td class=\"selflink\">";
6226 if ($tag{'type'} eq "tag") {
6227 print $cgi->a({-href => href(action=>"tag", hash=>$tag{'id'})}, "tag");
6228 } else {
6229 print "&nbsp;";
6231 print "</td>\n" .
6232 "<td class=\"link\">" . " | " .
6233 $cgi->a({-href => href(action=>$tag{'reftype'}, hash=>$tag{'refid'})}, $tag{'reftype'});
6234 if ($tag{'reftype'} eq "commit") {
6235 print " | " . $cgi->a({-href => href(action=>"shortlog", hash=>$tag{'fullname'})}, "shortlog") .
6236 " | " . $cgi->a({-href => href(action=>"log", hash=>$tag{'fullname'})}, "log");
6237 } elsif ($tag{'reftype'} eq "blob") {
6238 print " | " . $cgi->a({-href => href(action=>"blob_plain", hash=>$tag{'refid'})}, "raw");
6240 print "</td>\n" .
6241 "</tr>";
6243 if (defined $extra) {
6244 print "<tr>\n" .
6245 "<td colspan=\"5\">$extra</td>\n" .
6246 "</tr>\n";
6248 print "</table>\n";
6251 sub git_heads_body {
6252 # uses global variable $project
6253 my ($headlist, $head_at, $from, $to, $extra) = @_;
6254 $from = 0 unless defined $from;
6255 $to = $#{$headlist} if (!defined $to || $#{$headlist} < $to);
6257 print "<table class=\"heads\">\n";
6258 my $alternate = 1;
6259 for (my $i = $from; $i <= $to; $i++) {
6260 my $entry = $headlist->[$i];
6261 my %ref = %$entry;
6262 my $curr = defined $head_at && $ref{'id'} eq $head_at;
6263 if ($alternate) {
6264 print "<tr class=\"dark\">\n";
6265 } else {
6266 print "<tr class=\"light\">\n";
6268 $alternate ^= 1;
6269 print "<td><i>$ref{'age'}</i></td>\n" .
6270 ($curr ? "<td class=\"current_head\">" : "<td>") .
6271 $cgi->a({-href => href(action=>"shortlog", hash=>$ref{'fullname'}),
6272 -class => "list name"},esc_html($ref{'name'})) .
6273 "</td>\n" .
6274 "<td class=\"link\">" .
6275 $cgi->a({-href => href(action=>"shortlog", hash=>$ref{'fullname'})}, "shortlog") . " | " .
6276 $cgi->a({-href => href(action=>"log", hash=>$ref{'fullname'})}, "log") . " | " .
6277 $cgi->a({-href => href(action=>"tree", hash=>$ref{'fullname'}, hash_base=>$ref{'fullname'})}, "tree") .
6278 "</td>\n" .
6279 "</tr>";
6281 if (defined $extra) {
6282 print "<tr>\n" .
6283 "<td colspan=\"3\">$extra</td>\n" .
6284 "</tr>\n";
6286 print "</table>\n";
6289 # Display a single remote block
6290 sub git_remote_block {
6291 my ($remote, $rdata, $limit, $head) = @_;
6293 my $heads = $rdata->{'heads'};
6294 my $fetch = $rdata->{'fetch'};
6295 my $push = $rdata->{'push'};
6297 my $urls_table = "<table class=\"projects_list\">\n" ;
6299 if (defined $fetch) {
6300 if ($fetch eq $push) {
6301 $urls_table .= format_repo_url("URL", $fetch);
6302 } else {
6303 $urls_table .= format_repo_url("Fetch URL", $fetch);
6304 $urls_table .= format_repo_url("Push URL", $push) if defined $push;
6306 } elsif (defined $push) {
6307 $urls_table .= format_repo_url("Push URL", $push);
6308 } else {
6309 $urls_table .= format_repo_url("", "No remote URL");
6312 $urls_table .= "</table>\n";
6314 my $dots;
6315 if (defined $limit && $limit < @$heads) {
6316 $dots = $cgi->a({-href => href(action=>"remotes", hash=>$remote)}, "...");
6319 print $urls_table;
6320 git_heads_body($heads, $head, 0, $limit, $dots);
6323 # Display a list of remote names with the respective fetch and push URLs
6324 sub git_remotes_list {
6325 my ($remotedata, $limit) = @_;
6326 print "<table class=\"heads\">\n";
6327 my $alternate = 1;
6328 my @remotes = sort keys %$remotedata;
6330 my $limited = $limit && $limit < @remotes;
6332 $#remotes = $limit - 1 if $limited;
6334 while (my $remote = shift @remotes) {
6335 my $rdata = $remotedata->{$remote};
6336 my $fetch = $rdata->{'fetch'};
6337 my $push = $rdata->{'push'};
6338 if ($alternate) {
6339 print "<tr class=\"dark\">\n";
6340 } else {
6341 print "<tr class=\"light\">\n";
6343 $alternate ^= 1;
6344 print "<td>" .
6345 $cgi->a({-href=> href(action=>'remotes', hash=>$remote),
6346 -class=> "list name"},esc_html($remote)) .
6347 "</td>";
6348 print "<td class=\"link\">" .
6349 (defined $fetch ? $cgi->a({-href=> $fetch}, "fetch") : "fetch") .
6350 " | " .
6351 (defined $push ? $cgi->a({-href=> $push}, "push") : "push") .
6352 "</td>";
6354 print "</tr>\n";
6357 if ($limited) {
6358 print "<tr>\n" .
6359 "<td colspan=\"3\">" .
6360 $cgi->a({-href => href(action=>"remotes")}, "...") .
6361 "</td>\n" . "</tr>\n";
6364 print "</table>";
6367 # Display remote heads grouped by remote, unless there are too many
6368 # remotes, in which case we only display the remote names
6369 sub git_remotes_body {
6370 my ($remotedata, $limit, $head) = @_;
6371 if ($limit and $limit < keys %$remotedata) {
6372 git_remotes_list($remotedata, $limit);
6373 } else {
6374 fill_remote_heads($remotedata);
6375 while (my ($remote, $rdata) = each %$remotedata) {
6376 git_print_section({-class=>"remote", -id=>$remote},
6377 ["remotes", $remote, $remote], sub {
6378 git_remote_block($remote, $rdata, $limit, $head);
6384 sub git_search_message {
6385 my %co = @_;
6387 my $greptype;
6388 if ($searchtype eq 'commit') {
6389 $greptype = "--grep=";
6390 } elsif ($searchtype eq 'author') {
6391 $greptype = "--author=";
6392 } elsif ($searchtype eq 'committer') {
6393 $greptype = "--committer=";
6395 $greptype .= $searchtext;
6396 my @commitlist = parse_commits($hash, 101, (100 * $page), undef,
6397 $greptype, '--regexp-ignore-case',
6398 $search_use_regexp ? '--extended-regexp' : '--fixed-strings');
6400 my $paging_nav = '';
6401 if ($page > 0) {
6402 $paging_nav .=
6403 $cgi->a({-href => href(-replay=>1, page=>undef)},
6404 "first") .
6405 " &sdot; " .
6406 $cgi->a({-href => href(-replay=>1, page=>$page-1),
6407 -accesskey => "p", -title => "Alt-p"}, "prev");
6408 } else {
6409 $paging_nav .= "first &sdot; prev";
6411 my $next_link = '';
6412 if ($#commitlist >= 100) {
6413 $next_link =
6414 $cgi->a({-href => href(-replay=>1, page=>$page+1),
6415 -accesskey => "n", -title => "Alt-n"}, "next");
6416 $paging_nav .= " &sdot; $next_link";
6417 } else {
6418 $paging_nav .= " &sdot; next";
6421 git_header_html();
6423 git_print_page_nav('','', $hash,$co{'tree'},$hash, $paging_nav);
6424 git_print_header_div('commit', esc_html($co{'title'}), $hash);
6425 if ($page == 0 && !@commitlist) {
6426 print "<p>No match.</p>\n";
6427 } else {
6428 git_search_grep_body(\@commitlist, 0, 99, $next_link);
6431 git_footer_html();
6434 sub git_search_changes {
6435 my %co = @_;
6437 local $/ = "\n";
6438 defined(my $fd = git_cmd_pipe '--no-pager', 'log', @diff_opts,
6439 '--pretty=format:%H', '--no-abbrev', '--raw', "-S$searchtext",
6440 ($search_use_regexp ? '--pickaxe-regex' : ()))
6441 or die_error(500, "Open git-log failed");
6443 git_header_html();
6445 git_print_page_nav('','', $hash,$co{'tree'},$hash);
6446 git_print_header_div('commit', esc_html($co{'title'}), $hash);
6448 print "<table class=\"pickaxe search\">\n";
6449 my $alternate = 1;
6450 undef %co;
6451 my @files;
6452 while (my $line = <$fd>) {
6453 chomp $line;
6454 next unless $line;
6456 my %set = parse_difftree_raw_line($line);
6457 if (defined $set{'commit'}) {
6458 # finish previous commit
6459 if (%co) {
6460 print "</td>\n" .
6461 "<td class=\"link\">" .
6462 $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})},
6463 "commit") .
6464 " | " .
6465 $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'},
6466 hash_base=>$co{'id'})},
6467 "tree") .
6468 "</td>\n" .
6469 "</tr>\n";
6472 if ($alternate) {
6473 print "<tr class=\"dark\">\n";
6474 } else {
6475 print "<tr class=\"light\">\n";
6477 $alternate ^= 1;
6478 %co = parse_commit($set{'commit'});
6479 my $author = chop_and_escape_str($co{'author_name'}, 15, 5);
6480 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
6481 "<td><i>$author</i></td>\n" .
6482 "<td>" .
6483 $cgi->a({-href => href(action=>"commit", hash=>$co{'id'}),
6484 -class => "list subject"},
6485 chop_and_escape_str($co{'title'}, 50) . "<br/>");
6486 } elsif (defined $set{'to_id'}) {
6487 next if ($set{'to_id'} =~ m/^0{40}$/);
6489 print $cgi->a({-href => href(action=>"blob", hash_base=>$co{'id'},
6490 hash=>$set{'to_id'}, file_name=>$set{'to_file'}),
6491 -class => "list"},
6492 "<span class=\"match\">" . esc_path($set{'file'}) . "</span>") .
6493 "<br/>\n";
6496 close $fd;
6498 # finish last commit (warning: repetition!)
6499 if (%co) {
6500 print "</td>\n" .
6501 "<td class=\"link\">" .
6502 $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})},
6503 "commit") .
6504 " | " .
6505 $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'},
6506 hash_base=>$co{'id'})},
6507 "tree") .
6508 "</td>\n" .
6509 "</tr>\n";
6512 print "</table>\n";
6514 git_footer_html();
6517 sub git_search_files {
6518 my %co = @_;
6520 local $/ = "\n";
6521 defined(my $fd = git_cmd_pipe 'grep', '-n', '-z',
6522 $search_use_regexp ? ('-E', '-i') : '-F',
6523 $searchtext, $co{'tree'})
6524 or die_error(500, "Open git-grep failed");
6526 git_header_html();
6528 git_print_page_nav('','', $hash,$co{'tree'},$hash);
6529 git_print_header_div('commit', esc_html($co{'title'}), $hash);
6531 print "<table class=\"grep_search\">\n";
6532 my $alternate = 1;
6533 my $matches = 0;
6534 my $lastfile = '';
6535 my $file_href;
6536 while (my $line = <$fd>) {
6537 chomp $line;
6538 my ($file, $lno, $ltext, $binary);
6539 last if ($matches++ > 1000);
6540 if ($line =~ /^Binary file (.+) matches$/) {
6541 $file = $1;
6542 $binary = 1;
6543 } else {
6544 ($file, $lno, $ltext) = split(/\0/, $line, 3);
6545 $file =~ s/^$co{'tree'}://;
6547 if ($file ne $lastfile) {
6548 $lastfile and print "</td></tr>\n";
6549 if ($alternate++) {
6550 print "<tr class=\"dark\">\n";
6551 } else {
6552 print "<tr class=\"light\">\n";
6554 $file_href = href(action=>"blob", hash_base=>$co{'id'},
6555 file_name=>$file);
6556 print "<td class=\"list\">".
6557 $cgi->a({-href => $file_href, -class => "list"}, esc_path($file));
6558 print "</td><td>\n";
6559 $lastfile = $file;
6561 if ($binary) {
6562 print "<div class=\"binary\">Binary file</div>\n";
6563 } else {
6564 $ltext = untabify($ltext);
6565 if ($ltext =~ m/^(.*)($search_regexp)(.*)$/i) {
6566 $ltext = esc_html($1, -nbsp=>1);
6567 $ltext .= '<span class="match">';
6568 $ltext .= esc_html($2, -nbsp=>1);
6569 $ltext .= '</span>';
6570 $ltext .= esc_html($3, -nbsp=>1);
6571 } else {
6572 $ltext = esc_html($ltext, -nbsp=>1);
6574 print "<div class=\"pre\">" .
6575 $cgi->a({-href => $file_href.'#l'.$lno,
6576 -class => "linenr"}, sprintf('%4i', $lno)) .
6577 ' ' . $ltext . "</div>\n";
6580 if ($lastfile) {
6581 print "</td></tr>\n";
6582 if ($matches > 1000) {
6583 print "<div class=\"diff nodifferences\">Too many matches, listing trimmed</div>\n";
6585 } else {
6586 print "<div class=\"diff nodifferences\">No matches found</div>\n";
6588 close $fd;
6590 print "</table>\n";
6592 git_footer_html();
6595 sub git_search_grep_body {
6596 my ($commitlist, $from, $to, $extra) = @_;
6597 $from = 0 unless defined $from;
6598 $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
6600 print "<table class=\"commit_search\">\n";
6601 my $alternate = 1;
6602 for (my $i = $from; $i <= $to; $i++) {
6603 my %co = %{$commitlist->[$i]};
6604 if (!%co) {
6605 next;
6607 my $commit = $co{'id'};
6608 if ($alternate) {
6609 print "<tr class=\"dark\">\n";
6610 } else {
6611 print "<tr class=\"light\">\n";
6613 $alternate ^= 1;
6614 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
6615 format_author_html('td', \%co, 15, 5) .
6616 "<td>" .
6617 $cgi->a({-href => href(action=>"commit", hash=>$co{'id'}),
6618 -class => "list subject"},
6619 chop_and_escape_str($co{'title'}, 50) . "<br/>");
6620 my $comment = $co{'comment'};
6621 foreach my $line (@$comment) {
6622 if ($line =~ m/^(.*?)($search_regexp)(.*)$/i) {
6623 my ($lead, $match, $trail) = ($1, $2, $3);
6624 $match = chop_str($match, 70, 5, 'center');
6625 my $contextlen = int((80 - length($match))/2);
6626 $contextlen = 30 if ($contextlen > 30);
6627 $lead = chop_str($lead, $contextlen, 10, 'left');
6628 $trail = chop_str($trail, $contextlen, 10, 'right');
6630 $lead = esc_html($lead);
6631 $match = esc_html($match);
6632 $trail = esc_html($trail);
6634 print "$lead<span class=\"match\">$match</span>$trail<br />";
6637 print "</td>\n" .
6638 "<td class=\"link\">" .
6639 $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") .
6640 " | " .
6641 $cgi->a({-href => href(action=>"commitdiff", hash=>$co{'id'})}, "commitdiff") .
6642 " | " .
6643 $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree");
6644 print "</td>\n" .
6645 "</tr>\n";
6647 if (defined $extra) {
6648 print "<tr>\n" .
6649 "<td colspan=\"3\">$extra</td>\n" .
6650 "</tr>\n";
6652 print "</table>\n";
6655 ## ======================================================================
6656 ## ======================================================================
6657 ## actions
6659 sub git_project_list {
6660 my $order = $input_params{'order'};
6661 if (defined $order && $order !~ m/none|project|descr|owner|age/) {
6662 die_error(400, "Unknown order parameter");
6665 my @list = git_get_projects_list($project_filter, $strict_export);
6666 if (!@list) {
6667 die_error(404, "No projects found");
6670 git_header_html();
6671 if (defined $home_text && -f $home_text) {
6672 print "<div class=\"index_include\">\n";
6673 insert_file($home_text);
6674 print "</div>\n";
6677 git_project_search_form($searchtext, $search_use_regexp);
6678 git_project_list_body(\@list, $order);
6679 git_footer_html();
6682 sub git_forks {
6683 my $order = $input_params{'order'};
6684 if (defined $order && $order !~ m/none|project|descr|owner|age/) {
6685 die_error(400, "Unknown order parameter");
6688 my $filter = $project;
6689 $filter =~ s/\.git$//;
6690 my @list = git_get_projects_list($filter);
6691 if (!@list) {
6692 die_error(404, "No forks found");
6695 git_header_html();
6696 git_print_page_nav('','');
6697 git_print_header_div('summary', "$project forks");
6698 git_project_list_body(\@list, $order);
6699 git_footer_html();
6702 sub git_project_index {
6703 my @projects = git_get_projects_list($project_filter, $strict_export);
6704 if (!@projects) {
6705 die_error(404, "No projects found");
6708 print $cgi->header(
6709 -type => 'text/plain',
6710 -charset => 'utf-8',
6711 -content_disposition => 'inline; filename="index.aux"');
6713 foreach my $pr (@projects) {
6714 if (!exists $pr->{'owner'}) {
6715 $pr->{'owner'} = git_get_project_owner("$pr->{'path'}");
6718 my ($path, $owner) = ($pr->{'path'}, $pr->{'owner'});
6719 # quote as in CGI::Util::encode, but keep the slash, and use '+' for ' '
6720 $path =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf("%%%02X", ord($1))/eg;
6721 $owner =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf("%%%02X", ord($1))/eg;
6722 $path =~ s/ /\+/g;
6723 $owner =~ s/ /\+/g;
6725 print "$path $owner\n";
6729 sub git_summary {
6730 my $descr = git_get_project_description($project) || "none";
6731 my %co = parse_commit("HEAD");
6732 my %cd = %co ? parse_date($co{'committer_epoch'}, $co{'committer_tz'}) : ();
6733 my $head = $co{'id'};
6734 my $remote_heads = gitweb_check_feature('remote_heads');
6736 my $owner = git_get_project_owner($project);
6738 my $refs = git_get_references();
6739 # These get_*_list functions return one more to allow us to see if
6740 # there are more ...
6741 my @taglist = git_get_tags_list(16);
6742 my @headlist = git_get_heads_list(16);
6743 my %remotedata = $remote_heads ? git_get_remotes_list() : ();
6744 my @forklist;
6745 my $check_forks = gitweb_check_feature('forks');
6747 if ($check_forks) {
6748 # find forks of a project
6749 my $filter = $project;
6750 $filter =~ s/\.git$//;
6751 @forklist = git_get_projects_list($filter);
6752 # filter out forks of forks
6753 @forklist = filter_forks_from_projects_list(\@forklist)
6754 if (@forklist);
6757 git_header_html();
6758 git_print_page_nav('summary','', $head);
6760 print "<div class=\"title\">&nbsp;</div>\n";
6761 print "<table class=\"projects_list\">\n" .
6762 "<tr id=\"metadata_desc\"><td>description</td><td>" . esc_html($descr) . "</td></tr>\n";
6763 if ($owner and not $omit_owner) {
6764 print "<tr id=\"metadata_owner\"><td>owner</td><td>" . esc_html($owner) . "</td></tr>\n";
6766 if (defined $cd{'rfc2822'}) {
6767 print "<tr id=\"metadata_lchange\"><td>last change</td>" .
6768 "<td>".format_timestamp_html(\%cd)."</td></tr>\n";
6771 # use per project git URL list in $projectroot/$project/cloneurl
6772 # or make project git URL from git base URL and project name
6773 my $url_tag = "URL";
6774 my @url_list = git_get_project_url_list($project);
6775 @url_list = map { "$_/$project" } @git_base_url_list unless @url_list;
6776 foreach my $git_url (@url_list) {
6777 next unless $git_url;
6778 print format_repo_url($url_tag, $git_url);
6779 $url_tag = "";
6782 # Tag cloud
6783 my $show_ctags = gitweb_check_feature('ctags');
6784 if ($show_ctags) {
6785 my $ctags = git_get_project_ctags($project);
6786 if (%$ctags) {
6787 # without ability to add tags, don't show if there are none
6788 my $cloud = git_populate_project_tagcloud($ctags);
6789 print "<tr id=\"metadata_ctags\">" .
6790 "<td>content tags</td>" .
6791 "<td>".git_show_project_tagcloud($cloud, 48)."</td>" .
6792 "</tr>\n";
6796 print "</table>\n";
6798 # If XSS prevention is on, we don't include README.html.
6799 # TODO: Allow a readme in some safe format.
6800 if (!$prevent_xss && -s "$projectroot/$project/README.html") {
6801 print "<div class=\"title\">readme</div>\n" .
6802 "<div class=\"readme\">\n";
6803 insert_file("$projectroot/$project/README.html");
6804 print "\n</div>\n"; # class="readme"
6807 # we need to request one more than 16 (0..15) to check if
6808 # those 16 are all
6809 my @commitlist = $head ? parse_commits($head, 17) : ();
6810 if (@commitlist) {
6811 git_print_header_div('shortlog');
6812 git_shortlog_body(\@commitlist, 0, 15, $refs,
6813 $#commitlist <= 15 ? undef :
6814 $cgi->a({-href => href(action=>"shortlog")}, "..."));
6817 if (@taglist) {
6818 git_print_header_div('tags');
6819 git_tags_body(\@taglist, 0, 15,
6820 $#taglist <= 15 ? undef :
6821 $cgi->a({-href => href(action=>"tags")}, "..."));
6824 if (@headlist) {
6825 git_print_header_div('heads');
6826 git_heads_body(\@headlist, $head, 0, 15,
6827 $#headlist <= 15 ? undef :
6828 $cgi->a({-href => href(action=>"heads")}, "..."));
6831 if (%remotedata) {
6832 git_print_header_div('remotes');
6833 git_remotes_body(\%remotedata, 15, $head);
6836 if (@forklist) {
6837 git_print_header_div('forks');
6838 git_project_list_body(\@forklist, 'age', 0, 15,
6839 $#forklist <= 15 ? undef :
6840 $cgi->a({-href => href(action=>"forks")}, "..."),
6841 'no_header');
6844 git_footer_html();
6847 sub git_tag {
6848 my %tag = parse_tag($hash);
6850 if (! %tag) {
6851 die_error(404, "Unknown tag object");
6854 my $head = git_get_head_hash($project);
6855 git_header_html();
6856 git_print_page_nav('','', $head,undef,$head);
6857 git_print_header_div('commit', esc_html($tag{'name'}), $hash);
6858 print "<div class=\"title_text\">\n" .
6859 "<table class=\"object_header\">\n" .
6860 "<tr>\n" .
6861 "<td>object</td>\n" .
6862 "<td>" . $cgi->a({-class => "list", -href => href(action=>$tag{'type'}, hash=>$tag{'object'})},
6863 $tag{'object'}) . "</td>\n" .
6864 "<td class=\"link\">" . $cgi->a({-href => href(action=>$tag{'type'}, hash=>$tag{'object'})},
6865 $tag{'type'}) . "</td>\n" .
6866 "</tr>\n";
6867 if (defined($tag{'author'})) {
6868 git_print_authorship_rows(\%tag, 'author');
6870 print "</table>\n\n" .
6871 "</div>\n";
6872 print "<div class=\"page_body\">";
6873 my $comment = $tag{'comment'};
6874 foreach my $line (@$comment) {
6875 chomp $line;
6876 print esc_html($line, -nbsp=>1) . "<br/>\n";
6878 print "</div>\n";
6879 git_footer_html();
6882 sub git_blame_common {
6883 my $format = shift || 'porcelain';
6884 if ($format eq 'porcelain' && $input_params{'javascript'}) {
6885 $format = 'incremental';
6886 $action = 'blame_incremental'; # for page title etc
6889 # permissions
6890 gitweb_check_feature('blame')
6891 or die_error(403, "Blame view not allowed");
6893 # error checking
6894 die_error(400, "No file name given") unless $file_name;
6895 $hash_base ||= git_get_head_hash($project);
6896 die_error(404, "Couldn't find base commit") unless $hash_base;
6897 my %co = parse_commit($hash_base)
6898 or die_error(404, "Commit not found");
6899 my $ftype = "blob";
6900 if (!defined $hash) {
6901 $hash = git_get_hash_by_path($hash_base, $file_name, "blob")
6902 or die_error(404, "Error looking up file");
6903 } else {
6904 $ftype = git_get_type($hash);
6905 if ($ftype !~ "blob") {
6906 die_error(400, "Object is not a blob");
6910 my $fd;
6911 if ($format eq 'incremental') {
6912 # get file contents (as base)
6913 defined($fd = git_cmd_pipe 'cat-file', 'blob', $hash)
6914 or die_error(500, "Open git-cat-file failed");
6915 } elsif ($format eq 'data') {
6916 # run git-blame --incremental
6917 defined($fd = git_cmd_pipe "blame", "--incremental",
6918 $hash_base, "--", $file_name)
6919 or die_error(500, "Open git-blame --incremental failed");
6920 } else {
6921 # run git-blame --porcelain
6922 defined($fd = git_cmd_pipe "blame", '-p',
6923 $hash_base, '--', $file_name)
6924 or die_error(500, "Open git-blame --porcelain failed");
6926 binmode $fd, ':utf8';
6928 # incremental blame data returns early
6929 if ($format eq 'data') {
6930 print $cgi->header(
6931 -type=>"text/plain", -charset => "utf-8",
6932 -status=> "200 OK");
6933 local $| = 1; # output autoflush
6934 while (my $line = <$fd>) {
6935 print to_utf8($line);
6937 close $fd
6938 or print "ERROR $!\n";
6940 print 'END';
6941 if (defined $t0 && gitweb_check_feature('timed')) {
6942 print ' '.
6943 tv_interval($t0, [ gettimeofday() ]).
6944 ' '.$number_of_git_cmds;
6946 print "\n";
6948 return;
6951 # page header
6952 git_header_html();
6953 my $formats_nav =
6954 $cgi->a({-href => href(action=>"blob", -replay=>1)},
6955 "blob") .
6956 " | ";
6957 if ($format eq 'incremental') {
6958 $formats_nav .=
6959 $cgi->a({-href => href(action=>"blame", javascript=>0, -replay=>1)},
6960 "blame") . " (non-incremental)";
6961 } else {
6962 $formats_nav .=
6963 $cgi->a({-href => href(action=>"blame_incremental", -replay=>1)},
6964 "blame") . " (incremental)";
6966 $formats_nav .=
6967 " | " .
6968 $cgi->a({-href => href(action=>"history", -replay=>1)},
6969 "history") .
6970 " | " .
6971 $cgi->a({-href => href(action=>$action, file_name=>$file_name)},
6972 "HEAD");
6973 git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
6974 git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
6975 git_print_page_path($file_name, $ftype, $hash_base);
6977 # page body
6978 if ($format eq 'incremental') {
6979 print "<noscript>\n<div class=\"error\"><center><b>\n".
6980 "This page requires JavaScript to run.\n Use ".
6981 $cgi->a({-href => href(action=>'blame',javascript=>0,-replay=>1)},
6982 'this page').
6983 " instead.\n".
6984 "</b></center></div>\n</noscript>\n";
6986 print qq!<div id="progress_bar" style="width: 100%; background-color: yellow"></div>\n!;
6989 print qq!<div class="page_body">\n!;
6990 print qq!<div id="progress_info">... / ...</div>\n!
6991 if ($format eq 'incremental');
6992 print qq!<table id="blame_table" class="blame" width="100%">\n!.
6993 #qq!<col width="5.5em" /><col width="2.5em" /><col width="*" />\n!.
6994 qq!<thead>\n!.
6995 qq!<tr><th>Commit</th><th>Line</th><th>Data</th></tr>\n!.
6996 qq!</thead>\n!.
6997 qq!<tbody>\n!;
6999 my @rev_color = qw(light dark);
7000 my $num_colors = scalar(@rev_color);
7001 my $current_color = 0;
7003 if ($format eq 'incremental') {
7004 my $color_class = $rev_color[$current_color];
7006 #contents of a file
7007 my $linenr = 0;
7008 LINE:
7009 while (my $line = <$fd>) {
7010 chomp $line;
7011 $linenr++;
7013 print qq!<tr id="l$linenr" class="$color_class">!.
7014 qq!<td class="sha1"><a href=""> </a></td>!.
7015 qq!<td class="linenr">!.
7016 qq!<a class="linenr" href="">$linenr</a></td>!;
7017 print qq!<td class="pre">! . esc_html($line) . "</td>\n";
7018 print qq!</tr>\n!;
7021 } else { # porcelain, i.e. ordinary blame
7022 my %metainfo = (); # saves information about commits
7024 # blame data
7025 LINE:
7026 while (my $line = <$fd>) {
7027 chomp $line;
7028 # the header: <SHA-1> <src lineno> <dst lineno> [<lines in group>]
7029 # no <lines in group> for subsequent lines in group of lines
7030 my ($full_rev, $orig_lineno, $lineno, $group_size) =
7031 ($line =~ /^([0-9a-f]{40}) (\d+) (\d+)(?: (\d+))?$/);
7032 if (!exists $metainfo{$full_rev}) {
7033 $metainfo{$full_rev} = { 'nprevious' => 0 };
7035 my $meta = $metainfo{$full_rev};
7036 my $data;
7037 while ($data = <$fd>) {
7038 chomp $data;
7039 last if ($data =~ s/^\t//); # contents of line
7040 if ($data =~ /^(\S+)(?: (.*))?$/) {
7041 $meta->{$1} = $2 unless exists $meta->{$1};
7043 if ($data =~ /^previous /) {
7044 $meta->{'nprevious'}++;
7047 my $short_rev = substr($full_rev, 0, 8);
7048 my $author = $meta->{'author'};
7049 my %date =
7050 parse_date($meta->{'author-time'}, $meta->{'author-tz'});
7051 my $date = $date{'iso-tz'};
7052 if ($group_size) {
7053 $current_color = ($current_color + 1) % $num_colors;
7055 my $tr_class = $rev_color[$current_color];
7056 $tr_class .= ' boundary' if (exists $meta->{'boundary'});
7057 $tr_class .= ' no-previous' if ($meta->{'nprevious'} == 0);
7058 $tr_class .= ' multiple-previous' if ($meta->{'nprevious'} > 1);
7059 print "<tr id=\"l$lineno\" class=\"$tr_class\">\n";
7060 if ($group_size) {
7061 print "<td class=\"sha1\"";
7062 print " title=\"". esc_html($author) . ", $date\"";
7063 print " rowspan=\"$group_size\"" if ($group_size > 1);
7064 print ">";
7065 print $cgi->a({-href => href(action=>"commit",
7066 hash=>$full_rev,
7067 file_name=>$file_name)},
7068 esc_html($short_rev));
7069 if ($group_size >= 2) {
7070 my @author_initials = ($author =~ /\b([[:upper:]])\B/g);
7071 if (@author_initials) {
7072 print "<br />" .
7073 esc_html(join('', @author_initials));
7074 # or join('.', ...)
7077 print "</td>\n";
7079 # 'previous' <sha1 of parent commit> <filename at commit>
7080 if (exists $meta->{'previous'} &&
7081 $meta->{'previous'} =~ /^([a-fA-F0-9]{40}) (.*)$/) {
7082 $meta->{'parent'} = $1;
7083 $meta->{'file_parent'} = unquote($2);
7085 my $linenr_commit =
7086 exists($meta->{'parent'}) ?
7087 $meta->{'parent'} : $full_rev;
7088 my $linenr_filename =
7089 exists($meta->{'file_parent'}) ?
7090 $meta->{'file_parent'} : unquote($meta->{'filename'});
7091 my $blamed = href(action => 'blame',
7092 file_name => $linenr_filename,
7093 hash_base => $linenr_commit);
7094 print "<td class=\"linenr\">";
7095 print $cgi->a({ -href => "$blamed#l$orig_lineno",
7096 -class => "linenr" },
7097 esc_html($lineno));
7098 print "</td>";
7099 print "<td class=\"pre\">" . esc_html($data) . "</td>\n";
7100 print "</tr>\n";
7101 } # end while
7105 # footer
7106 print "</tbody>\n".
7107 "</table>\n"; # class="blame"
7108 print "</div>\n"; # class="blame_body"
7109 close $fd
7110 or print "Reading blob failed\n";
7112 git_footer_html();
7115 sub git_blame {
7116 git_blame_common();
7119 sub git_blame_incremental {
7120 git_blame_common('incremental');
7123 sub git_blame_data {
7124 git_blame_common('data');
7127 sub git_tags {
7128 my $head = git_get_head_hash($project);
7129 git_header_html();
7130 git_print_page_nav('','', $head,undef,$head,format_ref_views('tags'));
7131 git_print_header_div('summary', $project);
7133 my @tagslist = git_get_tags_list();
7134 if (@tagslist) {
7135 git_tags_body(\@tagslist);
7137 git_footer_html();
7140 sub git_heads {
7141 my $head = git_get_head_hash($project);
7142 git_header_html();
7143 git_print_page_nav('','', $head,undef,$head,format_ref_views('heads'));
7144 git_print_header_div('summary', $project);
7146 my @headslist = git_get_heads_list();
7147 if (@headslist) {
7148 git_heads_body(\@headslist, $head);
7150 git_footer_html();
7153 # used both for single remote view and for list of all the remotes
7154 sub git_remotes {
7155 gitweb_check_feature('remote_heads')
7156 or die_error(403, "Remote heads view is disabled");
7158 my $head = git_get_head_hash($project);
7159 my $remote = $input_params{'hash'};
7161 my $remotedata = git_get_remotes_list($remote);
7162 die_error(500, "Unable to get remote information") unless defined $remotedata;
7164 unless (%$remotedata) {
7165 die_error(404, defined $remote ?
7166 "Remote $remote not found" :
7167 "No remotes found");
7170 git_header_html(undef, undef, -action_extra => $remote);
7171 git_print_page_nav('', '', $head, undef, $head,
7172 format_ref_views($remote ? '' : 'remotes'));
7174 fill_remote_heads($remotedata);
7175 if (defined $remote) {
7176 git_print_header_div('remotes', "$remote remote for $project");
7177 git_remote_block($remote, $remotedata->{$remote}, undef, $head);
7178 } else {
7179 git_print_header_div('summary', "$project remotes");
7180 git_remotes_body($remotedata, undef, $head);
7183 git_footer_html();
7186 sub git_blob_plain {
7187 my $type = shift;
7188 my $expires;
7190 if (!defined $hash) {
7191 if (defined $file_name) {
7192 my $base = $hash_base || git_get_head_hash($project);
7193 $hash = git_get_hash_by_path($base, $file_name, "blob")
7194 or die_error(404, "Cannot find file");
7195 } else {
7196 die_error(400, "No file name defined");
7198 } elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) {
7199 # blobs defined by non-textual hash id's can be cached
7200 $expires = "+1d";
7203 defined(my $fd = git_cmd_pipe "cat-file", "blob", $hash)
7204 or die_error(500, "Open git-cat-file blob '$hash' failed");
7206 # content-type (can include charset)
7207 $type = blob_contenttype($fd, $file_name, $type);
7209 # "save as" filename, even when no $file_name is given
7210 my $save_as = "$hash";
7211 if (defined $file_name) {
7212 $save_as = $file_name;
7213 } elsif ($type =~ m/^text\//) {
7214 $save_as .= '.txt';
7217 # With XSS prevention on, blobs of all types except a few known safe
7218 # ones are served with "Content-Disposition: attachment" to make sure
7219 # they don't run in our security domain. For certain image types,
7220 # blob view writes an <img> tag referring to blob_plain view, and we
7221 # want to be sure not to break that by serving the image as an
7222 # attachment (though Firefox 3 doesn't seem to care).
7223 my $sandbox = $prevent_xss &&
7224 $type !~ m!^(?:text/[a-z]+|image/(?:gif|png|jpeg))(?:[ ;]|$)!;
7226 # serve text/* as text/plain
7227 if ($prevent_xss &&
7228 ($type =~ m!^text/[a-z]+\b(.*)$! ||
7229 ($type =~ m!^[a-z]+/[a-z]\+xml\b(.*)$! && -T $fd))) {
7230 my $rest = $1;
7231 $rest = defined $rest ? $rest : '';
7232 $type = "text/plain$rest";
7235 print $cgi->header(
7236 -type => $type,
7237 -expires => $expires,
7238 -content_disposition =>
7239 ($sandbox ? 'attachment' : 'inline')
7240 . '; filename="' . $save_as . '"');
7241 local $/ = undef;
7242 binmode STDOUT, ':raw';
7243 print <$fd>;
7244 binmode STDOUT, ':utf8'; # as set at the beginning of gitweb.cgi
7245 close $fd;
7248 sub git_blob {
7249 my $expires;
7251 if (!defined $hash) {
7252 if (defined $file_name) {
7253 my $base = $hash_base || git_get_head_hash($project);
7254 $hash = git_get_hash_by_path($base, $file_name, "blob")
7255 or die_error(404, "Cannot find file");
7256 } else {
7257 die_error(400, "No file name defined");
7259 } elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) {
7260 # blobs defined by non-textual hash id's can be cached
7261 $expires = "+1d";
7264 my $have_blame = gitweb_check_feature('blame');
7265 defined(my $fd = git_cmd_pipe "cat-file", "blob", $hash)
7266 or die_error(500, "Couldn't cat $file_name, $hash");
7267 my $mimetype = blob_mimetype($fd, $file_name);
7268 # use 'blob_plain' (aka 'raw') view for files that cannot be displayed
7269 if ($mimetype !~ m!^(?:text/|image/(?:gif|png|jpeg)$)! && -B $fd) {
7270 close $fd;
7271 return git_blob_plain($mimetype);
7273 # we can have blame only for text/* mimetype
7274 $have_blame &&= ($mimetype =~ m!^text/!);
7276 my $highlight = gitweb_check_feature('highlight') && defined $highlight_bin;
7277 my $syntax = guess_file_syntax($fd, $mimetype, $file_name) if $highlight;
7278 my $highlight_mode_active;
7279 ($fd, $highlight_mode_active) = run_highlighter($fd, $syntax) if $syntax;
7281 git_header_html(undef, $expires);
7282 my $formats_nav = '';
7283 if (defined $hash_base && (my %co = parse_commit($hash_base))) {
7284 if (defined $file_name) {
7285 if ($have_blame) {
7286 $formats_nav .=
7287 $cgi->a({-href => href(action=>"blame", -replay=>1)},
7288 "blame") .
7289 " | ";
7291 $formats_nav .=
7292 $cgi->a({-href => href(action=>"history", -replay=>1)},
7293 "history") .
7294 " | " .
7295 $cgi->a({-href => href(action=>"blob_plain", -replay=>1)},
7296 "raw") .
7297 " | " .
7298 $cgi->a({-href => href(action=>"blob",
7299 hash_base=>"HEAD", file_name=>$file_name)},
7300 "HEAD");
7301 } else {
7302 $formats_nav .=
7303 $cgi->a({-href => href(action=>"blob_plain", -replay=>1)},
7304 "raw");
7306 git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
7307 git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
7308 } else {
7309 print "<div class=\"page_nav\">\n" .
7310 "<br/><br/></div>\n" .
7311 "<div class=\"title\">".esc_html($hash)."</div>\n";
7313 git_print_page_path($file_name, "blob", $hash_base);
7314 print "<div class=\"page_body\">\n";
7315 if ($mimetype =~ m!^image/!) {
7316 print qq!<img class="blob" type="!.esc_attr($mimetype).qq!"!;
7317 if ($file_name) {
7318 print qq! alt="!.esc_attr($file_name).qq!" title="!.esc_attr($file_name).qq!"!;
7320 print qq! src="! .
7321 href(action=>"blob_plain", hash=>$hash,
7322 hash_base=>$hash_base, file_name=>$file_name) .
7323 qq!" />\n!;
7324 } else {
7325 my $nr;
7326 while (my $line = <$fd>) {
7327 chomp $line;
7328 $nr++;
7329 $line = untabify($line);
7330 printf qq!<div class="pre"><a id="l%i" href="%s#l%i" class="linenr">%4i</a> %s</div>\n!,
7331 $nr, esc_attr(href(-replay => 1)), $nr, $nr,
7332 $highlight_mode_active ? sanitize($line) : esc_html($line, -nbsp=>1);
7335 close $fd
7336 or print "Reading blob failed.\n";
7337 print "</div>";
7338 git_footer_html();
7341 sub git_tree {
7342 if (!defined $hash_base) {
7343 $hash_base = "HEAD";
7345 if (!defined $hash) {
7346 if (defined $file_name) {
7347 $hash = git_get_hash_by_path($hash_base, $file_name, "tree");
7348 } else {
7349 $hash = $hash_base;
7352 die_error(404, "No such tree") unless defined($hash);
7354 my $show_sizes = gitweb_check_feature('show-sizes');
7355 my $have_blame = gitweb_check_feature('blame');
7357 my @entries = ();
7359 local $/ = "\0";
7360 defined(my $fd = git_cmd_pipe "ls-tree", '-z',
7361 ($show_sizes ? '-l' : ()), @extra_options, $hash)
7362 or die_error(500, "Open git-ls-tree failed");
7363 @entries = map { chomp; $_ } <$fd>;
7364 close $fd
7365 or die_error(404, "Reading tree failed");
7368 my $refs = git_get_references();
7369 my $ref = format_ref_marker($refs, $hash_base);
7370 git_header_html();
7371 my $basedir = '';
7372 if (defined $hash_base && (my %co = parse_commit($hash_base))) {
7373 my @views_nav = ();
7374 if (defined $file_name) {
7375 push @views_nav,
7376 $cgi->a({-href => href(action=>"history", -replay=>1)},
7377 "history"),
7378 $cgi->a({-href => href(action=>"tree",
7379 hash_base=>"HEAD", file_name=>$file_name)},
7380 "HEAD"),
7382 my $snapshot_links = format_snapshot_links($hash);
7383 if (defined $snapshot_links) {
7384 # FIXME: Should be available when we have no hash base as well.
7385 push @views_nav, $snapshot_links;
7387 git_print_page_nav('tree','', $hash_base, undef, undef,
7388 join(' | ', @views_nav));
7389 git_print_header_div('commit', esc_html($co{'title'}) . $ref, $hash_base);
7390 } else {
7391 undef $hash_base;
7392 print "<div class=\"page_nav\">\n";
7393 print "<br/><br/></div>\n";
7394 print "<div class=\"title\">".esc_html($hash)."</div>\n";
7396 if (defined $file_name) {
7397 $basedir = $file_name;
7398 if ($basedir ne '' && substr($basedir, -1) ne '/') {
7399 $basedir .= '/';
7401 git_print_page_path($file_name, 'tree', $hash_base);
7403 print "<div class=\"page_body\">\n";
7404 print "<table class=\"tree\">\n";
7405 my $alternate = 1;
7406 # '..' (top directory) link if possible
7407 if (defined $hash_base &&
7408 defined $file_name && $file_name =~ m![^/]+$!) {
7409 if ($alternate) {
7410 print "<tr class=\"dark\">\n";
7411 } else {
7412 print "<tr class=\"light\">\n";
7414 $alternate ^= 1;
7416 my $up = $file_name;
7417 $up =~ s!/?[^/]+$!!;
7418 undef $up unless $up;
7419 # based on git_print_tree_entry
7420 print '<td class="mode">' . mode_str('040000') . "</td>\n";
7421 print '<td class="size">&nbsp;</td>'."\n" if $show_sizes;
7422 print '<td class="list">';
7423 print $cgi->a({-href => href(action=>"tree",
7424 hash_base=>$hash_base,
7425 file_name=>$up)},
7426 "..");
7427 print "</td>\n";
7428 print "<td class=\"link\"></td>\n";
7430 print "</tr>\n";
7432 foreach my $line (@entries) {
7433 my %t = parse_ls_tree_line($line, -z => 1, -l => $show_sizes);
7435 if ($alternate) {
7436 print "<tr class=\"dark\">\n";
7437 } else {
7438 print "<tr class=\"light\">\n";
7440 $alternate ^= 1;
7442 git_print_tree_entry(\%t, $basedir, $hash_base, $have_blame);
7444 print "</tr>\n";
7446 print "</table>\n" .
7447 "</div>";
7448 git_footer_html();
7451 sub sanitize_for_filename {
7452 my $name = shift;
7454 $name =~ s!/!-!g;
7455 $name =~ s/[^[:alnum:]_.-]//g;
7457 return $name;
7460 sub snapshot_name {
7461 my ($project, $hash) = @_;
7463 # path/to/project.git -> project
7464 # path/to/project/.git -> project
7465 my $name = to_utf8($project);
7466 $name =~ s,([^/])/*\.git$,$1,;
7467 $name = sanitize_for_filename(basename($name));
7469 my $ver = $hash;
7470 if ($hash =~ /^[0-9a-fA-F]+$/) {
7471 # shorten SHA-1 hash
7472 my $full_hash = git_get_full_hash($project, $hash);
7473 if ($full_hash =~ /^$hash/ && length($hash) > 7) {
7474 $ver = git_get_short_hash($project, $hash);
7476 } elsif ($hash =~ m!^refs/tags/(.*)$!) {
7477 # tags don't need shortened SHA-1 hash
7478 $ver = $1;
7479 } else {
7480 # branches and other need shortened SHA-1 hash
7481 my $strip_refs = join '|', map { quotemeta } get_branch_refs();
7482 if ($hash =~ m!^refs/($strip_refs|remotes)/(.*)$!) {
7483 my $ref_dir = (defined $1) ? $1 : '';
7484 $ver = $2;
7486 $ref_dir = sanitize_for_filename($ref_dir);
7487 # for refs neither in heads nor remotes we want to
7488 # add a ref dir to archive name
7489 if ($ref_dir ne '' and $ref_dir ne 'heads' and $ref_dir ne 'remotes') {
7490 $ver = $ref_dir . '-' . $ver;
7493 $ver .= '-' . git_get_short_hash($project, $hash);
7495 # special case of sanitization for filename - we change
7496 # slashes to dots instead of dashes
7497 # in case of hierarchical branch names
7498 $ver =~ s!/!.!g;
7499 $ver =~ s/[^[:alnum:]_.-]//g;
7501 # name = project-version_string
7502 $name = "$name-$ver";
7504 return wantarray ? ($name, $name) : $name;
7507 sub exit_if_unmodified_since {
7508 my ($latest_epoch) = @_;
7509 our $cgi;
7511 my $if_modified = $cgi->http('IF_MODIFIED_SINCE');
7512 if (defined $if_modified) {
7513 my $since;
7514 if (eval { require HTTP::Date; 1; }) {
7515 $since = HTTP::Date::str2time($if_modified);
7516 } elsif (eval { require Time::ParseDate; 1; }) {
7517 $since = Time::ParseDate::parsedate($if_modified, GMT => 1);
7519 if (defined $since && $latest_epoch <= $since) {
7520 my %latest_date = parse_date($latest_epoch);
7521 print $cgi->header(
7522 -last_modified => $latest_date{'rfc2822'},
7523 -status => '304 Not Modified');
7524 goto DONE_GITWEB;
7529 sub git_snapshot {
7530 my $format = $input_params{'snapshot_format'};
7531 if (!@snapshot_fmts) {
7532 die_error(403, "Snapshots not allowed");
7534 # default to first supported snapshot format
7535 $format ||= $snapshot_fmts[0];
7536 if ($format !~ m/^[a-z0-9]+$/) {
7537 die_error(400, "Invalid snapshot format parameter");
7538 } elsif (!exists($known_snapshot_formats{$format})) {
7539 die_error(400, "Unknown snapshot format");
7540 } elsif ($known_snapshot_formats{$format}{'disabled'}) {
7541 die_error(403, "Snapshot format not allowed");
7542 } elsif (!grep($_ eq $format, @snapshot_fmts)) {
7543 die_error(403, "Unsupported snapshot format");
7546 my $type = git_get_type("$hash^{}");
7547 if (!$type) {
7548 die_error(404, 'Object does not exist');
7549 } elsif ($type eq 'blob') {
7550 die_error(400, 'Object is not a tree-ish');
7553 my ($name, $prefix) = snapshot_name($project, $hash);
7554 my $filename = "$name$known_snapshot_formats{$format}{'suffix'}";
7556 my %co = parse_commit($hash);
7557 exit_if_unmodified_since($co{'committer_epoch'}) if %co;
7559 my $cmd = quote_command(
7560 git_cmd(), 'archive',
7561 "--format=$known_snapshot_formats{$format}{'format'}",
7562 "--prefix=$prefix/", $hash);
7563 if (exists $known_snapshot_formats{$format}{'compressor'}) {
7564 $cmd .= ' | ' . quote_command(@{$known_snapshot_formats{$format}{'compressor'}});
7567 $filename =~ s/(["\\])/\\$1/g;
7568 my %latest_date;
7569 if (%co) {
7570 %latest_date = parse_date($co{'committer_epoch'}, $co{'committer_tz'});
7573 print $cgi->header(
7574 -type => $known_snapshot_formats{$format}{'type'},
7575 -content_disposition => 'inline; filename="' . $filename . '"',
7576 %co ? (-last_modified => $latest_date{'rfc2822'}) : (),
7577 -status => '200 OK');
7579 defined(my $fd = cmd_pipe $cmd)
7580 or die_error(500, "Execute git-archive failed");
7581 binmode STDOUT, ':raw';
7582 print <$fd>;
7583 binmode STDOUT, ':utf8'; # as set at the beginning of gitweb.cgi
7584 close $fd;
7587 sub git_log_generic {
7588 my ($fmt_name, $body_subr, $base, $parent, $file_name, $file_hash) = @_;
7590 my $head = git_get_head_hash($project);
7591 if (!defined $base) {
7592 $base = $head;
7594 if (!defined $page) {
7595 $page = 0;
7597 my $refs = git_get_references();
7599 my $commit_hash = $base;
7600 if (defined $parent) {
7601 $commit_hash = "$parent..$base";
7603 my @commitlist =
7604 parse_commits($commit_hash, 101, (100 * $page),
7605 defined $file_name ? ($file_name, "--full-history") : ());
7607 my $ftype;
7608 if (!defined $file_hash && defined $file_name) {
7609 # some commits could have deleted file in question,
7610 # and not have it in tree, but one of them has to have it
7611 for (my $i = 0; $i < @commitlist; $i++) {
7612 $file_hash = git_get_hash_by_path($commitlist[$i]{'id'}, $file_name);
7613 last if defined $file_hash;
7616 if (defined $file_hash) {
7617 $ftype = git_get_type($file_hash);
7619 if (defined $file_name && !defined $ftype) {
7620 die_error(500, "Unknown type of object");
7622 my %co;
7623 if (defined $file_name) {
7624 %co = parse_commit($base)
7625 or die_error(404, "Unknown commit object");
7629 my $paging_nav = format_paging_nav($fmt_name, $page, $#commitlist >= 100);
7630 my $next_link = '';
7631 if ($#commitlist >= 100) {
7632 $next_link =
7633 $cgi->a({-href => href(-replay=>1, page=>$page+1),
7634 -accesskey => "n", -title => "Alt-n"}, "next");
7636 my $patch_max = gitweb_get_feature('patches');
7637 if ($patch_max && !defined $file_name) {
7638 if ($patch_max < 0 || @commitlist <= $patch_max) {
7639 $paging_nav .= " &sdot; " .
7640 $cgi->a({-href => href(action=>"patches", -replay=>1)},
7641 "patches");
7645 git_header_html();
7646 git_print_page_nav($fmt_name,'', $hash,$hash,$hash, $paging_nav);
7647 if (defined $file_name) {
7648 git_print_header_div('commit', esc_html($co{'title'}), $base);
7649 } else {
7650 git_print_header_div('summary', $project)
7652 git_print_page_path($file_name, $ftype, $hash_base)
7653 if (defined $file_name);
7655 $body_subr->(\@commitlist, 0, 99, $refs, $next_link,
7656 $file_name, $file_hash, $ftype);
7658 git_footer_html();
7661 sub git_log {
7662 git_log_generic('log', \&git_log_body,
7663 $hash, $hash_parent);
7666 sub git_commit {
7667 $hash ||= $hash_base || "HEAD";
7668 my %co = parse_commit($hash)
7669 or die_error(404, "Unknown commit object");
7671 my $parent = $co{'parent'};
7672 my $parents = $co{'parents'}; # listref
7674 # we need to prepare $formats_nav before any parameter munging
7675 my $formats_nav;
7676 if (!defined $parent) {
7677 # --root commitdiff
7678 $formats_nav .= '(initial)';
7679 } elsif (@$parents == 1) {
7680 # single parent commit
7681 $formats_nav .=
7682 '(parent: ' .
7683 $cgi->a({-href => href(action=>"commit",
7684 hash=>$parent)},
7685 esc_html(substr($parent, 0, 7))) .
7686 ')';
7687 } else {
7688 # merge commit
7689 $formats_nav .=
7690 '(merge: ' .
7691 join(' ', map {
7692 $cgi->a({-href => href(action=>"commit",
7693 hash=>$_)},
7694 esc_html(substr($_, 0, 7)));
7695 } @$parents ) .
7696 ')';
7698 if (gitweb_check_feature('patches') && @$parents <= 1) {
7699 $formats_nav .= " | " .
7700 $cgi->a({-href => href(action=>"patch", -replay=>1)},
7701 "patch");
7704 if (!defined $parent) {
7705 $parent = "--root";
7707 my @difftree;
7708 defined(my $fd = git_cmd_pipe "diff-tree", '-r', "--no-commit-id",
7709 @diff_opts,
7710 (@$parents <= 1 ? $parent : '-c'),
7711 $hash, "--")
7712 or die_error(500, "Open git-diff-tree failed");
7713 @difftree = map { chomp; $_ } <$fd>;
7714 close $fd or die_error(404, "Reading git-diff-tree failed");
7716 # non-textual hash id's can be cached
7717 my $expires;
7718 if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
7719 $expires = "+1d";
7721 my $refs = git_get_references();
7722 my $ref = format_ref_marker($refs, $co{'id'});
7724 git_header_html(undef, $expires);
7725 git_print_page_nav('commit', '',
7726 $hash, $co{'tree'}, $hash,
7727 $formats_nav);
7729 if (defined $co{'parent'}) {
7730 git_print_header_div('commitdiff', esc_html($co{'title'}) . $ref, $hash);
7731 } else {
7732 git_print_header_div('tree', esc_html($co{'title'}) . $ref, $co{'tree'}, $hash);
7734 print "<div class=\"title_text\">\n" .
7735 "<table class=\"object_header\">\n";
7736 git_print_authorship_rows(\%co);
7737 print "<tr><td>commit</td><td class=\"sha1\">$co{'id'}</td></tr>\n";
7738 print "<tr>" .
7739 "<td>tree</td>" .
7740 "<td class=\"sha1\">" .
7741 $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$hash),
7742 class => "list"}, $co{'tree'}) .
7743 "</td>" .
7744 "<td class=\"link\">" .
7745 $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$hash)},
7746 "tree");
7747 my $snapshot_links = format_snapshot_links($hash);
7748 if (defined $snapshot_links) {
7749 print " | " . $snapshot_links;
7751 print "</td>" .
7752 "</tr>\n";
7754 foreach my $par (@$parents) {
7755 print "<tr>" .
7756 "<td>parent</td>" .
7757 "<td class=\"sha1\">" .
7758 $cgi->a({-href => href(action=>"commit", hash=>$par),
7759 class => "list"}, $par) .
7760 "</td>" .
7761 "<td class=\"link\">" .
7762 $cgi->a({-href => href(action=>"commit", hash=>$par)}, "commit") .
7763 " | " .
7764 $cgi->a({-href => href(action=>"commitdiff", hash=>$hash, hash_parent=>$par)}, "diff") .
7765 "</td>" .
7766 "</tr>\n";
7768 print "</table>".
7769 "</div>\n";
7771 print "<div class=\"page_body\">\n";
7772 git_print_log($co{'comment'});
7773 print "</div>\n";
7775 git_difftree_body(\@difftree, $hash, @$parents);
7777 git_footer_html();
7780 sub git_object {
7781 # object is defined by:
7782 # - hash or hash_base alone
7783 # - hash_base and file_name
7784 my $type;
7786 # - hash or hash_base alone
7787 if ($hash || ($hash_base && !defined $file_name)) {
7788 my $object_id = $hash || $hash_base;
7790 defined(my $fd = git_cmd_pipe 'cat-file', '-t', $object_id)
7791 or die_error(404, "Object does not exist");
7792 $type = <$fd>;
7793 chomp $type;
7794 close $fd
7795 or die_error(404, "Object does not exist");
7797 # - hash_base and file_name
7798 } elsif ($hash_base && defined $file_name) {
7799 $file_name =~ s,/+$,,;
7801 system(git_cmd(), "cat-file", '-e', $hash_base) == 0
7802 or die_error(404, "Base object does not exist");
7804 # here errors should not happen
7805 defined(my $fd = git_cmd_pipe "ls-tree", $hash_base, "--", $file_name)
7806 or die_error(500, "Open git-ls-tree failed");
7807 my $line = <$fd>;
7808 close $fd;
7810 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'
7811 unless ($line && $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t/) {
7812 die_error(404, "File or directory for given base does not exist");
7814 $type = $2;
7815 $hash = $3;
7816 } else {
7817 die_error(400, "Not enough information to find object");
7820 print $cgi->redirect(-uri => href(action=>$type, -full=>1,
7821 hash=>$hash, hash_base=>$hash_base,
7822 file_name=>$file_name),
7823 -status => '302 Found');
7826 sub git_blobdiff {
7827 my $format = shift || 'html';
7828 my $diff_style = $input_params{'diff_style'} || 'inline';
7830 my $fd;
7831 my @difftree;
7832 my %diffinfo;
7833 my $expires;
7835 # preparing $fd and %diffinfo for git_patchset_body
7836 # new style URI
7837 if (defined $hash_base && defined $hash_parent_base) {
7838 if (defined $file_name) {
7839 # read raw output
7840 defined($fd = git_cmd_pipe "diff-tree", '-r', @diff_opts,
7841 $hash_parent_base, $hash_base,
7842 "--", (defined $file_parent ? $file_parent : ()), $file_name)
7843 or die_error(500, "Open git-diff-tree failed");
7844 @difftree = map { chomp; $_ } <$fd>;
7845 close $fd
7846 or die_error(404, "Reading git-diff-tree failed");
7847 @difftree
7848 or die_error(404, "Blob diff not found");
7850 } elsif (defined $hash &&
7851 $hash =~ /[0-9a-fA-F]{40}/) {
7852 # try to find filename from $hash
7854 # read filtered raw output
7855 defined($fd = git_cmd_pipe "diff-tree", '-r', @diff_opts,
7856 $hash_parent_base, $hash_base, "--")
7857 or die_error(500, "Open git-diff-tree failed");
7858 @difftree =
7859 # ':100644 100644 03b21826... 3b93d5e7... M ls-files.c'
7860 # $hash == to_id
7861 grep { /^:[0-7]{6} [0-7]{6} [0-9a-fA-F]{40} $hash/ }
7862 map { chomp; $_ } <$fd>;
7863 close $fd
7864 or die_error(404, "Reading git-diff-tree failed");
7865 @difftree
7866 or die_error(404, "Blob diff not found");
7868 } else {
7869 die_error(400, "Missing one of the blob diff parameters");
7872 if (@difftree > 1) {
7873 die_error(400, "Ambiguous blob diff specification");
7876 %diffinfo = parse_difftree_raw_line($difftree[0]);
7877 $file_parent ||= $diffinfo{'from_file'} || $file_name;
7878 $file_name ||= $diffinfo{'to_file'};
7880 $hash_parent ||= $diffinfo{'from_id'};
7881 $hash ||= $diffinfo{'to_id'};
7883 # non-textual hash id's can be cached
7884 if ($hash_base =~ m/^[0-9a-fA-F]{40}$/ &&
7885 $hash_parent_base =~ m/^[0-9a-fA-F]{40}$/) {
7886 $expires = '+1d';
7889 # open patch output
7890 defined($fd = git_cmd_pipe "diff-tree", '-r', @diff_opts,
7891 '-p', ($format eq 'html' ? "--full-index" : ()),
7892 $hash_parent_base, $hash_base,
7893 "--", (defined $file_parent ? $file_parent : ()), $file_name)
7894 or die_error(500, "Open git-diff-tree failed");
7897 # old/legacy style URI -- not generated anymore since 1.4.3.
7898 if (!%diffinfo) {
7899 die_error('404 Not Found', "Missing one of the blob diff parameters")
7902 # header
7903 if ($format eq 'html') {
7904 my $formats_nav =
7905 $cgi->a({-href => href(action=>"blobdiff_plain", -replay=>1)},
7906 "raw");
7907 $formats_nav .= diff_style_nav($diff_style);
7908 git_header_html(undef, $expires);
7909 if (defined $hash_base && (my %co = parse_commit($hash_base))) {
7910 git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
7911 git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
7912 } else {
7913 print "<div class=\"page_nav\"><br/>$formats_nav<br/></div>\n";
7914 print "<div class=\"title\">".esc_html("$hash vs $hash_parent")."</div>\n";
7916 if (defined $file_name) {
7917 git_print_page_path($file_name, "blob", $hash_base);
7918 } else {
7919 print "<div class=\"page_path\"></div>\n";
7922 } elsif ($format eq 'plain') {
7923 print $cgi->header(
7924 -type => 'text/plain',
7925 -charset => 'utf-8',
7926 -expires => $expires,
7927 -content_disposition => 'inline; filename="' . "$file_name" . '.patch"');
7929 print "X-Git-Url: " . $cgi->self_url() . "\n\n";
7931 } else {
7932 die_error(400, "Unknown blobdiff format");
7935 # patch
7936 if ($format eq 'html') {
7937 print "<div class=\"page_body\">\n";
7939 git_patchset_body($fd, $diff_style,
7940 [ \%diffinfo ], $hash_base, $hash_parent_base);
7941 close $fd;
7943 print "</div>\n"; # class="page_body"
7944 git_footer_html();
7946 } else {
7947 while (my $line = <$fd>) {
7948 $line =~ s!a/($hash|$hash_parent)!'a/'.esc_path($diffinfo{'from_file'})!eg;
7949 $line =~ s!b/($hash|$hash_parent)!'b/'.esc_path($diffinfo{'to_file'})!eg;
7951 print $line;
7953 last if $line =~ m!^\+\+\+!;
7955 local $/ = undef;
7956 print <$fd>;
7957 close $fd;
7961 sub git_blobdiff_plain {
7962 git_blobdiff('plain');
7965 # assumes that it is added as later part of already existing navigation,
7966 # so it returns "| foo | bar" rather than just "foo | bar"
7967 sub diff_style_nav {
7968 my ($diff_style, $is_combined) = @_;
7969 $diff_style ||= 'inline';
7971 return "" if ($is_combined);
7973 my @styles = (inline => 'inline', 'sidebyside' => 'side by side');
7974 my %styles = @styles;
7975 @styles =
7976 @styles[ map { $_ * 2 } 0..$#styles/2 ];
7978 return join '',
7979 map { " | ".$_ }
7980 map {
7981 $_ eq $diff_style ? $styles{$_} :
7982 $cgi->a({-href => href(-replay=>1, diff_style => $_)}, $styles{$_})
7983 } @styles;
7986 sub git_commitdiff {
7987 my %params = @_;
7988 my $format = $params{-format} || 'html';
7989 my $diff_style = $input_params{'diff_style'} || 'inline';
7991 my ($patch_max) = gitweb_get_feature('patches');
7992 if ($format eq 'patch') {
7993 die_error(403, "Patch view not allowed") unless $patch_max;
7996 $hash ||= $hash_base || "HEAD";
7997 my %co = parse_commit($hash)
7998 or die_error(404, "Unknown commit object");
8000 # choose format for commitdiff for merge
8001 if (! defined $hash_parent && @{$co{'parents'}} > 1) {
8002 $hash_parent = '--cc';
8004 # we need to prepare $formats_nav before almost any parameter munging
8005 my $formats_nav;
8006 if ($format eq 'html') {
8007 $formats_nav =
8008 $cgi->a({-href => href(action=>"commitdiff_plain", -replay=>1)},
8009 "raw");
8010 if ($patch_max && @{$co{'parents'}} <= 1) {
8011 $formats_nav .= " | " .
8012 $cgi->a({-href => href(action=>"patch", -replay=>1)},
8013 "patch");
8015 $formats_nav .= diff_style_nav($diff_style, @{$co{'parents'}} > 1);
8017 if (defined $hash_parent &&
8018 $hash_parent ne '-c' && $hash_parent ne '--cc') {
8019 # commitdiff with two commits given
8020 my $hash_parent_short = $hash_parent;
8021 if ($hash_parent =~ m/^[0-9a-fA-F]{40}$/) {
8022 $hash_parent_short = substr($hash_parent, 0, 7);
8024 $formats_nav .=
8025 ' (from';
8026 for (my $i = 0; $i < @{$co{'parents'}}; $i++) {
8027 if ($co{'parents'}[$i] eq $hash_parent) {
8028 $formats_nav .= ' parent ' . ($i+1);
8029 last;
8032 $formats_nav .= ': ' .
8033 $cgi->a({-href => href(-replay=>1,
8034 hash=>$hash_parent, hash_base=>undef)},
8035 esc_html($hash_parent_short)) .
8036 ')';
8037 } elsif (!$co{'parent'}) {
8038 # --root commitdiff
8039 $formats_nav .= ' (initial)';
8040 } elsif (scalar @{$co{'parents'}} == 1) {
8041 # single parent commit
8042 $formats_nav .=
8043 ' (parent: ' .
8044 $cgi->a({-href => href(-replay=>1,
8045 hash=>$co{'parent'}, hash_base=>undef)},
8046 esc_html(substr($co{'parent'}, 0, 7))) .
8047 ')';
8048 } else {
8049 # merge commit
8050 if ($hash_parent eq '--cc') {
8051 $formats_nav .= ' | ' .
8052 $cgi->a({-href => href(-replay=>1,
8053 hash=>$hash, hash_parent=>'-c')},
8054 'combined');
8055 } else { # $hash_parent eq '-c'
8056 $formats_nav .= ' | ' .
8057 $cgi->a({-href => href(-replay=>1,
8058 hash=>$hash, hash_parent=>'--cc')},
8059 'compact');
8061 $formats_nav .=
8062 ' (merge: ' .
8063 join(' ', map {
8064 $cgi->a({-href => href(-replay=>1,
8065 hash=>$_, hash_base=>undef)},
8066 esc_html(substr($_, 0, 7)));
8067 } @{$co{'parents'}} ) .
8068 ')';
8072 my $hash_parent_param = $hash_parent;
8073 if (!defined $hash_parent_param) {
8074 # --cc for multiple parents, --root for parentless
8075 $hash_parent_param =
8076 @{$co{'parents'}} > 1 ? '--cc' : $co{'parent'} || '--root';
8079 # read commitdiff
8080 my $fd;
8081 my @difftree;
8082 if ($format eq 'html') {
8083 defined($fd = git_cmd_pipe "diff-tree", '-r', @diff_opts,
8084 "--no-commit-id", "--patch-with-raw", "--full-index",
8085 $hash_parent_param, $hash, "--")
8086 or die_error(500, "Open git-diff-tree failed");
8088 while (my $line = <$fd>) {
8089 chomp $line;
8090 # empty line ends raw part of diff-tree output
8091 last unless $line;
8092 push @difftree, scalar parse_difftree_raw_line($line);
8095 } elsif ($format eq 'plain') {
8096 defined($fd = git_cmd_pipe "diff-tree", '-r', @diff_opts,
8097 '-p', $hash_parent_param, $hash, "--")
8098 or die_error(500, "Open git-diff-tree failed");
8099 } elsif ($format eq 'patch') {
8100 # For commit ranges, we limit the output to the number of
8101 # patches specified in the 'patches' feature.
8102 # For single commits, we limit the output to a single patch,
8103 # diverging from the git-format-patch default.
8104 my @commit_spec = ();
8105 if ($hash_parent) {
8106 if ($patch_max > 0) {
8107 push @commit_spec, "-$patch_max";
8109 push @commit_spec, '-n', "$hash_parent..$hash";
8110 } else {
8111 if ($params{-single}) {
8112 push @commit_spec, '-1';
8113 } else {
8114 if ($patch_max > 0) {
8115 push @commit_spec, "-$patch_max";
8117 push @commit_spec, "-n";
8119 push @commit_spec, '--root', $hash;
8121 defined($fd = git_cmd_pipe "format-patch", @diff_opts,
8122 '--encoding=utf8', '--stdout', @commit_spec)
8123 or die_error(500, "Open git-format-patch failed");
8124 } else {
8125 die_error(400, "Unknown commitdiff format");
8128 # non-textual hash id's can be cached
8129 my $expires;
8130 if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
8131 $expires = "+1d";
8134 # write commit message
8135 if ($format eq 'html') {
8136 my $refs = git_get_references();
8137 my $ref = format_ref_marker($refs, $co{'id'});
8139 git_header_html(undef, $expires);
8140 git_print_page_nav('commitdiff','', $hash,$co{'tree'},$hash, $formats_nav);
8141 git_print_header_div('commit', esc_html($co{'title'}) . $ref, $hash);
8142 print "<div class=\"title_text\">\n" .
8143 "<table class=\"object_header\">\n";
8144 git_print_authorship_rows(\%co);
8145 print "</table>".
8146 "</div>\n";
8147 print "<div class=\"page_body\">\n";
8148 if (@{$co{'comment'}} > 1) {
8149 print "<div class=\"log\">\n";
8150 git_print_log($co{'comment'}, -final_empty_line=> 1, -remove_title => 1);
8151 print "</div>\n"; # class="log"
8154 } elsif ($format eq 'plain') {
8155 my $refs = git_get_references("tags");
8156 my $tagname = git_get_rev_name_tags($hash);
8157 my $filename = basename($project) . "-$hash.patch";
8159 print $cgi->header(
8160 -type => 'text/plain',
8161 -charset => 'utf-8',
8162 -expires => $expires,
8163 -content_disposition => 'inline; filename="' . "$filename" . '"');
8164 my %ad = parse_date($co{'author_epoch'}, $co{'author_tz'});
8165 print "From: " . to_utf8($co{'author'}) . "\n";
8166 print "Date: $ad{'rfc2822'} ($ad{'tz_local'})\n";
8167 print "Subject: " . to_utf8($co{'title'}) . "\n";
8169 print "X-Git-Tag: $tagname\n" if $tagname;
8170 print "X-Git-Url: " . $cgi->self_url() . "\n\n";
8172 foreach my $line (@{$co{'comment'}}) {
8173 print to_utf8($line) . "\n";
8175 print "---\n\n";
8176 } elsif ($format eq 'patch') {
8177 my $filename = basename($project) . "-$hash.patch";
8179 print $cgi->header(
8180 -type => 'text/plain',
8181 -charset => 'utf-8',
8182 -expires => $expires,
8183 -content_disposition => 'inline; filename="' . "$filename" . '"');
8186 # write patch
8187 if ($format eq 'html') {
8188 my $use_parents = !defined $hash_parent ||
8189 $hash_parent eq '-c' || $hash_parent eq '--cc';
8190 git_difftree_body(\@difftree, $hash,
8191 $use_parents ? @{$co{'parents'}} : $hash_parent);
8192 print "<br/>\n";
8194 git_patchset_body($fd, $diff_style,
8195 \@difftree, $hash,
8196 $use_parents ? @{$co{'parents'}} : $hash_parent);
8197 close $fd;
8198 print "</div>\n"; # class="page_body"
8199 git_footer_html();
8201 } elsif ($format eq 'plain') {
8202 local $/ = undef;
8203 print <$fd>;
8204 close $fd
8205 or print "Reading git-diff-tree failed\n";
8206 } elsif ($format eq 'patch') {
8207 local $/ = undef;
8208 print <$fd>;
8209 close $fd
8210 or print "Reading git-format-patch failed\n";
8214 sub git_commitdiff_plain {
8215 git_commitdiff(-format => 'plain');
8218 # format-patch-style patches
8219 sub git_patch {
8220 git_commitdiff(-format => 'patch', -single => 1);
8223 sub git_patches {
8224 git_commitdiff(-format => 'patch');
8227 sub git_history {
8228 git_log_generic('history', \&git_history_body,
8229 $hash_base, $hash_parent_base,
8230 $file_name, $hash);
8233 sub git_search {
8234 $searchtype ||= 'commit';
8236 # check if appropriate features are enabled
8237 gitweb_check_feature('search')
8238 or die_error(403, "Search is disabled");
8239 if ($searchtype eq 'pickaxe') {
8240 # pickaxe may take all resources of your box and run for several minutes
8241 # with every query - so decide by yourself how public you make this feature
8242 gitweb_check_feature('pickaxe')
8243 or die_error(403, "Pickaxe search is disabled");
8245 if ($searchtype eq 'grep') {
8246 # grep search might be potentially CPU-intensive, too
8247 gitweb_check_feature('grep')
8248 or die_error(403, "Grep search is disabled");
8251 if (!defined $searchtext) {
8252 die_error(400, "Text field is empty");
8254 if (!defined $hash) {
8255 $hash = git_get_head_hash($project);
8257 my %co = parse_commit($hash);
8258 if (!%co) {
8259 die_error(404, "Unknown commit object");
8261 if (!defined $page) {
8262 $page = 0;
8265 if ($searchtype eq 'commit' ||
8266 $searchtype eq 'author' ||
8267 $searchtype eq 'committer') {
8268 git_search_message(%co);
8269 } elsif ($searchtype eq 'pickaxe') {
8270 git_search_changes(%co);
8271 } elsif ($searchtype eq 'grep') {
8272 git_search_files(%co);
8273 } else {
8274 die_error(400, "Unknown search type");
8278 sub git_search_help {
8279 git_header_html();
8280 git_print_page_nav('','', $hash,$hash,$hash);
8281 print <<EOT;
8282 <p><strong>Pattern</strong> is by default a normal string that is matched precisely (but without
8283 regard to case, except in the case of pickaxe). However, when you check the <em>re</em> checkbox,
8284 the pattern entered is recognized as the POSIX extended
8285 <a href="http://en.wikipedia.org/wiki/Regular_expression">regular expression</a> (also case
8286 insensitive).</p>
8287 <dl>
8288 <dt><b>commit</b></dt>
8289 <dd>The commit messages and authorship information will be scanned for the given pattern.</dd>
8291 my $have_grep = gitweb_check_feature('grep');
8292 if ($have_grep) {
8293 print <<EOT;
8294 <dt><b>grep</b></dt>
8295 <dd>All files in the currently selected tree (HEAD unless you are explicitly browsing
8296 a different one) are searched for the given pattern. On large trees, this search can take
8297 a while and put some strain on the server, so please use it with some consideration. Note that
8298 due to git-grep peculiarity, currently if regexp mode is turned off, the matches are
8299 case-sensitive.</dd>
8302 print <<EOT;
8303 <dt><b>author</b></dt>
8304 <dd>Name and e-mail of the change author and date of birth of the patch will be scanned for the given pattern.</dd>
8305 <dt><b>committer</b></dt>
8306 <dd>Name and e-mail of the committer and date of commit will be scanned for the given pattern.</dd>
8308 my $have_pickaxe = gitweb_check_feature('pickaxe');
8309 if ($have_pickaxe) {
8310 print <<EOT;
8311 <dt><b>pickaxe</b></dt>
8312 <dd>All commits that caused the string to appear or disappear from any file (changes that
8313 added, removed or "modified" the string) will be listed. This search can take a while and
8314 takes a lot of strain on the server, so please use it wisely. Note that since you may be
8315 interested even in changes just changing the case as well, this search is case sensitive.</dd>
8318 print "</dl>\n";
8319 git_footer_html();
8322 sub git_shortlog {
8323 git_log_generic('shortlog', \&git_shortlog_body,
8324 $hash, $hash_parent);
8327 ## ......................................................................
8328 ## feeds (RSS, Atom; OPML)
8330 sub git_feed {
8331 my $format = shift || 'atom';
8332 my $have_blame = gitweb_check_feature('blame');
8334 # Atom: http://www.atomenabled.org/developers/syndication/
8335 # RSS: http://www.notestips.com/80256B3A007F2692/1/NAMO5P9UPQ
8336 if ($format ne 'rss' && $format ne 'atom') {
8337 die_error(400, "Unknown web feed format");
8340 # log/feed of current (HEAD) branch, log of given branch, history of file/directory
8341 my $head = $hash || 'HEAD';
8342 my @commitlist = parse_commits($head, 150, 0, $file_name);
8344 my %latest_commit;
8345 my %latest_date;
8346 my $content_type = "application/$format+xml";
8347 if (defined $cgi->http('HTTP_ACCEPT') &&
8348 $cgi->Accept('text/xml') > $cgi->Accept($content_type)) {
8349 # browser (feed reader) prefers text/xml
8350 $content_type = 'text/xml';
8352 if (defined($commitlist[0])) {
8353 %latest_commit = %{$commitlist[0]};
8354 my $latest_epoch = $latest_commit{'committer_epoch'};
8355 exit_if_unmodified_since($latest_epoch);
8356 %latest_date = parse_date($latest_epoch, $latest_commit{'committer_tz'});
8358 print $cgi->header(
8359 -type => $content_type,
8360 -charset => 'utf-8',
8361 %latest_date ? (-last_modified => $latest_date{'rfc2822'}) : (),
8362 -status => '200 OK');
8364 # Optimization: skip generating the body if client asks only
8365 # for Last-Modified date.
8366 return if ($cgi->request_method() eq 'HEAD');
8368 # header variables
8369 my $title = "$site_name - $project/$action";
8370 my $feed_type = 'log';
8371 if (defined $hash) {
8372 $title .= " - '$hash'";
8373 $feed_type = 'branch log';
8374 if (defined $file_name) {
8375 $title .= " :: $file_name";
8376 $feed_type = 'history';
8378 } elsif (defined $file_name) {
8379 $title .= " - $file_name";
8380 $feed_type = 'history';
8382 $title .= " $feed_type";
8383 $title = esc_html($title);
8384 my $descr = git_get_project_description($project);
8385 if (defined $descr) {
8386 $descr = esc_html($descr);
8387 } else {
8388 $descr = "$project " .
8389 ($format eq 'rss' ? 'RSS' : 'Atom') .
8390 " feed";
8392 my $owner = git_get_project_owner($project);
8393 $owner = esc_html($owner);
8395 #header
8396 my $alt_url;
8397 if (defined $file_name) {
8398 $alt_url = href(-full=>1, action=>"history", hash=>$hash, file_name=>$file_name);
8399 } elsif (defined $hash) {
8400 $alt_url = href(-full=>1, action=>"log", hash=>$hash);
8401 } else {
8402 $alt_url = href(-full=>1, action=>"summary");
8404 print qq!<?xml version="1.0" encoding="utf-8"?>\n!;
8405 if ($format eq 'rss') {
8406 print <<XML;
8407 <rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
8408 <channel>
8410 print "<title>$title</title>\n" .
8411 "<link>$alt_url</link>\n" .
8412 "<description>$descr</description>\n" .
8413 "<language>en</language>\n" .
8414 # project owner is responsible for 'editorial' content
8415 "<managingEditor>$owner</managingEditor>\n";
8416 if (defined $logo || defined $favicon) {
8417 # prefer the logo to the favicon, since RSS
8418 # doesn't allow both
8419 my $img = esc_url($logo || $favicon);
8420 print "<image>\n" .
8421 "<url>$img</url>\n" .
8422 "<title>$title</title>\n" .
8423 "<link>$alt_url</link>\n" .
8424 "</image>\n";
8426 if (%latest_date) {
8427 print "<pubDate>$latest_date{'rfc2822'}</pubDate>\n";
8428 print "<lastBuildDate>$latest_date{'rfc2822'}</lastBuildDate>\n";
8430 print "<generator>gitweb v.$version/$git_version</generator>\n";
8431 } elsif ($format eq 'atom') {
8432 print <<XML;
8433 <feed xmlns="http://www.w3.org/2005/Atom">
8435 print "<title>$title</title>\n" .
8436 "<subtitle>$descr</subtitle>\n" .
8437 '<link rel="alternate" type="text/html" href="' .
8438 $alt_url . '" />' . "\n" .
8439 '<link rel="self" type="' . $content_type . '" href="' .
8440 $cgi->self_url() . '" />' . "\n" .
8441 "<id>" . href(-full=>1) . "</id>\n" .
8442 # use project owner for feed author
8443 "<author><name>$owner</name></author>\n";
8444 if (defined $favicon) {
8445 print "<icon>" . esc_url($favicon) . "</icon>\n";
8447 if (defined $logo) {
8448 # not twice as wide as tall: 72 x 27 pixels
8449 print "<logo>" . esc_url($logo) . "</logo>\n";
8451 if (! %latest_date) {
8452 # dummy date to keep the feed valid until commits trickle in:
8453 print "<updated>1970-01-01T00:00:00Z</updated>\n";
8454 } else {
8455 print "<updated>$latest_date{'iso-8601'}</updated>\n";
8457 print "<generator version='$version/$git_version'>gitweb</generator>\n";
8460 # contents
8461 for (my $i = 0; $i <= $#commitlist; $i++) {
8462 my %co = %{$commitlist[$i]};
8463 my $commit = $co{'id'};
8464 # we read 150, we always show 30 and the ones more recent than 48 hours
8465 if (($i >= 20) && ((time - $co{'author_epoch'}) > 48*60*60)) {
8466 last;
8468 my %cd = parse_date($co{'author_epoch'}, $co{'author_tz'});
8470 # get list of changed files
8471 defined(my $fd = git_cmd_pipe "diff-tree", '-r', @diff_opts,
8472 $co{'parent'} || "--root",
8473 $co{'id'}, "--", (defined $file_name ? $file_name : ()))
8474 or next;
8475 my @difftree = map { chomp; $_ } <$fd>;
8476 close $fd
8477 or next;
8479 # print element (entry, item)
8480 my $co_url = href(-full=>1, action=>"commitdiff", hash=>$commit);
8481 if ($format eq 'rss') {
8482 print "<item>\n" .
8483 "<title>" . esc_html($co{'title'}) . "</title>\n" .
8484 "<author>" . esc_html($co{'author'}) . "</author>\n" .
8485 "<pubDate>$cd{'rfc2822'}</pubDate>\n" .
8486 "<guid isPermaLink=\"true\">$co_url</guid>\n" .
8487 "<link>$co_url</link>\n" .
8488 "<description>" . esc_html($co{'title'}) . "</description>\n" .
8489 "<content:encoded>" .
8490 "<![CDATA[\n";
8491 } elsif ($format eq 'atom') {
8492 print "<entry>\n" .
8493 "<title type=\"html\">" . esc_html($co{'title'}) . "</title>\n" .
8494 "<updated>$cd{'iso-8601'}</updated>\n" .
8495 "<author>\n" .
8496 " <name>" . esc_html($co{'author_name'}) . "</name>\n";
8497 if ($co{'author_email'}) {
8498 print " <email>" . esc_html($co{'author_email'}) . "</email>\n";
8500 print "</author>\n" .
8501 # use committer for contributor
8502 "<contributor>\n" .
8503 " <name>" . esc_html($co{'committer_name'}) . "</name>\n";
8504 if ($co{'committer_email'}) {
8505 print " <email>" . esc_html($co{'committer_email'}) . "</email>\n";
8507 print "</contributor>\n" .
8508 "<published>$cd{'iso-8601'}</published>\n" .
8509 "<link rel=\"alternate\" type=\"text/html\" href=\"$co_url\" />\n" .
8510 "<id>$co_url</id>\n" .
8511 "<content type=\"xhtml\" xml:base=\"" . esc_url($my_url) . "\">\n" .
8512 "<div xmlns=\"http://www.w3.org/1999/xhtml\">\n";
8514 my $comment = $co{'comment'};
8515 print "<pre>\n";
8516 foreach my $line (@$comment) {
8517 $line = esc_html($line);
8518 print "$line\n";
8520 print "</pre><ul>\n";
8521 foreach my $difftree_line (@difftree) {
8522 my %difftree = parse_difftree_raw_line($difftree_line);
8523 next if !$difftree{'from_id'};
8525 my $file = $difftree{'file'} || $difftree{'to_file'};
8527 print "<li>" .
8528 "[" .
8529 $cgi->a({-href => href(-full=>1, action=>"blobdiff",
8530 hash=>$difftree{'to_id'}, hash_parent=>$difftree{'from_id'},
8531 hash_base=>$co{'id'}, hash_parent_base=>$co{'parent'},
8532 file_name=>$file, file_parent=>$difftree{'from_file'}),
8533 -title => "diff"}, 'D');
8534 if ($have_blame) {
8535 print $cgi->a({-href => href(-full=>1, action=>"blame",
8536 file_name=>$file, hash_base=>$commit),
8537 -title => "blame"}, 'B');
8539 # if this is not a feed of a file history
8540 if (!defined $file_name || $file_name ne $file) {
8541 print $cgi->a({-href => href(-full=>1, action=>"history",
8542 file_name=>$file, hash=>$commit),
8543 -title => "history"}, 'H');
8545 $file = esc_path($file);
8546 print "] ".
8547 "$file</li>\n";
8549 if ($format eq 'rss') {
8550 print "</ul>]]>\n" .
8551 "</content:encoded>\n" .
8552 "</item>\n";
8553 } elsif ($format eq 'atom') {
8554 print "</ul>\n</div>\n" .
8555 "</content>\n" .
8556 "</entry>\n";
8560 # end of feed
8561 if ($format eq 'rss') {
8562 print "</channel>\n</rss>\n";
8563 } elsif ($format eq 'atom') {
8564 print "</feed>\n";
8568 sub git_rss {
8569 git_feed('rss');
8572 sub git_atom {
8573 git_feed('atom');
8576 sub git_opml {
8577 my @list = git_get_projects_list($project_filter, $strict_export);
8578 if (!@list) {
8579 die_error(404, "No projects found");
8582 print $cgi->header(
8583 -type => 'text/xml',
8584 -charset => 'utf-8',
8585 -content_disposition => 'inline; filename="opml.xml"');
8587 my $title = esc_html($site_name);
8588 my $filter = " within subdirectory ";
8589 if (defined $project_filter) {
8590 $filter .= esc_html($project_filter);
8591 } else {
8592 $filter = "";
8594 print <<XML;
8595 <?xml version="1.0" encoding="utf-8"?>
8596 <opml version="1.0">
8597 <head>
8598 <title>$title OPML Export$filter</title>
8599 </head>
8600 <body>
8601 <outline text="git RSS feeds">
8604 foreach my $pr (@list) {
8605 my %proj = %$pr;
8606 my $head = git_get_head_hash($proj{'path'});
8607 if (!defined $head) {
8608 next;
8610 $git_dir = "$projectroot/$proj{'path'}";
8611 my %co = parse_commit($head);
8612 if (!%co) {
8613 next;
8616 my $path = esc_html(chop_str($proj{'path'}, 25, 5));
8617 my $rss = href('project' => $proj{'path'}, 'action' => 'rss', -full => 1);
8618 my $html = href('project' => $proj{'path'}, 'action' => 'summary', -full => 1);
8619 print "<outline type=\"rss\" text=\"$path\" title=\"$path\" xmlUrl=\"$rss\" htmlUrl=\"$html\"/>\n";
8621 print <<XML;
8622 </outline>
8623 </body>
8624 </opml>