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