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