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