gitweb: Create Gitweb::Git module
[git/gsoc2010-gitweb.git] / gitweb / gitweb.perl
blob778ac13237a497964d019f0db1269ea9459990d7
1 #!/usr/bin/perl
3 # gitweb - simple web interface to track changes in git repositories
5 # (C) 2005-2006, Kay Sievers <kay.sievers@vrfy.org>
6 # (C) 2005, Christian Gierke
8 # This program is licensed under the GPLv2
10 use strict;
11 use warnings;
13 use File::Spec;
14 # __DIR__ is taken from Dir::Self __DIR__ fragment
15 sub __DIR__ () {
16 File::Spec->rel2abs(join '', (File::Spec->splitpath(__FILE__))[0, 1]);
18 use lib __DIR__ . '/lib';
20 use CGI qw(:standard :escapeHTML -nosticky);
21 use CGI::Util qw(unescape);
22 use CGI::Carp qw(fatalsToBrowser set_message);
23 use Encode;
24 use Fcntl ':mode';
25 use File::Find qw();
26 use File::Basename qw(basename);
28 binmode STDOUT, ':utf8';
30 use Gitweb::Git;
32 our $t0;
33 if (eval { require Time::HiRes; 1; }) {
34 $t0 = [Time::HiRes::gettimeofday()];
37 BEGIN {
38 CGI->compile() if $ENV{'MOD_PERL'};
41 our $version = "++GIT_VERSION++";
43 our ($my_url, $my_uri, $base_url, $path_info, $home_link);
44 sub evaluate_uri {
45 our $cgi;
47 our $my_url = $cgi->url();
48 our $my_uri = $cgi->url(-absolute => 1);
50 # Base URL for relative URLs in gitweb ($logo, $favicon, ...),
51 # needed and used only for URLs with nonempty PATH_INFO
52 our $base_url = $my_url;
54 # When the script is used as DirectoryIndex, the URL does not contain the name
55 # of the script file itself, and $cgi->url() fails to strip PATH_INFO, so we
56 # have to do it ourselves. We make $path_info global because it's also used
57 # later on.
59 # Another issue with the script being the DirectoryIndex is that the resulting
60 # $my_url data is not the full script URL: this is good, because we want
61 # generated links to keep implying the script name if it wasn't explicitly
62 # indicated in the URL we're handling, but it means that $my_url cannot be used
63 # as base URL.
64 # Therefore, if we needed to strip PATH_INFO, then we know that we have
65 # to build the base URL ourselves:
66 our $path_info = $ENV{"PATH_INFO"};
67 if ($path_info) {
68 if ($my_url =~ s,\Q$path_info\E$,, &&
69 $my_uri =~ s,\Q$path_info\E$,, &&
70 defined $ENV{'SCRIPT_NAME'}) {
71 $base_url = $cgi->url(-base => 1) . $ENV{'SCRIPT_NAME'};
75 # target of the home link on top of all pages
76 our $home_link = $my_uri || "/";
79 # $GIT is from Gitweb::Git
80 $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 # name of your site or organization to appear in page titles
94 # replace this with something more descriptive for clearer bookmarks
95 our $site_name = "++GITWEB_SITENAME++"
96 || ($ENV{'SERVER_NAME'} || "Untitled") . " Git";
98 # filename of html text to include at top of each page
99 our $site_header = "++GITWEB_SITE_HEADER++";
100 # html text to include at home page
101 our $home_text = "++GITWEB_HOMETEXT++";
102 # filename of html text to include at bottom of each page
103 our $site_footer = "++GITWEB_SITE_FOOTER++";
105 # URI of stylesheets
106 our @stylesheets = ("++GITWEB_CSS++");
107 # URI of a single stylesheet, which can be overridden in GITWEB_CONFIG.
108 our $stylesheet = undef;
109 # URI of GIT logo (72x27 size)
110 our $logo = "++GITWEB_LOGO++";
111 # URI of GIT favicon, assumed to be image/png type
112 our $favicon = "++GITWEB_FAVICON++";
113 # URI of gitweb.js (JavaScript code for gitweb)
114 our $javascript = "++GITWEB_JS++";
116 # URI and label (title) of GIT logo link
117 #our $logo_url = "http://www.kernel.org/pub/software/scm/git/docs/";
118 #our $logo_label = "git documentation";
119 our $logo_url = "http://git-scm.com/";
120 our $logo_label = "git homepage";
122 # source of projects list
123 our $projects_list = "++GITWEB_LIST++";
125 # the width (in characters) of the projects list "Description" column
126 our $projects_list_description_width = 25;
128 # default order of projects list
129 # valid values are none, project, descr, owner, and age
130 our $default_projects_order = "project";
132 # show repository only if this file exists
133 # (only effective if this variable evaluates to true)
134 our $export_ok = "++GITWEB_EXPORT_OK++";
136 # show repository only if this subroutine returns true
137 # when given the path to the project, for example:
138 # sub { return -e "$_[0]/git-daemon-export-ok"; }
139 our $export_auth_hook = undef;
141 # only allow viewing of repositories also shown on the overview page
142 our $strict_export = "++GITWEB_STRICT_EXPORT++";
144 # list of git base URLs used for URL to where fetch project from,
145 # i.e. full URL is "$git_base_url/$project"
146 our @git_base_url_list = grep { $_ ne '' } ("++GITWEB_BASE_URL++");
148 # default blob_plain mimetype and default charset for text/plain blob
149 our $default_blob_plain_mimetype = 'text/plain';
150 our $default_text_plain_charset = undef;
152 # file to use for guessing MIME types before trying /etc/mime.types
153 # (relative to the current git repository)
154 our $mimetypes_file = undef;
156 # assume this charset if line contains non-UTF-8 characters;
157 # it should be valid encoding (see Encoding::Supported(3pm) for list),
158 # for which encoding all byte sequences are valid, for example
159 # 'iso-8859-1' aka 'latin1' (it is decoded without checking, so it
160 # could be even 'utf-8' for the old behavior)
161 our $fallback_encoding = 'latin1';
163 # rename detection options for git-diff and git-diff-tree
164 # - default is '-M', with the cost proportional to
165 # (number of removed files) * (number of new files).
166 # - more costly is '-C' (which implies '-M'), with the cost proportional to
167 # (number of changed files + number of removed files) * (number of new files)
168 # - even more costly is '-C', '--find-copies-harder' with cost
169 # (number of files in the original tree) * (number of new files)
170 # - one might want to include '-B' option, e.g. '-B', '-M'
171 our @diff_opts = ('-M'); # taken from git_commit
173 # Disables features that would allow repository owners to inject script into
174 # the gitweb domain.
175 our $prevent_xss = 0;
177 # information about snapshot formats that gitweb is capable of serving
178 our %known_snapshot_formats = (
179 # name => {
180 # 'display' => display name,
181 # 'type' => mime type,
182 # 'suffix' => filename suffix,
183 # 'format' => --format for git-archive,
184 # 'compressor' => [compressor command and arguments]
185 # (array reference, optional)
186 # 'disabled' => boolean (optional)}
188 'tgz' => {
189 'display' => 'tar.gz',
190 'type' => 'application/x-gzip',
191 'suffix' => '.tar.gz',
192 'format' => 'tar',
193 'compressor' => ['gzip']},
195 'tbz2' => {
196 'display' => 'tar.bz2',
197 'type' => 'application/x-bzip2',
198 'suffix' => '.tar.bz2',
199 'format' => 'tar',
200 'compressor' => ['bzip2']},
202 'txz' => {
203 'display' => 'tar.xz',
204 'type' => 'application/x-xz',
205 'suffix' => '.tar.xz',
206 'format' => 'tar',
207 'compressor' => ['xz'],
208 'disabled' => 1},
210 'zip' => {
211 'display' => 'zip',
212 'type' => 'application/x-zip',
213 'suffix' => '.zip',
214 'format' => 'zip'},
217 # Aliases so we understand old gitweb.snapshot values in repository
218 # configuration.
219 our %known_snapshot_format_aliases = (
220 'gzip' => 'tgz',
221 'bzip2' => 'tbz2',
222 'xz' => 'txz',
224 # backward compatibility: legacy gitweb config support
225 'x-gzip' => undef, 'gz' => undef,
226 'x-bzip2' => undef, 'bz2' => undef,
227 'x-zip' => undef, '' => undef,
230 # Pixel sizes for icons and avatars. If the default font sizes or lineheights
231 # are changed, it may be appropriate to change these values too via
232 # $GITWEB_CONFIG.
233 our %avatar_size = (
234 'default' => 16,
235 'double' => 32
238 # Used to set the maximum load that we will still respond to gitweb queries.
239 # If server load exceed this value then return "503 server busy" error.
240 # If gitweb cannot determined server load, it is taken to be 0.
241 # Leave it undefined (or set to 'undef') to turn off load checking.
242 our $maxload = 300;
244 # You define site-wide feature defaults here; override them with
245 # $GITWEB_CONFIG as necessary.
246 our %feature = (
247 # feature => {
248 # 'sub' => feature-sub (subroutine),
249 # 'override' => allow-override (boolean),
250 # 'default' => [ default options...] (array reference)}
252 # if feature is overridable (it means that allow-override has true value),
253 # then feature-sub will be called with default options as parameters;
254 # return value of feature-sub indicates if to enable specified feature
256 # if there is no 'sub' key (no feature-sub), then feature cannot be
257 # overriden
259 # use gitweb_get_feature(<feature>) to retrieve the <feature> value
260 # (an array) or gitweb_check_feature(<feature>) to check if <feature>
261 # is enabled
263 # Enable the 'blame' blob view, showing the last commit that modified
264 # each line in the file. This can be very CPU-intensive.
266 # To enable system wide have in $GITWEB_CONFIG
267 # $feature{'blame'}{'default'} = [1];
268 # To have project specific config enable override in $GITWEB_CONFIG
269 # $feature{'blame'}{'override'} = 1;
270 # and in project config gitweb.blame = 0|1;
271 'blame' => {
272 'sub' => sub { feature_bool('blame', @_) },
273 'override' => 0,
274 'default' => [0]},
276 # Enable the 'snapshot' link, providing a compressed archive of any
277 # tree. This can potentially generate high traffic if you have large
278 # project.
280 # Value is a list of formats defined in %known_snapshot_formats that
281 # you wish to offer.
282 # To disable system wide have in $GITWEB_CONFIG
283 # $feature{'snapshot'}{'default'} = [];
284 # To have project specific config enable override in $GITWEB_CONFIG
285 # $feature{'snapshot'}{'override'} = 1;
286 # and in project config, a comma-separated list of formats or "none"
287 # to disable. Example: gitweb.snapshot = tbz2,zip;
288 'snapshot' => {
289 'sub' => \&feature_snapshot,
290 'override' => 0,
291 'default' => ['tgz']},
293 # Enable text search, which will list the commits which match author,
294 # committer or commit text to a given string. Enabled by default.
295 # Project specific override is not supported.
296 'search' => {
297 'override' => 0,
298 'default' => [1]},
300 # Enable grep search, which will list the files in currently selected
301 # tree containing the given string. Enabled by default. This can be
302 # potentially CPU-intensive, of course.
304 # To enable system wide have in $GITWEB_CONFIG
305 # $feature{'grep'}{'default'} = [1];
306 # To have project specific config enable override in $GITWEB_CONFIG
307 # $feature{'grep'}{'override'} = 1;
308 # and in project config gitweb.grep = 0|1;
309 'grep' => {
310 'sub' => sub { feature_bool('grep', @_) },
311 'override' => 0,
312 'default' => [1]},
314 # Enable the pickaxe search, which will list the commits that modified
315 # a given string in a file. This can be practical and quite faster
316 # alternative to 'blame', but still potentially CPU-intensive.
318 # To enable system wide have in $GITWEB_CONFIG
319 # $feature{'pickaxe'}{'default'} = [1];
320 # To have project specific config enable override in $GITWEB_CONFIG
321 # $feature{'pickaxe'}{'override'} = 1;
322 # and in project config gitweb.pickaxe = 0|1;
323 'pickaxe' => {
324 'sub' => sub { feature_bool('pickaxe', @_) },
325 'override' => 0,
326 'default' => [1]},
328 # Enable showing size of blobs in a 'tree' view, in a separate
329 # column, similar to what 'ls -l' does. This cost a bit of IO.
331 # To disable system wide have in $GITWEB_CONFIG
332 # $feature{'show-sizes'}{'default'} = [0];
333 # To have project specific config enable override in $GITWEB_CONFIG
334 # $feature{'show-sizes'}{'override'} = 1;
335 # and in project config gitweb.showsizes = 0|1;
336 'show-sizes' => {
337 'sub' => sub { feature_bool('showsizes', @_) },
338 'override' => 0,
339 'default' => [1]},
341 # Make gitweb use an alternative format of the URLs which can be
342 # more readable and natural-looking: project name is embedded
343 # directly in the path and the query string contains other
344 # auxiliary information. All gitweb installations recognize
345 # URL in either format; this configures in which formats gitweb
346 # generates links.
348 # To enable system wide have in $GITWEB_CONFIG
349 # $feature{'pathinfo'}{'default'} = [1];
350 # Project specific override is not supported.
352 # Note that you will need to change the default location of CSS,
353 # favicon, logo and possibly other files to an absolute URL. Also,
354 # if gitweb.cgi serves as your indexfile, you will need to force
355 # $my_uri to contain the script name in your $GITWEB_CONFIG.
356 'pathinfo' => {
357 'override' => 0,
358 'default' => [0]},
360 # Make gitweb consider projects in project root subdirectories
361 # to be forks of existing projects. Given project $projname.git,
362 # projects matching $projname/*.git will not be shown in the main
363 # projects list, instead a '+' mark will be added to $projname
364 # there and a 'forks' view will be enabled for the project, listing
365 # all the forks. If project list is taken from a file, forks have
366 # to be listed after the main project.
368 # To enable system wide have in $GITWEB_CONFIG
369 # $feature{'forks'}{'default'} = [1];
370 # Project specific override is not supported.
371 'forks' => {
372 'override' => 0,
373 'default' => [0]},
375 # Insert custom links to the action bar of all project pages.
376 # This enables you mainly to link to third-party scripts integrating
377 # into gitweb; e.g. git-browser for graphical history representation
378 # or custom web-based repository administration interface.
380 # The 'default' value consists of a list of triplets in the form
381 # (label, link, position) where position is the label after which
382 # to insert the link and link is a format string where %n expands
383 # to the project name, %f to the project path within the filesystem,
384 # %h to the current hash (h gitweb parameter) and %b to the current
385 # hash base (hb gitweb parameter); %% expands to %.
387 # To enable system wide have in $GITWEB_CONFIG e.g.
388 # $feature{'actions'}{'default'} = [('graphiclog',
389 # '/git-browser/by-commit.html?r=%n', 'summary')];
390 # Project specific override is not supported.
391 'actions' => {
392 'override' => 0,
393 'default' => []},
395 # Allow gitweb scan project content tags described in ctags/
396 # of project repository, and display the popular Web 2.0-ish
397 # "tag cloud" near the project list. Note that this is something
398 # COMPLETELY different from the normal Git tags.
400 # gitweb by itself can show existing tags, but it does not handle
401 # tagging itself; you need an external application for that.
402 # For an example script, check Girocco's cgi/tagproj.cgi.
403 # You may want to install the HTML::TagCloud Perl module to get
404 # a pretty tag cloud instead of just a list of tags.
406 # To enable system wide have in $GITWEB_CONFIG
407 # $feature{'ctags'}{'default'} = ['path_to_tag_script'];
408 # Project specific override is not supported.
409 'ctags' => {
410 'override' => 0,
411 'default' => [0]},
413 # The maximum number of patches in a patchset generated in patch
414 # view. Set this to 0 or undef to disable patch view, or to a
415 # negative number to remove any limit.
417 # To disable system wide have in $GITWEB_CONFIG
418 # $feature{'patches'}{'default'} = [0];
419 # To have project specific config enable override in $GITWEB_CONFIG
420 # $feature{'patches'}{'override'} = 1;
421 # and in project config gitweb.patches = 0|n;
422 # where n is the maximum number of patches allowed in a patchset.
423 'patches' => {
424 'sub' => \&feature_patches,
425 'override' => 0,
426 'default' => [16]},
428 # Avatar support. When this feature is enabled, views such as
429 # shortlog or commit will display an avatar associated with
430 # the email of the committer(s) and/or author(s).
432 # Currently available providers are gravatar and picon.
433 # If an unknown provider is specified, the feature is disabled.
435 # Gravatar depends on Digest::MD5.
436 # Picon currently relies on the indiana.edu database.
438 # To enable system wide have in $GITWEB_CONFIG
439 # $feature{'avatar'}{'default'} = ['<provider>'];
440 # where <provider> is either gravatar or picon.
441 # To have project specific config enable override in $GITWEB_CONFIG
442 # $feature{'avatar'}{'override'} = 1;
443 # and in project config gitweb.avatar = <provider>;
444 'avatar' => {
445 'sub' => \&feature_avatar,
446 'override' => 0,
447 'default' => ['']},
449 # Enable displaying how much time and how many git commands
450 # it took to generate and display page. Disabled by default.
451 # Project specific override is not supported.
452 'timed' => {
453 'override' => 0,
454 'default' => [0]},
456 # Enable turning some links into links to actions which require
457 # JavaScript to run (like 'blame_incremental'). Not enabled by
458 # default. Project specific override is currently not supported.
459 'javascript-actions' => {
460 'override' => 0,
461 'default' => [0]},
463 # Syntax highlighting support. This is based on Daniel Svensson's
464 # and Sham Chukoury's work in gitweb-xmms2.git.
465 # It requires the 'highlight' program present in $PATH,
466 # and therefore is disabled by default.
468 # To enable system wide have in $GITWEB_CONFIG
469 # $feature{'highlight'}{'default'} = [1];
471 'highlight' => {
472 'sub' => sub { feature_bool('highlight', @_) },
473 'override' => 0,
474 'default' => [0]},
477 sub gitweb_get_feature {
478 my ($name) = @_;
479 return unless exists $feature{$name};
480 my ($sub, $override, @defaults) = (
481 $feature{$name}{'sub'},
482 $feature{$name}{'override'},
483 @{$feature{$name}{'default'}});
484 # project specific override is possible only if we have project
485 if (!$override || !defined $git_dir) {
486 return @defaults;
488 if (!defined $sub) {
489 warn "feature $name is not overridable";
490 return @defaults;
492 return $sub->(@defaults);
495 # A wrapper to check if a given feature is enabled.
496 # With this, you can say
498 # my $bool_feat = gitweb_check_feature('bool_feat');
499 # gitweb_check_feature('bool_feat') or somecode;
501 # instead of
503 # my ($bool_feat) = gitweb_get_feature('bool_feat');
504 # (gitweb_get_feature('bool_feat'))[0] or somecode;
506 sub gitweb_check_feature {
507 return (gitweb_get_feature(@_))[0];
511 sub feature_bool {
512 my $key = shift;
513 my ($val) = git_get_project_config($key, '--bool');
515 if (!defined $val) {
516 return ($_[0]);
517 } elsif ($val eq 'true') {
518 return (1);
519 } elsif ($val eq 'false') {
520 return (0);
524 sub feature_snapshot {
525 my (@fmts) = @_;
527 my ($val) = git_get_project_config('snapshot');
529 if ($val) {
530 @fmts = ($val eq 'none' ? () : split /\s*[,\s]\s*/, $val);
533 return @fmts;
536 sub feature_patches {
537 my @val = (git_get_project_config('patches', '--int'));
539 if (@val) {
540 return @val;
543 return ($_[0]);
546 sub feature_avatar {
547 my @val = (git_get_project_config('avatar'));
549 return @val ? @val : @_;
552 # checking HEAD file with -e is fragile if the repository was
553 # initialized long time ago (i.e. symlink HEAD) and was pack-ref'ed
554 # and then pruned.
555 sub check_head_link {
556 my ($dir) = @_;
557 my $headfile = "$dir/HEAD";
558 return ((-e $headfile) ||
559 (-l $headfile && readlink($headfile) =~ /^refs\/heads\//));
562 sub check_export_ok {
563 my ($dir) = @_;
564 return (check_head_link($dir) &&
565 (!$export_ok || -e "$dir/$export_ok") &&
566 (!$export_auth_hook || $export_auth_hook->($dir)));
569 # process alternate names for backward compatibility
570 # filter out unsupported (unknown) snapshot formats
571 sub filter_snapshot_fmts {
572 my @fmts = @_;
574 @fmts = map {
575 exists $known_snapshot_format_aliases{$_} ?
576 $known_snapshot_format_aliases{$_} : $_} @fmts;
577 @fmts = grep {
578 exists $known_snapshot_formats{$_} &&
579 !$known_snapshot_formats{$_}{'disabled'}} @fmts;
582 our ($GITWEB_CONFIG, $GITWEB_CONFIG_SYSTEM);
583 sub evaluate_gitweb_config {
584 our $GITWEB_CONFIG = $ENV{'GITWEB_CONFIG'} || "++GITWEB_CONFIG++";
585 our $GITWEB_CONFIG_SYSTEM = $ENV{'GITWEB_CONFIG_SYSTEM'} || "++GITWEB_CONFIG_SYSTEM++";
586 # die if there are errors parsing config file
587 if (-e $GITWEB_CONFIG) {
588 do $GITWEB_CONFIG;
589 die $@ if $@;
590 } elsif (-e $GITWEB_CONFIG_SYSTEM) {
591 do $GITWEB_CONFIG_SYSTEM;
592 die $@ if $@;
596 # Get loadavg of system, to compare against $maxload.
597 # Currently it requires '/proc/loadavg' present to get loadavg;
598 # if it is not present it returns 0, which means no load checking.
599 sub get_loadavg {
600 if( -e '/proc/loadavg' ){
601 open my $fd, '<', '/proc/loadavg'
602 or return 0;
603 my @load = split(/\s+/, scalar <$fd>);
604 close $fd;
606 # The first three columns measure CPU and IO utilization of the last one,
607 # five, and 10 minute periods. The fourth column shows the number of
608 # currently running processes and the total number of processes in the m/n
609 # format. The last column displays the last process ID used.
610 return $load[0] || 0;
612 # additional checks for load average should go here for things that don't export
613 # /proc/loadavg
615 return 0;
618 sub check_loadavg {
619 if (defined $maxload && get_loadavg() > $maxload) {
620 die_error(503, "The load average on the server is too high");
624 # ======================================================================
625 # input validation and dispatch
627 # input parameters can be collected from a variety of sources (presently, CGI
628 # and PATH_INFO), so we define an %input_params hash that collects them all
629 # together during validation: this allows subsequent uses (e.g. href()) to be
630 # agnostic of the parameter origin
632 our %input_params = ();
634 # input parameters are stored with the long parameter name as key. This will
635 # also be used in the href subroutine to convert parameters to their CGI
636 # equivalent, and since the href() usage is the most frequent one, we store
637 # the name -> CGI key mapping here, instead of the reverse.
639 # XXX: Warning: If you touch this, check the search form for updating,
640 # too.
642 our @cgi_param_mapping = (
643 project => "p",
644 action => "a",
645 file_name => "f",
646 file_parent => "fp",
647 hash => "h",
648 hash_parent => "hp",
649 hash_base => "hb",
650 hash_parent_base => "hpb",
651 page => "pg",
652 order => "o",
653 searchtext => "s",
654 searchtype => "st",
655 snapshot_format => "sf",
656 extra_options => "opt",
657 search_use_regexp => "sr",
658 # this must be last entry (for manipulation from JavaScript)
659 javascript => "js"
661 our %cgi_param_mapping = @cgi_param_mapping;
663 # we will also need to know the possible actions, for validation
664 our %actions = (
665 "blame" => \&git_blame,
666 "blame_incremental" => \&git_blame_incremental,
667 "blame_data" => \&git_blame_data,
668 "blobdiff" => \&git_blobdiff,
669 "blobdiff_plain" => \&git_blobdiff_plain,
670 "blob" => \&git_blob,
671 "blob_plain" => \&git_blob_plain,
672 "commitdiff" => \&git_commitdiff,
673 "commitdiff_plain" => \&git_commitdiff_plain,
674 "commit" => \&git_commit,
675 "forks" => \&git_forks,
676 "heads" => \&git_heads,
677 "history" => \&git_history,
678 "log" => \&git_log,
679 "patch" => \&git_patch,
680 "patches" => \&git_patches,
681 "rss" => \&git_rss,
682 "atom" => \&git_atom,
683 "search" => \&git_search,
684 "search_help" => \&git_search_help,
685 "shortlog" => \&git_shortlog,
686 "summary" => \&git_summary,
687 "tag" => \&git_tag,
688 "tags" => \&git_tags,
689 "tree" => \&git_tree,
690 "snapshot" => \&git_snapshot,
691 "object" => \&git_object,
692 # those below don't need $project
693 "opml" => \&git_opml,
694 "project_list" => \&git_project_list,
695 "project_index" => \&git_project_index,
698 # finally, we have the hash of allowed extra_options for the commands that
699 # allow them
700 our %allowed_options = (
701 "--no-merges" => [ qw(rss atom log shortlog history) ],
704 # fill %input_params with the CGI parameters. All values except for 'opt'
705 # should be single values, but opt can be an array. We should probably
706 # build an array of parameters that can be multi-valued, but since for the time
707 # being it's only this one, we just single it out
708 sub evaluate_query_params {
709 our $cgi;
711 while (my ($name, $symbol) = each %cgi_param_mapping) {
712 if ($symbol eq 'opt') {
713 $input_params{$name} = [ $cgi->param($symbol) ];
714 } else {
715 $input_params{$name} = $cgi->param($symbol);
720 # now read PATH_INFO and update the parameter list for missing parameters
721 sub evaluate_path_info {
722 return if defined $input_params{'project'};
723 return if !$path_info;
724 $path_info =~ s,^/+,,;
725 return if !$path_info;
727 # find which part of PATH_INFO is project
728 my $project = $path_info;
729 $project =~ s,/+$,,;
730 while ($project && !check_head_link("$projectroot/$project")) {
731 $project =~ s,/*[^/]*$,,;
733 return unless $project;
734 $input_params{'project'} = $project;
736 # do not change any parameters if an action is given using the query string
737 return if $input_params{'action'};
738 $path_info =~ s,^\Q$project\E/*,,;
740 # next, check if we have an action
741 my $action = $path_info;
742 $action =~ s,/.*$,,;
743 if (exists $actions{$action}) {
744 $path_info =~ s,^$action/*,,;
745 $input_params{'action'} = $action;
748 # list of actions that want hash_base instead of hash, but can have no
749 # pathname (f) parameter
750 my @wants_base = (
751 'tree',
752 'history',
755 # we want to catch
756 # [$hash_parent_base[:$file_parent]..]$hash_parent[:$file_name]
757 my ($parentrefname, $parentpathname, $refname, $pathname) =
758 ($path_info =~ /^(?:(.+?)(?::(.+))?\.\.)?(.+?)(?::(.+))?$/);
760 # first, analyze the 'current' part
761 if (defined $pathname) {
762 # we got "branch:filename" or "branch:dir/"
763 # we could use git_get_type(branch:pathname), but:
764 # - it needs $git_dir
765 # - it does a git() call
766 # - the convention of terminating directories with a slash
767 # makes it superfluous
768 # - embedding the action in the PATH_INFO would make it even
769 # more superfluous
770 $pathname =~ s,^/+,,;
771 if (!$pathname || substr($pathname, -1) eq "/") {
772 $input_params{'action'} ||= "tree";
773 $pathname =~ s,/$,,;
774 } else {
775 # the default action depends on whether we had parent info
776 # or not
777 if ($parentrefname) {
778 $input_params{'action'} ||= "blobdiff_plain";
779 } else {
780 $input_params{'action'} ||= "blob_plain";
783 $input_params{'hash_base'} ||= $refname;
784 $input_params{'file_name'} ||= $pathname;
785 } elsif (defined $refname) {
786 # we got "branch". In this case we have to choose if we have to
787 # set hash or hash_base.
789 # Most of the actions without a pathname only want hash to be
790 # set, except for the ones specified in @wants_base that want
791 # hash_base instead. It should also be noted that hand-crafted
792 # links having 'history' as an action and no pathname or hash
793 # set will fail, but that happens regardless of PATH_INFO.
794 $input_params{'action'} ||= "shortlog";
795 if (grep { $_ eq $input_params{'action'} } @wants_base) {
796 $input_params{'hash_base'} ||= $refname;
797 } else {
798 $input_params{'hash'} ||= $refname;
802 # next, handle the 'parent' part, if present
803 if (defined $parentrefname) {
804 # a missing pathspec defaults to the 'current' filename, allowing e.g.
805 # someproject/blobdiff/oldrev..newrev:/filename
806 if ($parentpathname) {
807 $parentpathname =~ s,^/+,,;
808 $parentpathname =~ s,/$,,;
809 $input_params{'file_parent'} ||= $parentpathname;
810 } else {
811 $input_params{'file_parent'} ||= $input_params{'file_name'};
813 # we assume that hash_parent_base is wanted if a path was specified,
814 # or if the action wants hash_base instead of hash
815 if (defined $input_params{'file_parent'} ||
816 grep { $_ eq $input_params{'action'} } @wants_base) {
817 $input_params{'hash_parent_base'} ||= $parentrefname;
818 } else {
819 $input_params{'hash_parent'} ||= $parentrefname;
823 # for the snapshot action, we allow URLs in the form
824 # $project/snapshot/$hash.ext
825 # where .ext determines the snapshot and gets removed from the
826 # passed $refname to provide the $hash.
828 # To be able to tell that $refname includes the format extension, we
829 # require the following two conditions to be satisfied:
830 # - the hash input parameter MUST have been set from the $refname part
831 # of the URL (i.e. they must be equal)
832 # - the snapshot format MUST NOT have been defined already (e.g. from
833 # CGI parameter sf)
834 # It's also useless to try any matching unless $refname has a dot,
835 # so we check for that too
836 if (defined $input_params{'action'} &&
837 $input_params{'action'} eq 'snapshot' &&
838 defined $refname && index($refname, '.') != -1 &&
839 $refname eq $input_params{'hash'} &&
840 !defined $input_params{'snapshot_format'}) {
841 # We loop over the known snapshot formats, checking for
842 # extensions. Allowed extensions are both the defined suffix
843 # (which includes the initial dot already) and the snapshot
844 # format key itself, with a prepended dot
845 while (my ($fmt, $opt) = each %known_snapshot_formats) {
846 my $hash = $refname;
847 unless ($hash =~ s/(\Q$opt->{'suffix'}\E|\Q.$fmt\E)$//) {
848 next;
850 my $sfx = $1;
851 # a valid suffix was found, so set the snapshot format
852 # and reset the hash parameter
853 $input_params{'snapshot_format'} = $fmt;
854 $input_params{'hash'} = $hash;
855 # we also set the format suffix to the one requested
856 # in the URL: this way a request for e.g. .tgz returns
857 # a .tgz instead of a .tar.gz
858 $known_snapshot_formats{$fmt}{'suffix'} = $sfx;
859 last;
864 our ($action, $project, $file_name, $file_parent, $hash, $hash_parent, $hash_base,
865 $hash_parent_base, @extra_options, $page, $searchtype, $search_use_regexp,
866 $searchtext, $search_regexp);
867 sub evaluate_and_validate_params {
868 our $action = $input_params{'action'};
869 if (defined $action) {
870 if (!validate_action($action)) {
871 die_error(400, "Invalid action parameter");
875 # parameters which are pathnames
876 our $project = $input_params{'project'};
877 if (defined $project) {
878 if (!validate_project($project)) {
879 undef $project;
880 die_error(404, "No such project");
884 our $file_name = $input_params{'file_name'};
885 if (defined $file_name) {
886 if (!validate_pathname($file_name)) {
887 die_error(400, "Invalid file parameter");
891 our $file_parent = $input_params{'file_parent'};
892 if (defined $file_parent) {
893 if (!validate_pathname($file_parent)) {
894 die_error(400, "Invalid file parent parameter");
898 # parameters which are refnames
899 our $hash = $input_params{'hash'};
900 if (defined $hash) {
901 if (!validate_refname($hash)) {
902 die_error(400, "Invalid hash parameter");
906 our $hash_parent = $input_params{'hash_parent'};
907 if (defined $hash_parent) {
908 if (!validate_refname($hash_parent)) {
909 die_error(400, "Invalid hash parent parameter");
913 our $hash_base = $input_params{'hash_base'};
914 if (defined $hash_base) {
915 if (!validate_refname($hash_base)) {
916 die_error(400, "Invalid hash base parameter");
920 our @extra_options = @{$input_params{'extra_options'}};
921 # @extra_options is always defined, since it can only be (currently) set from
922 # CGI, and $cgi->param() returns the empty array in array context if the param
923 # is not set
924 foreach my $opt (@extra_options) {
925 if (not exists $allowed_options{$opt}) {
926 die_error(400, "Invalid option parameter");
928 if (not grep(/^$action$/, @{$allowed_options{$opt}})) {
929 die_error(400, "Invalid option parameter for this action");
933 our $hash_parent_base = $input_params{'hash_parent_base'};
934 if (defined $hash_parent_base) {
935 if (!validate_refname($hash_parent_base)) {
936 die_error(400, "Invalid hash parent base parameter");
940 # other parameters
941 our $page = $input_params{'page'};
942 if (defined $page) {
943 if ($page =~ m/[^0-9]/) {
944 die_error(400, "Invalid page parameter");
948 our $searchtype = $input_params{'searchtype'};
949 if (defined $searchtype) {
950 if ($searchtype =~ m/[^a-z]/) {
951 die_error(400, "Invalid searchtype parameter");
955 our $search_use_regexp = $input_params{'search_use_regexp'};
957 our $searchtext = $input_params{'searchtext'};
958 our $search_regexp;
959 if (defined $searchtext) {
960 if (length($searchtext) < 2) {
961 die_error(403, "At least two characters are required for search parameter");
963 $search_regexp = $search_use_regexp ? $searchtext : quotemeta $searchtext;
967 sub evaluate_git_dir {
968 $git_dir = "$projectroot/$project" if $project;
971 our (@snapshot_fmts, $git_avatar);
972 sub configure_gitweb_features {
973 # list of supported snapshot formats
974 our @snapshot_fmts = gitweb_get_feature('snapshot');
975 @snapshot_fmts = filter_snapshot_fmts(@snapshot_fmts);
977 # check that the avatar feature is set to a known provider name,
978 # and for each provider check if the dependencies are satisfied.
979 # if the provider name is invalid or the dependencies are not met,
980 # reset $git_avatar to the empty string.
981 our ($git_avatar) = gitweb_get_feature('avatar');
982 if ($git_avatar eq 'gravatar') {
983 $git_avatar = '' unless (eval { require Digest::MD5; 1; });
984 } elsif ($git_avatar eq 'picon') {
985 # no dependencies
986 } else {
987 $git_avatar = '';
991 # custom error handler: 'die <message>' is Internal Server Error
992 sub handle_errors_html {
993 my $msg = shift; # it is already HTML escaped
995 # to avoid infinite loop where error occurs in die_error,
996 # change handler to default handler, disabling handle_errors_html
997 set_message("Error occured when inside die_error:\n$msg");
999 # you cannot jump out of die_error when called as error handler;
1000 # the subroutine set via CGI::Carp::set_message is called _after_
1001 # HTTP headers are already written, so it cannot write them itself
1002 die_error(undef, undef, $msg, -error_handler => 1, -no_http_header => 1);
1004 set_message(\&handle_errors_html);
1006 # dispatch
1007 sub dispatch {
1008 if (!defined $action) {
1009 if (defined $hash) {
1010 $action = git_get_type($hash);
1011 } elsif (defined $hash_base && defined $file_name) {
1012 $action = git_get_type("$hash_base:$file_name");
1013 } elsif (defined $project) {
1014 $action = 'summary';
1015 } else {
1016 $action = 'project_list';
1019 if (!defined($actions{$action})) {
1020 die_error(400, "Unknown action");
1022 if ($action !~ m/^(?:opml|project_list|project_index)$/ &&
1023 !$project) {
1024 die_error(400, "Project needed");
1026 $actions{$action}->();
1029 sub run_request {
1030 our $t0 = [Time::HiRes::gettimeofday()]
1031 if defined $t0;
1033 evaluate_uri();
1034 evaluate_gitweb_config();
1035 evaluate_git_version();
1036 check_loadavg();
1038 # $projectroot and $projects_list might be set in gitweb config file
1039 $projects_list ||= $projectroot;
1041 evaluate_query_params();
1042 evaluate_path_info();
1043 evaluate_and_validate_params();
1044 evaluate_git_dir();
1046 configure_gitweb_features();
1048 dispatch();
1051 our $is_last_request = sub { 1 };
1052 our ($pre_dispatch_hook, $post_dispatch_hook, $pre_listen_hook);
1053 our $CGI = 'CGI';
1054 our $cgi;
1055 sub configure_as_fcgi {
1056 require CGI::Fast;
1057 our $CGI = 'CGI::Fast';
1059 my $request_number = 0;
1060 # let each child service 100 requests
1061 our $is_last_request = sub { ++$request_number > 100 };
1063 sub evaluate_argv {
1064 my $script_name = $ENV{'SCRIPT_NAME'} || $ENV{'SCRIPT_FILENAME'} || __FILE__;
1065 configure_as_fcgi()
1066 if $script_name =~ /\.fcgi$/;
1068 return unless (@ARGV);
1070 require Getopt::Long;
1071 Getopt::Long::GetOptions(
1072 'fastcgi|fcgi|f' => \&configure_as_fcgi,
1073 'nproc|n=i' => sub {
1074 my ($arg, $val) = @_;
1075 return unless eval { require FCGI::ProcManager; 1; };
1076 my $proc_manager = FCGI::ProcManager->new({
1077 n_processes => $val,
1079 our $pre_listen_hook = sub { $proc_manager->pm_manage() };
1080 our $pre_dispatch_hook = sub { $proc_manager->pm_pre_dispatch() };
1081 our $post_dispatch_hook = sub { $proc_manager->pm_post_dispatch() };
1086 sub run {
1087 evaluate_argv();
1089 $pre_listen_hook->()
1090 if $pre_listen_hook;
1092 REQUEST:
1093 while ($cgi = $CGI->new()) {
1094 $pre_dispatch_hook->()
1095 if $pre_dispatch_hook;
1097 run_request();
1099 $pre_dispatch_hook->()
1100 if $post_dispatch_hook;
1102 last REQUEST if ($is_last_request->());
1105 DONE_GITWEB:
1109 run();
1111 if (defined caller) {
1112 # wrapped in a subroutine processing requests,
1113 # e.g. mod_perl with ModPerl::Registry, or PSGI with Plack::App::WrapCGI
1114 return;
1115 } else {
1116 # pure CGI script, serving single request
1117 exit;
1120 ## ======================================================================
1121 ## action links
1123 # possible values of extra options
1124 # -full => 0|1 - use absolute/full URL ($my_uri/$my_url as base)
1125 # -replay => 1 - start from a current view (replay with modifications)
1126 # -path_info => 0|1 - don't use/use path_info URL (if possible)
1127 sub href {
1128 my %params = @_;
1129 # default is to use -absolute url() i.e. $my_uri
1130 my $href = $params{-full} ? $my_url : $my_uri;
1132 $params{'project'} = $project unless exists $params{'project'};
1134 if ($params{-replay}) {
1135 while (my ($name, $symbol) = each %cgi_param_mapping) {
1136 if (!exists $params{$name}) {
1137 $params{$name} = $input_params{$name};
1142 my $use_pathinfo = gitweb_check_feature('pathinfo');
1143 if (defined $params{'project'} &&
1144 (exists $params{-path_info} ? $params{-path_info} : $use_pathinfo)) {
1145 # try to put as many parameters as possible in PATH_INFO:
1146 # - project name
1147 # - action
1148 # - hash_parent or hash_parent_base:/file_parent
1149 # - hash or hash_base:/filename
1150 # - the snapshot_format as an appropriate suffix
1152 # When the script is the root DirectoryIndex for the domain,
1153 # $href here would be something like http://gitweb.example.com/
1154 # Thus, we strip any trailing / from $href, to spare us double
1155 # slashes in the final URL
1156 $href =~ s,/$,,;
1158 # Then add the project name, if present
1159 $href .= "/".esc_url($params{'project'});
1160 delete $params{'project'};
1162 # since we destructively absorb parameters, we keep this
1163 # boolean that remembers if we're handling a snapshot
1164 my $is_snapshot = $params{'action'} eq 'snapshot';
1166 # Summary just uses the project path URL, any other action is
1167 # added to the URL
1168 if (defined $params{'action'}) {
1169 $href .= "/".esc_url($params{'action'}) unless $params{'action'} eq 'summary';
1170 delete $params{'action'};
1173 # Next, we put hash_parent_base:/file_parent..hash_base:/file_name,
1174 # stripping nonexistent or useless pieces
1175 $href .= "/" if ($params{'hash_base'} || $params{'hash_parent_base'}
1176 || $params{'hash_parent'} || $params{'hash'});
1177 if (defined $params{'hash_base'}) {
1178 if (defined $params{'hash_parent_base'}) {
1179 $href .= esc_url($params{'hash_parent_base'});
1180 # skip the file_parent if it's the same as the file_name
1181 if (defined $params{'file_parent'}) {
1182 if (defined $params{'file_name'} && $params{'file_parent'} eq $params{'file_name'}) {
1183 delete $params{'file_parent'};
1184 } elsif ($params{'file_parent'} !~ /\.\./) {
1185 $href .= ":/".esc_url($params{'file_parent'});
1186 delete $params{'file_parent'};
1189 $href .= "..";
1190 delete $params{'hash_parent'};
1191 delete $params{'hash_parent_base'};
1192 } elsif (defined $params{'hash_parent'}) {
1193 $href .= esc_url($params{'hash_parent'}). "..";
1194 delete $params{'hash_parent'};
1197 $href .= esc_url($params{'hash_base'});
1198 if (defined $params{'file_name'} && $params{'file_name'} !~ /\.\./) {
1199 $href .= ":/".esc_url($params{'file_name'});
1200 delete $params{'file_name'};
1202 delete $params{'hash'};
1203 delete $params{'hash_base'};
1204 } elsif (defined $params{'hash'}) {
1205 $href .= esc_url($params{'hash'});
1206 delete $params{'hash'};
1209 # If the action was a snapshot, we can absorb the
1210 # snapshot_format parameter too
1211 if ($is_snapshot) {
1212 my $fmt = $params{'snapshot_format'};
1213 # snapshot_format should always be defined when href()
1214 # is called, but just in case some code forgets, we
1215 # fall back to the default
1216 $fmt ||= $snapshot_fmts[0];
1217 $href .= $known_snapshot_formats{$fmt}{'suffix'};
1218 delete $params{'snapshot_format'};
1222 # now encode the parameters explicitly
1223 my @result = ();
1224 for (my $i = 0; $i < @cgi_param_mapping; $i += 2) {
1225 my ($name, $symbol) = ($cgi_param_mapping[$i], $cgi_param_mapping[$i+1]);
1226 if (defined $params{$name}) {
1227 if (ref($params{$name}) eq "ARRAY") {
1228 foreach my $par (@{$params{$name}}) {
1229 push @result, $symbol . "=" . esc_param($par);
1231 } else {
1232 push @result, $symbol . "=" . esc_param($params{$name});
1236 $href .= "?" . join(';', @result) if scalar @result;
1238 return $href;
1242 ## ======================================================================
1243 ## validation, quoting/unquoting and escaping
1245 sub validate_action {
1246 my $input = shift || return undef;
1247 return undef unless exists $actions{$input};
1248 return $input;
1251 sub validate_project {
1252 my $input = shift || return undef;
1253 if (!validate_pathname($input) ||
1254 !(-d "$projectroot/$input") ||
1255 !check_export_ok("$projectroot/$input") ||
1256 ($strict_export && !project_in_list($input))) {
1257 return undef;
1258 } else {
1259 return $input;
1263 sub validate_pathname {
1264 my $input = shift || return undef;
1266 # no '.' or '..' as elements of path, i.e. no '.' nor '..'
1267 # at the beginning, at the end, and between slashes.
1268 # also this catches doubled slashes
1269 if ($input =~ m!(^|/)(|\.|\.\.)(/|$)!) {
1270 return undef;
1272 # no null characters
1273 if ($input =~ m!\0!) {
1274 return undef;
1276 return $input;
1279 sub validate_refname {
1280 my $input = shift || return undef;
1282 # textual hashes are O.K.
1283 if ($input =~ m/^[0-9a-fA-F]{40}$/) {
1284 return $input;
1286 # it must be correct pathname
1287 $input = validate_pathname($input)
1288 or return undef;
1289 # restrictions on ref name according to git-check-ref-format
1290 if ($input =~ m!(/\.|\.\.|[\000-\040\177 ~^:?*\[]|/$)!) {
1291 return undef;
1293 return $input;
1296 # decode sequences of octets in utf8 into Perl's internal form,
1297 # which is utf-8 with utf8 flag set if needed. gitweb writes out
1298 # in utf-8 thanks to "binmode STDOUT, ':utf8'" at beginning
1299 sub to_utf8 {
1300 my $str = shift;
1301 return undef unless defined $str;
1302 if (utf8::valid($str)) {
1303 utf8::decode($str);
1304 return $str;
1305 } else {
1306 return decode($fallback_encoding, $str, Encode::FB_DEFAULT);
1310 # quote unsafe chars, but keep the slash, even when it's not
1311 # correct, but quoted slashes look too horrible in bookmarks
1312 sub esc_param {
1313 my $str = shift;
1314 return undef unless defined $str;
1315 $str =~ s/([^A-Za-z0-9\-_.~()\/:@ ]+)/CGI::escape($1)/eg;
1316 $str =~ s/ /\+/g;
1317 return $str;
1320 # quote unsafe chars in whole URL, so some charactrs cannot be quoted
1321 sub esc_url {
1322 my $str = shift;
1323 return undef unless defined $str;
1324 $str =~ s/([^A-Za-z0-9\-_.~();\/;?:@&= ]+)/CGI::escape($1)/eg;
1325 $str =~ s/ /\+/g;
1326 return $str;
1329 # replace invalid utf8 character with SUBSTITUTION sequence
1330 sub esc_html {
1331 my $str = shift;
1332 my %opts = @_;
1334 return undef unless defined $str;
1336 $str = to_utf8($str);
1337 $str = $cgi->escapeHTML($str);
1338 if ($opts{'-nbsp'}) {
1339 $str =~ s/ /&nbsp;/g;
1341 $str =~ s|([[:cntrl:]])|(($1 ne "\t") ? quot_cec($1) : $1)|eg;
1342 return $str;
1345 # quote control characters and escape filename to HTML
1346 sub esc_path {
1347 my $str = shift;
1348 my %opts = @_;
1350 return undef unless defined $str;
1352 $str = to_utf8($str);
1353 $str = $cgi->escapeHTML($str);
1354 if ($opts{'-nbsp'}) {
1355 $str =~ s/ /&nbsp;/g;
1357 $str =~ s|([[:cntrl:]])|quot_cec($1)|eg;
1358 return $str;
1361 # Make control characters "printable", using character escape codes (CEC)
1362 sub quot_cec {
1363 my $cntrl = shift;
1364 my %opts = @_;
1365 my %es = ( # character escape codes, aka escape sequences
1366 "\t" => '\t', # tab (HT)
1367 "\n" => '\n', # line feed (LF)
1368 "\r" => '\r', # carrige return (CR)
1369 "\f" => '\f', # form feed (FF)
1370 "\b" => '\b', # backspace (BS)
1371 "\a" => '\a', # alarm (bell) (BEL)
1372 "\e" => '\e', # escape (ESC)
1373 "\013" => '\v', # vertical tab (VT)
1374 "\000" => '\0', # nul character (NUL)
1376 my $chr = ( (exists $es{$cntrl})
1377 ? $es{$cntrl}
1378 : sprintf('\%2x', ord($cntrl)) );
1379 if ($opts{-nohtml}) {
1380 return $chr;
1381 } else {
1382 return "<span class=\"cntrl\">$chr</span>";
1386 # Alternatively use unicode control pictures codepoints,
1387 # Unicode "printable representation" (PR)
1388 sub quot_upr {
1389 my $cntrl = shift;
1390 my %opts = @_;
1392 my $chr = sprintf('&#%04d;', 0x2400+ord($cntrl));
1393 if ($opts{-nohtml}) {
1394 return $chr;
1395 } else {
1396 return "<span class=\"cntrl\">$chr</span>";
1400 # git may return quoted and escaped filenames
1401 sub unquote {
1402 my $str = shift;
1404 sub unq {
1405 my $seq = shift;
1406 my %es = ( # character escape codes, aka escape sequences
1407 't' => "\t", # tab (HT, TAB)
1408 'n' => "\n", # newline (NL)
1409 'r' => "\r", # return (CR)
1410 'f' => "\f", # form feed (FF)
1411 'b' => "\b", # backspace (BS)
1412 'a' => "\a", # alarm (bell) (BEL)
1413 'e' => "\e", # escape (ESC)
1414 'v' => "\013", # vertical tab (VT)
1417 if ($seq =~ m/^[0-7]{1,3}$/) {
1418 # octal char sequence
1419 return chr(oct($seq));
1420 } elsif (exists $es{$seq}) {
1421 # C escape sequence, aka character escape code
1422 return $es{$seq};
1424 # quoted ordinary character
1425 return $seq;
1428 if ($str =~ m/^"(.*)"$/) {
1429 # needs unquoting
1430 $str = $1;
1431 $str =~ s/\\([^0-7]|[0-7]{1,3})/unq($1)/eg;
1433 return $str;
1436 # escape tabs (convert tabs to spaces)
1437 sub untabify {
1438 my $line = shift;
1440 while ((my $pos = index($line, "\t")) != -1) {
1441 if (my $count = (8 - ($pos % 8))) {
1442 my $spaces = ' ' x $count;
1443 $line =~ s/\t/$spaces/;
1447 return $line;
1450 sub project_in_list {
1451 my $project = shift;
1452 my @list = git_get_projects_list();
1453 return @list && scalar(grep { $_->{'path'} eq $project } @list);
1456 ## ----------------------------------------------------------------------
1457 ## HTML aware string manipulation
1459 # Try to chop given string on a word boundary between position
1460 # $len and $len+$add_len. If there is no word boundary there,
1461 # chop at $len+$add_len. Do not chop if chopped part plus ellipsis
1462 # (marking chopped part) would be longer than given string.
1463 sub chop_str {
1464 my $str = shift;
1465 my $len = shift;
1466 my $add_len = shift || 10;
1467 my $where = shift || 'right'; # 'left' | 'center' | 'right'
1469 # Make sure perl knows it is utf8 encoded so we don't
1470 # cut in the middle of a utf8 multibyte char.
1471 $str = to_utf8($str);
1473 # allow only $len chars, but don't cut a word if it would fit in $add_len
1474 # if it doesn't fit, cut it if it's still longer than the dots we would add
1475 # remove chopped character entities entirely
1477 # when chopping in the middle, distribute $len into left and right part
1478 # return early if chopping wouldn't make string shorter
1479 if ($where eq 'center') {
1480 return $str if ($len + 5 >= length($str)); # filler is length 5
1481 $len = int($len/2);
1482 } else {
1483 return $str if ($len + 4 >= length($str)); # filler is length 4
1486 # regexps: ending and beginning with word part up to $add_len
1487 my $endre = qr/.{$len}\w{0,$add_len}/;
1488 my $begre = qr/\w{0,$add_len}.{$len}/;
1490 if ($where eq 'left') {
1491 $str =~ m/^(.*?)($begre)$/;
1492 my ($lead, $body) = ($1, $2);
1493 if (length($lead) > 4) {
1494 $lead = " ...";
1496 return "$lead$body";
1498 } elsif ($where eq 'center') {
1499 $str =~ m/^($endre)(.*)$/;
1500 my ($left, $str) = ($1, $2);
1501 $str =~ m/^(.*?)($begre)$/;
1502 my ($mid, $right) = ($1, $2);
1503 if (length($mid) > 5) {
1504 $mid = " ... ";
1506 return "$left$mid$right";
1508 } else {
1509 $str =~ m/^($endre)(.*)$/;
1510 my $body = $1;
1511 my $tail = $2;
1512 if (length($tail) > 4) {
1513 $tail = "... ";
1515 return "$body$tail";
1519 # takes the same arguments as chop_str, but also wraps a <span> around the
1520 # result with a title attribute if it does get chopped. Additionally, the
1521 # string is HTML-escaped.
1522 sub chop_and_escape_str {
1523 my ($str) = @_;
1525 my $chopped = chop_str(@_);
1526 if ($chopped eq $str) {
1527 return esc_html($chopped);
1528 } else {
1529 $str =~ s/[[:cntrl:]]/?/g;
1530 return $cgi->span({-title=>$str}, esc_html($chopped));
1534 ## ----------------------------------------------------------------------
1535 ## functions returning short strings
1537 # CSS class for given age value (in seconds)
1538 sub age_class {
1539 my $age = shift;
1541 if (!defined $age) {
1542 return "noage";
1543 } elsif ($age < 60*60*2) {
1544 return "age0";
1545 } elsif ($age < 60*60*24*2) {
1546 return "age1";
1547 } else {
1548 return "age2";
1552 # convert age in seconds to "nn units ago" string
1553 sub age_string {
1554 my $age = shift;
1555 my $age_str;
1557 if ($age > 60*60*24*365*2) {
1558 $age_str = (int $age/60/60/24/365);
1559 $age_str .= " years ago";
1560 } elsif ($age > 60*60*24*(365/12)*2) {
1561 $age_str = int $age/60/60/24/(365/12);
1562 $age_str .= " months ago";
1563 } elsif ($age > 60*60*24*7*2) {
1564 $age_str = int $age/60/60/24/7;
1565 $age_str .= " weeks ago";
1566 } elsif ($age > 60*60*24*2) {
1567 $age_str = int $age/60/60/24;
1568 $age_str .= " days ago";
1569 } elsif ($age > 60*60*2) {
1570 $age_str = int $age/60/60;
1571 $age_str .= " hours ago";
1572 } elsif ($age > 60*2) {
1573 $age_str = int $age/60;
1574 $age_str .= " min ago";
1575 } elsif ($age > 2) {
1576 $age_str = int $age;
1577 $age_str .= " sec ago";
1578 } else {
1579 $age_str .= " right now";
1581 return $age_str;
1584 use constant {
1585 S_IFINVALID => 0030000,
1586 S_IFGITLINK => 0160000,
1589 # submodule/subproject, a commit object reference
1590 sub S_ISGITLINK {
1591 my $mode = shift;
1593 return (($mode & S_IFMT) == S_IFGITLINK)
1596 # convert file mode in octal to symbolic file mode string
1597 sub mode_str {
1598 my $mode = oct shift;
1600 if (S_ISGITLINK($mode)) {
1601 return 'm---------';
1602 } elsif (S_ISDIR($mode & S_IFMT)) {
1603 return 'drwxr-xr-x';
1604 } elsif (S_ISLNK($mode)) {
1605 return 'lrwxrwxrwx';
1606 } elsif (S_ISREG($mode)) {
1607 # git cares only about the executable bit
1608 if ($mode & S_IXUSR) {
1609 return '-rwxr-xr-x';
1610 } else {
1611 return '-rw-r--r--';
1613 } else {
1614 return '----------';
1618 # convert file mode in octal to file type string
1619 sub file_type {
1620 my $mode = shift;
1622 if ($mode !~ m/^[0-7]+$/) {
1623 return $mode;
1624 } else {
1625 $mode = oct $mode;
1628 if (S_ISGITLINK($mode)) {
1629 return "submodule";
1630 } elsif (S_ISDIR($mode & S_IFMT)) {
1631 return "directory";
1632 } elsif (S_ISLNK($mode)) {
1633 return "symlink";
1634 } elsif (S_ISREG($mode)) {
1635 return "file";
1636 } else {
1637 return "unknown";
1641 # convert file mode in octal to file type description string
1642 sub file_type_long {
1643 my $mode = shift;
1645 if ($mode !~ m/^[0-7]+$/) {
1646 return $mode;
1647 } else {
1648 $mode = oct $mode;
1651 if (S_ISGITLINK($mode)) {
1652 return "submodule";
1653 } elsif (S_ISDIR($mode & S_IFMT)) {
1654 return "directory";
1655 } elsif (S_ISLNK($mode)) {
1656 return "symlink";
1657 } elsif (S_ISREG($mode)) {
1658 if ($mode & S_IXUSR) {
1659 return "executable";
1660 } else {
1661 return "file";
1663 } else {
1664 return "unknown";
1669 ## ----------------------------------------------------------------------
1670 ## functions returning short HTML fragments, or transforming HTML fragments
1671 ## which don't belong to other sections
1673 # format line of commit message.
1674 sub format_log_line_html {
1675 my $line = shift;
1677 $line = esc_html($line, -nbsp=>1);
1678 $line =~ s{\b([0-9a-fA-F]{8,40})\b}{
1679 $cgi->a({-href => href(action=>"object", hash=>$1),
1680 -class => "text"}, $1);
1681 }eg;
1683 return $line;
1686 # format marker of refs pointing to given object
1688 # the destination action is chosen based on object type and current context:
1689 # - for annotated tags, we choose the tag view unless it's the current view
1690 # already, in which case we go to shortlog view
1691 # - for other refs, we keep the current view if we're in history, shortlog or
1692 # log view, and select shortlog otherwise
1693 sub format_ref_marker {
1694 my ($refs, $id) = @_;
1695 my $markers = '';
1697 if (defined $refs->{$id}) {
1698 foreach my $ref (@{$refs->{$id}}) {
1699 # this code exploits the fact that non-lightweight tags are the
1700 # only indirect objects, and that they are the only objects for which
1701 # we want to use tag instead of shortlog as action
1702 my ($type, $name) = qw();
1703 my $indirect = ($ref =~ s/\^\{\}$//);
1704 # e.g. tags/v2.6.11 or heads/next
1705 if ($ref =~ m!^(.*?)s?/(.*)$!) {
1706 $type = $1;
1707 $name = $2;
1708 } else {
1709 $type = "ref";
1710 $name = $ref;
1713 my $class = $type;
1714 $class .= " indirect" if $indirect;
1716 my $dest_action = "shortlog";
1718 if ($indirect) {
1719 $dest_action = "tag" unless $action eq "tag";
1720 } elsif ($action =~ /^(history|(short)?log)$/) {
1721 $dest_action = $action;
1724 my $dest = "";
1725 $dest .= "refs/" unless $ref =~ m!^refs/!;
1726 $dest .= $ref;
1728 my $link = $cgi->a({
1729 -href => href(
1730 action=>$dest_action,
1731 hash=>$dest
1732 )}, $name);
1734 $markers .= " <span class=\"$class\" title=\"$ref\">" .
1735 $link . "</span>";
1739 if ($markers) {
1740 return ' <span class="refs">'. $markers . '</span>';
1741 } else {
1742 return "";
1746 # format, perhaps shortened and with markers, title line
1747 sub format_subject_html {
1748 my ($long, $short, $href, $extra) = @_;
1749 $extra = '' unless defined($extra);
1751 if (length($short) < length($long)) {
1752 $long =~ s/[[:cntrl:]]/?/g;
1753 return $cgi->a({-href => $href, -class => "list subject",
1754 -title => to_utf8($long)},
1755 esc_html($short)) . $extra;
1756 } else {
1757 return $cgi->a({-href => $href, -class => "list subject"},
1758 esc_html($long)) . $extra;
1762 # Rather than recomputing the url for an email multiple times, we cache it
1763 # after the first hit. This gives a visible benefit in views where the avatar
1764 # for the same email is used repeatedly (e.g. shortlog).
1765 # The cache is shared by all avatar engines (currently gravatar only), which
1766 # are free to use it as preferred. Since only one avatar engine is used for any
1767 # given page, there's no risk for cache conflicts.
1768 our %avatar_cache = ();
1770 # Compute the picon url for a given email, by using the picon search service over at
1771 # http://www.cs.indiana.edu/picons/search.html
1772 sub picon_url {
1773 my $email = lc shift;
1774 if (!$avatar_cache{$email}) {
1775 my ($user, $domain) = split('@', $email);
1776 $avatar_cache{$email} =
1777 "http://www.cs.indiana.edu/cgi-pub/kinzler/piconsearch.cgi/" .
1778 "$domain/$user/" .
1779 "users+domains+unknown/up/single";
1781 return $avatar_cache{$email};
1784 # Compute the gravatar url for a given email, if it's not in the cache already.
1785 # Gravatar stores only the part of the URL before the size, since that's the
1786 # one computationally more expensive. This also allows reuse of the cache for
1787 # different sizes (for this particular engine).
1788 sub gravatar_url {
1789 my $email = lc shift;
1790 my $size = shift;
1791 $avatar_cache{$email} ||=
1792 "http://www.gravatar.com/avatar/" .
1793 Digest::MD5::md5_hex($email) . "?s=";
1794 return $avatar_cache{$email} . $size;
1797 # Insert an avatar for the given $email at the given $size if the feature
1798 # is enabled.
1799 sub git_get_avatar {
1800 my ($email, %opts) = @_;
1801 my $pre_white = ($opts{-pad_before} ? "&nbsp;" : "");
1802 my $post_white = ($opts{-pad_after} ? "&nbsp;" : "");
1803 $opts{-size} ||= 'default';
1804 my $size = $avatar_size{$opts{-size}} || $avatar_size{'default'};
1805 my $url = "";
1806 if ($git_avatar eq 'gravatar') {
1807 $url = gravatar_url($email, $size);
1808 } elsif ($git_avatar eq 'picon') {
1809 $url = picon_url($email);
1811 # Other providers can be added by extending the if chain, defining $url
1812 # as needed. If no variant puts something in $url, we assume avatars
1813 # are completely disabled/unavailable.
1814 if ($url) {
1815 return $pre_white .
1816 "<img width=\"$size\" " .
1817 "class=\"avatar\" " .
1818 "src=\"$url\" " .
1819 "alt=\"\" " .
1820 "/>" . $post_white;
1821 } else {
1822 return "";
1826 sub format_search_author {
1827 my ($author, $searchtype, $displaytext) = @_;
1828 my $have_search = gitweb_check_feature('search');
1830 if ($have_search) {
1831 my $performed = "";
1832 if ($searchtype eq 'author') {
1833 $performed = "authored";
1834 } elsif ($searchtype eq 'committer') {
1835 $performed = "committed";
1838 return $cgi->a({-href => href(action=>"search", hash=>$hash,
1839 searchtext=>$author,
1840 searchtype=>$searchtype), class=>"list",
1841 title=>"Search for commits $performed by $author"},
1842 $displaytext);
1844 } else {
1845 return $displaytext;
1849 # format the author name of the given commit with the given tag
1850 # the author name is chopped and escaped according to the other
1851 # optional parameters (see chop_str).
1852 sub format_author_html {
1853 my $tag = shift;
1854 my $co = shift;
1855 my $author = chop_and_escape_str($co->{'author_name'}, @_);
1856 return "<$tag class=\"author\">" .
1857 format_search_author($co->{'author_name'}, "author",
1858 git_get_avatar($co->{'author_email'}, -pad_after => 1) .
1859 $author) .
1860 "</$tag>";
1863 # format git diff header line, i.e. "diff --(git|combined|cc) ..."
1864 sub format_git_diff_header_line {
1865 my $line = shift;
1866 my $diffinfo = shift;
1867 my ($from, $to) = @_;
1869 if ($diffinfo->{'nparents'}) {
1870 # combined diff
1871 $line =~ s!^(diff (.*?) )"?.*$!$1!;
1872 if ($to->{'href'}) {
1873 $line .= $cgi->a({-href => $to->{'href'}, -class => "path"},
1874 esc_path($to->{'file'}));
1875 } else { # file was deleted (no href)
1876 $line .= esc_path($to->{'file'});
1878 } else {
1879 # "ordinary" diff
1880 $line =~ s!^(diff (.*?) )"?a/.*$!$1!;
1881 if ($from->{'href'}) {
1882 $line .= $cgi->a({-href => $from->{'href'}, -class => "path"},
1883 'a/' . esc_path($from->{'file'}));
1884 } else { # file was added (no href)
1885 $line .= 'a/' . esc_path($from->{'file'});
1887 $line .= ' ';
1888 if ($to->{'href'}) {
1889 $line .= $cgi->a({-href => $to->{'href'}, -class => "path"},
1890 'b/' . esc_path($to->{'file'}));
1891 } else { # file was deleted
1892 $line .= 'b/' . esc_path($to->{'file'});
1896 return "<div class=\"diff header\">$line</div>\n";
1899 # format extended diff header line, before patch itself
1900 sub format_extended_diff_header_line {
1901 my $line = shift;
1902 my $diffinfo = shift;
1903 my ($from, $to) = @_;
1905 # match <path>
1906 if ($line =~ s!^((copy|rename) from ).*$!$1! && $from->{'href'}) {
1907 $line .= $cgi->a({-href=>$from->{'href'}, -class=>"path"},
1908 esc_path($from->{'file'}));
1910 if ($line =~ s!^((copy|rename) to ).*$!$1! && $to->{'href'}) {
1911 $line .= $cgi->a({-href=>$to->{'href'}, -class=>"path"},
1912 esc_path($to->{'file'}));
1914 # match single <mode>
1915 if ($line =~ m/\s(\d{6})$/) {
1916 $line .= '<span class="info"> (' .
1917 file_type_long($1) .
1918 ')</span>';
1920 # match <hash>
1921 if ($line =~ m/^index [0-9a-fA-F]{40},[0-9a-fA-F]{40}/) {
1922 # can match only for combined diff
1923 $line = 'index ';
1924 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
1925 if ($from->{'href'}[$i]) {
1926 $line .= $cgi->a({-href=>$from->{'href'}[$i],
1927 -class=>"hash"},
1928 substr($diffinfo->{'from_id'}[$i],0,7));
1929 } else {
1930 $line .= '0' x 7;
1932 # separator
1933 $line .= ',' if ($i < $diffinfo->{'nparents'} - 1);
1935 $line .= '..';
1936 if ($to->{'href'}) {
1937 $line .= $cgi->a({-href=>$to->{'href'}, -class=>"hash"},
1938 substr($diffinfo->{'to_id'},0,7));
1939 } else {
1940 $line .= '0' x 7;
1943 } elsif ($line =~ m/^index [0-9a-fA-F]{40}..[0-9a-fA-F]{40}/) {
1944 # can match only for ordinary diff
1945 my ($from_link, $to_link);
1946 if ($from->{'href'}) {
1947 $from_link = $cgi->a({-href=>$from->{'href'}, -class=>"hash"},
1948 substr($diffinfo->{'from_id'},0,7));
1949 } else {
1950 $from_link = '0' x 7;
1952 if ($to->{'href'}) {
1953 $to_link = $cgi->a({-href=>$to->{'href'}, -class=>"hash"},
1954 substr($diffinfo->{'to_id'},0,7));
1955 } else {
1956 $to_link = '0' x 7;
1958 my ($from_id, $to_id) = ($diffinfo->{'from_id'}, $diffinfo->{'to_id'});
1959 $line =~ s!$from_id\.\.$to_id!$from_link..$to_link!;
1962 return $line . "<br/>\n";
1965 # format from-file/to-file diff header
1966 sub format_diff_from_to_header {
1967 my ($from_line, $to_line, $diffinfo, $from, $to, @parents) = @_;
1968 my $line;
1969 my $result = '';
1971 $line = $from_line;
1972 #assert($line =~ m/^---/) if DEBUG;
1973 # no extra formatting for "^--- /dev/null"
1974 if (! $diffinfo->{'nparents'}) {
1975 # ordinary (single parent) diff
1976 if ($line =~ m!^--- "?a/!) {
1977 if ($from->{'href'}) {
1978 $line = '--- a/' .
1979 $cgi->a({-href=>$from->{'href'}, -class=>"path"},
1980 esc_path($from->{'file'}));
1981 } else {
1982 $line = '--- a/' .
1983 esc_path($from->{'file'});
1986 $result .= qq!<div class="diff from_file">$line</div>\n!;
1988 } else {
1989 # combined diff (merge commit)
1990 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
1991 if ($from->{'href'}[$i]) {
1992 $line = '--- ' .
1993 $cgi->a({-href=>href(action=>"blobdiff",
1994 hash_parent=>$diffinfo->{'from_id'}[$i],
1995 hash_parent_base=>$parents[$i],
1996 file_parent=>$from->{'file'}[$i],
1997 hash=>$diffinfo->{'to_id'},
1998 hash_base=>$hash,
1999 file_name=>$to->{'file'}),
2000 -class=>"path",
2001 -title=>"diff" . ($i+1)},
2002 $i+1) .
2003 '/' .
2004 $cgi->a({-href=>$from->{'href'}[$i], -class=>"path"},
2005 esc_path($from->{'file'}[$i]));
2006 } else {
2007 $line = '--- /dev/null';
2009 $result .= qq!<div class="diff from_file">$line</div>\n!;
2013 $line = $to_line;
2014 #assert($line =~ m/^\+\+\+/) if DEBUG;
2015 # no extra formatting for "^+++ /dev/null"
2016 if ($line =~ m!^\+\+\+ "?b/!) {
2017 if ($to->{'href'}) {
2018 $line = '+++ b/' .
2019 $cgi->a({-href=>$to->{'href'}, -class=>"path"},
2020 esc_path($to->{'file'}));
2021 } else {
2022 $line = '+++ b/' .
2023 esc_path($to->{'file'});
2026 $result .= qq!<div class="diff to_file">$line</div>\n!;
2028 return $result;
2031 # create note for patch simplified by combined diff
2032 sub format_diff_cc_simplified {
2033 my ($diffinfo, @parents) = @_;
2034 my $result = '';
2036 $result .= "<div class=\"diff header\">" .
2037 "diff --cc ";
2038 if (!is_deleted($diffinfo)) {
2039 $result .= $cgi->a({-href => href(action=>"blob",
2040 hash_base=>$hash,
2041 hash=>$diffinfo->{'to_id'},
2042 file_name=>$diffinfo->{'to_file'}),
2043 -class => "path"},
2044 esc_path($diffinfo->{'to_file'}));
2045 } else {
2046 $result .= esc_path($diffinfo->{'to_file'});
2048 $result .= "</div>\n" . # class="diff header"
2049 "<div class=\"diff nodifferences\">" .
2050 "Simple merge" .
2051 "</div>\n"; # class="diff nodifferences"
2053 return $result;
2056 # format patch (diff) line (not to be used for diff headers)
2057 sub format_diff_line {
2058 my $line = shift;
2059 my ($from, $to) = @_;
2060 my $diff_class = "";
2062 chomp $line;
2064 if ($from && $to && ref($from->{'href'}) eq "ARRAY") {
2065 # combined diff
2066 my $prefix = substr($line, 0, scalar @{$from->{'href'}});
2067 if ($line =~ m/^\@{3}/) {
2068 $diff_class = " chunk_header";
2069 } elsif ($line =~ m/^\\/) {
2070 $diff_class = " incomplete";
2071 } elsif ($prefix =~ tr/+/+/) {
2072 $diff_class = " add";
2073 } elsif ($prefix =~ tr/-/-/) {
2074 $diff_class = " rem";
2076 } else {
2077 # assume ordinary diff
2078 my $char = substr($line, 0, 1);
2079 if ($char eq '+') {
2080 $diff_class = " add";
2081 } elsif ($char eq '-') {
2082 $diff_class = " rem";
2083 } elsif ($char eq '@') {
2084 $diff_class = " chunk_header";
2085 } elsif ($char eq "\\") {
2086 $diff_class = " incomplete";
2089 $line = untabify($line);
2090 if ($from && $to && $line =~ m/^\@{2} /) {
2091 my ($from_text, $from_start, $from_lines, $to_text, $to_start, $to_lines, $section) =
2092 $line =~ m/^\@{2} (-(\d+)(?:,(\d+))?) (\+(\d+)(?:,(\d+))?) \@{2}(.*)$/;
2094 $from_lines = 0 unless defined $from_lines;
2095 $to_lines = 0 unless defined $to_lines;
2097 if ($from->{'href'}) {
2098 $from_text = $cgi->a({-href=>"$from->{'href'}#l$from_start",
2099 -class=>"list"}, $from_text);
2101 if ($to->{'href'}) {
2102 $to_text = $cgi->a({-href=>"$to->{'href'}#l$to_start",
2103 -class=>"list"}, $to_text);
2105 $line = "<span class=\"chunk_info\">@@ $from_text $to_text @@</span>" .
2106 "<span class=\"section\">" . esc_html($section, -nbsp=>1) . "</span>";
2107 return "<div class=\"diff$diff_class\">$line</div>\n";
2108 } elsif ($from && $to && $line =~ m/^\@{3}/) {
2109 my ($prefix, $ranges, $section) = $line =~ m/^(\@+) (.*?) \@+(.*)$/;
2110 my (@from_text, @from_start, @from_nlines, $to_text, $to_start, $to_nlines);
2112 @from_text = split(' ', $ranges);
2113 for (my $i = 0; $i < @from_text; ++$i) {
2114 ($from_start[$i], $from_nlines[$i]) =
2115 (split(',', substr($from_text[$i], 1)), 0);
2118 $to_text = pop @from_text;
2119 $to_start = pop @from_start;
2120 $to_nlines = pop @from_nlines;
2122 $line = "<span class=\"chunk_info\">$prefix ";
2123 for (my $i = 0; $i < @from_text; ++$i) {
2124 if ($from->{'href'}[$i]) {
2125 $line .= $cgi->a({-href=>"$from->{'href'}[$i]#l$from_start[$i]",
2126 -class=>"list"}, $from_text[$i]);
2127 } else {
2128 $line .= $from_text[$i];
2130 $line .= " ";
2132 if ($to->{'href'}) {
2133 $line .= $cgi->a({-href=>"$to->{'href'}#l$to_start",
2134 -class=>"list"}, $to_text);
2135 } else {
2136 $line .= $to_text;
2138 $line .= " $prefix</span>" .
2139 "<span class=\"section\">" . esc_html($section, -nbsp=>1) . "</span>";
2140 return "<div class=\"diff$diff_class\">$line</div>\n";
2142 return "<div class=\"diff$diff_class\">" . esc_html($line, -nbsp=>1) . "</div>\n";
2145 # Generates undef or something like "_snapshot_" or "snapshot (_tbz2_ _zip_)",
2146 # linked. Pass the hash of the tree/commit to snapshot.
2147 sub format_snapshot_links {
2148 my ($hash) = @_;
2149 my $num_fmts = @snapshot_fmts;
2150 if ($num_fmts > 1) {
2151 # A parenthesized list of links bearing format names.
2152 # e.g. "snapshot (_tar.gz_ _zip_)"
2153 return "snapshot (" . join(' ', map
2154 $cgi->a({
2155 -href => href(
2156 action=>"snapshot",
2157 hash=>$hash,
2158 snapshot_format=>$_
2160 }, $known_snapshot_formats{$_}{'display'})
2161 , @snapshot_fmts) . ")";
2162 } elsif ($num_fmts == 1) {
2163 # A single "snapshot" link whose tooltip bears the format name.
2164 # i.e. "_snapshot_"
2165 my ($fmt) = @snapshot_fmts;
2166 return
2167 $cgi->a({
2168 -href => href(
2169 action=>"snapshot",
2170 hash=>$hash,
2171 snapshot_format=>$fmt
2173 -title => "in format: $known_snapshot_formats{$fmt}{'display'}"
2174 }, "snapshot");
2175 } else { # $num_fmts == 0
2176 return undef;
2180 ## ......................................................................
2181 ## functions returning values to be passed, perhaps after some
2182 ## transformation, to other functions; e.g. returning arguments to href()
2184 # returns hash to be passed to href to generate gitweb URL
2185 # in -title key it returns description of link
2186 sub get_feed_info {
2187 my $format = shift || 'Atom';
2188 my %res = (action => lc($format));
2190 # feed links are possible only for project views
2191 return unless (defined $project);
2192 # some views should link to OPML, or to generic project feed,
2193 # or don't have specific feed yet (so they should use generic)
2194 return if ($action =~ /^(?:tags|heads|forks|tag|search)$/x);
2196 my $branch;
2197 # branches refs uses 'refs/heads/' prefix (fullname) to differentiate
2198 # from tag links; this also makes possible to detect branch links
2199 if ((defined $hash_base && $hash_base =~ m!^refs/heads/(.*)$!) ||
2200 (defined $hash && $hash =~ m!^refs/heads/(.*)$!)) {
2201 $branch = $1;
2203 # find log type for feed description (title)
2204 my $type = 'log';
2205 if (defined $file_name) {
2206 $type = "history of $file_name";
2207 $type .= "/" if ($action eq 'tree');
2208 $type .= " on '$branch'" if (defined $branch);
2209 } else {
2210 $type = "log of $branch" if (defined $branch);
2213 $res{-title} = $type;
2214 $res{'hash'} = (defined $branch ? "refs/heads/$branch" : undef);
2215 $res{'file_name'} = $file_name;
2217 return %res;
2220 ## ----------------------------------------------------------------------
2221 ## git utility subroutines, invoking git commands
2223 # get HEAD ref of given project as hash
2224 sub git_get_head_hash {
2225 return git_get_full_hash(shift, 'HEAD');
2228 sub git_get_full_hash {
2229 return git_get_hash(@_);
2232 sub git_get_short_hash {
2233 return git_get_hash(@_, '--short=7');
2236 sub git_get_hash {
2237 my ($project, $hash, @options) = @_;
2238 my $o_git_dir = $git_dir;
2239 my $retval = undef;
2240 $git_dir = "$projectroot/$project";
2241 if (open my $fd, '-|', git_cmd(), 'rev-parse',
2242 '--verify', '-q', @options, $hash) {
2243 $retval = <$fd>;
2244 chomp $retval if defined $retval;
2245 close $fd;
2247 if (defined $o_git_dir) {
2248 $git_dir = $o_git_dir;
2250 return $retval;
2253 # get type of given object
2254 sub git_get_type {
2255 my $hash = shift;
2257 open my $fd, "-|", git_cmd(), "cat-file", '-t', $hash or return;
2258 my $type = <$fd>;
2259 close $fd or return;
2260 chomp $type;
2261 return $type;
2264 # repository configuration
2265 our $config_file = '';
2266 our %config;
2268 # store multiple values for single key as anonymous array reference
2269 # single values stored directly in the hash, not as [ <value> ]
2270 sub hash_set_multi {
2271 my ($hash, $key, $value) = @_;
2273 if (!exists $hash->{$key}) {
2274 $hash->{$key} = $value;
2275 } elsif (!ref $hash->{$key}) {
2276 $hash->{$key} = [ $hash->{$key}, $value ];
2277 } else {
2278 push @{$hash->{$key}}, $value;
2282 # return hash of git project configuration
2283 # optionally limited to some section, e.g. 'gitweb'
2284 sub git_parse_project_config {
2285 my $section_regexp = shift;
2286 my %config;
2288 local $/ = "\0";
2290 open my $fh, "-|", git_cmd(), "config", '-z', '-l',
2291 or return;
2293 while (my $keyval = <$fh>) {
2294 chomp $keyval;
2295 my ($key, $value) = split(/\n/, $keyval, 2);
2297 hash_set_multi(\%config, $key, $value)
2298 if (!defined $section_regexp || $key =~ /^(?:$section_regexp)\./o);
2300 close $fh;
2302 return %config;
2305 # convert config value to boolean: 'true' or 'false'
2306 # no value, number > 0, 'true' and 'yes' values are true
2307 # rest of values are treated as false (never as error)
2308 sub config_to_bool {
2309 my $val = shift;
2311 return 1 if !defined $val; # section.key
2313 # strip leading and trailing whitespace
2314 $val =~ s/^\s+//;
2315 $val =~ s/\s+$//;
2317 return (($val =~ /^\d+$/ && $val) || # section.key = 1
2318 ($val =~ /^(?:true|yes)$/i)); # section.key = true
2321 # convert config value to simple decimal number
2322 # an optional value suffix of 'k', 'm', or 'g' will cause the value
2323 # to be multiplied by 1024, 1048576, or 1073741824
2324 sub config_to_int {
2325 my $val = shift;
2327 # strip leading and trailing whitespace
2328 $val =~ s/^\s+//;
2329 $val =~ s/\s+$//;
2331 if (my ($num, $unit) = ($val =~ /^([0-9]*)([kmg])$/i)) {
2332 $unit = lc($unit);
2333 # unknown unit is treated as 1
2334 return $num * ($unit eq 'g' ? 1073741824 :
2335 $unit eq 'm' ? 1048576 :
2336 $unit eq 'k' ? 1024 : 1);
2338 return $val;
2341 # convert config value to array reference, if needed
2342 sub config_to_multi {
2343 my $val = shift;
2345 return ref($val) ? $val : (defined($val) ? [ $val ] : []);
2348 sub git_get_project_config {
2349 my ($key, $type) = @_;
2351 return unless defined $git_dir;
2353 # key sanity check
2354 return unless ($key);
2355 $key =~ s/^gitweb\.//;
2356 return if ($key =~ m/\W/);
2358 # type sanity check
2359 if (defined $type) {
2360 $type =~ s/^--//;
2361 $type = undef
2362 unless ($type eq 'bool' || $type eq 'int');
2365 # get config
2366 if (!defined $config_file ||
2367 $config_file ne "$git_dir/config") {
2368 %config = git_parse_project_config('gitweb');
2369 $config_file = "$git_dir/config";
2372 # check if config variable (key) exists
2373 return unless exists $config{"gitweb.$key"};
2375 # ensure given type
2376 if (!defined $type) {
2377 return $config{"gitweb.$key"};
2378 } elsif ($type eq 'bool') {
2379 # backward compatibility: 'git config --bool' returns true/false
2380 return config_to_bool($config{"gitweb.$key"}) ? 'true' : 'false';
2381 } elsif ($type eq 'int') {
2382 return config_to_int($config{"gitweb.$key"});
2384 return $config{"gitweb.$key"};
2387 # get hash of given path at given ref
2388 sub git_get_hash_by_path {
2389 my $base = shift;
2390 my $path = shift || return undef;
2391 my $type = shift;
2393 $path =~ s,/+$,,;
2395 open my $fd, "-|", git_cmd(), "ls-tree", $base, "--", $path
2396 or die_error(500, "Open git-ls-tree failed");
2397 my $line = <$fd>;
2398 close $fd or return undef;
2400 if (!defined $line) {
2401 # there is no tree or hash given by $path at $base
2402 return undef;
2405 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'
2406 $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t/;
2407 if (defined $type && $type ne $2) {
2408 # type doesn't match
2409 return undef;
2411 return $3;
2414 # get path of entry with given hash at given tree-ish (ref)
2415 # used to get 'from' filename for combined diff (merge commit) for renames
2416 sub git_get_path_by_hash {
2417 my $base = shift || return;
2418 my $hash = shift || return;
2420 local $/ = "\0";
2422 open my $fd, "-|", git_cmd(), "ls-tree", '-r', '-t', '-z', $base
2423 or return undef;
2424 while (my $line = <$fd>) {
2425 chomp $line;
2427 #'040000 tree 595596a6a9117ddba9fe379b6b012b558bac8423 gitweb'
2428 #'100644 blob e02e90f0429be0d2a69b76571101f20b8f75530f gitweb/README'
2429 if ($line =~ m/(?:[0-9]+) (?:.+) $hash\t(.+)$/) {
2430 close $fd;
2431 return $1;
2434 close $fd;
2435 return undef;
2438 ## ......................................................................
2439 ## git utility functions, directly accessing git repository
2441 sub git_get_project_description {
2442 my $path = shift;
2444 $git_dir = "$projectroot/$path";
2445 open my $fd, '<', "$git_dir/description"
2446 or return git_get_project_config('description');
2447 my $descr = <$fd>;
2448 close $fd;
2449 if (defined $descr) {
2450 chomp $descr;
2452 return $descr;
2455 sub git_get_project_ctags {
2456 my $path = shift;
2457 my $ctags = {};
2459 $git_dir = "$projectroot/$path";
2460 opendir my $dh, "$git_dir/ctags"
2461 or return $ctags;
2462 foreach (grep { -f $_ } map { "$git_dir/ctags/$_" } readdir($dh)) {
2463 open my $ct, '<', $_ or next;
2464 my $val = <$ct>;
2465 chomp $val;
2466 close $ct;
2467 my $ctag = $_; $ctag =~ s#.*/##;
2468 $ctags->{$ctag} = $val;
2470 closedir $dh;
2471 $ctags;
2474 sub git_populate_project_tagcloud {
2475 my $ctags = shift;
2477 # First, merge different-cased tags; tags vote on casing
2478 my %ctags_lc;
2479 foreach (keys %$ctags) {
2480 $ctags_lc{lc $_}->{count} += $ctags->{$_};
2481 if (not $ctags_lc{lc $_}->{topcount}
2482 or $ctags_lc{lc $_}->{topcount} < $ctags->{$_}) {
2483 $ctags_lc{lc $_}->{topcount} = $ctags->{$_};
2484 $ctags_lc{lc $_}->{topname} = $_;
2488 my $cloud;
2489 if (eval { require HTML::TagCloud; 1; }) {
2490 $cloud = HTML::TagCloud->new;
2491 foreach (sort keys %ctags_lc) {
2492 # Pad the title with spaces so that the cloud looks
2493 # less crammed.
2494 my $title = $ctags_lc{$_}->{topname};
2495 $title =~ s/ /&nbsp;/g;
2496 $title =~ s/^/&nbsp;/g;
2497 $title =~ s/$/&nbsp;/g;
2498 $cloud->add($title, $home_link."?by_tag=".$_, $ctags_lc{$_}->{count});
2500 } else {
2501 $cloud = \%ctags_lc;
2503 $cloud;
2506 sub git_show_project_tagcloud {
2507 my ($cloud, $count) = @_;
2508 print STDERR ref($cloud)."..\n";
2509 if (ref $cloud eq 'HTML::TagCloud') {
2510 return $cloud->html_and_css($count);
2511 } else {
2512 my @tags = sort { $cloud->{$a}->{count} <=> $cloud->{$b}->{count} } keys %$cloud;
2513 return '<p align="center">' . join (', ', map {
2514 "<a href=\"$home_link?by_tag=$_\">$cloud->{$_}->{topname}</a>"
2515 } splice(@tags, 0, $count)) . '</p>';
2519 sub git_get_project_url_list {
2520 my $path = shift;
2522 $git_dir = "$projectroot/$path";
2523 open my $fd, '<', "$git_dir/cloneurl"
2524 or return wantarray ?
2525 @{ config_to_multi(git_get_project_config('url')) } :
2526 config_to_multi(git_get_project_config('url'));
2527 my @git_project_url_list = map { chomp; $_ } <$fd>;
2528 close $fd;
2530 return wantarray ? @git_project_url_list : \@git_project_url_list;
2533 sub git_get_projects_list {
2534 my ($filter) = @_;
2535 my @list;
2537 $filter ||= '';
2538 $filter =~ s/\.git$//;
2540 my $check_forks = gitweb_check_feature('forks');
2542 if (-d $projects_list) {
2543 # search in directory
2544 my $dir = $projects_list . ($filter ? "/$filter" : '');
2545 # remove the trailing "/"
2546 $dir =~ s!/+$!!;
2547 my $pfxlen = length("$dir");
2548 my $pfxdepth = ($dir =~ tr!/!!);
2550 File::Find::find({
2551 follow_fast => 1, # follow symbolic links
2552 follow_skip => 2, # ignore duplicates
2553 dangling_symlinks => 0, # ignore dangling symlinks, silently
2554 wanted => sub {
2555 # global variables
2556 our $project_maxdepth;
2557 our $projectroot;
2558 # skip project-list toplevel, if we get it.
2559 return if (m!^[/.]$!);
2560 # only directories can be git repositories
2561 return unless (-d $_);
2562 # don't traverse too deep (Find is super slow on os x)
2563 if (($File::Find::name =~ tr!/!!) - $pfxdepth > $project_maxdepth) {
2564 $File::Find::prune = 1;
2565 return;
2568 my $subdir = substr($File::Find::name, $pfxlen + 1);
2569 # we check related file in $projectroot
2570 my $path = ($filter ? "$filter/" : '') . $subdir;
2571 if (check_export_ok("$projectroot/$path")) {
2572 push @list, { path => $path };
2573 $File::Find::prune = 1;
2576 }, "$dir");
2578 } elsif (-f $projects_list) {
2579 # read from file(url-encoded):
2580 # 'git%2Fgit.git Linus+Torvalds'
2581 # 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin'
2582 # 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman'
2583 my %paths;
2584 open my $fd, '<', $projects_list or return;
2585 PROJECT:
2586 while (my $line = <$fd>) {
2587 chomp $line;
2588 my ($path, $owner) = split ' ', $line;
2589 $path = unescape($path);
2590 $owner = unescape($owner);
2591 if (!defined $path) {
2592 next;
2594 if ($filter ne '') {
2595 # looking for forks;
2596 my $pfx = substr($path, 0, length($filter));
2597 if ($pfx ne $filter) {
2598 next PROJECT;
2600 my $sfx = substr($path, length($filter));
2601 if ($sfx !~ /^\/.*\.git$/) {
2602 next PROJECT;
2604 } elsif ($check_forks) {
2605 PATH:
2606 foreach my $filter (keys %paths) {
2607 # looking for forks;
2608 my $pfx = substr($path, 0, length($filter));
2609 if ($pfx ne $filter) {
2610 next PATH;
2612 my $sfx = substr($path, length($filter));
2613 if ($sfx !~ /^\/.*\.git$/) {
2614 next PATH;
2616 # is a fork, don't include it in
2617 # the list
2618 next PROJECT;
2621 if (check_export_ok("$projectroot/$path")) {
2622 my $pr = {
2623 path => $path,
2624 owner => to_utf8($owner),
2626 push @list, $pr;
2627 (my $forks_path = $path) =~ s/\.git$//;
2628 $paths{$forks_path}++;
2631 close $fd;
2633 return @list;
2636 our $gitweb_project_owner = undef;
2637 sub git_get_project_list_from_file {
2639 return if (defined $gitweb_project_owner);
2641 $gitweb_project_owner = {};
2642 # read from file (url-encoded):
2643 # 'git%2Fgit.git Linus+Torvalds'
2644 # 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin'
2645 # 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman'
2646 if (-f $projects_list) {
2647 open(my $fd, '<', $projects_list);
2648 while (my $line = <$fd>) {
2649 chomp $line;
2650 my ($pr, $ow) = split ' ', $line;
2651 $pr = unescape($pr);
2652 $ow = unescape($ow);
2653 $gitweb_project_owner->{$pr} = to_utf8($ow);
2655 close $fd;
2659 sub git_get_project_owner {
2660 my $project = shift;
2661 my $owner;
2663 return undef unless $project;
2664 $git_dir = "$projectroot/$project";
2666 if (!defined $gitweb_project_owner) {
2667 git_get_project_list_from_file();
2670 if (exists $gitweb_project_owner->{$project}) {
2671 $owner = $gitweb_project_owner->{$project};
2673 if (!defined $owner){
2674 $owner = git_get_project_config('owner');
2676 if (!defined $owner) {
2677 $owner = get_file_owner("$git_dir");
2680 return $owner;
2683 sub git_get_last_activity {
2684 my ($path) = @_;
2685 my $fd;
2687 $git_dir = "$projectroot/$path";
2688 open($fd, "-|", git_cmd(), 'for-each-ref',
2689 '--format=%(committer)',
2690 '--sort=-committerdate',
2691 '--count=1',
2692 'refs/heads') or return;
2693 my $most_recent = <$fd>;
2694 close $fd or return;
2695 if (defined $most_recent &&
2696 $most_recent =~ / (\d+) [-+][01]\d\d\d$/) {
2697 my $timestamp = $1;
2698 my $age = time - $timestamp;
2699 return ($age, age_string($age));
2701 return (undef, undef);
2704 sub git_get_references {
2705 my $type = shift || "";
2706 my %refs;
2707 # 5dc01c595e6c6ec9ccda4f6f69c131c0dd945f8c refs/tags/v2.6.11
2708 # c39ae07f393806ccf406ef966e9a15afc43cc36a refs/tags/v2.6.11^{}
2709 open my $fd, "-|", git_cmd(), "show-ref", "--dereference",
2710 ($type ? ("--", "refs/$type") : ()) # use -- <pattern> if $type
2711 or return;
2713 while (my $line = <$fd>) {
2714 chomp $line;
2715 if ($line =~ m!^([0-9a-fA-F]{40})\srefs/($type.*)$!) {
2716 if (defined $refs{$1}) {
2717 push @{$refs{$1}}, $2;
2718 } else {
2719 $refs{$1} = [ $2 ];
2723 close $fd or return;
2724 return \%refs;
2727 sub git_get_rev_name_tags {
2728 my $hash = shift || return undef;
2730 open my $fd, "-|", git_cmd(), "name-rev", "--tags", $hash
2731 or return;
2732 my $name_rev = <$fd>;
2733 close $fd;
2735 if ($name_rev =~ m|^$hash tags/(.*)$|) {
2736 return $1;
2737 } else {
2738 # catches also '$hash undefined' output
2739 return undef;
2743 ## ----------------------------------------------------------------------
2744 ## parse to hash functions
2746 sub parse_date {
2747 my $epoch = shift;
2748 my $tz = shift || "-0000";
2750 my %date;
2751 my @months = ("Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec");
2752 my @days = ("Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat");
2753 my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($epoch);
2754 $date{'hour'} = $hour;
2755 $date{'minute'} = $min;
2756 $date{'mday'} = $mday;
2757 $date{'day'} = $days[$wday];
2758 $date{'month'} = $months[$mon];
2759 $date{'rfc2822'} = sprintf "%s, %d %s %4d %02d:%02d:%02d +0000",
2760 $days[$wday], $mday, $months[$mon], 1900+$year, $hour ,$min, $sec;
2761 $date{'mday-time'} = sprintf "%d %s %02d:%02d",
2762 $mday, $months[$mon], $hour ,$min;
2763 $date{'iso-8601'} = sprintf "%04d-%02d-%02dT%02d:%02d:%02dZ",
2764 1900+$year, 1+$mon, $mday, $hour ,$min, $sec;
2766 $tz =~ m/^([+\-][0-9][0-9])([0-9][0-9])$/;
2767 my $local = $epoch + ((int $1 + ($2/60)) * 3600);
2768 ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($local);
2769 $date{'hour_local'} = $hour;
2770 $date{'minute_local'} = $min;
2771 $date{'tz_local'} = $tz;
2772 $date{'iso-tz'} = sprintf("%04d-%02d-%02d %02d:%02d:%02d %s",
2773 1900+$year, $mon+1, $mday,
2774 $hour, $min, $sec, $tz);
2775 return %date;
2778 sub parse_tag {
2779 my $tag_id = shift;
2780 my %tag;
2781 my @comment;
2783 open my $fd, "-|", git_cmd(), "cat-file", "tag", $tag_id or return;
2784 $tag{'id'} = $tag_id;
2785 while (my $line = <$fd>) {
2786 chomp $line;
2787 if ($line =~ m/^object ([0-9a-fA-F]{40})$/) {
2788 $tag{'object'} = $1;
2789 } elsif ($line =~ m/^type (.+)$/) {
2790 $tag{'type'} = $1;
2791 } elsif ($line =~ m/^tag (.+)$/) {
2792 $tag{'name'} = $1;
2793 } elsif ($line =~ m/^tagger (.*) ([0-9]+) (.*)$/) {
2794 $tag{'author'} = $1;
2795 $tag{'author_epoch'} = $2;
2796 $tag{'author_tz'} = $3;
2797 if ($tag{'author'} =~ m/^([^<]+) <([^>]*)>/) {
2798 $tag{'author_name'} = $1;
2799 $tag{'author_email'} = $2;
2800 } else {
2801 $tag{'author_name'} = $tag{'author'};
2803 } elsif ($line =~ m/--BEGIN/) {
2804 push @comment, $line;
2805 last;
2806 } elsif ($line eq "") {
2807 last;
2810 push @comment, <$fd>;
2811 $tag{'comment'} = \@comment;
2812 close $fd or return;
2813 if (!defined $tag{'name'}) {
2814 return
2816 return %tag
2819 sub parse_commit_text {
2820 my ($commit_text, $withparents) = @_;
2821 my @commit_lines = split '\n', $commit_text;
2822 my %co;
2824 pop @commit_lines; # Remove '\0'
2826 if (! @commit_lines) {
2827 return;
2830 my $header = shift @commit_lines;
2831 if ($header !~ m/^[0-9a-fA-F]{40}/) {
2832 return;
2834 ($co{'id'}, my @parents) = split ' ', $header;
2835 while (my $line = shift @commit_lines) {
2836 last if $line eq "\n";
2837 if ($line =~ m/^tree ([0-9a-fA-F]{40})$/) {
2838 $co{'tree'} = $1;
2839 } elsif ((!defined $withparents) && ($line =~ m/^parent ([0-9a-fA-F]{40})$/)) {
2840 push @parents, $1;
2841 } elsif ($line =~ m/^author (.*) ([0-9]+) (.*)$/) {
2842 $co{'author'} = to_utf8($1);
2843 $co{'author_epoch'} = $2;
2844 $co{'author_tz'} = $3;
2845 if ($co{'author'} =~ m/^([^<]+) <([^>]*)>/) {
2846 $co{'author_name'} = $1;
2847 $co{'author_email'} = $2;
2848 } else {
2849 $co{'author_name'} = $co{'author'};
2851 } elsif ($line =~ m/^committer (.*) ([0-9]+) (.*)$/) {
2852 $co{'committer'} = to_utf8($1);
2853 $co{'committer_epoch'} = $2;
2854 $co{'committer_tz'} = $3;
2855 if ($co{'committer'} =~ m/^([^<]+) <([^>]*)>/) {
2856 $co{'committer_name'} = $1;
2857 $co{'committer_email'} = $2;
2858 } else {
2859 $co{'committer_name'} = $co{'committer'};
2863 if (!defined $co{'tree'}) {
2864 return;
2866 $co{'parents'} = \@parents;
2867 $co{'parent'} = $parents[0];
2869 foreach my $title (@commit_lines) {
2870 $title =~ s/^ //;
2871 if ($title ne "") {
2872 $co{'title'} = chop_str($title, 80, 5);
2873 # remove leading stuff of merges to make the interesting part visible
2874 if (length($title) > 50) {
2875 $title =~ s/^Automatic //;
2876 $title =~ s/^merge (of|with) /Merge ... /i;
2877 if (length($title) > 50) {
2878 $title =~ s/(http|rsync):\/\///;
2880 if (length($title) > 50) {
2881 $title =~ s/(master|www|rsync)\.//;
2883 if (length($title) > 50) {
2884 $title =~ s/kernel.org:?//;
2886 if (length($title) > 50) {
2887 $title =~ s/\/pub\/scm//;
2890 $co{'title_short'} = chop_str($title, 50, 5);
2891 last;
2894 if (! defined $co{'title'} || $co{'title'} eq "") {
2895 $co{'title'} = $co{'title_short'} = '(no commit message)';
2897 # remove added spaces
2898 foreach my $line (@commit_lines) {
2899 $line =~ s/^ //;
2901 $co{'comment'} = \@commit_lines;
2903 my $age = time - $co{'committer_epoch'};
2904 $co{'age'} = $age;
2905 $co{'age_string'} = age_string($age);
2906 my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($co{'committer_epoch'});
2907 if ($age > 60*60*24*7*2) {
2908 $co{'age_string_date'} = sprintf "%4i-%02u-%02i", 1900 + $year, $mon+1, $mday;
2909 $co{'age_string_age'} = $co{'age_string'};
2910 } else {
2911 $co{'age_string_date'} = $co{'age_string'};
2912 $co{'age_string_age'} = sprintf "%4i-%02u-%02i", 1900 + $year, $mon+1, $mday;
2914 return %co;
2917 sub parse_commit {
2918 my ($commit_id) = @_;
2919 my %co;
2921 local $/ = "\0";
2923 open my $fd, "-|", git_cmd(), "rev-list",
2924 "--parents",
2925 "--header",
2926 "--max-count=1",
2927 $commit_id,
2928 "--",
2929 or die_error(500, "Open git-rev-list failed");
2930 %co = parse_commit_text(<$fd>, 1);
2931 close $fd;
2933 return %co;
2936 sub parse_commits {
2937 my ($commit_id, $maxcount, $skip, $filename, @args) = @_;
2938 my @cos;
2940 $maxcount ||= 1;
2941 $skip ||= 0;
2943 local $/ = "\0";
2945 open my $fd, "-|", git_cmd(), "rev-list",
2946 "--header",
2947 @args,
2948 ("--max-count=" . $maxcount),
2949 ("--skip=" . $skip),
2950 @extra_options,
2951 $commit_id,
2952 "--",
2953 ($filename ? ($filename) : ())
2954 or die_error(500, "Open git-rev-list failed");
2955 while (my $line = <$fd>) {
2956 my %co = parse_commit_text($line);
2957 push @cos, \%co;
2959 close $fd;
2961 return wantarray ? @cos : \@cos;
2964 # parse line of git-diff-tree "raw" output
2965 sub parse_difftree_raw_line {
2966 my $line = shift;
2967 my %res;
2969 # ':100644 100644 03b218260e99b78c6df0ed378e59ed9205ccc96d 3b93d5e7cc7f7dd4ebed13a5cc1a4ad976fc94d8 M ls-files.c'
2970 # ':100644 100644 7f9281985086971d3877aca27704f2aaf9c448ce bc190ebc71bbd923f2b728e505408f5e54bd073a M rev-tree.c'
2971 if ($line =~ m/^:([0-7]{6}) ([0-7]{6}) ([0-9a-fA-F]{40}) ([0-9a-fA-F]{40}) (.)([0-9]{0,3})\t(.*)$/) {
2972 $res{'from_mode'} = $1;
2973 $res{'to_mode'} = $2;
2974 $res{'from_id'} = $3;
2975 $res{'to_id'} = $4;
2976 $res{'status'} = $5;
2977 $res{'similarity'} = $6;
2978 if ($res{'status'} eq 'R' || $res{'status'} eq 'C') { # renamed or copied
2979 ($res{'from_file'}, $res{'to_file'}) = map { unquote($_) } split("\t", $7);
2980 } else {
2981 $res{'from_file'} = $res{'to_file'} = $res{'file'} = unquote($7);
2984 # '::100755 100755 100755 60e79ca1b01bc8b057abe17ddab484699a7f5fdb 94067cc5f73388f33722d52ae02f44692bc07490 94067cc5f73388f33722d52ae02f44692bc07490 MR git-gui/git-gui.sh'
2985 # combined diff (for merge commit)
2986 elsif ($line =~ s/^(::+)((?:[0-7]{6} )+)((?:[0-9a-fA-F]{40} )+)([a-zA-Z]+)\t(.*)$//) {
2987 $res{'nparents'} = length($1);
2988 $res{'from_mode'} = [ split(' ', $2) ];
2989 $res{'to_mode'} = pop @{$res{'from_mode'}};
2990 $res{'from_id'} = [ split(' ', $3) ];
2991 $res{'to_id'} = pop @{$res{'from_id'}};
2992 $res{'status'} = [ split('', $4) ];
2993 $res{'to_file'} = unquote($5);
2995 # 'c512b523472485aef4fff9e57b229d9d243c967f'
2996 elsif ($line =~ m/^([0-9a-fA-F]{40})$/) {
2997 $res{'commit'} = $1;
3000 return wantarray ? %res : \%res;
3003 # wrapper: return parsed line of git-diff-tree "raw" output
3004 # (the argument might be raw line, or parsed info)
3005 sub parsed_difftree_line {
3006 my $line_or_ref = shift;
3008 if (ref($line_or_ref) eq "HASH") {
3009 # pre-parsed (or generated by hand)
3010 return $line_or_ref;
3011 } else {
3012 return parse_difftree_raw_line($line_or_ref);
3016 # parse line of git-ls-tree output
3017 sub parse_ls_tree_line {
3018 my $line = shift;
3019 my %opts = @_;
3020 my %res;
3022 if ($opts{'-l'}) {
3023 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa 16717 panic.c'
3024 $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40}) +(-|[0-9]+)\t(.+)$/s;
3026 $res{'mode'} = $1;
3027 $res{'type'} = $2;
3028 $res{'hash'} = $3;
3029 $res{'size'} = $4;
3030 if ($opts{'-z'}) {
3031 $res{'name'} = $5;
3032 } else {
3033 $res{'name'} = unquote($5);
3035 } else {
3036 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'
3037 $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t(.+)$/s;
3039 $res{'mode'} = $1;
3040 $res{'type'} = $2;
3041 $res{'hash'} = $3;
3042 if ($opts{'-z'}) {
3043 $res{'name'} = $4;
3044 } else {
3045 $res{'name'} = unquote($4);
3049 return wantarray ? %res : \%res;
3052 # generates _two_ hashes, references to which are passed as 2 and 3 argument
3053 sub parse_from_to_diffinfo {
3054 my ($diffinfo, $from, $to, @parents) = @_;
3056 if ($diffinfo->{'nparents'}) {
3057 # combined diff
3058 $from->{'file'} = [];
3059 $from->{'href'} = [];
3060 fill_from_file_info($diffinfo, @parents)
3061 unless exists $diffinfo->{'from_file'};
3062 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
3063 $from->{'file'}[$i] =
3064 defined $diffinfo->{'from_file'}[$i] ?
3065 $diffinfo->{'from_file'}[$i] :
3066 $diffinfo->{'to_file'};
3067 if ($diffinfo->{'status'}[$i] ne "A") { # not new (added) file
3068 $from->{'href'}[$i] = href(action=>"blob",
3069 hash_base=>$parents[$i],
3070 hash=>$diffinfo->{'from_id'}[$i],
3071 file_name=>$from->{'file'}[$i]);
3072 } else {
3073 $from->{'href'}[$i] = undef;
3076 } else {
3077 # ordinary (not combined) diff
3078 $from->{'file'} = $diffinfo->{'from_file'};
3079 if ($diffinfo->{'status'} ne "A") { # not new (added) file
3080 $from->{'href'} = href(action=>"blob", hash_base=>$hash_parent,
3081 hash=>$diffinfo->{'from_id'},
3082 file_name=>$from->{'file'});
3083 } else {
3084 delete $from->{'href'};
3088 $to->{'file'} = $diffinfo->{'to_file'};
3089 if (!is_deleted($diffinfo)) { # file exists in result
3090 $to->{'href'} = href(action=>"blob", hash_base=>$hash,
3091 hash=>$diffinfo->{'to_id'},
3092 file_name=>$to->{'file'});
3093 } else {
3094 delete $to->{'href'};
3098 ## ......................................................................
3099 ## parse to array of hashes functions
3101 sub git_get_heads_list {
3102 my $limit = shift;
3103 my @headslist;
3105 open my $fd, '-|', git_cmd(), 'for-each-ref',
3106 ($limit ? '--count='.($limit+1) : ()), '--sort=-committerdate',
3107 '--format=%(objectname) %(refname) %(subject)%00%(committer)',
3108 'refs/heads'
3109 or return;
3110 while (my $line = <$fd>) {
3111 my %ref_item;
3113 chomp $line;
3114 my ($refinfo, $committerinfo) = split(/\0/, $line);
3115 my ($hash, $name, $title) = split(' ', $refinfo, 3);
3116 my ($committer, $epoch, $tz) =
3117 ($committerinfo =~ /^(.*) ([0-9]+) (.*)$/);
3118 $ref_item{'fullname'} = $name;
3119 $name =~ s!^refs/heads/!!;
3121 $ref_item{'name'} = $name;
3122 $ref_item{'id'} = $hash;
3123 $ref_item{'title'} = $title || '(no commit message)';
3124 $ref_item{'epoch'} = $epoch;
3125 if ($epoch) {
3126 $ref_item{'age'} = age_string(time - $ref_item{'epoch'});
3127 } else {
3128 $ref_item{'age'} = "unknown";
3131 push @headslist, \%ref_item;
3133 close $fd;
3135 return wantarray ? @headslist : \@headslist;
3138 sub git_get_tags_list {
3139 my $limit = shift;
3140 my @tagslist;
3142 open my $fd, '-|', git_cmd(), 'for-each-ref',
3143 ($limit ? '--count='.($limit+1) : ()), '--sort=-creatordate',
3144 '--format=%(objectname) %(objecttype) %(refname) '.
3145 '%(*objectname) %(*objecttype) %(subject)%00%(creator)',
3146 'refs/tags'
3147 or return;
3148 while (my $line = <$fd>) {
3149 my %ref_item;
3151 chomp $line;
3152 my ($refinfo, $creatorinfo) = split(/\0/, $line);
3153 my ($id, $type, $name, $refid, $reftype, $title) = split(' ', $refinfo, 6);
3154 my ($creator, $epoch, $tz) =
3155 ($creatorinfo =~ /^(.*) ([0-9]+) (.*)$/);
3156 $ref_item{'fullname'} = $name;
3157 $name =~ s!^refs/tags/!!;
3159 $ref_item{'type'} = $type;
3160 $ref_item{'id'} = $id;
3161 $ref_item{'name'} = $name;
3162 if ($type eq "tag") {
3163 $ref_item{'subject'} = $title;
3164 $ref_item{'reftype'} = $reftype;
3165 $ref_item{'refid'} = $refid;
3166 } else {
3167 $ref_item{'reftype'} = $type;
3168 $ref_item{'refid'} = $id;
3171 if ($type eq "tag" || $type eq "commit") {
3172 $ref_item{'epoch'} = $epoch;
3173 if ($epoch) {
3174 $ref_item{'age'} = age_string(time - $ref_item{'epoch'});
3175 } else {
3176 $ref_item{'age'} = "unknown";
3180 push @tagslist, \%ref_item;
3182 close $fd;
3184 return wantarray ? @tagslist : \@tagslist;
3187 ## ----------------------------------------------------------------------
3188 ## filesystem-related functions
3190 sub get_file_owner {
3191 my $path = shift;
3193 my ($dev, $ino, $mode, $nlink, $st_uid, $st_gid, $rdev, $size) = stat($path);
3194 my ($name, $passwd, $uid, $gid, $quota, $comment, $gcos, $dir, $shell) = getpwuid($st_uid);
3195 if (!defined $gcos) {
3196 return undef;
3198 my $owner = $gcos;
3199 $owner =~ s/[,;].*$//;
3200 return to_utf8($owner);
3203 # assume that file exists
3204 sub insert_file {
3205 my $filename = shift;
3207 open my $fd, '<', $filename;
3208 print map { to_utf8($_) } <$fd>;
3209 close $fd;
3212 ## ......................................................................
3213 ## mimetype related functions
3215 sub mimetype_guess_file {
3216 my $filename = shift;
3217 my $mimemap = shift;
3218 -r $mimemap or return undef;
3220 my %mimemap;
3221 open(my $mh, '<', $mimemap) or return undef;
3222 while (<$mh>) {
3223 next if m/^#/; # skip comments
3224 my ($mimetype, $exts) = split(/\t+/);
3225 if (defined $exts) {
3226 my @exts = split(/\s+/, $exts);
3227 foreach my $ext (@exts) {
3228 $mimemap{$ext} = $mimetype;
3232 close($mh);
3234 $filename =~ /\.([^.]*)$/;
3235 return $mimemap{$1};
3238 sub mimetype_guess {
3239 my $filename = shift;
3240 my $mime;
3241 $filename =~ /\./ or return undef;
3243 if ($mimetypes_file) {
3244 my $file = $mimetypes_file;
3245 if ($file !~ m!^/!) { # if it is relative path
3246 # it is relative to project
3247 $file = "$projectroot/$project/$file";
3249 $mime = mimetype_guess_file($filename, $file);
3251 $mime ||= mimetype_guess_file($filename, '/etc/mime.types');
3252 return $mime;
3255 sub blob_mimetype {
3256 my $fd = shift;
3257 my $filename = shift;
3259 if ($filename) {
3260 my $mime = mimetype_guess($filename);
3261 $mime and return $mime;
3264 # just in case
3265 return $default_blob_plain_mimetype unless $fd;
3267 if (-T $fd) {
3268 return 'text/plain';
3269 } elsif (! $filename) {
3270 return 'application/octet-stream';
3271 } elsif ($filename =~ m/\.png$/i) {
3272 return 'image/png';
3273 } elsif ($filename =~ m/\.gif$/i) {
3274 return 'image/gif';
3275 } elsif ($filename =~ m/\.jpe?g$/i) {
3276 return 'image/jpeg';
3277 } else {
3278 return 'application/octet-stream';
3282 sub blob_contenttype {
3283 my ($fd, $file_name, $type) = @_;
3285 $type ||= blob_mimetype($fd, $file_name);
3286 if ($type eq 'text/plain' && defined $default_text_plain_charset) {
3287 $type .= "; charset=$default_text_plain_charset";
3290 return $type;
3293 # guess file syntax for syntax highlighting; return undef if no highlighting
3294 # the name of syntax can (in the future) depend on syntax highlighter used
3295 sub guess_file_syntax {
3296 my ($highlight, $mimetype, $file_name) = @_;
3297 return undef unless ($highlight && defined $file_name);
3299 # configuration for 'highlight' (http://www.andre-simon.de/)
3300 # match by basename
3301 my %highlight_basename = (
3302 #'Program' => 'py',
3303 #'Library' => 'py',
3304 'SConstruct' => 'py', # SCons equivalent of Makefile
3305 'Makefile' => 'make',
3307 # match by extension
3308 my %highlight_ext = (
3309 # main extensions, defining name of syntax;
3310 # see files in /usr/share/highlight/langDefs/ directory
3311 map { $_ => $_ }
3312 qw(py c cpp rb java css php sh pl js tex bib xml awk bat ini spec tcl),
3313 # alternate extensions, see /etc/highlight/filetypes.conf
3314 'h' => 'c',
3315 map { $_ => 'cpp' } qw(cxx c++ cc),
3316 map { $_ => 'php' } qw(php3 php4),
3317 map { $_ => 'pl' } qw(perl pm), # perhaps also 'cgi'
3318 'mak' => 'make',
3319 map { $_ => 'xml' } qw(xhtml html htm),
3322 my $basename = basename($file_name, '.in');
3323 return $highlight_basename{$basename}
3324 if exists $highlight_basename{$basename};
3326 $basename =~ /\.([^.]*)$/;
3327 my $ext = $1 or return undef;
3328 return $highlight_ext{$ext}
3329 if exists $highlight_ext{$ext};
3331 return undef;
3334 # run highlighter and return FD of its output,
3335 # or return original FD if no highlighting
3336 sub run_highlighter {
3337 my ($fd, $highlight, $syntax) = @_;
3338 return $fd unless ($highlight && defined $syntax);
3340 close $fd
3341 or die_error(404, "Reading blob failed");
3342 open $fd, quote_command(git_cmd(), "cat-file", "blob", $hash)." | ".
3343 "highlight --xhtml --fragment --syntax $syntax |"
3344 or die_error(500, "Couldn't open file or run syntax highlighter");
3345 return $fd;
3348 ## ======================================================================
3349 ## functions printing HTML: header, footer, error page
3351 sub get_page_title {
3352 my $title = to_utf8($site_name);
3354 return $title unless (defined $project);
3355 $title .= " - " . to_utf8($project);
3357 return $title unless (defined $action);
3358 $title .= "/$action"; # $action is US-ASCII (7bit ASCII)
3360 return $title unless (defined $file_name);
3361 $title .= " - " . esc_path($file_name);
3362 if ($action eq "tree" && $file_name !~ m|/$|) {
3363 $title .= "/";
3366 return $title;
3369 sub git_header_html {
3370 my $status = shift || "200 OK";
3371 my $expires = shift;
3372 my %opts = @_;
3374 my $title = get_page_title();
3375 my $content_type;
3376 # require explicit support from the UA if we are to send the page as
3377 # 'application/xhtml+xml', otherwise send it as plain old 'text/html'.
3378 # we have to do this because MSIE sometimes globs '*/*', pretending to
3379 # support xhtml+xml but choking when it gets what it asked for.
3380 if (defined $cgi->http('HTTP_ACCEPT') &&
3381 $cgi->http('HTTP_ACCEPT') =~ m/(,|;|\s|^)application\/xhtml\+xml(,|;|\s|$)/ &&
3382 $cgi->Accept('application/xhtml+xml') != 0) {
3383 $content_type = 'application/xhtml+xml';
3384 } else {
3385 $content_type = 'text/html';
3387 print $cgi->header(-type=>$content_type, -charset => 'utf-8',
3388 -status=> $status, -expires => $expires)
3389 unless ($opts{'-no_http_header'});
3390 my $mod_perl_version = $ENV{'MOD_PERL'} ? " $ENV{'MOD_PERL'}" : '';
3391 print <<EOF;
3392 <?xml version="1.0" encoding="utf-8"?>
3393 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
3394 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US" lang="en-US">
3395 <!-- git web interface version $version, (C) 2005-2006, Kay Sievers <kay.sievers\@vrfy.org>, Christian Gierke -->
3396 <!-- git core binaries version $git_version -->
3397 <head>
3398 <meta http-equiv="content-type" content="$content_type; charset=utf-8"/>
3399 <meta name="generator" content="gitweb/$version git/$git_version$mod_perl_version"/>
3400 <meta name="robots" content="index, nofollow"/>
3401 <title>$title</title>
3403 # the stylesheet, favicon etc urls won't work correctly with path_info
3404 # unless we set the appropriate base URL
3405 if ($ENV{'PATH_INFO'}) {
3406 print "<base href=\"".esc_url($base_url)."\" />\n";
3408 # print out each stylesheet that exist, providing backwards capability
3409 # for those people who defined $stylesheet in a config file
3410 if (defined $stylesheet) {
3411 print '<link rel="stylesheet" type="text/css" href="'.$stylesheet.'"/>'."\n";
3412 } else {
3413 foreach my $stylesheet (@stylesheets) {
3414 next unless $stylesheet;
3415 print '<link rel="stylesheet" type="text/css" href="'.$stylesheet.'"/>'."\n";
3418 if (defined $project) {
3419 my %href_params = get_feed_info();
3420 if (!exists $href_params{'-title'}) {
3421 $href_params{'-title'} = 'log';
3424 foreach my $format qw(RSS Atom) {
3425 my $type = lc($format);
3426 my %link_attr = (
3427 '-rel' => 'alternate',
3428 '-title' => "$project - $href_params{'-title'} - $format feed",
3429 '-type' => "application/$type+xml"
3432 $href_params{'action'} = $type;
3433 $link_attr{'-href'} = href(%href_params);
3434 print "<link ".
3435 "rel=\"$link_attr{'-rel'}\" ".
3436 "title=\"$link_attr{'-title'}\" ".
3437 "href=\"$link_attr{'-href'}\" ".
3438 "type=\"$link_attr{'-type'}\" ".
3439 "/>\n";
3441 $href_params{'extra_options'} = '--no-merges';
3442 $link_attr{'-href'} = href(%href_params);
3443 $link_attr{'-title'} .= ' (no merges)';
3444 print "<link ".
3445 "rel=\"$link_attr{'-rel'}\" ".
3446 "title=\"$link_attr{'-title'}\" ".
3447 "href=\"$link_attr{'-href'}\" ".
3448 "type=\"$link_attr{'-type'}\" ".
3449 "/>\n";
3452 } else {
3453 printf('<link rel="alternate" title="%s projects list" '.
3454 'href="%s" type="text/plain; charset=utf-8" />'."\n",
3455 $site_name, href(project=>undef, action=>"project_index"));
3456 printf('<link rel="alternate" title="%s projects feeds" '.
3457 'href="%s" type="text/x-opml" />'."\n",
3458 $site_name, href(project=>undef, action=>"opml"));
3460 if (defined $favicon) {
3461 print qq(<link rel="shortcut icon" href="$favicon" type="image/png" />\n);
3464 print "</head>\n" .
3465 "<body>\n";
3467 if (defined $site_header && -f $site_header) {
3468 insert_file($site_header);
3471 print "<div class=\"page_header\">\n" .
3472 $cgi->a({-href => esc_url($logo_url),
3473 -title => $logo_label},
3474 qq(<img src="$logo" width="72" height="27" alt="git" class="logo"/>));
3475 print $cgi->a({-href => esc_url($home_link)}, $home_link_str) . " / ";
3476 if (defined $project) {
3477 print $cgi->a({-href => href(action=>"summary")}, esc_html($project));
3478 if (defined $action) {
3479 print " / $action";
3481 print "\n";
3483 print "</div>\n";
3485 my $have_search = gitweb_check_feature('search');
3486 if (defined $project && $have_search) {
3487 if (!defined $searchtext) {
3488 $searchtext = "";
3490 my $search_hash;
3491 if (defined $hash_base) {
3492 $search_hash = $hash_base;
3493 } elsif (defined $hash) {
3494 $search_hash = $hash;
3495 } else {
3496 $search_hash = "HEAD";
3498 my $action = $my_uri;
3499 my $use_pathinfo = gitweb_check_feature('pathinfo');
3500 if ($use_pathinfo) {
3501 $action .= "/".esc_url($project);
3503 print $cgi->startform(-method => "get", -action => $action) .
3504 "<div class=\"search\">\n" .
3505 (!$use_pathinfo &&
3506 $cgi->input({-name=>"p", -value=>$project, -type=>"hidden"}) . "\n") .
3507 $cgi->input({-name=>"a", -value=>"search", -type=>"hidden"}) . "\n" .
3508 $cgi->input({-name=>"h", -value=>$search_hash, -type=>"hidden"}) . "\n" .
3509 $cgi->popup_menu(-name => 'st', -default => 'commit',
3510 -values => ['commit', 'grep', 'author', 'committer', 'pickaxe']) .
3511 $cgi->sup($cgi->a({-href => href(action=>"search_help")}, "?")) .
3512 " search:\n",
3513 $cgi->textfield(-name => "s", -value => $searchtext) . "\n" .
3514 "<span title=\"Extended regular expression\">" .
3515 $cgi->checkbox(-name => 'sr', -value => 1, -label => 're',
3516 -checked => $search_use_regexp) .
3517 "</span>" .
3518 "</div>" .
3519 $cgi->end_form() . "\n";
3523 sub git_footer_html {
3524 my $feed_class = 'rss_logo';
3526 print "<div class=\"page_footer\">\n";
3527 if (defined $project) {
3528 my $descr = git_get_project_description($project);
3529 if (defined $descr) {
3530 print "<div class=\"page_footer_text\">" . esc_html($descr) . "</div>\n";
3533 my %href_params = get_feed_info();
3534 if (!%href_params) {
3535 $feed_class .= ' generic';
3537 $href_params{'-title'} ||= 'log';
3539 foreach my $format qw(RSS Atom) {
3540 $href_params{'action'} = lc($format);
3541 print $cgi->a({-href => href(%href_params),
3542 -title => "$href_params{'-title'} $format feed",
3543 -class => $feed_class}, $format)."\n";
3546 } else {
3547 print $cgi->a({-href => href(project=>undef, action=>"opml"),
3548 -class => $feed_class}, "OPML") . " ";
3549 print $cgi->a({-href => href(project=>undef, action=>"project_index"),
3550 -class => $feed_class}, "TXT") . "\n";
3552 print "</div>\n"; # class="page_footer"
3554 if (defined $t0 && gitweb_check_feature('timed')) {
3555 print "<div id=\"generating_info\">\n";
3556 print 'This page took '.
3557 '<span id="generating_time" class="time_span">'.
3558 Time::HiRes::tv_interval($t0, [Time::HiRes::gettimeofday()]).
3559 ' seconds </span>'.
3560 ' and '.
3561 '<span id="generating_cmd">'.
3562 $number_of_git_cmds.
3563 '</span> git commands '.
3564 " to generate.\n";
3565 print "</div>\n"; # class="page_footer"
3568 if (defined $site_footer && -f $site_footer) {
3569 insert_file($site_footer);
3572 print qq!<script type="text/javascript" src="$javascript"></script>\n!;
3573 if (defined $action &&
3574 $action eq 'blame_incremental') {
3575 print qq!<script type="text/javascript">\n!.
3576 qq!startBlame("!. href(action=>"blame_data", -replay=>1) .qq!",\n!.
3577 qq! "!. href() .qq!");\n!.
3578 qq!</script>\n!;
3579 } elsif (gitweb_check_feature('javascript-actions')) {
3580 print qq!<script type="text/javascript">\n!.
3581 qq!window.onload = fixLinks;\n!.
3582 qq!</script>\n!;
3585 print "</body>\n" .
3586 "</html>";
3589 # die_error(<http_status_code>, <error_message>[, <detailed_html_description>])
3590 # Example: die_error(404, 'Hash not found')
3591 # By convention, use the following status codes (as defined in RFC 2616):
3592 # 400: Invalid or missing CGI parameters, or
3593 # requested object exists but has wrong type.
3594 # 403: Requested feature (like "pickaxe" or "snapshot") not enabled on
3595 # this server or project.
3596 # 404: Requested object/revision/project doesn't exist.
3597 # 500: The server isn't configured properly, or
3598 # an internal error occurred (e.g. failed assertions caused by bugs), or
3599 # an unknown error occurred (e.g. the git binary died unexpectedly).
3600 # 503: The server is currently unavailable (because it is overloaded,
3601 # or down for maintenance). Generally, this is a temporary state.
3602 sub die_error {
3603 my $status = shift || 500;
3604 my $error = esc_html(shift) || "Internal Server Error";
3605 my $extra = shift;
3606 my %opts = @_;
3608 my %http_responses = (
3609 400 => '400 Bad Request',
3610 403 => '403 Forbidden',
3611 404 => '404 Not Found',
3612 500 => '500 Internal Server Error',
3613 503 => '503 Service Unavailable',
3615 git_header_html($http_responses{$status}, undef, %opts);
3616 print <<EOF;
3617 <div class="page_body">
3618 <br /><br />
3619 $status - $error
3620 <br />
3622 if (defined $extra) {
3623 print "<hr />\n" .
3624 "$extra\n";
3626 print "</div>\n";
3628 git_footer_html();
3629 goto DONE_GITWEB
3630 unless ($opts{'-error_handler'});
3633 ## ----------------------------------------------------------------------
3634 ## functions printing or outputting HTML: navigation
3636 sub git_print_page_nav {
3637 my ($current, $suppress, $head, $treehead, $treebase, $extra) = @_;
3638 $extra = '' if !defined $extra; # pager or formats
3640 my @navs = qw(summary shortlog log commit commitdiff tree);
3641 if ($suppress) {
3642 @navs = grep { $_ ne $suppress } @navs;
3645 my %arg = map { $_ => {action=>$_} } @navs;
3646 if (defined $head) {
3647 for (qw(commit commitdiff)) {
3648 $arg{$_}{'hash'} = $head;
3650 if ($current =~ m/^(tree | log | shortlog | commit | commitdiff | search)$/x) {
3651 for (qw(shortlog log)) {
3652 $arg{$_}{'hash'} = $head;
3657 $arg{'tree'}{'hash'} = $treehead if defined $treehead;
3658 $arg{'tree'}{'hash_base'} = $treebase if defined $treebase;
3660 my @actions = gitweb_get_feature('actions');
3661 my %repl = (
3662 '%' => '%',
3663 'n' => $project, # project name
3664 'f' => $git_dir, # project path within filesystem
3665 'h' => $treehead || '', # current hash ('h' parameter)
3666 'b' => $treebase || '', # hash base ('hb' parameter)
3668 while (@actions) {
3669 my ($label, $link, $pos) = splice(@actions,0,3);
3670 # insert
3671 @navs = map { $_ eq $pos ? ($_, $label) : $_ } @navs;
3672 # munch munch
3673 $link =~ s/%([%nfhb])/$repl{$1}/g;
3674 $arg{$label}{'_href'} = $link;
3677 print "<div class=\"page_nav\">\n" .
3678 (join " | ",
3679 map { $_ eq $current ?
3680 $_ : $cgi->a({-href => ($arg{$_}{_href} ? $arg{$_}{_href} : href(%{$arg{$_}}))}, "$_")
3681 } @navs);
3682 print "<br/>\n$extra<br/>\n" .
3683 "</div>\n";
3686 sub format_paging_nav {
3687 my ($action, $page, $has_next_link) = @_;
3688 my $paging_nav;
3691 if ($page > 0) {
3692 $paging_nav .=
3693 $cgi->a({-href => href(-replay=>1, page=>undef)}, "first") .
3694 " &sdot; " .
3695 $cgi->a({-href => href(-replay=>1, page=>$page-1),
3696 -accesskey => "p", -title => "Alt-p"}, "prev");
3697 } else {
3698 $paging_nav .= "first &sdot; prev";
3701 if ($has_next_link) {
3702 $paging_nav .= " &sdot; " .
3703 $cgi->a({-href => href(-replay=>1, page=>$page+1),
3704 -accesskey => "n", -title => "Alt-n"}, "next");
3705 } else {
3706 $paging_nav .= " &sdot; next";
3709 return $paging_nav;
3712 ## ......................................................................
3713 ## functions printing or outputting HTML: div
3715 sub git_print_header_div {
3716 my ($action, $title, $hash, $hash_base) = @_;
3717 my %args = ();
3719 $args{'action'} = $action;
3720 $args{'hash'} = $hash if $hash;
3721 $args{'hash_base'} = $hash_base if $hash_base;
3723 print "<div class=\"header\">\n" .
3724 $cgi->a({-href => href(%args), -class => "title"},
3725 $title ? $title : $action) .
3726 "\n</div>\n";
3729 sub print_local_time {
3730 print format_local_time(@_);
3733 sub format_local_time {
3734 my $localtime = '';
3735 my %date = @_;
3736 if ($date{'hour_local'} < 6) {
3737 $localtime .= sprintf(" (<span class=\"atnight\">%02d:%02d</span> %s)",
3738 $date{'hour_local'}, $date{'minute_local'}, $date{'tz_local'});
3739 } else {
3740 $localtime .= sprintf(" (%02d:%02d %s)",
3741 $date{'hour_local'}, $date{'minute_local'}, $date{'tz_local'});
3744 return $localtime;
3747 # Outputs the author name and date in long form
3748 sub git_print_authorship {
3749 my $co = shift;
3750 my %opts = @_;
3751 my $tag = $opts{-tag} || 'div';
3752 my $author = $co->{'author_name'};
3754 my %ad = parse_date($co->{'author_epoch'}, $co->{'author_tz'});
3755 print "<$tag class=\"author_date\">" .
3756 format_search_author($author, "author", esc_html($author)) .
3757 " [$ad{'rfc2822'}";
3758 print_local_time(%ad) if ($opts{-localtime});
3759 print "]" . git_get_avatar($co->{'author_email'}, -pad_before => 1)
3760 . "</$tag>\n";
3763 # Outputs table rows containing the full author or committer information,
3764 # in the format expected for 'commit' view (& similia).
3765 # Parameters are a commit hash reference, followed by the list of people
3766 # to output information for. If the list is empty it defalts to both
3767 # author and committer.
3768 sub git_print_authorship_rows {
3769 my $co = shift;
3770 # too bad we can't use @people = @_ || ('author', 'committer')
3771 my @people = @_;
3772 @people = ('author', 'committer') unless @people;
3773 foreach my $who (@people) {
3774 my %wd = parse_date($co->{"${who}_epoch"}, $co->{"${who}_tz"});
3775 print "<tr><td>$who</td><td>" .
3776 format_search_author($co->{"${who}_name"}, $who,
3777 esc_html($co->{"${who}_name"})) . " " .
3778 format_search_author($co->{"${who}_email"}, $who,
3779 esc_html("<" . $co->{"${who}_email"} . ">")) .
3780 "</td><td rowspan=\"2\">" .
3781 git_get_avatar($co->{"${who}_email"}, -size => 'double') .
3782 "</td></tr>\n" .
3783 "<tr>" .
3784 "<td></td><td> $wd{'rfc2822'}";
3785 print_local_time(%wd);
3786 print "</td>" .
3787 "</tr>\n";
3791 sub git_print_page_path {
3792 my $name = shift;
3793 my $type = shift;
3794 my $hb = shift;
3797 print "<div class=\"page_path\">";
3798 print $cgi->a({-href => href(action=>"tree", hash_base=>$hb),
3799 -title => 'tree root'}, to_utf8("[$project]"));
3800 print " / ";
3801 if (defined $name) {
3802 my @dirname = split '/', $name;
3803 my $basename = pop @dirname;
3804 my $fullname = '';
3806 foreach my $dir (@dirname) {
3807 $fullname .= ($fullname ? '/' : '') . $dir;
3808 print $cgi->a({-href => href(action=>"tree", file_name=>$fullname,
3809 hash_base=>$hb),
3810 -title => $fullname}, esc_path($dir));
3811 print " / ";
3813 if (defined $type && $type eq 'blob') {
3814 print $cgi->a({-href => href(action=>"blob_plain", file_name=>$file_name,
3815 hash_base=>$hb),
3816 -title => $name}, esc_path($basename));
3817 } elsif (defined $type && $type eq 'tree') {
3818 print $cgi->a({-href => href(action=>"tree", file_name=>$file_name,
3819 hash_base=>$hb),
3820 -title => $name}, esc_path($basename));
3821 print " / ";
3822 } else {
3823 print esc_path($basename);
3826 print "<br/></div>\n";
3829 sub git_print_log {
3830 my $log = shift;
3831 my %opts = @_;
3833 if ($opts{'-remove_title'}) {
3834 # remove title, i.e. first line of log
3835 shift @$log;
3837 # remove leading empty lines
3838 while (defined $log->[0] && $log->[0] eq "") {
3839 shift @$log;
3842 # print log
3843 my $signoff = 0;
3844 my $empty = 0;
3845 foreach my $line (@$log) {
3846 if ($line =~ m/^ *(signed[ \-]off[ \-]by[ :]|acked[ \-]by[ :]|cc[ :])/i) {
3847 $signoff = 1;
3848 $empty = 0;
3849 if (! $opts{'-remove_signoff'}) {
3850 print "<span class=\"signoff\">" . esc_html($line) . "</span><br/>\n";
3851 next;
3852 } else {
3853 # remove signoff lines
3854 next;
3856 } else {
3857 $signoff = 0;
3860 # print only one empty line
3861 # do not print empty line after signoff
3862 if ($line eq "") {
3863 next if ($empty || $signoff);
3864 $empty = 1;
3865 } else {
3866 $empty = 0;
3869 print format_log_line_html($line) . "<br/>\n";
3872 if ($opts{'-final_empty_line'}) {
3873 # end with single empty line
3874 print "<br/>\n" unless $empty;
3878 # return link target (what link points to)
3879 sub git_get_link_target {
3880 my $hash = shift;
3881 my $link_target;
3883 # read link
3884 open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
3885 or return;
3887 local $/ = undef;
3888 $link_target = <$fd>;
3890 close $fd
3891 or return;
3893 return $link_target;
3896 # given link target, and the directory (basedir) the link is in,
3897 # return target of link relative to top directory (top tree);
3898 # return undef if it is not possible (including absolute links).
3899 sub normalize_link_target {
3900 my ($link_target, $basedir) = @_;
3902 # absolute symlinks (beginning with '/') cannot be normalized
3903 return if (substr($link_target, 0, 1) eq '/');
3905 # normalize link target to path from top (root) tree (dir)
3906 my $path;
3907 if ($basedir) {
3908 $path = $basedir . '/' . $link_target;
3909 } else {
3910 # we are in top (root) tree (dir)
3911 $path = $link_target;
3914 # remove //, /./, and /../
3915 my @path_parts;
3916 foreach my $part (split('/', $path)) {
3917 # discard '.' and ''
3918 next if (!$part || $part eq '.');
3919 # handle '..'
3920 if ($part eq '..') {
3921 if (@path_parts) {
3922 pop @path_parts;
3923 } else {
3924 # link leads outside repository (outside top dir)
3925 return;
3927 } else {
3928 push @path_parts, $part;
3931 $path = join('/', @path_parts);
3933 return $path;
3936 # print tree entry (row of git_tree), but without encompassing <tr> element
3937 sub git_print_tree_entry {
3938 my ($t, $basedir, $hash_base, $have_blame) = @_;
3940 my %base_key = ();
3941 $base_key{'hash_base'} = $hash_base if defined $hash_base;
3943 # The format of a table row is: mode list link. Where mode is
3944 # the mode of the entry, list is the name of the entry, an href,
3945 # and link is the action links of the entry.
3947 print "<td class=\"mode\">" . mode_str($t->{'mode'}) . "</td>\n";
3948 if (exists $t->{'size'}) {
3949 print "<td class=\"size\">$t->{'size'}</td>\n";
3951 if ($t->{'type'} eq "blob") {
3952 print "<td class=\"list\">" .
3953 $cgi->a({-href => href(action=>"blob", hash=>$t->{'hash'},
3954 file_name=>"$basedir$t->{'name'}", %base_key),
3955 -class => "list"}, esc_path($t->{'name'}));
3956 if (S_ISLNK(oct $t->{'mode'})) {
3957 my $link_target = git_get_link_target($t->{'hash'});
3958 if ($link_target) {
3959 my $norm_target = normalize_link_target($link_target, $basedir);
3960 if (defined $norm_target) {
3961 print " -> " .
3962 $cgi->a({-href => href(action=>"object", hash_base=>$hash_base,
3963 file_name=>$norm_target),
3964 -title => $norm_target}, esc_path($link_target));
3965 } else {
3966 print " -> " . esc_path($link_target);
3970 print "</td>\n";
3971 print "<td class=\"link\">";
3972 print $cgi->a({-href => href(action=>"blob", hash=>$t->{'hash'},
3973 file_name=>"$basedir$t->{'name'}", %base_key)},
3974 "blob");
3975 if ($have_blame) {
3976 print " | " .
3977 $cgi->a({-href => href(action=>"blame", hash=>$t->{'hash'},
3978 file_name=>"$basedir$t->{'name'}", %base_key)},
3979 "blame");
3981 if (defined $hash_base) {
3982 print " | " .
3983 $cgi->a({-href => href(action=>"history", hash_base=>$hash_base,
3984 hash=>$t->{'hash'}, file_name=>"$basedir$t->{'name'}")},
3985 "history");
3987 print " | " .
3988 $cgi->a({-href => href(action=>"blob_plain", hash_base=>$hash_base,
3989 file_name=>"$basedir$t->{'name'}")},
3990 "raw");
3991 print "</td>\n";
3993 } elsif ($t->{'type'} eq "tree") {
3994 print "<td class=\"list\">";
3995 print $cgi->a({-href => href(action=>"tree", hash=>$t->{'hash'},
3996 file_name=>"$basedir$t->{'name'}",
3997 %base_key)},
3998 esc_path($t->{'name'}));
3999 print "</td>\n";
4000 print "<td class=\"link\">";
4001 print $cgi->a({-href => href(action=>"tree", hash=>$t->{'hash'},
4002 file_name=>"$basedir$t->{'name'}",
4003 %base_key)},
4004 "tree");
4005 if (defined $hash_base) {
4006 print " | " .
4007 $cgi->a({-href => href(action=>"history", hash_base=>$hash_base,
4008 file_name=>"$basedir$t->{'name'}")},
4009 "history");
4011 print "</td>\n";
4012 } else {
4013 # unknown object: we can only present history for it
4014 # (this includes 'commit' object, i.e. submodule support)
4015 print "<td class=\"list\">" .
4016 esc_path($t->{'name'}) .
4017 "</td>\n";
4018 print "<td class=\"link\">";
4019 if (defined $hash_base) {
4020 print $cgi->a({-href => href(action=>"history",
4021 hash_base=>$hash_base,
4022 file_name=>"$basedir$t->{'name'}")},
4023 "history");
4025 print "</td>\n";
4029 ## ......................................................................
4030 ## functions printing large fragments of HTML
4032 # get pre-image filenames for merge (combined) diff
4033 sub fill_from_file_info {
4034 my ($diff, @parents) = @_;
4036 $diff->{'from_file'} = [ ];
4037 $diff->{'from_file'}[$diff->{'nparents'} - 1] = undef;
4038 for (my $i = 0; $i < $diff->{'nparents'}; $i++) {
4039 if ($diff->{'status'}[$i] eq 'R' ||
4040 $diff->{'status'}[$i] eq 'C') {
4041 $diff->{'from_file'}[$i] =
4042 git_get_path_by_hash($parents[$i], $diff->{'from_id'}[$i]);
4046 return $diff;
4049 # is current raw difftree line of file deletion
4050 sub is_deleted {
4051 my $diffinfo = shift;
4053 return $diffinfo->{'to_id'} eq ('0' x 40);
4056 # does patch correspond to [previous] difftree raw line
4057 # $diffinfo - hashref of parsed raw diff format
4058 # $patchinfo - hashref of parsed patch diff format
4059 # (the same keys as in $diffinfo)
4060 sub is_patch_split {
4061 my ($diffinfo, $patchinfo) = @_;
4063 return defined $diffinfo && defined $patchinfo
4064 && $diffinfo->{'to_file'} eq $patchinfo->{'to_file'};
4068 sub git_difftree_body {
4069 my ($difftree, $hash, @parents) = @_;
4070 my ($parent) = $parents[0];
4071 my $have_blame = gitweb_check_feature('blame');
4072 print "<div class=\"list_head\">\n";
4073 if ($#{$difftree} > 10) {
4074 print(($#{$difftree} + 1) . " files changed:\n");
4076 print "</div>\n";
4078 print "<table class=\"" .
4079 (@parents > 1 ? "combined " : "") .
4080 "diff_tree\">\n";
4082 # header only for combined diff in 'commitdiff' view
4083 my $has_header = @$difftree && @parents > 1 && $action eq 'commitdiff';
4084 if ($has_header) {
4085 # table header
4086 print "<thead><tr>\n" .
4087 "<th></th><th></th>\n"; # filename, patchN link
4088 for (my $i = 0; $i < @parents; $i++) {
4089 my $par = $parents[$i];
4090 print "<th>" .
4091 $cgi->a({-href => href(action=>"commitdiff",
4092 hash=>$hash, hash_parent=>$par),
4093 -title => 'commitdiff to parent number ' .
4094 ($i+1) . ': ' . substr($par,0,7)},
4095 $i+1) .
4096 "&nbsp;</th>\n";
4098 print "</tr></thead>\n<tbody>\n";
4101 my $alternate = 1;
4102 my $patchno = 0;
4103 foreach my $line (@{$difftree}) {
4104 my $diff = parsed_difftree_line($line);
4106 if ($alternate) {
4107 print "<tr class=\"dark\">\n";
4108 } else {
4109 print "<tr class=\"light\">\n";
4111 $alternate ^= 1;
4113 if (exists $diff->{'nparents'}) { # combined diff
4115 fill_from_file_info($diff, @parents)
4116 unless exists $diff->{'from_file'};
4118 if (!is_deleted($diff)) {
4119 # file exists in the result (child) commit
4120 print "<td>" .
4121 $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
4122 file_name=>$diff->{'to_file'},
4123 hash_base=>$hash),
4124 -class => "list"}, esc_path($diff->{'to_file'})) .
4125 "</td>\n";
4126 } else {
4127 print "<td>" .
4128 esc_path($diff->{'to_file'}) .
4129 "</td>\n";
4132 if ($action eq 'commitdiff') {
4133 # link to patch
4134 $patchno++;
4135 print "<td class=\"link\">" .
4136 $cgi->a({-href => "#patch$patchno"}, "patch") .
4137 " | " .
4138 "</td>\n";
4141 my $has_history = 0;
4142 my $not_deleted = 0;
4143 for (my $i = 0; $i < $diff->{'nparents'}; $i++) {
4144 my $hash_parent = $parents[$i];
4145 my $from_hash = $diff->{'from_id'}[$i];
4146 my $from_path = $diff->{'from_file'}[$i];
4147 my $status = $diff->{'status'}[$i];
4149 $has_history ||= ($status ne 'A');
4150 $not_deleted ||= ($status ne 'D');
4152 if ($status eq 'A') {
4153 print "<td class=\"link\" align=\"right\"> | </td>\n";
4154 } elsif ($status eq 'D') {
4155 print "<td class=\"link\">" .
4156 $cgi->a({-href => href(action=>"blob",
4157 hash_base=>$hash,
4158 hash=>$from_hash,
4159 file_name=>$from_path)},
4160 "blob" . ($i+1)) .
4161 " | </td>\n";
4162 } else {
4163 if ($diff->{'to_id'} eq $from_hash) {
4164 print "<td class=\"link nochange\">";
4165 } else {
4166 print "<td class=\"link\">";
4168 print $cgi->a({-href => href(action=>"blobdiff",
4169 hash=>$diff->{'to_id'},
4170 hash_parent=>$from_hash,
4171 hash_base=>$hash,
4172 hash_parent_base=>$hash_parent,
4173 file_name=>$diff->{'to_file'},
4174 file_parent=>$from_path)},
4175 "diff" . ($i+1)) .
4176 " | </td>\n";
4180 print "<td class=\"link\">";
4181 if ($not_deleted) {
4182 print $cgi->a({-href => href(action=>"blob",
4183 hash=>$diff->{'to_id'},
4184 file_name=>$diff->{'to_file'},
4185 hash_base=>$hash)},
4186 "blob");
4187 print " | " if ($has_history);
4189 if ($has_history) {
4190 print $cgi->a({-href => href(action=>"history",
4191 file_name=>$diff->{'to_file'},
4192 hash_base=>$hash)},
4193 "history");
4195 print "</td>\n";
4197 print "</tr>\n";
4198 next; # instead of 'else' clause, to avoid extra indent
4200 # else ordinary diff
4202 my ($to_mode_oct, $to_mode_str, $to_file_type);
4203 my ($from_mode_oct, $from_mode_str, $from_file_type);
4204 if ($diff->{'to_mode'} ne ('0' x 6)) {
4205 $to_mode_oct = oct $diff->{'to_mode'};
4206 if (S_ISREG($to_mode_oct)) { # only for regular file
4207 $to_mode_str = sprintf("%04o", $to_mode_oct & 0777); # permission bits
4209 $to_file_type = file_type($diff->{'to_mode'});
4211 if ($diff->{'from_mode'} ne ('0' x 6)) {
4212 $from_mode_oct = oct $diff->{'from_mode'};
4213 if (S_ISREG($to_mode_oct)) { # only for regular file
4214 $from_mode_str = sprintf("%04o", $from_mode_oct & 0777); # permission bits
4216 $from_file_type = file_type($diff->{'from_mode'});
4219 if ($diff->{'status'} eq "A") { # created
4220 my $mode_chng = "<span class=\"file_status new\">[new $to_file_type";
4221 $mode_chng .= " with mode: $to_mode_str" if $to_mode_str;
4222 $mode_chng .= "]</span>";
4223 print "<td>";
4224 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
4225 hash_base=>$hash, file_name=>$diff->{'file'}),
4226 -class => "list"}, esc_path($diff->{'file'}));
4227 print "</td>\n";
4228 print "<td>$mode_chng</td>\n";
4229 print "<td class=\"link\">";
4230 if ($action eq 'commitdiff') {
4231 # link to patch
4232 $patchno++;
4233 print $cgi->a({-href => "#patch$patchno"}, "patch");
4234 print " | ";
4236 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
4237 hash_base=>$hash, file_name=>$diff->{'file'})},
4238 "blob");
4239 print "</td>\n";
4241 } elsif ($diff->{'status'} eq "D") { # deleted
4242 my $mode_chng = "<span class=\"file_status deleted\">[deleted $from_file_type]</span>";
4243 print "<td>";
4244 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'from_id'},
4245 hash_base=>$parent, file_name=>$diff->{'file'}),
4246 -class => "list"}, esc_path($diff->{'file'}));
4247 print "</td>\n";
4248 print "<td>$mode_chng</td>\n";
4249 print "<td class=\"link\">";
4250 if ($action eq 'commitdiff') {
4251 # link to patch
4252 $patchno++;
4253 print $cgi->a({-href => "#patch$patchno"}, "patch");
4254 print " | ";
4256 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'from_id'},
4257 hash_base=>$parent, file_name=>$diff->{'file'})},
4258 "blob") . " | ";
4259 if ($have_blame) {
4260 print $cgi->a({-href => href(action=>"blame", hash_base=>$parent,
4261 file_name=>$diff->{'file'})},
4262 "blame") . " | ";
4264 print $cgi->a({-href => href(action=>"history", hash_base=>$parent,
4265 file_name=>$diff->{'file'})},
4266 "history");
4267 print "</td>\n";
4269 } elsif ($diff->{'status'} eq "M" || $diff->{'status'} eq "T") { # modified, or type changed
4270 my $mode_chnge = "";
4271 if ($diff->{'from_mode'} != $diff->{'to_mode'}) {
4272 $mode_chnge = "<span class=\"file_status mode_chnge\">[changed";
4273 if ($from_file_type ne $to_file_type) {
4274 $mode_chnge .= " from $from_file_type to $to_file_type";
4276 if (($from_mode_oct & 0777) != ($to_mode_oct & 0777)) {
4277 if ($from_mode_str && $to_mode_str) {
4278 $mode_chnge .= " mode: $from_mode_str->$to_mode_str";
4279 } elsif ($to_mode_str) {
4280 $mode_chnge .= " mode: $to_mode_str";
4283 $mode_chnge .= "]</span>\n";
4285 print "<td>";
4286 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
4287 hash_base=>$hash, file_name=>$diff->{'file'}),
4288 -class => "list"}, esc_path($diff->{'file'}));
4289 print "</td>\n";
4290 print "<td>$mode_chnge</td>\n";
4291 print "<td class=\"link\">";
4292 if ($action eq 'commitdiff') {
4293 # link to patch
4294 $patchno++;
4295 print $cgi->a({-href => "#patch$patchno"}, "patch") .
4296 " | ";
4297 } elsif ($diff->{'to_id'} ne $diff->{'from_id'}) {
4298 # "commit" view and modified file (not onlu mode changed)
4299 print $cgi->a({-href => href(action=>"blobdiff",
4300 hash=>$diff->{'to_id'}, hash_parent=>$diff->{'from_id'},
4301 hash_base=>$hash, hash_parent_base=>$parent,
4302 file_name=>$diff->{'file'})},
4303 "diff") .
4304 " | ";
4306 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
4307 hash_base=>$hash, file_name=>$diff->{'file'})},
4308 "blob") . " | ";
4309 if ($have_blame) {
4310 print $cgi->a({-href => href(action=>"blame", hash_base=>$hash,
4311 file_name=>$diff->{'file'})},
4312 "blame") . " | ";
4314 print $cgi->a({-href => href(action=>"history", hash_base=>$hash,
4315 file_name=>$diff->{'file'})},
4316 "history");
4317 print "</td>\n";
4319 } elsif ($diff->{'status'} eq "R" || $diff->{'status'} eq "C") { # renamed or copied
4320 my %status_name = ('R' => 'moved', 'C' => 'copied');
4321 my $nstatus = $status_name{$diff->{'status'}};
4322 my $mode_chng = "";
4323 if ($diff->{'from_mode'} != $diff->{'to_mode'}) {
4324 # mode also for directories, so we cannot use $to_mode_str
4325 $mode_chng = sprintf(", mode: %04o", $to_mode_oct & 0777);
4327 print "<td>" .
4328 $cgi->a({-href => href(action=>"blob", hash_base=>$hash,
4329 hash=>$diff->{'to_id'}, file_name=>$diff->{'to_file'}),
4330 -class => "list"}, esc_path($diff->{'to_file'})) . "</td>\n" .
4331 "<td><span class=\"file_status $nstatus\">[$nstatus from " .
4332 $cgi->a({-href => href(action=>"blob", hash_base=>$parent,
4333 hash=>$diff->{'from_id'}, file_name=>$diff->{'from_file'}),
4334 -class => "list"}, esc_path($diff->{'from_file'})) .
4335 " with " . (int $diff->{'similarity'}) . "% similarity$mode_chng]</span></td>\n" .
4336 "<td class=\"link\">";
4337 if ($action eq 'commitdiff') {
4338 # link to patch
4339 $patchno++;
4340 print $cgi->a({-href => "#patch$patchno"}, "patch") .
4341 " | ";
4342 } elsif ($diff->{'to_id'} ne $diff->{'from_id'}) {
4343 # "commit" view and modified file (not only pure rename or copy)
4344 print $cgi->a({-href => href(action=>"blobdiff",
4345 hash=>$diff->{'to_id'}, hash_parent=>$diff->{'from_id'},
4346 hash_base=>$hash, hash_parent_base=>$parent,
4347 file_name=>$diff->{'to_file'}, file_parent=>$diff->{'from_file'})},
4348 "diff") .
4349 " | ";
4351 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
4352 hash_base=>$parent, file_name=>$diff->{'to_file'})},
4353 "blob") . " | ";
4354 if ($have_blame) {
4355 print $cgi->a({-href => href(action=>"blame", hash_base=>$hash,
4356 file_name=>$diff->{'to_file'})},
4357 "blame") . " | ";
4359 print $cgi->a({-href => href(action=>"history", hash_base=>$hash,
4360 file_name=>$diff->{'to_file'})},
4361 "history");
4362 print "</td>\n";
4364 } # we should not encounter Unmerged (U) or Unknown (X) status
4365 print "</tr>\n";
4367 print "</tbody>" if $has_header;
4368 print "</table>\n";
4371 sub git_patchset_body {
4372 my ($fd, $difftree, $hash, @hash_parents) = @_;
4373 my ($hash_parent) = $hash_parents[0];
4375 my $is_combined = (@hash_parents > 1);
4376 my $patch_idx = 0;
4377 my $patch_number = 0;
4378 my $patch_line;
4379 my $diffinfo;
4380 my $to_name;
4381 my (%from, %to);
4383 print "<div class=\"patchset\">\n";
4385 # skip to first patch
4386 while ($patch_line = <$fd>) {
4387 chomp $patch_line;
4389 last if ($patch_line =~ m/^diff /);
4392 PATCH:
4393 while ($patch_line) {
4395 # parse "git diff" header line
4396 if ($patch_line =~ m/^diff --git (\"(?:[^\\\"]*(?:\\.[^\\\"]*)*)\"|[^ "]*) (.*)$/) {
4397 # $1 is from_name, which we do not use
4398 $to_name = unquote($2);
4399 $to_name =~ s!^b/!!;
4400 } elsif ($patch_line =~ m/^diff --(cc|combined) ("?.*"?)$/) {
4401 # $1 is 'cc' or 'combined', which we do not use
4402 $to_name = unquote($2);
4403 } else {
4404 $to_name = undef;
4407 # check if current patch belong to current raw line
4408 # and parse raw git-diff line if needed
4409 if (is_patch_split($diffinfo, { 'to_file' => $to_name })) {
4410 # this is continuation of a split patch
4411 print "<div class=\"patch cont\">\n";
4412 } else {
4413 # advance raw git-diff output if needed
4414 $patch_idx++ if defined $diffinfo;
4416 # read and prepare patch information
4417 $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
4419 # compact combined diff output can have some patches skipped
4420 # find which patch (using pathname of result) we are at now;
4421 if ($is_combined) {
4422 while ($to_name ne $diffinfo->{'to_file'}) {
4423 print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n" .
4424 format_diff_cc_simplified($diffinfo, @hash_parents) .
4425 "</div>\n"; # class="patch"
4427 $patch_idx++;
4428 $patch_number++;
4430 last if $patch_idx > $#$difftree;
4431 $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
4435 # modifies %from, %to hashes
4436 parse_from_to_diffinfo($diffinfo, \%from, \%to, @hash_parents);
4438 # this is first patch for raw difftree line with $patch_idx index
4439 # we index @$difftree array from 0, but number patches from 1
4440 print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n";
4443 # git diff header
4444 #assert($patch_line =~ m/^diff /) if DEBUG;
4445 #assert($patch_line !~ m!$/$!) if DEBUG; # is chomp-ed
4446 $patch_number++;
4447 # print "git diff" header
4448 print format_git_diff_header_line($patch_line, $diffinfo,
4449 \%from, \%to);
4451 # print extended diff header
4452 print "<div class=\"diff extended_header\">\n";
4453 EXTENDED_HEADER:
4454 while ($patch_line = <$fd>) {
4455 chomp $patch_line;
4457 last EXTENDED_HEADER if ($patch_line =~ m/^--- |^diff /);
4459 print format_extended_diff_header_line($patch_line, $diffinfo,
4460 \%from, \%to);
4462 print "</div>\n"; # class="diff extended_header"
4464 # from-file/to-file diff header
4465 if (! $patch_line) {
4466 print "</div>\n"; # class="patch"
4467 last PATCH;
4469 next PATCH if ($patch_line =~ m/^diff /);
4470 #assert($patch_line =~ m/^---/) if DEBUG;
4472 my $last_patch_line = $patch_line;
4473 $patch_line = <$fd>;
4474 chomp $patch_line;
4475 #assert($patch_line =~ m/^\+\+\+/) if DEBUG;
4477 print format_diff_from_to_header($last_patch_line, $patch_line,
4478 $diffinfo, \%from, \%to,
4479 @hash_parents);
4481 # the patch itself
4482 LINE:
4483 while ($patch_line = <$fd>) {
4484 chomp $patch_line;
4486 next PATCH if ($patch_line =~ m/^diff /);
4488 print format_diff_line($patch_line, \%from, \%to);
4491 } continue {
4492 print "</div>\n"; # class="patch"
4495 # for compact combined (--cc) format, with chunk and patch simpliciaction
4496 # patchset might be empty, but there might be unprocessed raw lines
4497 for (++$patch_idx if $patch_number > 0;
4498 $patch_idx < @$difftree;
4499 ++$patch_idx) {
4500 # read and prepare patch information
4501 $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
4503 # generate anchor for "patch" links in difftree / whatchanged part
4504 print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n" .
4505 format_diff_cc_simplified($diffinfo, @hash_parents) .
4506 "</div>\n"; # class="patch"
4508 $patch_number++;
4511 if ($patch_number == 0) {
4512 if (@hash_parents > 1) {
4513 print "<div class=\"diff nodifferences\">Trivial merge</div>\n";
4514 } else {
4515 print "<div class=\"diff nodifferences\">No differences found</div>\n";
4519 print "</div>\n"; # class="patchset"
4522 # . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
4524 # fills project list info (age, description, owner, forks) for each
4525 # project in the list, removing invalid projects from returned list
4526 # NOTE: modifies $projlist, but does not remove entries from it
4527 sub fill_project_list_info {
4528 my ($projlist, $check_forks) = @_;
4529 my @projects;
4531 my $show_ctags = gitweb_check_feature('ctags');
4532 PROJECT:
4533 foreach my $pr (@$projlist) {
4534 my (@activity) = git_get_last_activity($pr->{'path'});
4535 unless (@activity) {
4536 next PROJECT;
4538 ($pr->{'age'}, $pr->{'age_string'}) = @activity;
4539 if (!defined $pr->{'descr'}) {
4540 my $descr = git_get_project_description($pr->{'path'}) || "";
4541 $descr = to_utf8($descr);
4542 $pr->{'descr_long'} = $descr;
4543 $pr->{'descr'} = chop_str($descr, $projects_list_description_width, 5);
4545 if (!defined $pr->{'owner'}) {
4546 $pr->{'owner'} = git_get_project_owner("$pr->{'path'}") || "";
4548 if ($check_forks) {
4549 my $pname = $pr->{'path'};
4550 if (($pname =~ s/\.git$//) &&
4551 ($pname !~ /\/$/) &&
4552 (-d "$projectroot/$pname")) {
4553 $pr->{'forks'} = "-d $projectroot/$pname";
4554 } else {
4555 $pr->{'forks'} = 0;
4558 $show_ctags and $pr->{'ctags'} = git_get_project_ctags($pr->{'path'});
4559 push @projects, $pr;
4562 return @projects;
4565 # print 'sort by' <th> element, generating 'sort by $name' replay link
4566 # if that order is not selected
4567 sub print_sort_th {
4568 print format_sort_th(@_);
4571 sub format_sort_th {
4572 my ($name, $order, $header) = @_;
4573 my $sort_th = "";
4574 $header ||= ucfirst($name);
4576 if ($order eq $name) {
4577 $sort_th .= "<th>$header</th>\n";
4578 } else {
4579 $sort_th .= "<th>" .
4580 $cgi->a({-href => href(-replay=>1, order=>$name),
4581 -class => "header"}, $header) .
4582 "</th>\n";
4585 return $sort_th;
4588 sub git_project_list_body {
4589 # actually uses global variable $project
4590 my ($projlist, $order, $from, $to, $extra, $no_header) = @_;
4592 my $check_forks = gitweb_check_feature('forks');
4593 my @projects = fill_project_list_info($projlist, $check_forks);
4595 $order ||= $default_projects_order;
4596 $from = 0 unless defined $from;
4597 $to = $#projects if (!defined $to || $#projects < $to);
4599 my %order_info = (
4600 project => { key => 'path', type => 'str' },
4601 descr => { key => 'descr_long', type => 'str' },
4602 owner => { key => 'owner', type => 'str' },
4603 age => { key => 'age', type => 'num' }
4605 my $oi = $order_info{$order};
4606 if ($oi->{'type'} eq 'str') {
4607 @projects = sort {$a->{$oi->{'key'}} cmp $b->{$oi->{'key'}}} @projects;
4608 } else {
4609 @projects = sort {$a->{$oi->{'key'}} <=> $b->{$oi->{'key'}}} @projects;
4612 my $show_ctags = gitweb_check_feature('ctags');
4613 if ($show_ctags) {
4614 my %ctags;
4615 foreach my $p (@projects) {
4616 foreach my $ct (keys %{$p->{'ctags'}}) {
4617 $ctags{$ct} += $p->{'ctags'}->{$ct};
4620 my $cloud = git_populate_project_tagcloud(\%ctags);
4621 print git_show_project_tagcloud($cloud, 64);
4624 print "<table class=\"project_list\">\n";
4625 unless ($no_header) {
4626 print "<tr>\n";
4627 if ($check_forks) {
4628 print "<th></th>\n";
4630 print_sort_th('project', $order, 'Project');
4631 print_sort_th('descr', $order, 'Description');
4632 print_sort_th('owner', $order, 'Owner');
4633 print_sort_th('age', $order, 'Last Change');
4634 print "<th></th>\n" . # for links
4635 "</tr>\n";
4637 my $alternate = 1;
4638 my $tagfilter = $cgi->param('by_tag');
4639 for (my $i = $from; $i <= $to; $i++) {
4640 my $pr = $projects[$i];
4642 next if $tagfilter and $show_ctags and not grep { lc $_ eq lc $tagfilter } keys %{$pr->{'ctags'}};
4643 next if $searchtext and not $pr->{'path'} =~ /$searchtext/
4644 and not $pr->{'descr_long'} =~ /$searchtext/;
4645 # Weed out forks or non-matching entries of search
4646 if ($check_forks) {
4647 my $forkbase = $project; $forkbase ||= ''; $forkbase =~ s#\.git$#/#;
4648 $forkbase="^$forkbase" if $forkbase;
4649 next if not $searchtext and not $tagfilter and $show_ctags
4650 and $pr->{'path'} =~ m#$forkbase.*/.*#; # regexp-safe
4653 if ($alternate) {
4654 print "<tr class=\"dark\">\n";
4655 } else {
4656 print "<tr class=\"light\">\n";
4658 $alternate ^= 1;
4659 if ($check_forks) {
4660 print "<td>";
4661 if ($pr->{'forks'}) {
4662 print "<!-- $pr->{'forks'} -->\n";
4663 print $cgi->a({-href => href(project=>$pr->{'path'}, action=>"forks")}, "+");
4665 print "</td>\n";
4667 print "<td>" . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary"),
4668 -class => "list"}, esc_html($pr->{'path'})) . "</td>\n" .
4669 "<td>" . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary"),
4670 -class => "list", -title => $pr->{'descr_long'}},
4671 esc_html($pr->{'descr'})) . "</td>\n" .
4672 "<td><i>" . chop_and_escape_str($pr->{'owner'}, 15) . "</i></td>\n";
4673 print "<td class=\"". age_class($pr->{'age'}) . "\">" .
4674 (defined $pr->{'age_string'} ? $pr->{'age_string'} : "No commits") . "</td>\n" .
4675 "<td class=\"link\">" .
4676 $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary")}, "summary") . " | " .
4677 $cgi->a({-href => href(project=>$pr->{'path'}, action=>"shortlog")}, "shortlog") . " | " .
4678 $cgi->a({-href => href(project=>$pr->{'path'}, action=>"log")}, "log") . " | " .
4679 $cgi->a({-href => href(project=>$pr->{'path'}, action=>"tree")}, "tree") .
4680 ($pr->{'forks'} ? " | " . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"forks")}, "forks") : '') .
4681 "</td>\n" .
4682 "</tr>\n";
4684 if (defined $extra) {
4685 print "<tr>\n";
4686 if ($check_forks) {
4687 print "<td></td>\n";
4689 print "<td colspan=\"5\">$extra</td>\n" .
4690 "</tr>\n";
4692 print "</table>\n";
4695 sub git_log_body {
4696 # uses global variable $project
4697 my ($commitlist, $from, $to, $refs, $extra) = @_;
4699 $from = 0 unless defined $from;
4700 $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
4702 for (my $i = 0; $i <= $to; $i++) {
4703 my %co = %{$commitlist->[$i]};
4704 next if !%co;
4705 my $commit = $co{'id'};
4706 my $ref = format_ref_marker($refs, $commit);
4707 my %ad = parse_date($co{'author_epoch'});
4708 git_print_header_div('commit',
4709 "<span class=\"age\">$co{'age_string'}</span>" .
4710 esc_html($co{'title'}) . $ref,
4711 $commit);
4712 print "<div class=\"title_text\">\n" .
4713 "<div class=\"log_link\">\n" .
4714 $cgi->a({-href => href(action=>"commit", hash=>$commit)}, "commit") .
4715 " | " .
4716 $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff") .
4717 " | " .
4718 $cgi->a({-href => href(action=>"tree", hash=>$commit, hash_base=>$commit)}, "tree") .
4719 "<br/>\n" .
4720 "</div>\n";
4721 git_print_authorship(\%co, -tag => 'span');
4722 print "<br/>\n</div>\n";
4724 print "<div class=\"log_body\">\n";
4725 git_print_log($co{'comment'}, -final_empty_line=> 1);
4726 print "</div>\n";
4728 if ($extra) {
4729 print "<div class=\"page_nav\">\n";
4730 print "$extra\n";
4731 print "</div>\n";
4735 sub git_shortlog_body {
4736 # uses global variable $project
4737 my ($commitlist, $from, $to, $refs, $extra) = @_;
4739 $from = 0 unless defined $from;
4740 $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
4742 print "<table class=\"shortlog\">\n";
4743 my $alternate = 1;
4744 for (my $i = $from; $i <= $to; $i++) {
4745 my %co = %{$commitlist->[$i]};
4746 my $commit = $co{'id'};
4747 my $ref = format_ref_marker($refs, $commit);
4748 if ($alternate) {
4749 print "<tr class=\"dark\">\n";
4750 } else {
4751 print "<tr class=\"light\">\n";
4753 $alternate ^= 1;
4754 # git_summary() used print "<td><i>$co{'age_string'}</i></td>\n" .
4755 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
4756 format_author_html('td', \%co, 10) . "<td>";
4757 print format_subject_html($co{'title'}, $co{'title_short'},
4758 href(action=>"commit", hash=>$commit), $ref);
4759 print "</td>\n" .
4760 "<td class=\"link\">" .
4761 $cgi->a({-href => href(action=>"commit", hash=>$commit)}, "commit") . " | " .
4762 $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff") . " | " .
4763 $cgi->a({-href => href(action=>"tree", hash=>$commit, hash_base=>$commit)}, "tree");
4764 my $snapshot_links = format_snapshot_links($commit);
4765 if (defined $snapshot_links) {
4766 print " | " . $snapshot_links;
4768 print "</td>\n" .
4769 "</tr>\n";
4771 if (defined $extra) {
4772 print "<tr>\n" .
4773 "<td colspan=\"4\">$extra</td>\n" .
4774 "</tr>\n";
4776 print "</table>\n";
4779 sub git_history_body {
4780 # Warning: assumes constant type (blob or tree) during history
4781 my ($commitlist, $from, $to, $refs, $extra,
4782 $file_name, $file_hash, $ftype) = @_;
4784 $from = 0 unless defined $from;
4785 $to = $#{$commitlist} unless (defined $to && $to <= $#{$commitlist});
4787 print "<table class=\"history\">\n";
4788 my $alternate = 1;
4789 for (my $i = $from; $i <= $to; $i++) {
4790 my %co = %{$commitlist->[$i]};
4791 if (!%co) {
4792 next;
4794 my $commit = $co{'id'};
4796 my $ref = format_ref_marker($refs, $commit);
4798 if ($alternate) {
4799 print "<tr class=\"dark\">\n";
4800 } else {
4801 print "<tr class=\"light\">\n";
4803 $alternate ^= 1;
4804 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
4805 # shortlog: format_author_html('td', \%co, 10)
4806 format_author_html('td', \%co, 15, 3) . "<td>";
4807 # originally git_history used chop_str($co{'title'}, 50)
4808 print format_subject_html($co{'title'}, $co{'title_short'},
4809 href(action=>"commit", hash=>$commit), $ref);
4810 print "</td>\n" .
4811 "<td class=\"link\">" .
4812 $cgi->a({-href => href(action=>$ftype, hash_base=>$commit, file_name=>$file_name)}, $ftype) . " | " .
4813 $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff");
4815 if ($ftype eq 'blob') {
4816 my $blob_current = $file_hash;
4817 my $blob_parent = git_get_hash_by_path($commit, $file_name);
4818 if (defined $blob_current && defined $blob_parent &&
4819 $blob_current ne $blob_parent) {
4820 print " | " .
4821 $cgi->a({-href => href(action=>"blobdiff",
4822 hash=>$blob_current, hash_parent=>$blob_parent,
4823 hash_base=>$hash_base, hash_parent_base=>$commit,
4824 file_name=>$file_name)},
4825 "diff to current");
4828 print "</td>\n" .
4829 "</tr>\n";
4831 if (defined $extra) {
4832 print "<tr>\n" .
4833 "<td colspan=\"4\">$extra</td>\n" .
4834 "</tr>\n";
4836 print "</table>\n";
4839 sub git_tags_body {
4840 # uses global variable $project
4841 my ($taglist, $from, $to, $extra) = @_;
4842 $from = 0 unless defined $from;
4843 $to = $#{$taglist} if (!defined $to || $#{$taglist} < $to);
4845 print "<table class=\"tags\">\n";
4846 my $alternate = 1;
4847 for (my $i = $from; $i <= $to; $i++) {
4848 my $entry = $taglist->[$i];
4849 my %tag = %$entry;
4850 my $comment = $tag{'subject'};
4851 my $comment_short;
4852 if (defined $comment) {
4853 $comment_short = chop_str($comment, 30, 5);
4855 if ($alternate) {
4856 print "<tr class=\"dark\">\n";
4857 } else {
4858 print "<tr class=\"light\">\n";
4860 $alternate ^= 1;
4861 if (defined $tag{'age'}) {
4862 print "<td><i>$tag{'age'}</i></td>\n";
4863 } else {
4864 print "<td></td>\n";
4866 print "<td>" .
4867 $cgi->a({-href => href(action=>$tag{'reftype'}, hash=>$tag{'refid'}),
4868 -class => "list name"}, esc_html($tag{'name'})) .
4869 "</td>\n" .
4870 "<td>";
4871 if (defined $comment) {
4872 print format_subject_html($comment, $comment_short,
4873 href(action=>"tag", hash=>$tag{'id'}));
4875 print "</td>\n" .
4876 "<td class=\"selflink\">";
4877 if ($tag{'type'} eq "tag") {
4878 print $cgi->a({-href => href(action=>"tag", hash=>$tag{'id'})}, "tag");
4879 } else {
4880 print "&nbsp;";
4882 print "</td>\n" .
4883 "<td class=\"link\">" . " | " .
4884 $cgi->a({-href => href(action=>$tag{'reftype'}, hash=>$tag{'refid'})}, $tag{'reftype'});
4885 if ($tag{'reftype'} eq "commit") {
4886 print " | " . $cgi->a({-href => href(action=>"shortlog", hash=>$tag{'fullname'})}, "shortlog") .
4887 " | " . $cgi->a({-href => href(action=>"log", hash=>$tag{'fullname'})}, "log");
4888 } elsif ($tag{'reftype'} eq "blob") {
4889 print " | " . $cgi->a({-href => href(action=>"blob_plain", hash=>$tag{'refid'})}, "raw");
4891 print "</td>\n" .
4892 "</tr>";
4894 if (defined $extra) {
4895 print "<tr>\n" .
4896 "<td colspan=\"5\">$extra</td>\n" .
4897 "</tr>\n";
4899 print "</table>\n";
4902 sub git_heads_body {
4903 # uses global variable $project
4904 my ($headlist, $head, $from, $to, $extra) = @_;
4905 $from = 0 unless defined $from;
4906 $to = $#{$headlist} if (!defined $to || $#{$headlist} < $to);
4908 print "<table class=\"heads\">\n";
4909 my $alternate = 1;
4910 for (my $i = $from; $i <= $to; $i++) {
4911 my $entry = $headlist->[$i];
4912 my %ref = %$entry;
4913 my $curr = $ref{'id'} eq $head;
4914 if ($alternate) {
4915 print "<tr class=\"dark\">\n";
4916 } else {
4917 print "<tr class=\"light\">\n";
4919 $alternate ^= 1;
4920 print "<td><i>$ref{'age'}</i></td>\n" .
4921 ($curr ? "<td class=\"current_head\">" : "<td>") .
4922 $cgi->a({-href => href(action=>"shortlog", hash=>$ref{'fullname'}),
4923 -class => "list name"},esc_html($ref{'name'})) .
4924 "</td>\n" .
4925 "<td class=\"link\">" .
4926 $cgi->a({-href => href(action=>"shortlog", hash=>$ref{'fullname'})}, "shortlog") . " | " .
4927 $cgi->a({-href => href(action=>"log", hash=>$ref{'fullname'})}, "log") . " | " .
4928 $cgi->a({-href => href(action=>"tree", hash=>$ref{'fullname'}, hash_base=>$ref{'name'})}, "tree") .
4929 "</td>\n" .
4930 "</tr>";
4932 if (defined $extra) {
4933 print "<tr>\n" .
4934 "<td colspan=\"3\">$extra</td>\n" .
4935 "</tr>\n";
4937 print "</table>\n";
4940 sub git_search_grep_body {
4941 my ($commitlist, $from, $to, $extra) = @_;
4942 $from = 0 unless defined $from;
4943 $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
4945 print "<table class=\"commit_search\">\n";
4946 my $alternate = 1;
4947 for (my $i = $from; $i <= $to; $i++) {
4948 my %co = %{$commitlist->[$i]};
4949 if (!%co) {
4950 next;
4952 my $commit = $co{'id'};
4953 if ($alternate) {
4954 print "<tr class=\"dark\">\n";
4955 } else {
4956 print "<tr class=\"light\">\n";
4958 $alternate ^= 1;
4959 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
4960 format_author_html('td', \%co, 15, 5) .
4961 "<td>" .
4962 $cgi->a({-href => href(action=>"commit", hash=>$co{'id'}),
4963 -class => "list subject"},
4964 chop_and_escape_str($co{'title'}, 50) . "<br/>");
4965 my $comment = $co{'comment'};
4966 foreach my $line (@$comment) {
4967 if ($line =~ m/^(.*?)($search_regexp)(.*)$/i) {
4968 my ($lead, $match, $trail) = ($1, $2, $3);
4969 $match = chop_str($match, 70, 5, 'center');
4970 my $contextlen = int((80 - length($match))/2);
4971 $contextlen = 30 if ($contextlen > 30);
4972 $lead = chop_str($lead, $contextlen, 10, 'left');
4973 $trail = chop_str($trail, $contextlen, 10, 'right');
4975 $lead = esc_html($lead);
4976 $match = esc_html($match);
4977 $trail = esc_html($trail);
4979 print "$lead<span class=\"match\">$match</span>$trail<br />";
4982 print "</td>\n" .
4983 "<td class=\"link\">" .
4984 $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") .
4985 " | " .
4986 $cgi->a({-href => href(action=>"commitdiff", hash=>$co{'id'})}, "commitdiff") .
4987 " | " .
4988 $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree");
4989 print "</td>\n" .
4990 "</tr>\n";
4992 if (defined $extra) {
4993 print "<tr>\n" .
4994 "<td colspan=\"3\">$extra</td>\n" .
4995 "</tr>\n";
4997 print "</table>\n";
5000 ## ======================================================================
5001 ## ======================================================================
5002 ## actions
5004 sub git_project_list {
5005 my $order = $input_params{'order'};
5006 if (defined $order && $order !~ m/none|project|descr|owner|age/) {
5007 die_error(400, "Unknown order parameter");
5010 my @list = git_get_projects_list();
5011 if (!@list) {
5012 die_error(404, "No projects found");
5015 git_header_html();
5016 if (defined $home_text && -f $home_text) {
5017 print "<div class=\"index_include\">\n";
5018 insert_file($home_text);
5019 print "</div>\n";
5021 print $cgi->startform(-method => "get") .
5022 "<p class=\"projsearch\">Search:\n" .
5023 $cgi->textfield(-name => "s", -value => $searchtext) . "\n" .
5024 "</p>" .
5025 $cgi->end_form() . "\n";
5026 git_project_list_body(\@list, $order);
5027 git_footer_html();
5030 sub git_forks {
5031 my $order = $input_params{'order'};
5032 if (defined $order && $order !~ m/none|project|descr|owner|age/) {
5033 die_error(400, "Unknown order parameter");
5036 my @list = git_get_projects_list($project);
5037 if (!@list) {
5038 die_error(404, "No forks found");
5041 git_header_html();
5042 git_print_page_nav('','');
5043 git_print_header_div('summary', "$project forks");
5044 git_project_list_body(\@list, $order);
5045 git_footer_html();
5048 sub git_project_index {
5049 my @projects = git_get_projects_list($project);
5051 print $cgi->header(
5052 -type => 'text/plain',
5053 -charset => 'utf-8',
5054 -content_disposition => 'inline; filename="index.aux"');
5056 foreach my $pr (@projects) {
5057 if (!exists $pr->{'owner'}) {
5058 $pr->{'owner'} = git_get_project_owner("$pr->{'path'}");
5061 my ($path, $owner) = ($pr->{'path'}, $pr->{'owner'});
5062 # quote as in CGI::Util::encode, but keep the slash, and use '+' for ' '
5063 $path =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf("%%%02X", ord($1))/eg;
5064 $owner =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf("%%%02X", ord($1))/eg;
5065 $path =~ s/ /\+/g;
5066 $owner =~ s/ /\+/g;
5068 print "$path $owner\n";
5072 sub git_summary {
5073 my $descr = git_get_project_description($project) || "none";
5074 my %co = parse_commit("HEAD");
5075 my %cd = %co ? parse_date($co{'committer_epoch'}, $co{'committer_tz'}) : ();
5076 my $head = $co{'id'};
5078 my $owner = git_get_project_owner($project);
5080 my $refs = git_get_references();
5081 # These get_*_list functions return one more to allow us to see if
5082 # there are more ...
5083 my @taglist = git_get_tags_list(16);
5084 my @headlist = git_get_heads_list(16);
5085 my @forklist;
5086 my $check_forks = gitweb_check_feature('forks');
5088 if ($check_forks) {
5089 @forklist = git_get_projects_list($project);
5092 git_header_html();
5093 git_print_page_nav('summary','', $head);
5095 print "<div class=\"title\">&nbsp;</div>\n";
5096 print "<table class=\"projects_list\">\n" .
5097 "<tr id=\"metadata_desc\"><td>description</td><td>" . esc_html($descr) . "</td></tr>\n" .
5098 "<tr id=\"metadata_owner\"><td>owner</td><td>" . esc_html($owner) . "</td></tr>\n";
5099 if (defined $cd{'rfc2822'}) {
5100 print "<tr id=\"metadata_lchange\"><td>last change</td><td>$cd{'rfc2822'}</td></tr>\n";
5103 # use per project git URL list in $projectroot/$project/cloneurl
5104 # or make project git URL from git base URL and project name
5105 my $url_tag = "URL";
5106 my @url_list = git_get_project_url_list($project);
5107 @url_list = map { "$_/$project" } @git_base_url_list unless @url_list;
5108 foreach my $git_url (@url_list) {
5109 next unless $git_url;
5110 print "<tr class=\"metadata_url\"><td>$url_tag</td><td>$git_url</td></tr>\n";
5111 $url_tag = "";
5114 # Tag cloud
5115 my $show_ctags = gitweb_check_feature('ctags');
5116 if ($show_ctags) {
5117 my $ctags = git_get_project_ctags($project);
5118 my $cloud = git_populate_project_tagcloud($ctags);
5119 print "<tr id=\"metadata_ctags\"><td>Content tags:<br />";
5120 print "</td>\n<td>" unless %$ctags;
5121 print "<form action=\"$show_ctags\" method=\"post\"><input type=\"hidden\" name=\"p\" value=\"$project\" />Add: <input type=\"text\" name=\"t\" size=\"8\" /></form>";
5122 print "</td>\n<td>" if %$ctags;
5123 print git_show_project_tagcloud($cloud, 48);
5124 print "</td></tr>";
5127 print "</table>\n";
5129 # If XSS prevention is on, we don't include README.html.
5130 # TODO: Allow a readme in some safe format.
5131 if (!$prevent_xss && -s "$projectroot/$project/README.html") {
5132 print "<div class=\"title\">readme</div>\n" .
5133 "<div class=\"readme\">\n";
5134 insert_file("$projectroot/$project/README.html");
5135 print "\n</div>\n"; # class="readme"
5138 # we need to request one more than 16 (0..15) to check if
5139 # those 16 are all
5140 my @commitlist = $head ? parse_commits($head, 17) : ();
5141 if (@commitlist) {
5142 git_print_header_div('shortlog');
5143 git_shortlog_body(\@commitlist, 0, 15, $refs,
5144 $#commitlist <= 15 ? undef :
5145 $cgi->a({-href => href(action=>"shortlog")}, "..."));
5148 if (@taglist) {
5149 git_print_header_div('tags');
5150 git_tags_body(\@taglist, 0, 15,
5151 $#taglist <= 15 ? undef :
5152 $cgi->a({-href => href(action=>"tags")}, "..."));
5155 if (@headlist) {
5156 git_print_header_div('heads');
5157 git_heads_body(\@headlist, $head, 0, 15,
5158 $#headlist <= 15 ? undef :
5159 $cgi->a({-href => href(action=>"heads")}, "..."));
5162 if (@forklist) {
5163 git_print_header_div('forks');
5164 git_project_list_body(\@forklist, 'age', 0, 15,
5165 $#forklist <= 15 ? undef :
5166 $cgi->a({-href => href(action=>"forks")}, "..."),
5167 'no_header');
5170 git_footer_html();
5173 sub git_tag {
5174 my $head = git_get_head_hash($project);
5175 git_header_html();
5176 git_print_page_nav('','', $head,undef,$head);
5177 my %tag = parse_tag($hash);
5179 if (! %tag) {
5180 die_error(404, "Unknown tag object");
5183 git_print_header_div('commit', esc_html($tag{'name'}), $hash);
5184 print "<div class=\"title_text\">\n" .
5185 "<table class=\"object_header\">\n" .
5186 "<tr>\n" .
5187 "<td>object</td>\n" .
5188 "<td>" . $cgi->a({-class => "list", -href => href(action=>$tag{'type'}, hash=>$tag{'object'})},
5189 $tag{'object'}) . "</td>\n" .
5190 "<td class=\"link\">" . $cgi->a({-href => href(action=>$tag{'type'}, hash=>$tag{'object'})},
5191 $tag{'type'}) . "</td>\n" .
5192 "</tr>\n";
5193 if (defined($tag{'author'})) {
5194 git_print_authorship_rows(\%tag, 'author');
5196 print "</table>\n\n" .
5197 "</div>\n";
5198 print "<div class=\"page_body\">";
5199 my $comment = $tag{'comment'};
5200 foreach my $line (@$comment) {
5201 chomp $line;
5202 print esc_html($line, -nbsp=>1) . "<br/>\n";
5204 print "</div>\n";
5205 git_footer_html();
5208 sub git_blame_common {
5209 my $format = shift || 'porcelain';
5210 if ($format eq 'porcelain' && $cgi->param('js')) {
5211 $format = 'incremental';
5212 $action = 'blame_incremental'; # for page title etc
5215 # permissions
5216 gitweb_check_feature('blame')
5217 or die_error(403, "Blame view not allowed");
5219 # error checking
5220 die_error(400, "No file name given") unless $file_name;
5221 $hash_base ||= git_get_head_hash($project);
5222 die_error(404, "Couldn't find base commit") unless $hash_base;
5223 my %co = parse_commit($hash_base)
5224 or die_error(404, "Commit not found");
5225 my $ftype = "blob";
5226 if (!defined $hash) {
5227 $hash = git_get_hash_by_path($hash_base, $file_name, "blob")
5228 or die_error(404, "Error looking up file");
5229 } else {
5230 $ftype = git_get_type($hash);
5231 if ($ftype !~ "blob") {
5232 die_error(400, "Object is not a blob");
5236 my $fd;
5237 if ($format eq 'incremental') {
5238 # get file contents (as base)
5239 open $fd, "-|", git_cmd(), 'cat-file', 'blob', $hash
5240 or die_error(500, "Open git-cat-file failed");
5241 } elsif ($format eq 'data') {
5242 # run git-blame --incremental
5243 open $fd, "-|", git_cmd(), "blame", "--incremental",
5244 $hash_base, "--", $file_name
5245 or die_error(500, "Open git-blame --incremental failed");
5246 } else {
5247 # run git-blame --porcelain
5248 open $fd, "-|", git_cmd(), "blame", '-p',
5249 $hash_base, '--', $file_name
5250 or die_error(500, "Open git-blame --porcelain failed");
5253 # incremental blame data returns early
5254 if ($format eq 'data') {
5255 print $cgi->header(
5256 -type=>"text/plain", -charset => "utf-8",
5257 -status=> "200 OK");
5258 local $| = 1; # output autoflush
5259 print while <$fd>;
5260 close $fd
5261 or print "ERROR $!\n";
5263 print 'END';
5264 if (defined $t0 && gitweb_check_feature('timed')) {
5265 print ' '.
5266 Time::HiRes::tv_interval($t0, [Time::HiRes::gettimeofday()]).
5267 ' '.$number_of_git_cmds;
5269 print "\n";
5271 return;
5274 # page header
5275 git_header_html();
5276 my $formats_nav =
5277 $cgi->a({-href => href(action=>"blob", -replay=>1)},
5278 "blob") .
5279 " | ";
5280 if ($format eq 'incremental') {
5281 $formats_nav .=
5282 $cgi->a({-href => href(action=>"blame", javascript=>0, -replay=>1)},
5283 "blame") . " (non-incremental)";
5284 } else {
5285 $formats_nav .=
5286 $cgi->a({-href => href(action=>"blame_incremental", -replay=>1)},
5287 "blame") . " (incremental)";
5289 $formats_nav .=
5290 " | " .
5291 $cgi->a({-href => href(action=>"history", -replay=>1)},
5292 "history") .
5293 " | " .
5294 $cgi->a({-href => href(action=>$action, file_name=>$file_name)},
5295 "HEAD");
5296 git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
5297 git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
5298 git_print_page_path($file_name, $ftype, $hash_base);
5300 # page body
5301 if ($format eq 'incremental') {
5302 print "<noscript>\n<div class=\"error\"><center><b>\n".
5303 "This page requires JavaScript to run.\n Use ".
5304 $cgi->a({-href => href(action=>'blame',javascript=>0,-replay=>1)},
5305 'this page').
5306 " instead.\n".
5307 "</b></center></div>\n</noscript>\n";
5309 print qq!<div id="progress_bar" style="width: 100%; background-color: yellow"></div>\n!;
5312 print qq!<div class="page_body">\n!;
5313 print qq!<div id="progress_info">... / ...</div>\n!
5314 if ($format eq 'incremental');
5315 print qq!<table id="blame_table" class="blame" width="100%">\n!.
5316 #qq!<col width="5.5em" /><col width="2.5em" /><col width="*" />\n!.
5317 qq!<thead>\n!.
5318 qq!<tr><th>Commit</th><th>Line</th><th>Data</th></tr>\n!.
5319 qq!</thead>\n!.
5320 qq!<tbody>\n!;
5322 my @rev_color = qw(light dark);
5323 my $num_colors = scalar(@rev_color);
5324 my $current_color = 0;
5326 if ($format eq 'incremental') {
5327 my $color_class = $rev_color[$current_color];
5329 #contents of a file
5330 my $linenr = 0;
5331 LINE:
5332 while (my $line = <$fd>) {
5333 chomp $line;
5334 $linenr++;
5336 print qq!<tr id="l$linenr" class="$color_class">!.
5337 qq!<td class="sha1"><a href=""> </a></td>!.
5338 qq!<td class="linenr">!.
5339 qq!<a class="linenr" href="">$linenr</a></td>!;
5340 print qq!<td class="pre">! . esc_html($line) . "</td>\n";
5341 print qq!</tr>\n!;
5344 } else { # porcelain, i.e. ordinary blame
5345 my %metainfo = (); # saves information about commits
5347 # blame data
5348 LINE:
5349 while (my $line = <$fd>) {
5350 chomp $line;
5351 # the header: <SHA-1> <src lineno> <dst lineno> [<lines in group>]
5352 # no <lines in group> for subsequent lines in group of lines
5353 my ($full_rev, $orig_lineno, $lineno, $group_size) =
5354 ($line =~ /^([0-9a-f]{40}) (\d+) (\d+)(?: (\d+))?$/);
5355 if (!exists $metainfo{$full_rev}) {
5356 $metainfo{$full_rev} = { 'nprevious' => 0 };
5358 my $meta = $metainfo{$full_rev};
5359 my $data;
5360 while ($data = <$fd>) {
5361 chomp $data;
5362 last if ($data =~ s/^\t//); # contents of line
5363 if ($data =~ /^(\S+)(?: (.*))?$/) {
5364 $meta->{$1} = $2 unless exists $meta->{$1};
5366 if ($data =~ /^previous /) {
5367 $meta->{'nprevious'}++;
5370 my $short_rev = substr($full_rev, 0, 8);
5371 my $author = $meta->{'author'};
5372 my %date =
5373 parse_date($meta->{'author-time'}, $meta->{'author-tz'});
5374 my $date = $date{'iso-tz'};
5375 if ($group_size) {
5376 $current_color = ($current_color + 1) % $num_colors;
5378 my $tr_class = $rev_color[$current_color];
5379 $tr_class .= ' boundary' if (exists $meta->{'boundary'});
5380 $tr_class .= ' no-previous' if ($meta->{'nprevious'} == 0);
5381 $tr_class .= ' multiple-previous' if ($meta->{'nprevious'} > 1);
5382 print "<tr id=\"l$lineno\" class=\"$tr_class\">\n";
5383 if ($group_size) {
5384 print "<td class=\"sha1\"";
5385 print " title=\"". esc_html($author) . ", $date\"";
5386 print " rowspan=\"$group_size\"" if ($group_size > 1);
5387 print ">";
5388 print $cgi->a({-href => href(action=>"commit",
5389 hash=>$full_rev,
5390 file_name=>$file_name)},
5391 esc_html($short_rev));
5392 if ($group_size >= 2) {
5393 my @author_initials = ($author =~ /\b([[:upper:]])\B/g);
5394 if (@author_initials) {
5395 print "<br />" .
5396 esc_html(join('', @author_initials));
5397 # or join('.', ...)
5400 print "</td>\n";
5402 # 'previous' <sha1 of parent commit> <filename at commit>
5403 if (exists $meta->{'previous'} &&
5404 $meta->{'previous'} =~ /^([a-fA-F0-9]{40}) (.*)$/) {
5405 $meta->{'parent'} = $1;
5406 $meta->{'file_parent'} = unquote($2);
5408 my $linenr_commit =
5409 exists($meta->{'parent'}) ?
5410 $meta->{'parent'} : $full_rev;
5411 my $linenr_filename =
5412 exists($meta->{'file_parent'}) ?
5413 $meta->{'file_parent'} : unquote($meta->{'filename'});
5414 my $blamed = href(action => 'blame',
5415 file_name => $linenr_filename,
5416 hash_base => $linenr_commit);
5417 print "<td class=\"linenr\">";
5418 print $cgi->a({ -href => "$blamed#l$orig_lineno",
5419 -class => "linenr" },
5420 esc_html($lineno));
5421 print "</td>";
5422 print "<td class=\"pre\">" . esc_html($data) . "</td>\n";
5423 print "</tr>\n";
5424 } # end while
5428 # footer
5429 print "</tbody>\n".
5430 "</table>\n"; # class="blame"
5431 print "</div>\n"; # class="blame_body"
5432 close $fd
5433 or print "Reading blob failed\n";
5435 git_footer_html();
5438 sub git_blame {
5439 git_blame_common();
5442 sub git_blame_incremental {
5443 git_blame_common('incremental');
5446 sub git_blame_data {
5447 git_blame_common('data');
5450 sub git_tags {
5451 my $head = git_get_head_hash($project);
5452 git_header_html();
5453 git_print_page_nav('','', $head,undef,$head);
5454 git_print_header_div('summary', $project);
5456 my @tagslist = git_get_tags_list();
5457 if (@tagslist) {
5458 git_tags_body(\@tagslist);
5460 git_footer_html();
5463 sub git_heads {
5464 my $head = git_get_head_hash($project);
5465 git_header_html();
5466 git_print_page_nav('','', $head,undef,$head);
5467 git_print_header_div('summary', $project);
5469 my @headslist = git_get_heads_list();
5470 if (@headslist) {
5471 git_heads_body(\@headslist, $head);
5473 git_footer_html();
5476 sub git_blob_plain {
5477 my $type = shift;
5478 my $expires;
5480 if (!defined $hash) {
5481 if (defined $file_name) {
5482 my $base = $hash_base || git_get_head_hash($project);
5483 $hash = git_get_hash_by_path($base, $file_name, "blob")
5484 or die_error(404, "Cannot find file");
5485 } else {
5486 die_error(400, "No file name defined");
5488 } elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) {
5489 # blobs defined by non-textual hash id's can be cached
5490 $expires = "+1d";
5493 open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
5494 or die_error(500, "Open git-cat-file blob '$hash' failed");
5496 # content-type (can include charset)
5497 $type = blob_contenttype($fd, $file_name, $type);
5499 # "save as" filename, even when no $file_name is given
5500 my $save_as = "$hash";
5501 if (defined $file_name) {
5502 $save_as = $file_name;
5503 } elsif ($type =~ m/^text\//) {
5504 $save_as .= '.txt';
5507 # With XSS prevention on, blobs of all types except a few known safe
5508 # ones are served with "Content-Disposition: attachment" to make sure
5509 # they don't run in our security domain. For certain image types,
5510 # blob view writes an <img> tag referring to blob_plain view, and we
5511 # want to be sure not to break that by serving the image as an
5512 # attachment (though Firefox 3 doesn't seem to care).
5513 my $sandbox = $prevent_xss &&
5514 $type !~ m!^(?:text/plain|image/(?:gif|png|jpeg))$!;
5516 print $cgi->header(
5517 -type => $type,
5518 -expires => $expires,
5519 -content_disposition =>
5520 ($sandbox ? 'attachment' : 'inline')
5521 . '; filename="' . $save_as . '"');
5522 local $/ = undef;
5523 binmode STDOUT, ':raw';
5524 print <$fd>;
5525 binmode STDOUT, ':utf8'; # as set at the beginning of gitweb.cgi
5526 close $fd;
5529 sub git_blob {
5530 my $expires;
5532 if (!defined $hash) {
5533 if (defined $file_name) {
5534 my $base = $hash_base || git_get_head_hash($project);
5535 $hash = git_get_hash_by_path($base, $file_name, "blob")
5536 or die_error(404, "Cannot find file");
5537 } else {
5538 die_error(400, "No file name defined");
5540 } elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) {
5541 # blobs defined by non-textual hash id's can be cached
5542 $expires = "+1d";
5545 my $have_blame = gitweb_check_feature('blame');
5546 open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
5547 or die_error(500, "Couldn't cat $file_name, $hash");
5548 my $mimetype = blob_mimetype($fd, $file_name);
5549 # use 'blob_plain' (aka 'raw') view for files that cannot be displayed
5550 if ($mimetype !~ m!^(?:text/|image/(?:gif|png|jpeg)$)! && -B $fd) {
5551 close $fd;
5552 return git_blob_plain($mimetype);
5554 # we can have blame only for text/* mimetype
5555 $have_blame &&= ($mimetype =~ m!^text/!);
5557 my $highlight = gitweb_check_feature('highlight');
5558 my $syntax = guess_file_syntax($highlight, $mimetype, $file_name);
5559 $fd = run_highlighter($fd, $highlight, $syntax)
5560 if $syntax;
5562 git_header_html(undef, $expires);
5563 my $formats_nav = '';
5564 if (defined $hash_base && (my %co = parse_commit($hash_base))) {
5565 if (defined $file_name) {
5566 if ($have_blame) {
5567 $formats_nav .=
5568 $cgi->a({-href => href(action=>"blame", -replay=>1)},
5569 "blame") .
5570 " | ";
5572 $formats_nav .=
5573 $cgi->a({-href => href(action=>"history", -replay=>1)},
5574 "history") .
5575 " | " .
5576 $cgi->a({-href => href(action=>"blob_plain", -replay=>1)},
5577 "raw") .
5578 " | " .
5579 $cgi->a({-href => href(action=>"blob",
5580 hash_base=>"HEAD", file_name=>$file_name)},
5581 "HEAD");
5582 } else {
5583 $formats_nav .=
5584 $cgi->a({-href => href(action=>"blob_plain", -replay=>1)},
5585 "raw");
5587 git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
5588 git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
5589 } else {
5590 print "<div class=\"page_nav\">\n" .
5591 "<br/><br/></div>\n" .
5592 "<div class=\"title\">$hash</div>\n";
5594 git_print_page_path($file_name, "blob", $hash_base);
5595 print "<div class=\"page_body\">\n";
5596 if ($mimetype =~ m!^image/!) {
5597 print qq!<img type="$mimetype"!;
5598 if ($file_name) {
5599 print qq! alt="$file_name" title="$file_name"!;
5601 print qq! src="! .
5602 href(action=>"blob_plain", hash=>$hash,
5603 hash_base=>$hash_base, file_name=>$file_name) .
5604 qq!" />\n!;
5605 } else {
5606 my $nr;
5607 while (my $line = <$fd>) {
5608 chomp $line;
5609 $nr++;
5610 $line = untabify($line);
5611 printf qq!<div class="pre"><a id="l%i" href="%s#l%i" class="linenr">%4i</a> %s</div>\n!,
5612 $nr, href(-replay => 1), $nr, $nr, $syntax ? $line : esc_html($line, -nbsp=>1);
5615 close $fd
5616 or print "Reading blob failed.\n";
5617 print "</div>";
5618 git_footer_html();
5621 sub git_tree {
5622 if (!defined $hash_base) {
5623 $hash_base = "HEAD";
5625 if (!defined $hash) {
5626 if (defined $file_name) {
5627 $hash = git_get_hash_by_path($hash_base, $file_name, "tree");
5628 } else {
5629 $hash = $hash_base;
5632 die_error(404, "No such tree") unless defined($hash);
5634 my $show_sizes = gitweb_check_feature('show-sizes');
5635 my $have_blame = gitweb_check_feature('blame');
5637 my @entries = ();
5639 local $/ = "\0";
5640 open my $fd, "-|", git_cmd(), "ls-tree", '-z',
5641 ($show_sizes ? '-l' : ()), @extra_options, $hash
5642 or die_error(500, "Open git-ls-tree failed");
5643 @entries = map { chomp; $_ } <$fd>;
5644 close $fd
5645 or die_error(404, "Reading tree failed");
5648 my $refs = git_get_references();
5649 my $ref = format_ref_marker($refs, $hash_base);
5650 git_header_html();
5651 my $basedir = '';
5652 if (defined $hash_base && (my %co = parse_commit($hash_base))) {
5653 my @views_nav = ();
5654 if (defined $file_name) {
5655 push @views_nav,
5656 $cgi->a({-href => href(action=>"history", -replay=>1)},
5657 "history"),
5658 $cgi->a({-href => href(action=>"tree",
5659 hash_base=>"HEAD", file_name=>$file_name)},
5660 "HEAD"),
5662 my $snapshot_links = format_snapshot_links($hash);
5663 if (defined $snapshot_links) {
5664 # FIXME: Should be available when we have no hash base as well.
5665 push @views_nav, $snapshot_links;
5667 git_print_page_nav('tree','', $hash_base, undef, undef,
5668 join(' | ', @views_nav));
5669 git_print_header_div('commit', esc_html($co{'title'}) . $ref, $hash_base);
5670 } else {
5671 undef $hash_base;
5672 print "<div class=\"page_nav\">\n";
5673 print "<br/><br/></div>\n";
5674 print "<div class=\"title\">$hash</div>\n";
5676 if (defined $file_name) {
5677 $basedir = $file_name;
5678 if ($basedir ne '' && substr($basedir, -1) ne '/') {
5679 $basedir .= '/';
5681 git_print_page_path($file_name, 'tree', $hash_base);
5683 print "<div class=\"page_body\">\n";
5684 print "<table class=\"tree\">\n";
5685 my $alternate = 1;
5686 # '..' (top directory) link if possible
5687 if (defined $hash_base &&
5688 defined $file_name && $file_name =~ m![^/]+$!) {
5689 if ($alternate) {
5690 print "<tr class=\"dark\">\n";
5691 } else {
5692 print "<tr class=\"light\">\n";
5694 $alternate ^= 1;
5696 my $up = $file_name;
5697 $up =~ s!/?[^/]+$!!;
5698 undef $up unless $up;
5699 # based on git_print_tree_entry
5700 print '<td class="mode">' . mode_str('040000') . "</td>\n";
5701 print '<td class="size">&nbsp;</td>'."\n" if $show_sizes;
5702 print '<td class="list">';
5703 print $cgi->a({-href => href(action=>"tree",
5704 hash_base=>$hash_base,
5705 file_name=>$up)},
5706 "..");
5707 print "</td>\n";
5708 print "<td class=\"link\"></td>\n";
5710 print "</tr>\n";
5712 foreach my $line (@entries) {
5713 my %t = parse_ls_tree_line($line, -z => 1, -l => $show_sizes);
5715 if ($alternate) {
5716 print "<tr class=\"dark\">\n";
5717 } else {
5718 print "<tr class=\"light\">\n";
5720 $alternate ^= 1;
5722 git_print_tree_entry(\%t, $basedir, $hash_base, $have_blame);
5724 print "</tr>\n";
5726 print "</table>\n" .
5727 "</div>";
5728 git_footer_html();
5731 sub snapshot_name {
5732 my ($project, $hash) = @_;
5734 # path/to/project.git -> project
5735 # path/to/project/.git -> project
5736 my $name = to_utf8($project);
5737 $name =~ s,([^/])/*\.git$,$1,;
5738 $name = basename($name);
5739 # sanitize name
5740 $name =~ s/[[:cntrl:]]/?/g;
5742 my $ver = $hash;
5743 if ($hash =~ /^[0-9a-fA-F]+$/) {
5744 # shorten SHA-1 hash
5745 my $full_hash = git_get_full_hash($project, $hash);
5746 if ($full_hash =~ /^$hash/ && length($hash) > 7) {
5747 $ver = git_get_short_hash($project, $hash);
5749 } elsif ($hash =~ m!^refs/tags/(.*)$!) {
5750 # tags don't need shortened SHA-1 hash
5751 $ver = $1;
5752 } else {
5753 # branches and other need shortened SHA-1 hash
5754 if ($hash =~ m!^refs/(?:heads|remotes)/(.*)$!) {
5755 $ver = $1;
5757 $ver .= '-' . git_get_short_hash($project, $hash);
5759 # in case of hierarchical branch names
5760 $ver =~ s!/!.!g;
5762 # name = project-version_string
5763 $name = "$name-$ver";
5765 return wantarray ? ($name, $name) : $name;
5768 sub git_snapshot {
5769 my $format = $input_params{'snapshot_format'};
5770 if (!@snapshot_fmts) {
5771 die_error(403, "Snapshots not allowed");
5773 # default to first supported snapshot format
5774 $format ||= $snapshot_fmts[0];
5775 if ($format !~ m/^[a-z0-9]+$/) {
5776 die_error(400, "Invalid snapshot format parameter");
5777 } elsif (!exists($known_snapshot_formats{$format})) {
5778 die_error(400, "Unknown snapshot format");
5779 } elsif ($known_snapshot_formats{$format}{'disabled'}) {
5780 die_error(403, "Snapshot format not allowed");
5781 } elsif (!grep($_ eq $format, @snapshot_fmts)) {
5782 die_error(403, "Unsupported snapshot format");
5785 my $type = git_get_type("$hash^{}");
5786 if (!$type) {
5787 die_error(404, 'Object does not exist');
5788 } elsif ($type eq 'blob') {
5789 die_error(400, 'Object is not a tree-ish');
5792 my ($name, $prefix) = snapshot_name($project, $hash);
5793 my $filename = "$name$known_snapshot_formats{$format}{'suffix'}";
5794 my $cmd = quote_command(
5795 git_cmd(), 'archive',
5796 "--format=$known_snapshot_formats{$format}{'format'}",
5797 "--prefix=$prefix/", $hash);
5798 if (exists $known_snapshot_formats{$format}{'compressor'}) {
5799 $cmd .= ' | ' . quote_command(@{$known_snapshot_formats{$format}{'compressor'}});
5802 $filename =~ s/(["\\])/\\$1/g;
5803 print $cgi->header(
5804 -type => $known_snapshot_formats{$format}{'type'},
5805 -content_disposition => 'inline; filename="' . $filename . '"',
5806 -status => '200 OK');
5808 open my $fd, "-|", $cmd
5809 or die_error(500, "Execute git-archive failed");
5810 binmode STDOUT, ':raw';
5811 print <$fd>;
5812 binmode STDOUT, ':utf8'; # as set at the beginning of gitweb.cgi
5813 close $fd;
5816 sub git_log_generic {
5817 my ($fmt_name, $body_subr, $base, $parent, $file_name, $file_hash) = @_;
5819 my $head = git_get_head_hash($project);
5820 if (!defined $base) {
5821 $base = $head;
5823 if (!defined $page) {
5824 $page = 0;
5826 my $refs = git_get_references();
5828 my $commit_hash = $base;
5829 if (defined $parent) {
5830 $commit_hash = "$parent..$base";
5832 my @commitlist =
5833 parse_commits($commit_hash, 101, (100 * $page),
5834 defined $file_name ? ($file_name, "--full-history") : ());
5836 my $ftype;
5837 if (!defined $file_hash && defined $file_name) {
5838 # some commits could have deleted file in question,
5839 # and not have it in tree, but one of them has to have it
5840 for (my $i = 0; $i < @commitlist; $i++) {
5841 $file_hash = git_get_hash_by_path($commitlist[$i]{'id'}, $file_name);
5842 last if defined $file_hash;
5845 if (defined $file_hash) {
5846 $ftype = git_get_type($file_hash);
5848 if (defined $file_name && !defined $ftype) {
5849 die_error(500, "Unknown type of object");
5851 my %co;
5852 if (defined $file_name) {
5853 %co = parse_commit($base)
5854 or die_error(404, "Unknown commit object");
5858 my $paging_nav = format_paging_nav($fmt_name, $page, $#commitlist >= 100);
5859 my $next_link = '';
5860 if ($#commitlist >= 100) {
5861 $next_link =
5862 $cgi->a({-href => href(-replay=>1, page=>$page+1),
5863 -accesskey => "n", -title => "Alt-n"}, "next");
5865 my $patch_max = gitweb_get_feature('patches');
5866 if ($patch_max && !defined $file_name) {
5867 if ($patch_max < 0 || @commitlist <= $patch_max) {
5868 $paging_nav .= " &sdot; " .
5869 $cgi->a({-href => href(action=>"patches", -replay=>1)},
5870 "patches");
5874 git_header_html();
5875 git_print_page_nav($fmt_name,'', $hash,$hash,$hash, $paging_nav);
5876 if (defined $file_name) {
5877 git_print_header_div('commit', esc_html($co{'title'}), $base);
5878 } else {
5879 git_print_header_div('summary', $project)
5881 git_print_page_path($file_name, $ftype, $hash_base)
5882 if (defined $file_name);
5884 $body_subr->(\@commitlist, 0, 99, $refs, $next_link,
5885 $file_name, $file_hash, $ftype);
5887 git_footer_html();
5890 sub git_log {
5891 git_log_generic('log', \&git_log_body,
5892 $hash, $hash_parent);
5895 sub git_commit {
5896 $hash ||= $hash_base || "HEAD";
5897 my %co = parse_commit($hash)
5898 or die_error(404, "Unknown commit object");
5900 my $parent = $co{'parent'};
5901 my $parents = $co{'parents'}; # listref
5903 # we need to prepare $formats_nav before any parameter munging
5904 my $formats_nav;
5905 if (!defined $parent) {
5906 # --root commitdiff
5907 $formats_nav .= '(initial)';
5908 } elsif (@$parents == 1) {
5909 # single parent commit
5910 $formats_nav .=
5911 '(parent: ' .
5912 $cgi->a({-href => href(action=>"commit",
5913 hash=>$parent)},
5914 esc_html(substr($parent, 0, 7))) .
5915 ')';
5916 } else {
5917 # merge commit
5918 $formats_nav .=
5919 '(merge: ' .
5920 join(' ', map {
5921 $cgi->a({-href => href(action=>"commit",
5922 hash=>$_)},
5923 esc_html(substr($_, 0, 7)));
5924 } @$parents ) .
5925 ')';
5927 if (gitweb_check_feature('patches') && @$parents <= 1) {
5928 $formats_nav .= " | " .
5929 $cgi->a({-href => href(action=>"patch", -replay=>1)},
5930 "patch");
5933 if (!defined $parent) {
5934 $parent = "--root";
5936 my @difftree;
5937 open my $fd, "-|", git_cmd(), "diff-tree", '-r', "--no-commit-id",
5938 @diff_opts,
5939 (@$parents <= 1 ? $parent : '-c'),
5940 $hash, "--"
5941 or die_error(500, "Open git-diff-tree failed");
5942 @difftree = map { chomp; $_ } <$fd>;
5943 close $fd or die_error(404, "Reading git-diff-tree failed");
5945 # non-textual hash id's can be cached
5946 my $expires;
5947 if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
5948 $expires = "+1d";
5950 my $refs = git_get_references();
5951 my $ref = format_ref_marker($refs, $co{'id'});
5953 git_header_html(undef, $expires);
5954 git_print_page_nav('commit', '',
5955 $hash, $co{'tree'}, $hash,
5956 $formats_nav);
5958 if (defined $co{'parent'}) {
5959 git_print_header_div('commitdiff', esc_html($co{'title'}) . $ref, $hash);
5960 } else {
5961 git_print_header_div('tree', esc_html($co{'title'}) . $ref, $co{'tree'}, $hash);
5963 print "<div class=\"title_text\">\n" .
5964 "<table class=\"object_header\">\n";
5965 git_print_authorship_rows(\%co);
5966 print "<tr><td>commit</td><td class=\"sha1\">$co{'id'}</td></tr>\n";
5967 print "<tr>" .
5968 "<td>tree</td>" .
5969 "<td class=\"sha1\">" .
5970 $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$hash),
5971 class => "list"}, $co{'tree'}) .
5972 "</td>" .
5973 "<td class=\"link\">" .
5974 $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$hash)},
5975 "tree");
5976 my $snapshot_links = format_snapshot_links($hash);
5977 if (defined $snapshot_links) {
5978 print " | " . $snapshot_links;
5980 print "</td>" .
5981 "</tr>\n";
5983 foreach my $par (@$parents) {
5984 print "<tr>" .
5985 "<td>parent</td>" .
5986 "<td class=\"sha1\">" .
5987 $cgi->a({-href => href(action=>"commit", hash=>$par),
5988 class => "list"}, $par) .
5989 "</td>" .
5990 "<td class=\"link\">" .
5991 $cgi->a({-href => href(action=>"commit", hash=>$par)}, "commit") .
5992 " | " .
5993 $cgi->a({-href => href(action=>"commitdiff", hash=>$hash, hash_parent=>$par)}, "diff") .
5994 "</td>" .
5995 "</tr>\n";
5997 print "</table>".
5998 "</div>\n";
6000 print "<div class=\"page_body\">\n";
6001 git_print_log($co{'comment'});
6002 print "</div>\n";
6004 git_difftree_body(\@difftree, $hash, @$parents);
6006 git_footer_html();
6009 sub git_object {
6010 # object is defined by:
6011 # - hash or hash_base alone
6012 # - hash_base and file_name
6013 my $type;
6015 # - hash or hash_base alone
6016 if ($hash || ($hash_base && !defined $file_name)) {
6017 my $object_id = $hash || $hash_base;
6019 open my $fd, "-|", quote_command(
6020 git_cmd(), 'cat-file', '-t', $object_id) . ' 2> /dev/null'
6021 or die_error(404, "Object does not exist");
6022 $type = <$fd>;
6023 chomp $type;
6024 close $fd
6025 or die_error(404, "Object does not exist");
6027 # - hash_base and file_name
6028 } elsif ($hash_base && defined $file_name) {
6029 $file_name =~ s,/+$,,;
6031 system(git_cmd(), "cat-file", '-e', $hash_base) == 0
6032 or die_error(404, "Base object does not exist");
6034 # here errors should not hapen
6035 open my $fd, "-|", git_cmd(), "ls-tree", $hash_base, "--", $file_name
6036 or die_error(500, "Open git-ls-tree failed");
6037 my $line = <$fd>;
6038 close $fd;
6040 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'
6041 unless ($line && $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t/) {
6042 die_error(404, "File or directory for given base does not exist");
6044 $type = $2;
6045 $hash = $3;
6046 } else {
6047 die_error(400, "Not enough information to find object");
6050 print $cgi->redirect(-uri => href(action=>$type, -full=>1,
6051 hash=>$hash, hash_base=>$hash_base,
6052 file_name=>$file_name),
6053 -status => '302 Found');
6056 sub git_blobdiff {
6057 my $format = shift || 'html';
6059 my $fd;
6060 my @difftree;
6061 my %diffinfo;
6062 my $expires;
6064 # preparing $fd and %diffinfo for git_patchset_body
6065 # new style URI
6066 if (defined $hash_base && defined $hash_parent_base) {
6067 if (defined $file_name) {
6068 # read raw output
6069 open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
6070 $hash_parent_base, $hash_base,
6071 "--", (defined $file_parent ? $file_parent : ()), $file_name
6072 or die_error(500, "Open git-diff-tree failed");
6073 @difftree = map { chomp; $_ } <$fd>;
6074 close $fd
6075 or die_error(404, "Reading git-diff-tree failed");
6076 @difftree
6077 or die_error(404, "Blob diff not found");
6079 } elsif (defined $hash &&
6080 $hash =~ /[0-9a-fA-F]{40}/) {
6081 # try to find filename from $hash
6083 # read filtered raw output
6084 open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
6085 $hash_parent_base, $hash_base, "--"
6086 or die_error(500, "Open git-diff-tree failed");
6087 @difftree =
6088 # ':100644 100644 03b21826... 3b93d5e7... M ls-files.c'
6089 # $hash == to_id
6090 grep { /^:[0-7]{6} [0-7]{6} [0-9a-fA-F]{40} $hash/ }
6091 map { chomp; $_ } <$fd>;
6092 close $fd
6093 or die_error(404, "Reading git-diff-tree failed");
6094 @difftree
6095 or die_error(404, "Blob diff not found");
6097 } else {
6098 die_error(400, "Missing one of the blob diff parameters");
6101 if (@difftree > 1) {
6102 die_error(400, "Ambiguous blob diff specification");
6105 %diffinfo = parse_difftree_raw_line($difftree[0]);
6106 $file_parent ||= $diffinfo{'from_file'} || $file_name;
6107 $file_name ||= $diffinfo{'to_file'};
6109 $hash_parent ||= $diffinfo{'from_id'};
6110 $hash ||= $diffinfo{'to_id'};
6112 # non-textual hash id's can be cached
6113 if ($hash_base =~ m/^[0-9a-fA-F]{40}$/ &&
6114 $hash_parent_base =~ m/^[0-9a-fA-F]{40}$/) {
6115 $expires = '+1d';
6118 # open patch output
6119 open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
6120 '-p', ($format eq 'html' ? "--full-index" : ()),
6121 $hash_parent_base, $hash_base,
6122 "--", (defined $file_parent ? $file_parent : ()), $file_name
6123 or die_error(500, "Open git-diff-tree failed");
6126 # old/legacy style URI -- not generated anymore since 1.4.3.
6127 if (!%diffinfo) {
6128 die_error('404 Not Found', "Missing one of the blob diff parameters")
6131 # header
6132 if ($format eq 'html') {
6133 my $formats_nav =
6134 $cgi->a({-href => href(action=>"blobdiff_plain", -replay=>1)},
6135 "raw");
6136 git_header_html(undef, $expires);
6137 if (defined $hash_base && (my %co = parse_commit($hash_base))) {
6138 git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
6139 git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
6140 } else {
6141 print "<div class=\"page_nav\"><br/>$formats_nav<br/></div>\n";
6142 print "<div class=\"title\">$hash vs $hash_parent</div>\n";
6144 if (defined $file_name) {
6145 git_print_page_path($file_name, "blob", $hash_base);
6146 } else {
6147 print "<div class=\"page_path\"></div>\n";
6150 } elsif ($format eq 'plain') {
6151 print $cgi->header(
6152 -type => 'text/plain',
6153 -charset => 'utf-8',
6154 -expires => $expires,
6155 -content_disposition => 'inline; filename="' . "$file_name" . '.patch"');
6157 print "X-Git-Url: " . $cgi->self_url() . "\n\n";
6159 } else {
6160 die_error(400, "Unknown blobdiff format");
6163 # patch
6164 if ($format eq 'html') {
6165 print "<div class=\"page_body\">\n";
6167 git_patchset_body($fd, [ \%diffinfo ], $hash_base, $hash_parent_base);
6168 close $fd;
6170 print "</div>\n"; # class="page_body"
6171 git_footer_html();
6173 } else {
6174 while (my $line = <$fd>) {
6175 $line =~ s!a/($hash|$hash_parent)!'a/'.esc_path($diffinfo{'from_file'})!eg;
6176 $line =~ s!b/($hash|$hash_parent)!'b/'.esc_path($diffinfo{'to_file'})!eg;
6178 print $line;
6180 last if $line =~ m!^\+\+\+!;
6182 local $/ = undef;
6183 print <$fd>;
6184 close $fd;
6188 sub git_blobdiff_plain {
6189 git_blobdiff('plain');
6192 sub git_commitdiff {
6193 my %params = @_;
6194 my $format = $params{-format} || 'html';
6196 my ($patch_max) = gitweb_get_feature('patches');
6197 if ($format eq 'patch') {
6198 die_error(403, "Patch view not allowed") unless $patch_max;
6201 $hash ||= $hash_base || "HEAD";
6202 my %co = parse_commit($hash)
6203 or die_error(404, "Unknown commit object");
6205 # choose format for commitdiff for merge
6206 if (! defined $hash_parent && @{$co{'parents'}} > 1) {
6207 $hash_parent = '--cc';
6209 # we need to prepare $formats_nav before almost any parameter munging
6210 my $formats_nav;
6211 if ($format eq 'html') {
6212 $formats_nav =
6213 $cgi->a({-href => href(action=>"commitdiff_plain", -replay=>1)},
6214 "raw");
6215 if ($patch_max && @{$co{'parents'}} <= 1) {
6216 $formats_nav .= " | " .
6217 $cgi->a({-href => href(action=>"patch", -replay=>1)},
6218 "patch");
6221 if (defined $hash_parent &&
6222 $hash_parent ne '-c' && $hash_parent ne '--cc') {
6223 # commitdiff with two commits given
6224 my $hash_parent_short = $hash_parent;
6225 if ($hash_parent =~ m/^[0-9a-fA-F]{40}$/) {
6226 $hash_parent_short = substr($hash_parent, 0, 7);
6228 $formats_nav .=
6229 ' (from';
6230 for (my $i = 0; $i < @{$co{'parents'}}; $i++) {
6231 if ($co{'parents'}[$i] eq $hash_parent) {
6232 $formats_nav .= ' parent ' . ($i+1);
6233 last;
6236 $formats_nav .= ': ' .
6237 $cgi->a({-href => href(action=>"commitdiff",
6238 hash=>$hash_parent)},
6239 esc_html($hash_parent_short)) .
6240 ')';
6241 } elsif (!$co{'parent'}) {
6242 # --root commitdiff
6243 $formats_nav .= ' (initial)';
6244 } elsif (scalar @{$co{'parents'}} == 1) {
6245 # single parent commit
6246 $formats_nav .=
6247 ' (parent: ' .
6248 $cgi->a({-href => href(action=>"commitdiff",
6249 hash=>$co{'parent'})},
6250 esc_html(substr($co{'parent'}, 0, 7))) .
6251 ')';
6252 } else {
6253 # merge commit
6254 if ($hash_parent eq '--cc') {
6255 $formats_nav .= ' | ' .
6256 $cgi->a({-href => href(action=>"commitdiff",
6257 hash=>$hash, hash_parent=>'-c')},
6258 'combined');
6259 } else { # $hash_parent eq '-c'
6260 $formats_nav .= ' | ' .
6261 $cgi->a({-href => href(action=>"commitdiff",
6262 hash=>$hash, hash_parent=>'--cc')},
6263 'compact');
6265 $formats_nav .=
6266 ' (merge: ' .
6267 join(' ', map {
6268 $cgi->a({-href => href(action=>"commitdiff",
6269 hash=>$_)},
6270 esc_html(substr($_, 0, 7)));
6271 } @{$co{'parents'}} ) .
6272 ')';
6276 my $hash_parent_param = $hash_parent;
6277 if (!defined $hash_parent_param) {
6278 # --cc for multiple parents, --root for parentless
6279 $hash_parent_param =
6280 @{$co{'parents'}} > 1 ? '--cc' : $co{'parent'} || '--root';
6283 # read commitdiff
6284 my $fd;
6285 my @difftree;
6286 if ($format eq 'html') {
6287 open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
6288 "--no-commit-id", "--patch-with-raw", "--full-index",
6289 $hash_parent_param, $hash, "--"
6290 or die_error(500, "Open git-diff-tree failed");
6292 while (my $line = <$fd>) {
6293 chomp $line;
6294 # empty line ends raw part of diff-tree output
6295 last unless $line;
6296 push @difftree, scalar parse_difftree_raw_line($line);
6299 } elsif ($format eq 'plain') {
6300 open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
6301 '-p', $hash_parent_param, $hash, "--"
6302 or die_error(500, "Open git-diff-tree failed");
6303 } elsif ($format eq 'patch') {
6304 # For commit ranges, we limit the output to the number of
6305 # patches specified in the 'patches' feature.
6306 # For single commits, we limit the output to a single patch,
6307 # diverging from the git-format-patch default.
6308 my @commit_spec = ();
6309 if ($hash_parent) {
6310 if ($patch_max > 0) {
6311 push @commit_spec, "-$patch_max";
6313 push @commit_spec, '-n', "$hash_parent..$hash";
6314 } else {
6315 if ($params{-single}) {
6316 push @commit_spec, '-1';
6317 } else {
6318 if ($patch_max > 0) {
6319 push @commit_spec, "-$patch_max";
6321 push @commit_spec, "-n";
6323 push @commit_spec, '--root', $hash;
6325 open $fd, "-|", git_cmd(), "format-patch", @diff_opts,
6326 '--encoding=utf8', '--stdout', @commit_spec
6327 or die_error(500, "Open git-format-patch failed");
6328 } else {
6329 die_error(400, "Unknown commitdiff format");
6332 # non-textual hash id's can be cached
6333 my $expires;
6334 if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
6335 $expires = "+1d";
6338 # write commit message
6339 if ($format eq 'html') {
6340 my $refs = git_get_references();
6341 my $ref = format_ref_marker($refs, $co{'id'});
6343 git_header_html(undef, $expires);
6344 git_print_page_nav('commitdiff','', $hash,$co{'tree'},$hash, $formats_nav);
6345 git_print_header_div('commit', esc_html($co{'title'}) . $ref, $hash);
6346 print "<div class=\"title_text\">\n" .
6347 "<table class=\"object_header\">\n";
6348 git_print_authorship_rows(\%co);
6349 print "</table>".
6350 "</div>\n";
6351 print "<div class=\"page_body\">\n";
6352 if (@{$co{'comment'}} > 1) {
6353 print "<div class=\"log\">\n";
6354 git_print_log($co{'comment'}, -final_empty_line=> 1, -remove_title => 1);
6355 print "</div>\n"; # class="log"
6358 } elsif ($format eq 'plain') {
6359 my $refs = git_get_references("tags");
6360 my $tagname = git_get_rev_name_tags($hash);
6361 my $filename = basename($project) . "-$hash.patch";
6363 print $cgi->header(
6364 -type => 'text/plain',
6365 -charset => 'utf-8',
6366 -expires => $expires,
6367 -content_disposition => 'inline; filename="' . "$filename" . '"');
6368 my %ad = parse_date($co{'author_epoch'}, $co{'author_tz'});
6369 print "From: " . to_utf8($co{'author'}) . "\n";
6370 print "Date: $ad{'rfc2822'} ($ad{'tz_local'})\n";
6371 print "Subject: " . to_utf8($co{'title'}) . "\n";
6373 print "X-Git-Tag: $tagname\n" if $tagname;
6374 print "X-Git-Url: " . $cgi->self_url() . "\n\n";
6376 foreach my $line (@{$co{'comment'}}) {
6377 print to_utf8($line) . "\n";
6379 print "---\n\n";
6380 } elsif ($format eq 'patch') {
6381 my $filename = basename($project) . "-$hash.patch";
6383 print $cgi->header(
6384 -type => 'text/plain',
6385 -charset => 'utf-8',
6386 -expires => $expires,
6387 -content_disposition => 'inline; filename="' . "$filename" . '"');
6390 # write patch
6391 if ($format eq 'html') {
6392 my $use_parents = !defined $hash_parent ||
6393 $hash_parent eq '-c' || $hash_parent eq '--cc';
6394 git_difftree_body(\@difftree, $hash,
6395 $use_parents ? @{$co{'parents'}} : $hash_parent);
6396 print "<br/>\n";
6398 git_patchset_body($fd, \@difftree, $hash,
6399 $use_parents ? @{$co{'parents'}} : $hash_parent);
6400 close $fd;
6401 print "</div>\n"; # class="page_body"
6402 git_footer_html();
6404 } elsif ($format eq 'plain') {
6405 local $/ = undef;
6406 print <$fd>;
6407 close $fd
6408 or print "Reading git-diff-tree failed\n";
6409 } elsif ($format eq 'patch') {
6410 local $/ = undef;
6411 print <$fd>;
6412 close $fd
6413 or print "Reading git-format-patch failed\n";
6417 sub git_commitdiff_plain {
6418 git_commitdiff(-format => 'plain');
6421 # format-patch-style patches
6422 sub git_patch {
6423 git_commitdiff(-format => 'patch', -single => 1);
6426 sub git_patches {
6427 git_commitdiff(-format => 'patch');
6430 sub git_history {
6431 git_log_generic('history', \&git_history_body,
6432 $hash_base, $hash_parent_base,
6433 $file_name, $hash);
6436 sub git_search {
6437 gitweb_check_feature('search') or die_error(403, "Search is disabled");
6438 if (!defined $searchtext) {
6439 die_error(400, "Text field is empty");
6441 if (!defined $hash) {
6442 $hash = git_get_head_hash($project);
6444 my %co = parse_commit($hash);
6445 if (!%co) {
6446 die_error(404, "Unknown commit object");
6448 if (!defined $page) {
6449 $page = 0;
6452 $searchtype ||= 'commit';
6453 if ($searchtype eq 'pickaxe') {
6454 # pickaxe may take all resources of your box and run for several minutes
6455 # with every query - so decide by yourself how public you make this feature
6456 gitweb_check_feature('pickaxe')
6457 or die_error(403, "Pickaxe is disabled");
6459 if ($searchtype eq 'grep') {
6460 gitweb_check_feature('grep')
6461 or die_error(403, "Grep is disabled");
6464 git_header_html();
6466 if ($searchtype eq 'commit' or $searchtype eq 'author' or $searchtype eq 'committer') {
6467 my $greptype;
6468 if ($searchtype eq 'commit') {
6469 $greptype = "--grep=";
6470 } elsif ($searchtype eq 'author') {
6471 $greptype = "--author=";
6472 } elsif ($searchtype eq 'committer') {
6473 $greptype = "--committer=";
6475 $greptype .= $searchtext;
6476 my @commitlist = parse_commits($hash, 101, (100 * $page), undef,
6477 $greptype, '--regexp-ignore-case',
6478 $search_use_regexp ? '--extended-regexp' : '--fixed-strings');
6480 my $paging_nav = '';
6481 if ($page > 0) {
6482 $paging_nav .=
6483 $cgi->a({-href => href(action=>"search", hash=>$hash,
6484 searchtext=>$searchtext,
6485 searchtype=>$searchtype)},
6486 "first");
6487 $paging_nav .= " &sdot; " .
6488 $cgi->a({-href => href(-replay=>1, page=>$page-1),
6489 -accesskey => "p", -title => "Alt-p"}, "prev");
6490 } else {
6491 $paging_nav .= "first";
6492 $paging_nav .= " &sdot; prev";
6494 my $next_link = '';
6495 if ($#commitlist >= 100) {
6496 $next_link =
6497 $cgi->a({-href => href(-replay=>1, page=>$page+1),
6498 -accesskey => "n", -title => "Alt-n"}, "next");
6499 $paging_nav .= " &sdot; $next_link";
6500 } else {
6501 $paging_nav .= " &sdot; next";
6504 if ($#commitlist >= 100) {
6507 git_print_page_nav('','', $hash,$co{'tree'},$hash, $paging_nav);
6508 git_print_header_div('commit', esc_html($co{'title'}), $hash);
6509 git_search_grep_body(\@commitlist, 0, 99, $next_link);
6512 if ($searchtype eq 'pickaxe') {
6513 git_print_page_nav('','', $hash,$co{'tree'},$hash);
6514 git_print_header_div('commit', esc_html($co{'title'}), $hash);
6516 print "<table class=\"pickaxe search\">\n";
6517 my $alternate = 1;
6518 local $/ = "\n";
6519 open my $fd, '-|', git_cmd(), '--no-pager', 'log', @diff_opts,
6520 '--pretty=format:%H', '--no-abbrev', '--raw', "-S$searchtext",
6521 ($search_use_regexp ? '--pickaxe-regex' : ());
6522 undef %co;
6523 my @files;
6524 while (my $line = <$fd>) {
6525 chomp $line;
6526 next unless $line;
6528 my %set = parse_difftree_raw_line($line);
6529 if (defined $set{'commit'}) {
6530 # finish previous commit
6531 if (%co) {
6532 print "</td>\n" .
6533 "<td class=\"link\">" .
6534 $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") .
6535 " | " .
6536 $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree");
6537 print "</td>\n" .
6538 "</tr>\n";
6541 if ($alternate) {
6542 print "<tr class=\"dark\">\n";
6543 } else {
6544 print "<tr class=\"light\">\n";
6546 $alternate ^= 1;
6547 %co = parse_commit($set{'commit'});
6548 my $author = chop_and_escape_str($co{'author_name'}, 15, 5);
6549 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
6550 "<td><i>$author</i></td>\n" .
6551 "<td>" .
6552 $cgi->a({-href => href(action=>"commit", hash=>$co{'id'}),
6553 -class => "list subject"},
6554 chop_and_escape_str($co{'title'}, 50) . "<br/>");
6555 } elsif (defined $set{'to_id'}) {
6556 next if ($set{'to_id'} =~ m/^0{40}$/);
6558 print $cgi->a({-href => href(action=>"blob", hash_base=>$co{'id'},
6559 hash=>$set{'to_id'}, file_name=>$set{'to_file'}),
6560 -class => "list"},
6561 "<span class=\"match\">" . esc_path($set{'file'}) . "</span>") .
6562 "<br/>\n";
6565 close $fd;
6567 # finish last commit (warning: repetition!)
6568 if (%co) {
6569 print "</td>\n" .
6570 "<td class=\"link\">" .
6571 $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") .
6572 " | " .
6573 $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree");
6574 print "</td>\n" .
6575 "</tr>\n";
6578 print "</table>\n";
6581 if ($searchtype eq 'grep') {
6582 git_print_page_nav('','', $hash,$co{'tree'},$hash);
6583 git_print_header_div('commit', esc_html($co{'title'}), $hash);
6585 print "<table class=\"grep_search\">\n";
6586 my $alternate = 1;
6587 my $matches = 0;
6588 local $/ = "\n";
6589 open my $fd, "-|", git_cmd(), 'grep', '-n',
6590 $search_use_regexp ? ('-E', '-i') : '-F',
6591 $searchtext, $co{'tree'};
6592 my $lastfile = '';
6593 while (my $line = <$fd>) {
6594 chomp $line;
6595 my ($file, $lno, $ltext, $binary);
6596 last if ($matches++ > 1000);
6597 if ($line =~ /^Binary file (.+) matches$/) {
6598 $file = $1;
6599 $binary = 1;
6600 } else {
6601 (undef, $file, $lno, $ltext) = split(/:/, $line, 4);
6603 if ($file ne $lastfile) {
6604 $lastfile and print "</td></tr>\n";
6605 if ($alternate++) {
6606 print "<tr class=\"dark\">\n";
6607 } else {
6608 print "<tr class=\"light\">\n";
6610 print "<td class=\"list\">".
6611 $cgi->a({-href => href(action=>"blob", hash=>$co{'hash'},
6612 file_name=>"$file"),
6613 -class => "list"}, esc_path($file));
6614 print "</td><td>\n";
6615 $lastfile = $file;
6617 if ($binary) {
6618 print "<div class=\"binary\">Binary file</div>\n";
6619 } else {
6620 $ltext = untabify($ltext);
6621 if ($ltext =~ m/^(.*)($search_regexp)(.*)$/i) {
6622 $ltext = esc_html($1, -nbsp=>1);
6623 $ltext .= '<span class="match">';
6624 $ltext .= esc_html($2, -nbsp=>1);
6625 $ltext .= '</span>';
6626 $ltext .= esc_html($3, -nbsp=>1);
6627 } else {
6628 $ltext = esc_html($ltext, -nbsp=>1);
6630 print "<div class=\"pre\">" .
6631 $cgi->a({-href => href(action=>"blob", hash=>$co{'hash'},
6632 file_name=>"$file").'#l'.$lno,
6633 -class => "linenr"}, sprintf('%4i', $lno))
6634 . ' ' . $ltext . "</div>\n";
6637 if ($lastfile) {
6638 print "</td></tr>\n";
6639 if ($matches > 1000) {
6640 print "<div class=\"diff nodifferences\">Too many matches, listing trimmed</div>\n";
6642 } else {
6643 print "<div class=\"diff nodifferences\">No matches found</div>\n";
6645 close $fd;
6647 print "</table>\n";
6649 git_footer_html();
6652 sub git_search_help {
6653 git_header_html();
6654 git_print_page_nav('','', $hash,$hash,$hash);
6655 print <<EOT;
6656 <p><strong>Pattern</strong> is by default a normal string that is matched precisely (but without
6657 regard to case, except in the case of pickaxe). However, when you check the <em>re</em> checkbox,
6658 the pattern entered is recognized as the POSIX extended
6659 <a href="http://en.wikipedia.org/wiki/Regular_expression">regular expression</a> (also case
6660 insensitive).</p>
6661 <dl>
6662 <dt><b>commit</b></dt>
6663 <dd>The commit messages and authorship information will be scanned for the given pattern.</dd>
6665 my $have_grep = gitweb_check_feature('grep');
6666 if ($have_grep) {
6667 print <<EOT;
6668 <dt><b>grep</b></dt>
6669 <dd>All files in the currently selected tree (HEAD unless you are explicitly browsing
6670 a different one) are searched for the given pattern. On large trees, this search can take
6671 a while and put some strain on the server, so please use it with some consideration. Note that
6672 due to git-grep peculiarity, currently if regexp mode is turned off, the matches are
6673 case-sensitive.</dd>
6676 print <<EOT;
6677 <dt><b>author</b></dt>
6678 <dd>Name and e-mail of the change author and date of birth of the patch will be scanned for the given pattern.</dd>
6679 <dt><b>committer</b></dt>
6680 <dd>Name and e-mail of the committer and date of commit will be scanned for the given pattern.</dd>
6682 my $have_pickaxe = gitweb_check_feature('pickaxe');
6683 if ($have_pickaxe) {
6684 print <<EOT;
6685 <dt><b>pickaxe</b></dt>
6686 <dd>All commits that caused the string to appear or disappear from any file (changes that
6687 added, removed or "modified" the string) will be listed. This search can take a while and
6688 takes a lot of strain on the server, so please use it wisely. Note that since you may be
6689 interested even in changes just changing the case as well, this search is case sensitive.</dd>
6692 print "</dl>\n";
6693 git_footer_html();
6696 sub git_shortlog {
6697 git_log_generic('shortlog', \&git_shortlog_body,
6698 $hash, $hash_parent);
6701 ## ......................................................................
6702 ## feeds (RSS, Atom; OPML)
6704 sub git_feed {
6705 my $format = shift || 'atom';
6706 my $have_blame = gitweb_check_feature('blame');
6708 # Atom: http://www.atomenabled.org/developers/syndication/
6709 # RSS: http://www.notestips.com/80256B3A007F2692/1/NAMO5P9UPQ
6710 if ($format ne 'rss' && $format ne 'atom') {
6711 die_error(400, "Unknown web feed format");
6714 # log/feed of current (HEAD) branch, log of given branch, history of file/directory
6715 my $head = $hash || 'HEAD';
6716 my @commitlist = parse_commits($head, 150, 0, $file_name);
6718 my %latest_commit;
6719 my %latest_date;
6720 my $content_type = "application/$format+xml";
6721 if (defined $cgi->http('HTTP_ACCEPT') &&
6722 $cgi->Accept('text/xml') > $cgi->Accept($content_type)) {
6723 # browser (feed reader) prefers text/xml
6724 $content_type = 'text/xml';
6726 if (defined($commitlist[0])) {
6727 %latest_commit = %{$commitlist[0]};
6728 my $latest_epoch = $latest_commit{'committer_epoch'};
6729 %latest_date = parse_date($latest_epoch);
6730 my $if_modified = $cgi->http('IF_MODIFIED_SINCE');
6731 if (defined $if_modified) {
6732 my $since;
6733 if (eval { require HTTP::Date; 1; }) {
6734 $since = HTTP::Date::str2time($if_modified);
6735 } elsif (eval { require Time::ParseDate; 1; }) {
6736 $since = Time::ParseDate::parsedate($if_modified, GMT => 1);
6738 if (defined $since && $latest_epoch <= $since) {
6739 print $cgi->header(
6740 -type => $content_type,
6741 -charset => 'utf-8',
6742 -last_modified => $latest_date{'rfc2822'},
6743 -status => '304 Not Modified');
6744 return;
6747 print $cgi->header(
6748 -type => $content_type,
6749 -charset => 'utf-8',
6750 -last_modified => $latest_date{'rfc2822'});
6751 } else {
6752 print $cgi->header(
6753 -type => $content_type,
6754 -charset => 'utf-8');
6757 # Optimization: skip generating the body if client asks only
6758 # for Last-Modified date.
6759 return if ($cgi->request_method() eq 'HEAD');
6761 # header variables
6762 my $title = "$site_name - $project/$action";
6763 my $feed_type = 'log';
6764 if (defined $hash) {
6765 $title .= " - '$hash'";
6766 $feed_type = 'branch log';
6767 if (defined $file_name) {
6768 $title .= " :: $file_name";
6769 $feed_type = 'history';
6771 } elsif (defined $file_name) {
6772 $title .= " - $file_name";
6773 $feed_type = 'history';
6775 $title .= " $feed_type";
6776 my $descr = git_get_project_description($project);
6777 if (defined $descr) {
6778 $descr = esc_html($descr);
6779 } else {
6780 $descr = "$project " .
6781 ($format eq 'rss' ? 'RSS' : 'Atom') .
6782 " feed";
6784 my $owner = git_get_project_owner($project);
6785 $owner = esc_html($owner);
6787 #header
6788 my $alt_url;
6789 if (defined $file_name) {
6790 $alt_url = href(-full=>1, action=>"history", hash=>$hash, file_name=>$file_name);
6791 } elsif (defined $hash) {
6792 $alt_url = href(-full=>1, action=>"log", hash=>$hash);
6793 } else {
6794 $alt_url = href(-full=>1, action=>"summary");
6796 print qq!<?xml version="1.0" encoding="utf-8"?>\n!;
6797 if ($format eq 'rss') {
6798 print <<XML;
6799 <rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
6800 <channel>
6802 print "<title>$title</title>\n" .
6803 "<link>$alt_url</link>\n" .
6804 "<description>$descr</description>\n" .
6805 "<language>en</language>\n" .
6806 # project owner is responsible for 'editorial' content
6807 "<managingEditor>$owner</managingEditor>\n";
6808 if (defined $logo || defined $favicon) {
6809 # prefer the logo to the favicon, since RSS
6810 # doesn't allow both
6811 my $img = esc_url($logo || $favicon);
6812 print "<image>\n" .
6813 "<url>$img</url>\n" .
6814 "<title>$title</title>\n" .
6815 "<link>$alt_url</link>\n" .
6816 "</image>\n";
6818 if (%latest_date) {
6819 print "<pubDate>$latest_date{'rfc2822'}</pubDate>\n";
6820 print "<lastBuildDate>$latest_date{'rfc2822'}</lastBuildDate>\n";
6822 print "<generator>gitweb v.$version/$git_version</generator>\n";
6823 } elsif ($format eq 'atom') {
6824 print <<XML;
6825 <feed xmlns="http://www.w3.org/2005/Atom">
6827 print "<title>$title</title>\n" .
6828 "<subtitle>$descr</subtitle>\n" .
6829 '<link rel="alternate" type="text/html" href="' .
6830 $alt_url . '" />' . "\n" .
6831 '<link rel="self" type="' . $content_type . '" href="' .
6832 $cgi->self_url() . '" />' . "\n" .
6833 "<id>" . href(-full=>1) . "</id>\n" .
6834 # use project owner for feed author
6835 "<author><name>$owner</name></author>\n";
6836 if (defined $favicon) {
6837 print "<icon>" . esc_url($favicon) . "</icon>\n";
6839 if (defined $logo_url) {
6840 # not twice as wide as tall: 72 x 27 pixels
6841 print "<logo>" . esc_url($logo) . "</logo>\n";
6843 if (! %latest_date) {
6844 # dummy date to keep the feed valid until commits trickle in:
6845 print "<updated>1970-01-01T00:00:00Z</updated>\n";
6846 } else {
6847 print "<updated>$latest_date{'iso-8601'}</updated>\n";
6849 print "<generator version='$version/$git_version'>gitweb</generator>\n";
6852 # contents
6853 for (my $i = 0; $i <= $#commitlist; $i++) {
6854 my %co = %{$commitlist[$i]};
6855 my $commit = $co{'id'};
6856 # we read 150, we always show 30 and the ones more recent than 48 hours
6857 if (($i >= 20) && ((time - $co{'author_epoch'}) > 48*60*60)) {
6858 last;
6860 my %cd = parse_date($co{'author_epoch'});
6862 # get list of changed files
6863 open my $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
6864 $co{'parent'} || "--root",
6865 $co{'id'}, "--", (defined $file_name ? $file_name : ())
6866 or next;
6867 my @difftree = map { chomp; $_ } <$fd>;
6868 close $fd
6869 or next;
6871 # print element (entry, item)
6872 my $co_url = href(-full=>1, action=>"commitdiff", hash=>$commit);
6873 if ($format eq 'rss') {
6874 print "<item>\n" .
6875 "<title>" . esc_html($co{'title'}) . "</title>\n" .
6876 "<author>" . esc_html($co{'author'}) . "</author>\n" .
6877 "<pubDate>$cd{'rfc2822'}</pubDate>\n" .
6878 "<guid isPermaLink=\"true\">$co_url</guid>\n" .
6879 "<link>$co_url</link>\n" .
6880 "<description>" . esc_html($co{'title'}) . "</description>\n" .
6881 "<content:encoded>" .
6882 "<![CDATA[\n";
6883 } elsif ($format eq 'atom') {
6884 print "<entry>\n" .
6885 "<title type=\"html\">" . esc_html($co{'title'}) . "</title>\n" .
6886 "<updated>$cd{'iso-8601'}</updated>\n" .
6887 "<author>\n" .
6888 " <name>" . esc_html($co{'author_name'}) . "</name>\n";
6889 if ($co{'author_email'}) {
6890 print " <email>" . esc_html($co{'author_email'}) . "</email>\n";
6892 print "</author>\n" .
6893 # use committer for contributor
6894 "<contributor>\n" .
6895 " <name>" . esc_html($co{'committer_name'}) . "</name>\n";
6896 if ($co{'committer_email'}) {
6897 print " <email>" . esc_html($co{'committer_email'}) . "</email>\n";
6899 print "</contributor>\n" .
6900 "<published>$cd{'iso-8601'}</published>\n" .
6901 "<link rel=\"alternate\" type=\"text/html\" href=\"$co_url\" />\n" .
6902 "<id>$co_url</id>\n" .
6903 "<content type=\"xhtml\" xml:base=\"" . esc_url($my_url) . "\">\n" .
6904 "<div xmlns=\"http://www.w3.org/1999/xhtml\">\n";
6906 my $comment = $co{'comment'};
6907 print "<pre>\n";
6908 foreach my $line (@$comment) {
6909 $line = esc_html($line);
6910 print "$line\n";
6912 print "</pre><ul>\n";
6913 foreach my $difftree_line (@difftree) {
6914 my %difftree = parse_difftree_raw_line($difftree_line);
6915 next if !$difftree{'from_id'};
6917 my $file = $difftree{'file'} || $difftree{'to_file'};
6919 print "<li>" .
6920 "[" .
6921 $cgi->a({-href => href(-full=>1, action=>"blobdiff",
6922 hash=>$difftree{'to_id'}, hash_parent=>$difftree{'from_id'},
6923 hash_base=>$co{'id'}, hash_parent_base=>$co{'parent'},
6924 file_name=>$file, file_parent=>$difftree{'from_file'}),
6925 -title => "diff"}, 'D');
6926 if ($have_blame) {
6927 print $cgi->a({-href => href(-full=>1, action=>"blame",
6928 file_name=>$file, hash_base=>$commit),
6929 -title => "blame"}, 'B');
6931 # if this is not a feed of a file history
6932 if (!defined $file_name || $file_name ne $file) {
6933 print $cgi->a({-href => href(-full=>1, action=>"history",
6934 file_name=>$file, hash=>$commit),
6935 -title => "history"}, 'H');
6937 $file = esc_path($file);
6938 print "] ".
6939 "$file</li>\n";
6941 if ($format eq 'rss') {
6942 print "</ul>]]>\n" .
6943 "</content:encoded>\n" .
6944 "</item>\n";
6945 } elsif ($format eq 'atom') {
6946 print "</ul>\n</div>\n" .
6947 "</content>\n" .
6948 "</entry>\n";
6952 # end of feed
6953 if ($format eq 'rss') {
6954 print "</channel>\n</rss>\n";
6955 } elsif ($format eq 'atom') {
6956 print "</feed>\n";
6960 sub git_rss {
6961 git_feed('rss');
6964 sub git_atom {
6965 git_feed('atom');
6968 sub git_opml {
6969 my @list = git_get_projects_list();
6971 print $cgi->header(
6972 -type => 'text/xml',
6973 -charset => 'utf-8',
6974 -content_disposition => 'inline; filename="opml.xml"');
6976 print <<XML;
6977 <?xml version="1.0" encoding="utf-8"?>
6978 <opml version="1.0">
6979 <head>
6980 <title>$site_name OPML Export</title>
6981 </head>
6982 <body>
6983 <outline text="git RSS feeds">
6986 foreach my $pr (@list) {
6987 my %proj = %$pr;
6988 my $head = git_get_head_hash($proj{'path'});
6989 if (!defined $head) {
6990 next;
6992 $git_dir = "$projectroot/$proj{'path'}";
6993 my %co = parse_commit($head);
6994 if (!%co) {
6995 next;
6998 my $path = esc_html(chop_str($proj{'path'}, 25, 5));
6999 my $rss = href('project' => $proj{'path'}, 'action' => 'rss', -full => 1);
7000 my $html = href('project' => $proj{'path'}, 'action' => 'summary', -full => 1);
7001 print "<outline type=\"rss\" text=\"$path\" title=\"$path\" xmlUrl=\"$rss\" htmlUrl=\"$html\"/>\n";
7003 print <<XML;
7004 </outline>
7005 </body>
7006 </opml>