3 # gitweb - simple web interface to track changes in git repositories
5 # (C) 2005-2006, Kay Sievers <kay.sievers@vrfy.org>
6 # (C) 2005, Christian Gierke
8 # This program is licensed under the GPLv2
13 use CGI
qw(:standard :escapeHTML -nosticky);
14 use CGI
::Util
qw(unescape);
15 use CGI
::Carp
qw(fatalsToBrowser set_message);
19 use File
::Basename
qw(basename);
21 use Time
::HiRes
qw(gettimeofday tv_interval);
22 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;
32 CGI
->compile() if $ENV{'MOD_PERL'};
35 our $version = "++GIT_VERSION++";
37 our ($my_url, $my_uri, $base_url, $path_info, $home_link);
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
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
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"});
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
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++";
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
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
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 = (
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)}
214 'display' => 'tar.gz',
215 'type' => 'application/x-gzip',
216 'suffix' => '.tar.gz',
218 'compressor' => ['gzip', '-n']},
221 'display' => 'tar.bz2',
222 'type' => 'application/x-bzip2',
223 'suffix' => '.tar.bz2',
225 'compressor' => ['bzip2']},
228 'display' => 'tar.xz',
229 'type' => 'application/x-xz',
230 'suffix' => '.tar.xz',
232 'compressor' => ['xz'],
237 'type' => 'application/x-zip',
242 # Aliases so we understand old gitweb.snapshot values in repository
244 our %known_snapshot_format_aliases = (
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
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.
269 # configuration for 'highlight' (http://www.andre-simon.de/)
271 our %highlight_basename = (
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 ],
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
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.
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
423 # use gitweb_get_feature(<feature>) to retrieve the <feature> value
424 # (an array) or gitweb_check_feature(<feature>) to check if <feature>
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;
436 'sub' => sub { feature_bool
('blame', @_) },
440 # Enable the 'snapshot' link, providing a compressed archive of any
441 # tree. This can potentially generate high traffic if you have large
444 # Value is a list of formats defined in %known_snapshot_formats that
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;
453 'sub' => \
&feature_snapshot
,
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
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;
479 'sub' => sub { feature_bool
('grep', @_) },
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;
494 'sub' => sub { feature_bool
('pickaxe', @_) },
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;
507 'sub' => sub { feature_bool
('showsizes', @_) },
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
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.
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.
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.
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
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.
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.
597 'sub' => \
&feature_patches
,
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>;
618 'sub' => \
&feature_avatar
,
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.
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' => {
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' => {
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];
657 'sub' => sub { feature_bool
('highlight', @_) },
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;
669 'sub' => sub { feature_bool
('remote_heads', @_) },
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
,
688 sub gitweb_get_feature
{
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) {
701 warn "feature $name is not overridable";
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;
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];
725 my ($val) = git_get_project_config
($key, '--bool');
729 } elsif ($val eq 'true') {
731 } elsif ($val eq 'false') {
736 sub feature_snapshot
{
739 my ($val) = git_get_project_config
('snapshot');
742 @fmts = ($val eq 'none' ?
() : split /\s*[,\s]\s*/, $val);
748 sub feature_patches
{
749 my @val = (git_get_project_config
('patches', '--int'));
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');
769 $values = config_to_multi
($values);
771 foreach my $value (@
{$values}) {
772 push @branch_refs, split /\s+/, $value;
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
782 sub check_head_link
{
784 my $headfile = "$dir/HEAD";
785 return ((-e
$headfile) ||
786 (-l
$headfile && readlink($headfile) =~ /^refs\/heads\
//));
789 sub check_export_ok
{
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
{
802 exists $known_snapshot_format_aliases{$_} ?
803 $known_snapshot_format_aliases{$_} : $_} @fmts;
805 exists $known_snapshot_formats{$_} &&
806 !$known_snapshot_formats{$_}{'disabled'}} @fmts;
809 sub filter_and_validate_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
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.
871 if( -e
'/proc/loadavg' ){
872 open my $fd, '<', '/proc/loadavg'
874 my @load = split(/\s+/, scalar <$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
889 # version of the core git binary
891 sub evaluate_git_version
{
892 our $git_version = qx("$GIT" --version
) =~ m/git version (.*)$/ ?
$1 : "unknown";
893 $number_of_git_cmds++;
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,
920 our @cgi_param_mapping = (
928 hash_parent_base
=> "hpb",
933 snapshot_format
=> "sf",
934 extra_options
=> "opt",
935 search_use_regexp
=> "sr",
938 project_filter
=> "pf",
939 # this must be last entry (for manipulation from JavaScript)
942 our %cgi_param_mapping = @cgi_param_mapping;
944 # we will also need to know the possible actions, for validation
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
,
960 "patch" => \
&git_patch
,
961 "patches" => \
&git_patches
,
962 "remotes" => \
&git_remotes
,
964 "atom" => \
&git_atom
,
965 "search" => \
&git_search
,
966 "search_help" => \
&git_search_help
,
967 "shortlog" => \
&git_shortlog
,
968 "summary" => \
&git_summary
,
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
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
{
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) ];
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
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
1052 $pathname =~ s
,^/+,,;
1053 if (!$pathname || substr($pathname, -1) eq "/") {
1054 $input_params{'action'} ||= "tree";
1055 $pathname =~ s
,/$,,;
1057 # the default action depends on whether we had parent info
1059 if ($parentrefname) {
1060 $input_params{'action'} ||= "blobdiff_plain";
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;
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;
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;
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
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)$//) {
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;
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)) {
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
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");
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'",
1267 $search_regexp = quotemeta $searchtext;
1272 # path to the current git repository
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') {
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
);
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';
1332 $action = 'project_list';
1335 if (!defined($actions{$action})) {
1336 die_error
(400, "Unknown action");
1338 if ($action !~ m/^(?:opml|project_list|project_index)$/ &&
1340 die_error
(400, "Project needed");
1342 $actions{$action}->();
1346 our $t0 = [ gettimeofday
() ]
1348 our $number_of_git_cmds = 0;
1351 our $first_request = 1;
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
();
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
();
1377 configure_gitweb_features
();
1382 our $is_last_request = sub { 1 };
1383 our ($pre_dispatch_hook, $post_dispatch_hook, $pre_listen_hook);
1386 sub configure_as_fcgi
{
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 };
1395 my $script_name = $ENV{'SCRIPT_NAME'} || $ENV{'SCRIPT_FILENAME'} || __FILE__
;
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() };
1421 $pre_listen_hook->()
1422 if $pre_listen_hook;
1425 while ($cgi = $CGI->new()) {
1426 $pre_dispatch_hook->()
1427 if $pre_dispatch_hook;
1431 $post_dispatch_hook->()
1432 if $post_dispatch_hook;
1435 last REQUEST
if ($is_last_request->());
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
1449 # pure CGI script, serving single request
1453 ## ======================================================================
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
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:
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
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
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'};
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
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
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);
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
});
1587 ## ======================================================================
1588 ## validation, quoting/unquoting and escaping
1590 sub is_valid_action
{
1592 return undef unless exists $actions{$input};
1596 sub is_valid_project
{
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))) {
1610 sub is_valid_pathname
{
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!(^|/)(|\.|\.\.)(/|$)!) {
1620 # no null characters
1621 if ($input =~ m!\0!) {
1627 sub is_valid_ref_format
{
1630 return undef unless defined $input;
1631 # restrictions on ref name according to git-check-ref-format
1632 if ($input =~ m!(/\.|\.\.|[\000-\040\177 ~^:?*\[]|/$)!) {
1638 sub is_valid_refname
{
1641 return undef unless defined $input;
1642 # textual hashes are O.K.
1643 if ($input =~ m/^[0-9a-fA-F]{40}$/) {
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;
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
1658 return undef unless defined $str;
1660 if (utf8
::is_utf8
($str) || utf8
::decode
($str)) {
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
1671 return undef unless defined $str;
1672 $str =~ s/([^A-Za-z0-9\-_.~()\/:@ ]+)/CGI
::escape
($1)/eg
;
1677 # the quoting rules for path_info fragment are slightly different
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
;
1688 # quote unsafe chars in whole URL, so some characters cannot be quoted
1691 return undef unless defined $str;
1692 $str =~ s/([^A-Za-z0-9\-_.~();\/;?:@&= ]+)/CGI
::escape
($1)/eg
;
1697 # quote unsafe characters in HTML attributes
1700 # for XHTML conformance escaping '"' to '"' is not enough
1701 return esc_html
(@_);
1704 # replace invalid utf8 character with SUBSTITUTION sequence
1709 return undef unless defined $str;
1711 $str = to_utf8
($str);
1712 $str = $cgi->escapeHTML($str);
1713 if ($opts{'-nbsp'}) {
1714 $str =~ s/ / /g;
1716 $str =~ s
|([[:cntrl
:]])|(($1 ne "\t") ? quot_cec
($1) : $1)|eg
;
1720 # quote control characters and escape filename to HTML
1725 return undef unless defined $str;
1727 $str = to_utf8
($str);
1728 $str = $cgi->escapeHTML($str);
1729 if ($opts{'-nbsp'}) {
1730 $str =~ s/ / /g;
1732 $str =~ s
|([[:cntrl
:]])|quot_cec
($1)|eg
;
1736 # Sanitize for use in XHTML + application/xml+xhtm (valid XML 1.0)
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
;
1747 # Make control characters "printable", using character escape codes (CEC)
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})
1764 : sprintf('\%2x', ord($cntrl)) );
1765 if ($opts{-nohtml
}) {
1768 return "<span class=\"cntrl\">$chr</span>";
1772 # Alternatively use unicode control pictures codepoints,
1773 # Unicode "printable representation" (PR)
1778 my $chr = sprintf('&#%04d;', 0x2400+ord($cntrl));
1779 if ($opts{-nohtml
}) {
1782 return "<span class=\"cntrl\">$chr</span>";
1786 # git may return quoted and escaped filenames
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
1810 # quoted ordinary character
1814 if ($str =~ m/^"(.*)"$/) {
1817 $str =~ s/\\([^0-7]|[0-7]{1,3})/unq($1)/eg;
1822 # escape tabs (convert tabs to spaces)
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/;
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.
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
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) {
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) {
1892 return "$left$mid$right";
1895 $str =~ m/^($endre)(.*)$/;
1898 if (length($tail) > 4) {
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
{
1911 my $chopped = chop_str
(@_);
1912 $str = to_utf8
($str);
1913 if ($chopped eq $str) {
1914 return esc_html
($chopped);
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;
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),
1945 $out .= esc_html
(substr($str, $pos, $begin - $pos), %opts)
1946 if ($begin - $pos > 0);
1947 $out .= $cgi->span({-class => $css_class}, $escaped);
1951 $out .= esc_html
(substr($str, $pos), %opts)
1952 if ($pos < length($str));
1957 # return positions of beginning and end of each match
1959 my ($str, $regexp) = @_;
1960 return unless (defined $str && defined $regexp);
1963 while ($str =~ /$regexp/g) {
1964 push @matches, [$-[0], $+[0]];
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$//) {
1994 my $chop_len = length($chopped);
1995 my $tail_len = length($tail);
1998 for my $m (@matches) {
1999 if ($m->[0] > $chop_len) {
2000 push @filtered, [ $chop_len, $chop_len + $tail_len ] if ($tail_len > 0);
2002 } elsif ($m->[1] > $chop_len) {
2003 push @filtered, [ $m->[0], $chop_len + $tail_len ];
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)
2019 if (!defined $age) {
2021 } elsif ($age < 60*60*2) {
2023 } elsif ($age < 60*60*24*2) {
2030 # convert age in seconds to "nn units ago" string
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";
2057 $age_str .= " right now";
2063 S_IFINVALID
=> 0030000,
2064 S_IFGITLINK
=> 0160000,
2067 # submodule/subproject, a commit object reference
2071 return (($mode & S_IFMT
) == S_IFGITLINK
)
2074 # convert file mode in octal to symbolic file mode string
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';
2089 return '-rw-r--r--';
2092 return '----------';
2096 # convert file mode in octal to file type string
2100 if ($mode !~ m/^[0-7]+$/) {
2106 if (S_ISGITLINK
($mode)) {
2108 } elsif (S_ISDIR
($mode & S_IFMT
)) {
2110 } elsif (S_ISLNK
($mode)) {
2112 } elsif (S_ISREG
($mode)) {
2119 # convert file mode in octal to file type description string
2120 sub file_type_long
{
2123 if ($mode !~ m/^[0-7]+$/) {
2129 if (S_ISGITLINK
($mode)) {
2131 } elsif (S_ISDIR
($mode & S_IFMT
)) {
2133 } elsif (S_ISLNK
($mode)) {
2135 } elsif (S_ISREG
($mode)) {
2136 if ($mode & S_IXUSR
) {
2137 return "executable";
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
{
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);
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) = @_;
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?/(.*)$!) {
2192 $class .= " indirect" if $indirect;
2194 my $dest_action = "shortlog";
2197 $dest_action = "tag" unless $action eq "tag";
2198 } elsif ($action =~ /^(history|(short)?log)$/) {
2199 $dest_action = $action;
2203 $dest .= "refs/" unless $ref =~ m
!^refs
/!;
2206 my $link = $cgi->a({
2208 action
=>$dest_action,
2212 $markers .= " <span class=\"".esc_attr
($class)."\" title=\"".esc_attr
($ref)."\">" .
2218 return ' <span class="refs">'. $markers . '</span>';
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;
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
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/" .
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).
2267 my $email = lc 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
2277 sub git_get_avatar
{
2278 my ($email, %opts) = @_;
2279 my $pre_white = ($opts{-pad_before
} ?
" " : "");
2280 my $post_white = ($opts{-pad_after
} ?
" " : "");
2281 $opts{-size
} ||= 'default';
2282 my $size = $avatar_size{$opts{-size
}} || $avatar_size{'default'};
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.
2294 "<img width=\"$size\" " .
2295 "class=\"avatar\" " .
2296 "src=\"".esc_url
($url)."\" " .
2304 sub format_search_author
{
2305 my ($author, $searchtype, $displaytext) = @_;
2306 my $have_search = gitweb_check_feature
('search');
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"},
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
{
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) .
2341 # format git diff header line, i.e. "diff --(git|combined|cc) ..."
2342 sub format_git_diff_header_line
{
2344 my $diffinfo = shift;
2345 my ($from, $to) = @_;
2347 if ($diffinfo->{'nparents'}) {
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'});
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'});
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
{
2380 my $diffinfo = shift;
2381 my ($from, $to) = @_;
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) .
2399 if ($line =~ m/^index [0-9a-fA-F]{40},[0-9a-fA-F]{40}/) {
2400 # can match only for combined diff
2402 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
2403 if ($from->{'href'}[$i]) {
2404 $line .= $cgi->a({-href
=>$from->{'href'}[$i],
2406 substr($diffinfo->{'from_id'}[$i],0,7));
2411 $line .= ',' if ($i < $diffinfo->{'nparents'} - 1);
2414 if ($to->{'href'}) {
2415 $line .= $cgi->a({-href
=>$to->{'href'}, -class=>"hash"},
2416 substr($diffinfo->{'to_id'},0,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));
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));
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) = @_;
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'}) {
2457 $cgi->a({-href
=>$from->{'href'}, -class=>"path"},
2458 esc_path
($from->{'file'}));
2461 esc_path
($from->{'file'});
2464 $result .= qq!<div
class="diff from_file">$line</div
>\n!;
2467 # combined diff (merge commit)
2468 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
2469 if ($from->{'href'}[$i]) {
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'},
2477 file_name
=>$to->{'file'}),
2479 -title
=>"diff" . ($i+1)},
2482 $cgi->a({-href
=>$from->{'href'}[$i], -class=>"path"},
2483 esc_path
($from->{'file'}[$i]));
2485 $line = '--- /dev/null';
2487 $result .= qq!<div
class="diff from_file">$line</div
>\n!;
2492 #assert($line =~ m/^\+\+\+/) if DEBUG;
2493 # no extra formatting for "^+++ /dev/null"
2494 if ($line =~ m!^\+\+\+ "?b/!) {
2495 if ($to->{'href'}) {
2497 $cgi->a({-href
=>$to->{'href'}, -class=>"path"},
2498 esc_path
($to->{'file'}));
2501 esc_path
($to->{'file'});
2504 $result .= qq!<div
class="diff to_file">$line</div
>\n!;
2509 # create note for patch simplified by combined diff
2510 sub format_diff_cc_simplified
{
2511 my ($diffinfo, @parents) = @_;
2514 $result .= "<div class=\"diff header\">" .
2516 if (!is_deleted
($diffinfo)) {
2517 $result .= $cgi->a({-href
=> href
(action
=>"blob",
2519 hash
=>$diffinfo->{'to_id'},
2520 file_name
=>$diffinfo->{'to_file'}),
2522 esc_path
($diffinfo->{'to_file'}));
2524 $result .= esc_path
($diffinfo->{'to_file'});
2526 $result .= "</div>\n" . # class="diff header"
2527 "<div class=\"diff nodifferences\">" .
2529 "</div>\n"; # class="diff nodifferences"
2534 sub diff_line_class
{
2535 my ($line, $from, $to) = @_;
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'});
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>";
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]);
2611 $line .= $from_text[$i];
2615 if ($to->{'href'}) {
2616 $line .= $cgi->a({-href
=>"$to->{'href'}#l$to_start",
2617 -class=>"list"}, $to_text);
2621 $line .= " $prefix</span>" .
2622 "<span class=\"section\">" . esc_html
($section, -nbsp
=>1) . "</span>";
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
2630 sub format_diff_line
{
2631 my ($line, $diff_class, $from, $to) = @_;
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);
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";
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
{
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
2670 }, $known_snapshot_formats{$_}{'display'})
2671 , @snapshot_fmts) . ")";
2672 } elsif ($num_fmts == 1) {
2673 # A single "snapshot" link whose tooltip bears the format name.
2675 my ($fmt) = @snapshot_fmts;
2681 snapshot_format
=>$fmt
2683 -title
=> "in format: $known_snapshot_formats{$fmt}{'display'}"
2685 } else { # $num_fmts == 0
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
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);
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/(.*)$!)) {
2715 $matched_ref = $ref;
2719 # find log type for feed description (title)
2721 if (defined $file_name) {
2722 $type = "history of $file_name";
2723 $type .= "/" if ($action eq 'tree');
2724 $type .= " on '$branch'" if (defined $branch);
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;
2736 ## ----------------------------------------------------------------------
2737 ## git utility subroutines, invoking git commands
2739 # returns path to the core git executable and the --git-dir parameter as list
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
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
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.
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');
2794 my ($project, $hash, @options) = @_;
2795 my $o_git_dir = $git_dir;
2797 $git_dir = "$projectroot/$project";
2798 if (defined(my $fd = git_cmd_pipe
'rev-parse',
2799 '--verify', '-q', @options, $hash)) {
2801 chomp $retval if defined $retval;
2804 if (defined $o_git_dir) {
2805 $git_dir = $o_git_dir;
2810 # get type of given object
2814 defined(my $fd = git_cmd_pipe
"cat-file", '-t', $hash) or return;
2816 close $fd or return;
2821 # repository configuration
2822 our $config_file = '';
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 ];
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;
2847 defined(my $fh = git_cmd_pipe
"config", '-z', '-l')
2850 while (my $keyval = <$fh>) {
2852 my ($key, $value) = split(/\n/, $keyval, 2);
2854 hash_set_multi
(\
%config, $key, $value)
2855 if (!defined $section_regexp || $key =~ /^(?:$section_regexp)\./o);
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
{
2868 return 1 if !defined $val; # section.key
2870 # strip leading and trailing whitespace
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
2884 # strip leading and trailing whitespace
2888 if (my ($num, $unit) = ($val =~ /^([0-9]*)([kmg])$/i)) {
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);
2898 # convert config value to array reference, if needed
2899 sub config_to_multi
{
2902 return ref($val) ?
$val : (defined($val) ?
[ $val ] : []);
2905 sub git_get_project_config
{
2906 my ($key, $type) = @_;
2908 return unless defined $git_dir;
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 =~ /^([^.]*)\.(.*)\.([^.]*)$/)) {
2916 $key = join(".", lc($hi), $mi, lc($lo));
2917 return if ($lo =~ /\W/ || $hi =~ /\W/);
2921 return if ($key =~ /\W/);
2923 $key =~ s/^gitweb\.//;
2926 if (defined $type) {
2929 unless ($type eq 'bool' || $type eq 'int');
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"};
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
{
2957 my $path = shift || return undef;
2962 defined(my $fd = git_cmd_pipe
"ls-tree", $base, "--", $path)
2963 or die_error
(500, "Open git-ls-tree failed");
2965 close $fd or return undef;
2967 if (!defined $line) {
2968 # there is no tree or hash given by $path at $base
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
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;
2989 defined(my $fd = git_cmd_pipe
"ls-tree", '-r', '-t', '-z', $base)
2991 while (my $line = <$fd>) {
2994 #'040000 tree 595596a6a9117ddba9fe379b6b012b558bac8423 gitweb'
2995 #'100644 blob e02e90f0429be0d2a69b76571101f20b8f75530f gitweb/README'
2996 if ($line =~ m/(?:[0-9]+) (?:.+) $hash\t(.+)$/) {
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);
3019 if (defined $conf) {
3025 sub git_get_project_description
{
3027 return git_get_file_or_project_config
($path, 'description');
3030 sub git_get_project_category
{
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;
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
3057 (my $ctag = $tagfile) =~ s
#.*/##;
3058 if ($val =~ /^\d+$/) {
3059 $ctags->{$ctag} = $val;
3061 $ctags->{$ctag} = 1;
3066 } elsif (open my $fh, '<', "$git_dir/ctags") {
3067 while (my $line = <$fh>) {
3069 $ctags->{$line}++ if $line;
3074 my $taglist = config_to_multi
(git_get_project_config
('ctag'));
3075 foreach my $tag (@
$taglist) {
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;
3089 foreach my $p (@
$projects) {
3090 foreach my $ct (keys %{$p->{'ctags'}}) {
3091 $ctags->{$ct} += $p->{'ctags'}->{$ct};
3098 sub git_populate_project_tagcloud
{
3101 # First, merge different-cased tags; tags vote on casing
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
} = $_;
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
3119 my $title = esc_html
($ctags_lc{$ctag}->{topname
});
3120 $title =~ s/ / /g;
3121 $title =~ s/^/ /g;
3122 $title =~ s/$/ /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
});
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);
3144 sub git_show_project_tagcloud
{
3145 my ($cloud, $count) = @_;
3146 if (ref $cloud eq 'HTML::TagCloud') {
3147 return $cloud->html_and_css($count);
3149 my @tags = sort { $cloud->{$a}->{'count'} <=> $cloud->{$b}->{'count'} } keys %$cloud;
3151 '<div id="htmltagcloud"'.($project ?
'' : ' align="center"').'>' .
3153 $cloud->{$_}->{'ctag'}
3154 } splice(@tags, 0, $count)) .
3159 sub git_get_project_url_list
{
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>;
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;
3178 if (-d
$projects_list) {
3179 # search in directory
3180 my $dir = $projects_list;
3181 # remove the trailing "/"
3183 my $pfxlen = length("$dir");
3184 my $pfxdepth = ($dir =~ tr!/!!);
3185 # when filtering, search only given subdirectory
3186 if ($filter && !$paranoid) {
3192 follow_fast
=> 1, # follow symbolic links
3193 follow_skip
=> 2, # ignore duplicates
3194 dangling_symlinks
=> 0, # ignore dangling symlinks, silently
3197 our $project_maxdepth;
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;
3210 my $path = substr($File::Find
::name
, $pfxlen + 1);
3211 # paranoidly only filter here
3212 if ($paranoid && $filter && $path !~ m!^\Q$filter\E/!) {
3215 # we check related file in $projectroot
3216 if (check_export_ok
("$projectroot/$path")) {
3217 push @list, { path
=> $path };
3218 $File::Find
::prune
= 1;
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;
3230 while (my $line = <$fd>) {
3232 my ($path, $owner) = split ' ', $line;
3233 $path = unescape
($path);
3234 $owner = unescape
($owner);
3235 if (!defined $path) {
3238 # if $filter is rpovided, check if $path begins with $filter
3239 if ($filter && $path !~ m!^\Q$filter\E/!) {
3242 if (check_export_ok
("$projectroot/$path")) {
3247 $pr->{'owner'} = to_utf8
($owner);
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
3273 my @dirs = split('/', $path);
3274 # walk the trie, until either runs out of components or out of 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
3291 foreach my $pr (@
$projects) {
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;
3301 if (!exists $ref->{$dir}) {
3302 # not in trie, cannot have prefix, not a fork
3303 push @filtered, $pr;
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;
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'};
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') : ());
3333 foreach my $pr (@
$projlist) {
3336 next unless ref($pr->{'ctags'}) eq 'HASH';
3338 grep { lc($_) eq lc($tagfilter) } keys %{$pr->{'ctags'}};
3343 $pr->{'path'} =~ /$search_re/ ||
3344 $pr->{'descr_long'} =~ /$search_re/;
3347 push @projects, $pr;
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>) {
3367 my ($pr, $ow) = split ' ', $line;
3368 $pr = unescape
($pr);
3369 $ow = unescape
($ow);
3370 $gitweb_project_owner->{$pr} = to_utf8
($ow);
3376 sub git_get_project_owner
{
3377 my $project = shift;
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");
3400 sub git_get_last_activity
{
3404 $git_dir = "$projectroot/$path";
3405 defined($fd = git_cmd_pipe
'for-each-ref',
3406 '--format=%(committer)',
3407 '--sort=-committerdate',
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$/) {
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
{
3431 my $fd = git_cmd_pipe
'remote', '-v';
3433 while (my $remote = <$fd>) {
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/!!
3459 sub git_get_references
{
3460 my $type = shift || "";
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
3468 while (my $line = <$fd>) {
3470 if ($line =~ m!^([0-9a-fA-F]{40})\srefs/($type.*)$!) {
3471 if (defined $refs{$1}) {
3472 push @
{$refs{$1}}, $2;
3478 close $fd or return;
3482 sub git_get_rev_name_tags
{
3483 my $hash = shift || return undef;
3485 defined(my $fd = git_cmd_pipe
"name-rev", "--tags", $hash)
3487 my $name_rev = <$fd>;
3490 if ($name_rev =~ m
|^$hash tags
/(.*)$|) {
3493 # catches also '$hash undefined' output
3498 ## ----------------------------------------------------------------------
3499 ## parse to hash functions
3503 my $tz = shift || "-0000";
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);
3540 defined(my $fd = git_cmd_pipe
"cat-file", "tag", $tag_id) or return;
3541 $tag{'id'} = $tag_id;
3542 while (my $line = <$fd>) {
3544 if ($line =~ m/^object ([0-9a-fA-F]{40})$/) {
3545 $tag{'object'} = $1;
3546 } elsif ($line =~ m/^type (.+)$/) {
3548 } elsif ($line =~ m/^tag (.+)$/) {
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;
3558 $tag{'author_name'} = $tag{'author'};
3560 } elsif ($line =~ m/--BEGIN/) {
3561 push @comment, $line;
3563 } elsif ($line eq "") {
3567 push @comment, <$fd>;
3568 $tag{'comment'} = \
@comment;
3569 close $fd or return;
3570 if (!defined $tag{'name'}) {
3576 sub parse_commit_text
{
3577 my ($commit_text, $withparents) = @_;
3578 my @commit_lines = split '\n', $commit_text;
3581 pop @commit_lines; # Remove '\0'
3583 if (! @commit_lines) {
3587 my $header = shift @commit_lines;
3588 if ($header !~ m/^[0-9a-fA-F]{40}/) {
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})$/) {
3596 } elsif ((!defined $withparents) && ($line =~ m/^parent ([0-9a-fA-F]{40})$/)) {
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;
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;
3616 $co{'committer_name'} = $co{'committer'};
3620 if (!defined $co{'tree'}) {
3623 $co{'parents'} = \
@parents;
3624 $co{'parent'} = $parents[0];
3626 foreach my $title (@commit_lines) {
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);
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) {
3658 $co{'comment'} = \
@commit_lines;
3660 my $age = time - $co{'committer_epoch'};
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'};
3668 $co{'age_string_date'} = $co{'age_string'};
3669 $co{'age_string_age'} = sprintf "%4i-%02u-%02i", 1900 + $year, $mon+1, $mday;
3675 my ($commit_id) = @_;
3680 defined(my $fd = git_cmd_pipe
"rev-list",
3686 or die_error
(500, "Open git-rev-list failed");
3687 %co = parse_commit_text
(<$fd>, 1);
3694 my ($commit_id, $maxcount, $skip, $filename, @args) = @_;
3702 defined(my $fd = git_cmd_pipe
"rev-list",
3705 ("--max-count=" . $maxcount),
3706 ("--skip=" . $skip),
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);
3718 return wantarray ?
@cos : \
@cos;
3721 # parse line of git-diff-tree "raw" output
3722 sub parse_difftree_raw_line
{
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;
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);
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;
3769 return parse_difftree_raw_line
($line_or_ref);
3773 # parse line of git-ls-tree output
3774 sub parse_ls_tree_line
{
3780 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa 16717 panic.c'
3781 $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40}) +(-|[0-9]+)\t(.+)$/s;
3790 $res{'name'} = unquote
($5);
3793 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'
3794 $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t(.+)$/s;
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'}) {
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]);
3830 $from->{'href'}[$i] = undef;
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'});
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'});
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;
3864 defined(my $fd = git_cmd_pipe
'for-each-ref',
3865 ($limit ?
'--count='.($limit+1) : ()), '--sort=-committerdate',
3866 '--format=%(objectname) %(refname) %(subject)%00%(committer)',
3869 while (my $line = <$fd>) {
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;
3892 $ref_item{'age'} = age_string
(time - $ref_item{'epoch'});
3894 $ref_item{'age'} = "unknown";
3897 push @headslist, \
%ref_item;
3901 return wantarray ?
@headslist : \
@headslist;
3904 sub git_get_tags_list
{
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)',
3914 while (my $line = <$fd>) {
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;
3933 $ref_item{'reftype'} = $type;
3934 $ref_item{'refid'} = $id;
3937 if ($type eq "tag" || $type eq "commit") {
3938 $ref_item{'epoch'} = $epoch;
3940 $ref_item{'age'} = age_string
(time - $ref_item{'epoch'});
3942 $ref_item{'age'} = "unknown";
3946 push @tagslist, \
%ref_item;
3950 return wantarray ?
@tagslist : \
@tagslist;
3953 ## ----------------------------------------------------------------------
3954 ## filesystem-related functions
3956 sub get_file_owner
{
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) {
3965 $owner =~ s/[,;].*$//;
3966 return to_utf8
($owner);
3969 # assume that file exists
3971 my $filename = shift;
3973 open my $fd, '<', $filename;
3974 print map { to_utf8
($_) } <$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;
3987 open(my $mh, '<', $mimemap) or return undef;
3989 next if m/^#/; # skip comments
3990 my ($mimetype, @exts) = split(/\s+/);
3991 foreach my $ext (@exts) {
3992 $mimemap{$ext} = $mimetype;
3997 $filename =~ /\.([^.]*)$/;
3998 return $mimemap{$1};
4001 sub mimetype_guess
{
4002 my $filename = shift;
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');
4020 my $filename = shift;
4023 my $mime = mimetype_guess
($filename);
4024 $mime and return $mime;
4028 return $default_blob_plain_mimetype unless $fd;
4031 return 'text/plain';
4032 } elsif (! $filename) {
4033 return 'application/octet-stream';
4034 } elsif ($filename =~ m/\.png$/i) {
4036 } elsif ($filename =~ m/\.gif$/i) {
4038 } elsif ($filename =~ m/\.jpe?g$/i) {
4039 return 'image/jpeg';
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";
4056 # peek the first upto 128 bytes off a file handle
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).
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);
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.
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};
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");
4142 # just in case, should not happen as we tested !eof($fd) above
4143 return $fd if close($hifd);
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", $?
);
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) . "'";
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
|/$|) {
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';
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);
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);
4218 "rel=\"$link_attr{'-rel'}\" ".
4219 "title=\"$link_attr{'-title'}\" ".
4220 "href=\"$link_attr{'-href'}\" ".
4221 "type=\"$link_attr{'-type'}\" ".
4224 $href_params{'extra_options'} = '--no-merges';
4225 $link_attr{'-href'} = href
(%href_params);
4226 $link_attr{'-title'} .= ' (no merges)';
4228 "rel=\"$link_attr{'-rel'}\" ".
4229 "title=\"$link_attr{'-title'}\" ".
4230 "href=\"$link_attr{'-href'}\" ".
4231 "type=\"$link_attr{'-type'}\" ".
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
{
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";
4253 foreach my $stylesheet (@stylesheets) {
4254 next unless $stylesheet;
4255 print '<link rel="stylesheet" type="text/css" href="'.esc_url
($stylesheet).'"/>'."\n";
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
{
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)},
4294 print " / $action_print";
4296 if (defined $opts{-action_extra
}) {
4297 print " / $opts{-action_extra}";
4300 } elsif (defined $project_filter) {
4301 print_nav_breadcrumbs_path
(split '/', $project_filter);
4305 sub print_search_form
{
4306 if (!defined $searchtext) {
4310 if (defined $hash_base) {
4311 $search_hash = $hash_base;
4312 } elsif (defined $hash) {
4313 $search_hash = $hash;
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" .
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) .
4338 $cgi->end_form() . "\n";
4341 sub git_header_html
{
4342 my $status = shift || "200 OK";
4343 my $expires = shift;
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'}" : '';
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 -->
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);
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,
4389 -class => "logo"}));
4391 print_nav_breadcrumbs
(%opts);
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";
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
() ]).
4440 '<span id="generating_cmd">'.
4441 $number_of_git_cmds.
4442 '</span> git commands '.
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!.
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!;
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.
4493 my $status = shift || 500;
4494 my $error = esc_html
(shift) || "Internal Server Error";
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);
4507 <div class="page_body">
4512 if (defined $extra) {
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);
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');
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)
4559 my ($label, $link, $pos) = splice(@actions,0,3);
4561 @navs = map { $_ eq $pos ?
($_, $label) : $_ } @navs;
4563 $link =~ s/%([%nfhb])/$repl{$1}/g;
4564 $arg{$label}{'_href'} = $link;
4567 print "<div class=\"page_nav\">\n" .
4569 map { $_ eq $current ?
4570 $_ : $cgi->a({-href
=> ($arg{$_}{_href
} ?
$arg{$_}{_href
} : href
(%{$arg{$_}}))}, "$_")
4572 print "<br/>\n$extra<br/>\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
{
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
=>$_)}, $_)
4589 sub format_paging_nav
{
4590 my ($action, $page, $has_next_link) = @_;
4596 $cgi->a({-href
=> href
(-replay
=>1, page
=>undef)}, "first") .
4598 $cgi->a({-href
=> href
(-replay
=>1, page
=>$page-1),
4599 -accesskey
=> "p", -title
=> "Alt-p"}, "prev");
4601 $paging_nav .= "first ⋅ prev";
4604 if ($has_next_link) {
4605 $paging_nav .= " ⋅ " .
4606 $cgi->a({-href
=> href
(-replay
=>1, page
=>$page+1),
4607 -accesskey
=> "n", -title
=> "Alt-n"}, "next");
4609 $paging_nav .= " ⋅ next";
4615 ## ......................................................................
4616 ## functions printing or outputting HTML: div
4618 sub git_print_header_div
{
4619 my ($action, $title, $hash, $hash_base) = @_;
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) .
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);
4649 if (ref($arg) eq 'HASH') {
4653 if (ref($arg) eq 'ARRAY') {
4654 $header_args = $arg;
4659 print $cgi->start_div($div_args);
4660 git_print_header_div
(@
$header_args);
4662 if (ref($content) eq 'CODE') {
4664 } elsif (ref($content) eq 'SCALAR') {
4665 print esc_html
($$content);
4666 } elsif (ref($content) eq 'GLOB' or ref($content) eq 'IO::Handle') {
4668 } elsif (!ref($content) && defined($content)) {
4672 print $cgi->end_div;
4675 sub format_timestamp_html
{
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)';
4690 sprintf($localtime_format,
4691 $date->{'hour_local'}, $date->{'minute_local'}, $date->{'tz_local'});
4696 # Outputs the author name and date in long form
4697 sub git_print_authorship
{
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) .
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
{
4718 # too bad we can't use @people = @_ || ('author', 'committer')
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') .
4733 format_timestamp_html
(\
%wd) .
4739 sub git_print_page_path
{
4745 print "<div class=\"page_path\">";
4746 print $cgi->a({-href
=> href
(action
=>"tree", hash_base
=>$hb),
4747 -title
=> 'tree root'}, to_utf8
("[$project]"));
4749 if (defined $name) {
4750 my @dirname = split '/', $name;
4751 my $basename = pop @dirname;
4754 foreach my $dir (@dirname) {
4755 $fullname .= ($fullname ?
'/' : '') . $dir;
4756 print $cgi->a({-href
=> href
(action
=>"tree", file_name
=>$fullname,
4758 -title
=> $fullname}, esc_path
($dir));
4761 if (defined $type && $type eq 'blob') {
4762 print $cgi->a({-href
=> href
(action
=>"blob_plain", file_name
=>$file_name,
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,
4768 -title
=> $name}, esc_path
($basename));
4771 print esc_path
($basename);
4774 print "<br/></div>\n";
4781 if ($opts{'-remove_title'}) {
4782 # remove title, i.e. first line of log
4785 # remove leading empty lines
4786 while (defined $log->[0] && $log->[0] eq "") {
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;
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>" .
4806 $skip_blank_line = 1;
4811 # print only one empty line
4812 # do not print empty line after signoff
4814 next if ($skip_blank_line);
4815 $skip_blank_line = 1;
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
{
4835 defined(my $fd = git_cmd_pipe
"cat-file", "blob", $hash)
4839 $link_target = <$fd>;
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)
4859 $path = $basedir . '/' . $link_target;
4861 # we are in top (root) tree (dir)
4862 $path = $link_target;
4865 # remove //, /./, and /../
4867 foreach my $part (split('/', $path)) {
4868 # discard '.' and ''
4869 next if (!$part || $part eq '.');
4871 if ($part eq '..') {
4875 # link leads outside repository (outside top dir)
4879 push @path_parts, $part;
4882 $path = join('/', @path_parts);
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) = @_;
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'});
4910 my $norm_target = normalize_link_target
($link_target, $basedir);
4911 if (defined $norm_target) {
4913 $cgi->a({-href
=> href
(action
=>"object", hash_base
=>$hash_base,
4914 file_name
=>$norm_target),
4915 -title
=> $norm_target}, esc_path
($link_target));
4917 print " -> " . esc_path
($link_target);
4922 print "<td class=\"link\">";
4923 print $cgi->a({-href
=> href
(action
=>"blob", hash
=>$t->{'hash'},
4924 file_name
=>"$basedir$t->{'name'}", %base_key)},
4928 $cgi->a({-href
=> href
(action
=>"blame", hash
=>$t->{'hash'},
4929 file_name
=>"$basedir$t->{'name'}", %base_key)},
4932 if (defined $hash_base) {
4934 $cgi->a({-href
=> href
(action
=>"history", hash_base
=>$hash_base,
4935 hash
=>$t->{'hash'}, file_name
=>"$basedir$t->{'name'}")},
4939 $cgi->a({-href
=> href
(action
=>"blob_plain", hash_base
=>$hash_base,
4940 file_name
=>"$basedir$t->{'name'}")},
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'}",
4949 esc_path
($t->{'name'}));
4951 print "<td class=\"link\">";
4952 print $cgi->a({-href
=> href
(action
=>"tree", hash
=>$t->{'hash'},
4953 file_name
=>"$basedir$t->{'name'}",
4956 if (defined $hash_base) {
4958 $cgi->a({-href
=> href
(action
=>"history", hash_base
=>$hash_base,
4959 file_name
=>"$basedir$t->{'name'}")},
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'}) .
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'}")},
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]);
5000 # is current raw difftree line of file deletion
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");
5029 print "<table class=\"" .
5030 (@parents > 1 ?
"combined " : "") .
5033 # header only for combined diff in 'commitdiff' view
5034 my $has_header = @
$difftree && @parents > 1 && $action eq 'commitdiff';
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];
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)},
5049 print "</tr></thead>\n<tbody>\n";
5054 foreach my $line (@
{$difftree}) {
5055 my $diff = parsed_difftree_line
($line);
5058 print "<tr class=\"dark\">\n";
5060 print "<tr class=\"light\">\n";
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
5072 $cgi->a({-href
=> href
(action
=>"blob", hash
=>$diff->{'to_id'},
5073 file_name
=>$diff->{'to_file'},
5075 -class => "list"}, esc_path
($diff->{'to_file'})) .
5079 esc_path
($diff->{'to_file'}) .
5083 if ($action eq 'commitdiff') {
5086 print "<td class=\"link\">" .
5087 $cgi->a({-href
=> href
(-anchor
=>"patch$patchno")},
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",
5111 file_name
=>$from_path)},
5115 if ($diff->{'to_id'} eq $from_hash) {
5116 print "<td class=\"link nochange\">";
5118 print "<td class=\"link\">";
5120 print $cgi->a({-href
=> href
(action
=>"blobdiff",
5121 hash
=>$diff->{'to_id'},
5122 hash_parent
=>$from_hash,
5124 hash_parent_base
=>$hash_parent,
5125 file_name
=>$diff->{'to_file'},
5126 file_parent
=>$from_path)},
5132 print "<td class=\"link\">";
5134 print $cgi->a({-href
=> href
(action
=>"blob",
5135 hash
=>$diff->{'to_id'},
5136 file_name
=>$diff->{'to_file'},
5139 print " | " if ($has_history);
5142 print $cgi->a({-href
=> href
(action
=>"history",
5143 file_name
=>$diff->{'to_file'},
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>";
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'}));
5180 print "<td>$mode_chng</td>\n";
5181 print "<td class=\"link\">";
5182 if ($action eq 'commitdiff') {
5185 print $cgi->a({-href
=> href
(-anchor
=>"patch$patchno")},
5189 print $cgi->a({-href
=> href
(action
=>"blob", hash
=>$diff->{'to_id'},
5190 hash_base
=>$hash, file_name
=>$diff->{'file'})},
5194 } elsif ($diff->{'status'} eq "D") { # deleted
5195 my $mode_chng = "<span class=\"file_status deleted\">[deleted $from_file_type]</span>";
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'}));
5201 print "<td>$mode_chng</td>\n";
5202 print "<td class=\"link\">";
5203 if ($action eq 'commitdiff') {
5206 print $cgi->a({-href
=> href
(-anchor
=>"patch$patchno")},
5210 print $cgi->a({-href
=> href
(action
=>"blob", hash
=>$diff->{'from_id'},
5211 hash_base
=>$parent, file_name
=>$diff->{'file'})},
5214 print $cgi->a({-href
=> href
(action
=>"blame", hash_base
=>$parent,
5215 file_name
=>$diff->{'file'})},
5218 print $cgi->a({-href
=> href
(action
=>"history", hash_base
=>$parent,
5219 file_name
=>$diff->{'file'})},
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";
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'}));
5244 print "<td>$mode_chnge</td>\n";
5245 print "<td class=\"link\">";
5246 if ($action eq 'commitdiff') {
5249 print $cgi->a({-href
=> href
(-anchor
=>"patch$patchno")},
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'})},
5261 print $cgi->a({-href
=> href
(action
=>"blob", hash
=>$diff->{'to_id'},
5262 hash_base
=>$hash, file_name
=>$diff->{'file'})},
5265 print $cgi->a({-href
=> href
(action
=>"blame", hash_base
=>$hash,
5266 file_name
=>$diff->{'file'})},
5269 print $cgi->a({-href
=> href
(action
=>"history", hash_base
=>$hash,
5270 file_name
=>$diff->{'file'})},
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'}};
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);
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') {
5295 print $cgi->a({-href
=> href
(-anchor
=>"patch$patchno")},
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'})},
5307 print $cgi->a({-href
=> href
(action
=>"blob", hash
=>$diff->{'to_id'},
5308 hash_base
=>$parent, file_name
=>$diff->{'to_file'})},
5311 print $cgi->a({-href
=> href
(action
=>"blame", hash_base
=>$hash,
5312 file_name
=>$diff->{'to_file'})},
5315 print $cgi->a({-href
=> href
(action
=>"history", hash_base
=>$hash,
5316 file_name
=>$diff->{'to_file'})},
5320 } # we should not encounter Unmerged (U) or Unknown (X) status
5323 print "</tbody>" if $has_header;
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
5334 '<div class="chunk_block ctx">',
5335 '<div class="old">',
5338 '<div class="new">',
5347 '<div class="chunk_block rem">',
5348 '<div class="old">',
5355 '<div class="chunk_block add">',
5356 '<div class="new">',
5362 '<div class="chunk_block chg">',
5363 '<div class="old">',
5366 '<div class="new">',
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.
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/);
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/);
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);
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) {
5443 # Highlight lines in combined diff only if the chunk contains
5444 # diff between the same version, e.g.
5451 # Otherwise the highlightling would be confusing.
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) {
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;
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,
5492 if ($diff_style eq 'sidebyside' && !$is_combined) {
5493 print_sidebyside_diff_lines
($ctx, $rem, $add);
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];
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);
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
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
5543 # rem, add or change
5544 if ($class eq 'rem') {
5546 } elsif ($class eq 'add') {
5550 if ($class eq 'ctx') {
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);
5564 my $patch_number = 0;
5569 my @chunk; # for side-by-side diff
5571 print "<div class=\"patchset\">\n";
5573 # skip to first patch
5574 while ($patch_line = <$fd>) {
5577 last if ($patch_line =~ m/^diff /);
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);
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";
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;
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"
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";
5632 #assert($patch_line =~ m/^diff /) if DEBUG;
5633 #assert($patch_line !~ m!$/$!) if DEBUG; # is chomp-ed
5635 # print "git diff" header
5636 print format_git_diff_header_line
($patch_line, $diffinfo,
5639 # print extended diff header
5640 print "<div class=\"diff extended_header\">\n";
5642 while ($patch_line = <$fd>) {
5645 last EXTENDED_HEADER
if ($patch_line =~ m/^--- |^diff /);
5647 print format_extended_diff_header_line
($patch_line, $diffinfo,
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"
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>;
5663 #assert($patch_line =~ m/^\+\+\+/) if DEBUG;
5665 print format_diff_from_to_header
($last_patch_line, $patch_line,
5666 $diffinfo, \
%from, \
%to,
5671 while ($patch_line = <$fd>) {
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);
5683 push @chunk, [ $class, $patch_line ];
5688 print_diff_chunk
($diff_style, scalar @hash_parents, \
%from, \
%to, @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;
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"
5710 if ($patch_number == 0) {
5711 if (@hash_parents > 1) {
5712 print "<div class=\"diff nodifferences\">Trivial merge</div>\n";
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) = @_;
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) .
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";
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}) {
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.
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) = @_;
5784 my $filter_set = sub { return @_; };
5786 my %wanted_keys = map { $_ => 1 } @wanted_keys;
5787 $filter_set = sub { return grep { $wanted_keys{$_} } @_; };
5790 my $show_ctags = gitweb_check_feature
('ctags');
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) {
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'}") || "";
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;
5826 sub sort_projects_list
{
5827 my ($projlist, $order) = @_;
5831 return sub { $a->{$key} cmp $b->{$key} };
5834 sub order_num_then_undef
{
5837 defined $a->{$key} ?
5838 (defined $b->{$key} ?
$a->{$key} <=> $b->{$key} : -1) :
5839 (defined $b->{$key} ?
1 : 0)
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) = @_;
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
5874 print format_sort_th
(@_);
5877 sub format_sort_th
{
5878 my ($name, $order, $header) = @_;
5880 $header ||= ucfirst($name);
5882 if ($order eq $name) {
5883 $sort_th .= "<th>$header</th>\n";
5885 $sort_th .= "<th>" .
5886 $cgi->a({-href
=> href
(-replay
=>1, order
=>$name),
5887 -class => "header"}, $header) .
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);
5901 for (my $i = $from; $i <= $to; $i++) {
5902 my $pr = $projlist->[$i];
5905 print "<tr class=\"dark\">\n";
5907 print "<tr class=\"light\">\n";
5913 if ($pr->{'forks'}) {
5914 my $nforks = scalar @
{$pr->{'forks'}};
5916 print $cgi->a({-href
=> href
(project
=>$pr->{'path'}, action
=>"forks"),
5917 -title
=> "$nforks forks"}, "+");
5919 print $cgi->span({-title
=> "$nforks forks"}, "+");
5924 print "<td>" . $cgi->a({-href
=> href
(project
=>$pr->{'path'}, action
=>"summary"),
5926 esc_html_match_hl
($pr->{'path'}, $search_regexp)) .
5928 "<td>" . $cgi->a({-href
=> href
(project
=>$pr->{'path'}, action
=>"summary"),
5930 -title
=> $pr->{'descr_long'}},
5932 ? esc_html_match_hl_chopped
($pr->{'descr_long'},
5933 $pr->{'descr'}, $search_regexp)
5934 : esc_html
($pr->{'descr'})) .
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") : '') .
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)
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);
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);
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";
5992 @projects = sort_projects_list
(\
@projects, $order);
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) {
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
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 "") {
6022 print "<td></td>\n";
6024 print "<td class=\"category\" colspan=\"5\">".esc_html
($cat)."</td>\n";
6028 git_project_list_rows
($categories{$cat}, undef, undef, $check_forks);
6031 git_project_list_rows
(\
@projects, $from, $to, $check_forks);
6034 if (defined $extra) {
6037 print "<td></td>\n";
6039 print "<td colspan=\"5\">$extra</td>\n" .
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]};
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,
6061 print "<div class=\"title_text\">\n" .
6062 "<div class=\"log_link\">\n" .
6063 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$commit)}, "commit") .
6065 $cgi->a({-href
=> href
(action
=>"commitdiff", hash
=>$commit)}, "commitdiff") .
6067 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$commit, hash_base
=>$commit)}, "tree") .
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);
6078 print "<div class=\"page_nav\">\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";
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);
6098 print "<tr class=\"dark\">\n";
6100 print "<tr class=\"light\">\n";
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);
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;
6120 if (defined $extra) {
6122 "<td colspan=\"4\">$extra</td>\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";
6138 for (my $i = $from; $i <= $to; $i++) {
6139 my %co = %{$commitlist->[$i]};
6143 my $commit = $co{'id'};
6145 my $ref = format_ref_marker
($refs, $commit);
6148 print "<tr class=\"dark\">\n";
6150 print "<tr class=\"light\">\n";
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);
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) {
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)},
6180 if (defined $extra) {
6182 "<td colspan=\"4\">$extra</td>\n" .
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";
6196 for (my $i = $from; $i <= $to; $i++) {
6197 my $entry = $taglist->[$i];
6199 my $comment = $tag{'subject'};
6201 if (defined $comment) {
6202 $comment_short = chop_str
($comment, 30, 5);
6205 print "<tr class=\"dark\">\n";
6207 print "<tr class=\"light\">\n";
6210 if (defined $tag{'age'}) {
6211 print "<td><i>$tag{'age'}</i></td>\n";
6213 print "<td></td>\n";
6216 $cgi->a({-href
=> href
(action
=>$tag{'reftype'}, hash
=>$tag{'refid'}),
6217 -class => "list name"}, esc_html
($tag{'name'})) .
6220 if (defined $comment) {
6221 print format_subject_html
($comment, $comment_short,
6222 href
(action
=>"tag", hash
=>$tag{'id'}));
6225 "<td class=\"selflink\">";
6226 if ($tag{'type'} eq "tag") {
6227 print $cgi->a({-href
=> href
(action
=>"tag", hash
=>$tag{'id'})}, "tag");
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");
6243 if (defined $extra) {
6245 "<td colspan=\"5\">$extra</td>\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";
6259 for (my $i = $from; $i <= $to; $i++) {
6260 my $entry = $headlist->[$i];
6262 my $curr = defined $head_at && $ref{'id'} eq $head_at;
6264 print "<tr class=\"dark\">\n";
6266 print "<tr class=\"light\">\n";
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'})) .
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") .
6281 if (defined $extra) {
6283 "<td colspan=\"3\">$extra</td>\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);
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);
6309 $urls_table .= format_repo_url
("", "No remote URL");
6312 $urls_table .= "</table>\n";
6315 if (defined $limit && $limit < @
$heads) {
6316 $dots = $cgi->a({-href
=> href
(action
=>"remotes", hash
=>$remote)}, "...");
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";
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'};
6339 print "<tr class=\"dark\">\n";
6341 print "<tr class=\"light\">\n";
6345 $cgi->a({-href
=> href
(action
=>'remotes', hash
=>$remote),
6346 -class=> "list name"},esc_html
($remote)) .
6348 print "<td class=\"link\">" .
6349 (defined $fetch ?
$cgi->a({-href
=> $fetch}, "fetch") : "fetch") .
6351 (defined $push ?
$cgi->a({-href
=> $push}, "push") : "push") .
6359 "<td colspan=\"3\">" .
6360 $cgi->a({-href
=> href
(action
=>"remotes")}, "...") .
6361 "</td>\n" . "</tr>\n";
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);
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
{
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 = '';
6403 $cgi->a({-href
=> href
(-replay
=>1, page
=>undef)},
6406 $cgi->a({-href
=> href
(-replay
=>1, page
=>$page-1),
6407 -accesskey
=> "p", -title
=> "Alt-p"}, "prev");
6409 $paging_nav .= "first ⋅ prev";
6412 if ($#commitlist >= 100) {
6414 $cgi->a({-href
=> href
(-replay
=>1, page
=>$page+1),
6415 -accesskey
=> "n", -title
=> "Alt-n"}, "next");
6416 $paging_nav .= " ⋅ $next_link";
6418 $paging_nav .= " ⋅ next";
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";
6428 git_search_grep_body
(\
@commitlist, 0, 99, $next_link);
6434 sub git_search_changes
{
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");
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";
6452 while (my $line = <$fd>) {
6456 my %set = parse_difftree_raw_line
($line);
6457 if (defined $set{'commit'}) {
6458 # finish previous commit
6461 "<td class=\"link\">" .
6462 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$co{'id'})},
6465 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$co{'tree'},
6466 hash_base
=>$co{'id'})},
6473 print "<tr class=\"dark\">\n";
6475 print "<tr class=\"light\">\n";
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" .
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'}),
6492 "<span class=\"match\">" . esc_path
($set{'file'}) . "</span>") .
6498 # finish last commit (warning: repetition!)
6501 "<td class=\"link\">" .
6502 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$co{'id'})},
6505 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$co{'tree'},
6506 hash_base
=>$co{'id'})},
6517 sub git_search_files
{
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");
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";
6536 while (my $line = <$fd>) {
6538 my ($file, $lno, $ltext, $binary);
6539 last if ($matches++ > 1000);
6540 if ($line =~ /^Binary file (.+) matches$/) {
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";
6550 print "<tr class=\"dark\">\n";
6552 print "<tr class=\"light\">\n";
6554 $file_href = href
(action
=>"blob", hash_base
=>$co{'id'},
6556 print "<td class=\"list\">".
6557 $cgi->a({-href
=> $file_href, -class => "list"}, esc_path
($file));
6558 print "</td><td>\n";
6562 print "<div class=\"binary\">Binary file</div>\n";
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);
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";
6581 print "</td></tr>\n";
6582 if ($matches > 1000) {
6583 print "<div class=\"diff nodifferences\">Too many matches, listing trimmed</div>\n";
6586 print "<div class=\"diff nodifferences\">No matches found</div>\n";
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";
6602 for (my $i = $from; $i <= $to; $i++) {
6603 my %co = %{$commitlist->[$i]};
6607 my $commit = $co{'id'};
6609 print "<tr class=\"dark\">\n";
6611 print "<tr class=\"light\">\n";
6614 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
6615 format_author_html
('td', \
%co, 15, 5) .
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 />";
6638 "<td class=\"link\">" .
6639 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$co{'id'})}, "commit") .
6641 $cgi->a({-href
=> href
(action
=>"commitdiff", hash
=>$co{'id'})}, "commitdiff") .
6643 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$co{'tree'}, hash_base
=>$co{'id'})}, "tree");
6647 if (defined $extra) {
6649 "<td colspan=\"3\">$extra</td>\n" .
6655 ## ======================================================================
6656 ## ======================================================================
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);
6667 die_error
(404, "No projects found");
6671 if (defined $home_text && -f
$home_text) {
6672 print "<div class=\"index_include\">\n";
6673 insert_file
($home_text);
6677 git_project_search_form
($searchtext, $search_use_regexp);
6678 git_project_list_body
(\
@list, $order);
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);
6692 die_error
(404, "No forks found");
6696 git_print_page_nav
('','');
6697 git_print_header_div
('summary', "$project forks");
6698 git_project_list_body
(\
@list, $order);
6702 sub git_project_index
{
6703 my @projects = git_get_projects_list
($project_filter, $strict_export);
6705 die_error
(404, "No projects found");
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
;
6725 print "$path $owner\n";
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
() : ();
6745 my $check_forks = gitweb_check_feature
('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)
6758 git_print_page_nav
('summary','', $head);
6760 print "<div class=\"title\"> </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);
6783 my $show_ctags = gitweb_check_feature
('ctags');
6785 my $ctags = git_get_project_ctags
($project);
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>" .
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
6809 my @commitlist = $head ? parse_commits
($head, 17) : ();
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")}, "..."));
6818 git_print_header_div
('tags');
6819 git_tags_body
(\
@taglist, 0, 15,
6820 $#taglist <= 15 ?
undef :
6821 $cgi->a({-href
=> href
(action
=>"tags")}, "..."));
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")}, "..."));
6832 git_print_header_div
('remotes');
6833 git_remotes_body
(\
%remotedata, 15, $head);
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")}, "..."),
6848 my %tag = parse_tag
($hash);
6851 die_error
(404, "Unknown tag object");
6854 my $head = git_get_head_hash
($project);
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" .
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" .
6867 if (defined($tag{'author'})) {
6868 git_print_authorship_rows
(\
%tag, 'author');
6870 print "</table>\n\n" .
6872 print "<div class=\"page_body\">";
6873 my $comment = $tag{'comment'};
6874 foreach my $line (@
$comment) {
6876 print esc_html
($line, -nbsp
=>1) . "<br/>\n";
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
6890 gitweb_check_feature
('blame')
6891 or die_error
(403, "Blame view not allowed");
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");
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");
6904 $ftype = git_get_type
($hash);
6905 if ($ftype !~ "blob") {
6906 die_error
(400, "Object is not a blob");
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");
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') {
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);
6938 or print "ERROR $!\n";
6941 if (defined $t0 && gitweb_check_feature
('timed')) {
6943 tv_interval
($t0, [ gettimeofday
() ]).
6944 ' '.$number_of_git_cmds;
6954 $cgi->a({-href
=> href
(action
=>"blob", -replay
=>1)},
6957 if ($format eq 'incremental') {
6959 $cgi->a({-href
=> href
(action
=>"blame", javascript
=>0, -replay
=>1)},
6960 "blame") . " (non-incremental)";
6963 $cgi->a({-href
=> href
(action
=>"blame_incremental", -replay
=>1)},
6964 "blame") . " (incremental)";
6968 $cgi->a({-href
=> href
(action
=>"history", -replay
=>1)},
6971 $cgi->a({-href
=> href
(action
=>$action, file_name
=>$file_name)},
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);
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)},
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!.
6995 qq!<tr
><th
>Commit
</th><th>Line</th
><th
>Data
</th></tr
>\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];
7009 while (my $line = <$fd>) {
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";
7021 } else { # porcelain, i.e. ordinary blame
7022 my %metainfo = (); # saves information about commits
7026 while (my $line = <$fd>) {
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};
7037 while ($data = <$fd>) {
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'};
7050 parse_date
($meta->{'author-time'}, $meta->{'author-tz'});
7051 my $date = $date{'iso-tz'};
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";
7061 print "<td class=\"sha1\"";
7062 print " title=\"". esc_html
($author) . ", $date\"";
7063 print " rowspan=\"$group_size\"" if ($group_size > 1);
7065 print $cgi->a({-href
=> href
(action
=>"commit",
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) {
7073 esc_html
(join('', @author_initials));
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);
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" },
7099 print "<td class=\"pre\">" . esc_html
($data) . "</td>\n";
7107 "</table>\n"; # class="blame"
7108 print "</div>\n"; # class="blame_body"
7110 or print "Reading blob failed\n";
7119 sub git_blame_incremental
{
7120 git_blame_common
('incremental');
7123 sub git_blame_data
{
7124 git_blame_common
('data');
7128 my $head = git_get_head_hash
($project);
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
();
7135 git_tags_body
(\
@tagslist);
7141 my $head = git_get_head_hash
($project);
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
();
7148 git_heads_body
(\
@headslist, $head);
7153 # used both for single remote view and for list of all the 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);
7179 git_print_header_div
('summary', "$project remotes");
7180 git_remotes_body
($remotedata, undef, $head);
7186 sub git_blob_plain
{
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");
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
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\//) {
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
7228 ($type =~ m!^text/[a-z]+\b(.*)$! ||
7229 ($type =~ m!^[a-z]+/[a-z]\+xml\b(.*)$! && -T
$fd))) {
7231 $rest = defined $rest ?
$rest : '';
7232 $type = "text/plain$rest";
7237 -expires
=> $expires,
7238 -content_disposition
=>
7239 ($sandbox ?
'attachment' : 'inline')
7240 . '; filename="' . $save_as . '"');
7242 binmode STDOUT
, ':raw';
7244 binmode STDOUT
, ':utf8'; # as set at the beginning of gitweb.cgi
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");
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
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) {
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) {
7287 $cgi->a({-href
=> href
(action
=>"blame", -replay
=>1)},
7292 $cgi->a({-href
=> href
(action
=>"history", -replay
=>1)},
7295 $cgi->a({-href
=> href
(action
=>"blob_plain", -replay
=>1)},
7298 $cgi->a({-href
=> href
(action
=>"blob",
7299 hash_base
=>"HEAD", file_name
=>$file_name)},
7303 $cgi->a({-href
=> href
(action
=>"blob_plain", -replay
=>1)},
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);
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!"!;
7318 print qq! alt
="!.esc_attr($file_name).qq!" title
="!.esc_attr($file_name).qq!"!;
7321 href(action=>"blob_plain
", hash=>$hash,
7322 hash_base=>$hash_base, file_name=>$file_name) .
7326 while (my $line = <$fd>) {
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);
7336 or print "Reading blob failed.\n";
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");
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');
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>;
7365 or die_error
(404, "Reading tree failed");
7368 my $refs = git_get_references
();
7369 my $ref = format_ref_marker
($refs, $hash_base);
7372 if (defined $hash_base && (my %co = parse_commit
($hash_base))) {
7374 if (defined $file_name) {
7376 $cgi->a({-href
=> href
(action
=>"history", -replay
=>1)},
7378 $cgi->a({-href
=> href
(action
=>"tree",
7379 hash_base
=>"HEAD", file_name
=>$file_name)},
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);
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 '/') {
7401 git_print_page_path
($file_name, 'tree', $hash_base);
7403 print "<div class=\"page_body\">\n";
7404 print "<table class=\"tree\">\n";
7406 # '..' (top directory) link if possible
7407 if (defined $hash_base &&
7408 defined $file_name && $file_name =~ m![^/]+$!) {
7410 print "<tr class=\"dark\">\n";
7412 print "<tr class=\"light\">\n";
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"> </td>'."\n" if $show_sizes;
7422 print '<td class="list">';
7423 print $cgi->a({-href
=> href
(action
=>"tree",
7424 hash_base
=>$hash_base,
7428 print "<td class=\"link\"></td>\n";
7432 foreach my $line (@entries) {
7433 my %t = parse_ls_tree_line
($line, -z
=> 1, -l
=> $show_sizes);
7436 print "<tr class=\"dark\">\n";
7438 print "<tr class=\"light\">\n";
7442 git_print_tree_entry
(\
%t, $basedir, $hash_base, $have_blame);
7446 print "</table>\n" .
7451 sub sanitize_for_filename
{
7455 $name =~ s/[^[:alnum:]_.-]//g;
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));
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
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 : '';
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
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) = @_;
7511 my $if_modified = $cgi->http('IF_MODIFIED_SINCE');
7512 if (defined $if_modified) {
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);
7522 -last_modified
=> $latest_date{'rfc2822'},
7523 -status
=> '304 Not Modified');
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^{}");
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;
7570 %latest_date = parse_date
($co{'committer_epoch'}, $co{'committer_tz'});
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';
7583 binmode STDOUT
, ':utf8'; # as set at the beginning of gitweb.cgi
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) {
7594 if (!defined $page) {
7597 my $refs = git_get_references
();
7599 my $commit_hash = $base;
7600 if (defined $parent) {
7601 $commit_hash = "$parent..$base";
7604 parse_commits
($commit_hash, 101, (100 * $page),
7605 defined $file_name ?
($file_name, "--full-history") : ());
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");
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);
7631 if ($#commitlist >= 100) {
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 .= " ⋅ " .
7640 $cgi->a({-href
=> href
(action
=>"patches", -replay
=>1)},
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);
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);
7662 git_log_generic
('log', \
&git_log_body
,
7663 $hash, $hash_parent);
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
7676 if (!defined $parent) {
7678 $formats_nav .= '(initial)';
7679 } elsif (@
$parents == 1) {
7680 # single parent commit
7683 $cgi->a({-href
=> href
(action
=>"commit",
7685 esc_html
(substr($parent, 0, 7))) .
7692 $cgi->a({-href
=> href
(action
=>"commit",
7694 esc_html
(substr($_, 0, 7)));
7698 if (gitweb_check_feature
('patches') && @
$parents <= 1) {
7699 $formats_nav .= " | " .
7700 $cgi->a({-href
=> href
(action
=>"patch", -replay
=>1)},
7704 if (!defined $parent) {
7708 defined(my $fd = git_cmd_pipe
"diff-tree", '-r', "--no-commit-id",
7710 (@
$parents <= 1 ?
$parent : '-c'),
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
7718 if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
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,
7729 if (defined $co{'parent'}) {
7730 git_print_header_div
('commitdiff', esc_html
($co{'title'}) . $ref, $hash);
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";
7740 "<td class=\"sha1\">" .
7741 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$co{'tree'}, hash_base
=>$hash),
7742 class => "list"}, $co{'tree'}) .
7744 "<td class=\"link\">" .
7745 $cgi->a({-href
=> href
(action
=>"tree", hash
=>$co{'tree'}, hash_base
=>$hash)},
7747 my $snapshot_links = format_snapshot_links
($hash);
7748 if (defined $snapshot_links) {
7749 print " | " . $snapshot_links;
7754 foreach my $par (@
$parents) {
7757 "<td class=\"sha1\">" .
7758 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$par),
7759 class => "list"}, $par) .
7761 "<td class=\"link\">" .
7762 $cgi->a({-href
=> href
(action
=>"commit", hash
=>$par)}, "commit") .
7764 $cgi->a({-href
=> href
(action
=>"commitdiff", hash
=>$hash, hash_parent
=>$par)}, "diff") .
7771 print "<div class=\"page_body\">\n";
7772 git_print_log
($co{'comment'});
7775 git_difftree_body
(\
@difftree, $hash, @
$parents);
7781 # object is defined by:
7782 # - hash or hash_base alone
7783 # - hash_base and file_name
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");
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");
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");
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');
7827 my $format = shift || 'html';
7828 my $diff_style = $input_params{'diff_style'} || 'inline';
7835 # preparing $fd and %diffinfo for git_patchset_body
7837 if (defined $hash_base && defined $hash_parent_base) {
7838 if (defined $file_name) {
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>;
7846 or die_error
(404, "Reading git-diff-tree failed");
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");
7859 # ':100644 100644 03b21826... 3b93d5e7... M ls-files.c'
7861 grep { /^:[0-7]{6} [0-7]{6} [0-9a-fA-F]{40} $hash/ }
7862 map { chomp; $_ } <$fd>;
7864 or die_error
(404, "Reading git-diff-tree failed");
7866 or die_error
(404, "Blob diff not found");
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}$/) {
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.
7899 die_error
('404 Not Found', "Missing one of the blob diff parameters")
7903 if ($format eq 'html') {
7905 $cgi->a({-href
=> href
(action
=>"blobdiff_plain", -replay
=>1)},
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);
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);
7919 print "<div class=\"page_path\"></div>\n";
7922 } elsif ($format eq 'plain') {
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";
7932 die_error
(400, "Unknown blobdiff format");
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);
7943 print "</div>\n"; # class="page_body"
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;
7953 last if $line =~ m!^\+\+\+!;
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;
7976 @styles[ map { $_ * 2 } 0..$#styles/2 ];
7981 $_ eq $diff_style ?
$styles{$_} :
7982 $cgi->a({-href
=> href
(-replay
=>1, diff_style
=> $_)}, $styles{$_})
7986 sub git_commitdiff
{
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
8006 if ($format eq 'html') {
8008 $cgi->a({-href
=> href
(action
=>"commitdiff_plain", -replay
=>1)},
8010 if ($patch_max && @
{$co{'parents'}} <= 1) {
8011 $formats_nav .= " | " .
8012 $cgi->a({-href
=> href
(action
=>"patch", -replay
=>1)},
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);
8026 for (my $i = 0; $i < @
{$co{'parents'}}; $i++) {
8027 if ($co{'parents'}[$i] eq $hash_parent) {
8028 $formats_nav .= ' parent ' . ($i+1);
8032 $formats_nav .= ': ' .
8033 $cgi->a({-href
=> href
(-replay
=>1,
8034 hash
=>$hash_parent, hash_base
=>undef)},
8035 esc_html
($hash_parent_short)) .
8037 } elsif (!$co{'parent'}) {
8039 $formats_nav .= ' (initial)';
8040 } elsif (scalar @
{$co{'parents'}} == 1) {
8041 # single parent commit
8044 $cgi->a({-href
=> href
(-replay
=>1,
8045 hash
=>$co{'parent'}, hash_base
=>undef)},
8046 esc_html
(substr($co{'parent'}, 0, 7))) .
8050 if ($hash_parent eq '--cc') {
8051 $formats_nav .= ' | ' .
8052 $cgi->a({-href
=> href
(-replay
=>1,
8053 hash
=>$hash, hash_parent
=>'-c')},
8055 } else { # $hash_parent eq '-c'
8056 $formats_nav .= ' | ' .
8057 $cgi->a({-href
=> href
(-replay
=>1,
8058 hash
=>$hash, hash_parent
=>'--cc')},
8064 $cgi->a({-href
=> href
(-replay
=>1,
8065 hash
=>$_, hash_base
=>undef)},
8066 esc_html
(substr($_, 0, 7)));
8067 } @
{$co{'parents'}} ) .
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';
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>) {
8090 # empty line ends raw part of diff-tree output
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 = ();
8106 if ($patch_max > 0) {
8107 push @commit_spec, "-$patch_max";
8109 push @commit_spec, '-n', "$hash_parent..$hash";
8111 if ($params{-single
}) {
8112 push @commit_spec, '-1';
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");
8125 die_error
(400, "Unknown commitdiff format");
8128 # non-textual hash id's can be cached
8130 if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
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);
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";
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";
8176 } elsif ($format eq 'patch') {
8177 my $filename = basename
($project) . "-$hash.patch";
8180 -type
=> 'text/plain',
8181 -charset
=> 'utf-8',
8182 -expires
=> $expires,
8183 -content_disposition
=> 'inline; filename="' . "$filename" . '"');
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);
8194 git_patchset_body
($fd, $diff_style,
8196 $use_parents ? @
{$co{'parents'}} : $hash_parent);
8198 print "</div>\n"; # class="page_body"
8201 } elsif ($format eq 'plain') {
8205 or print "Reading git-diff-tree failed\n";
8206 } elsif ($format eq 'patch') {
8210 or print "Reading git-format-patch failed\n";
8214 sub git_commitdiff_plain
{
8215 git_commitdiff
(-format
=> 'plain');
8218 # format-patch-style patches
8220 git_commitdiff
(-format
=> 'patch', -single
=> 1);
8224 git_commitdiff
(-format
=> 'patch');
8228 git_log_generic
('history', \
&git_history_body
,
8229 $hash_base, $hash_parent_base,
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);
8259 die_error
(404, "Unknown commit object");
8261 if (!defined $page) {
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);
8274 die_error
(400, "Unknown search type");
8278 sub git_search_help
{
8280 git_print_page_nav
('','', $hash,$hash,$hash);
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
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');
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>
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) {
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>
8323 git_log_generic
('shortlog', \
&git_shortlog_body
,
8324 $hash, $hash_parent);
8327 ## ......................................................................
8328 ## feeds (RSS, Atom; OPML)
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);
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'});
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');
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);
8388 $descr = "$project " .
8389 ($format eq 'rss' ?
'RSS' : 'Atom') .
8392 my $owner = git_get_project_owner
($project);
8393 $owner = esc_html
($owner);
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);
8402 $alt_url = href
(-full
=>1, action
=>"summary");
8404 print qq!<?xml version
="1.0" encoding
="utf-8"?
>\n!;
8405 if ($format eq 'rss') {
8407 <rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
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);
8421 "<url>$img</url>\n" .
8422 "<title>$title</title>\n" .
8423 "<link>$alt_url</link>\n" .
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') {
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";
8455 print "<updated>$latest_date{'iso-8601'}</updated>\n";
8457 print "<generator version='$version/$git_version'>gitweb</generator>\n";
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)) {
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 : ()))
8475 my @difftree = map { chomp; $_ } <$fd>;
8479 # print element (entry, item)
8480 my $co_url = href
(-full
=>1, action
=>"commitdiff", hash
=>$commit);
8481 if ($format eq 'rss') {
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>" .
8491 } elsif ($format eq 'atom') {
8493 "<title type=\"html\">" . esc_html
($co{'title'}) . "</title>\n" .
8494 "<updated>$cd{'iso-8601'}</updated>\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
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'};
8516 foreach my $line (@
$comment) {
8517 $line = esc_html
($line);
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'};
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');
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);
8549 if ($format eq 'rss') {
8550 print "</ul>]]>\n" .
8551 "</content:encoded>\n" .
8553 } elsif ($format eq 'atom') {
8554 print "</ul>\n</div>\n" .
8561 if ($format eq 'rss') {
8562 print "</channel>\n</rss>\n";
8563 } elsif ($format eq 'atom') {
8577 my @list = git_get_projects_list
($project_filter, $strict_export);
8579 die_error
(404, "No projects found");
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);
8595 <?xml version="1.0" encoding="utf-8"?>
8596 <opml version="1.0">
8598 <title>$title OPML Export$filter</title>
8601 <outline text="git RSS feeds">
8604 foreach my $pr (@list) {
8606 my $head = git_get_head_hash
($proj{'path'});
8607 if (!defined $head) {
8610 $git_dir = "$projectroot/$proj{'path'}";
8611 my %co = parse_commit
($head);
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";