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