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