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