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