Git 1.6.5.9
[git/ydirson.git] / gitweb / gitweb.perl
blob620b5bdbbe238a79f3d9d8fcf852c8d0f3609818
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 # quote unsafe characters in HTML attributes
1101 sub esc_attr {
1103 # for XHTML conformance escaping '"' to '&quot;' is not enough
1104 return esc_html(@_);
1107 # replace invalid utf8 character with SUBSTITUTION sequence
1108 sub esc_html {
1109 my $str = shift;
1110 my %opts = @_;
1112 $str = to_utf8($str);
1113 $str = $cgi->escapeHTML($str);
1114 if ($opts{'-nbsp'}) {
1115 $str =~ s/ /&nbsp;/g;
1117 $str =~ s|([[:cntrl:]])|(($1 ne "\t") ? quot_cec($1) : $1)|eg;
1118 return $str;
1121 # quote control characters and escape filename to HTML
1122 sub esc_path {
1123 my $str = shift;
1124 my %opts = @_;
1126 $str = to_utf8($str);
1127 $str = $cgi->escapeHTML($str);
1128 if ($opts{'-nbsp'}) {
1129 $str =~ s/ /&nbsp;/g;
1131 $str =~ s|([[:cntrl:]])|quot_cec($1)|eg;
1132 return $str;
1135 # Make control characters "printable", using character escape codes (CEC)
1136 sub quot_cec {
1137 my $cntrl = shift;
1138 my %opts = @_;
1139 my %es = ( # character escape codes, aka escape sequences
1140 "\t" => '\t', # tab (HT)
1141 "\n" => '\n', # line feed (LF)
1142 "\r" => '\r', # carrige return (CR)
1143 "\f" => '\f', # form feed (FF)
1144 "\b" => '\b', # backspace (BS)
1145 "\a" => '\a', # alarm (bell) (BEL)
1146 "\e" => '\e', # escape (ESC)
1147 "\013" => '\v', # vertical tab (VT)
1148 "\000" => '\0', # nul character (NUL)
1150 my $chr = ( (exists $es{$cntrl})
1151 ? $es{$cntrl}
1152 : sprintf('\%2x', ord($cntrl)) );
1153 if ($opts{-nohtml}) {
1154 return $chr;
1155 } else {
1156 return "<span class=\"cntrl\">$chr</span>";
1160 # Alternatively use unicode control pictures codepoints,
1161 # Unicode "printable representation" (PR)
1162 sub quot_upr {
1163 my $cntrl = shift;
1164 my %opts = @_;
1166 my $chr = sprintf('&#%04d;', 0x2400+ord($cntrl));
1167 if ($opts{-nohtml}) {
1168 return $chr;
1169 } else {
1170 return "<span class=\"cntrl\">$chr</span>";
1174 # git may return quoted and escaped filenames
1175 sub unquote {
1176 my $str = shift;
1178 sub unq {
1179 my $seq = shift;
1180 my %es = ( # character escape codes, aka escape sequences
1181 't' => "\t", # tab (HT, TAB)
1182 'n' => "\n", # newline (NL)
1183 'r' => "\r", # return (CR)
1184 'f' => "\f", # form feed (FF)
1185 'b' => "\b", # backspace (BS)
1186 'a' => "\a", # alarm (bell) (BEL)
1187 'e' => "\e", # escape (ESC)
1188 'v' => "\013", # vertical tab (VT)
1191 if ($seq =~ m/^[0-7]{1,3}$/) {
1192 # octal char sequence
1193 return chr(oct($seq));
1194 } elsif (exists $es{$seq}) {
1195 # C escape sequence, aka character escape code
1196 return $es{$seq};
1198 # quoted ordinary character
1199 return $seq;
1202 if ($str =~ m/^"(.*)"$/) {
1203 # needs unquoting
1204 $str = $1;
1205 $str =~ s/\\([^0-7]|[0-7]{1,3})/unq($1)/eg;
1207 return $str;
1210 # escape tabs (convert tabs to spaces)
1211 sub untabify {
1212 my $line = shift;
1214 while ((my $pos = index($line, "\t")) != -1) {
1215 if (my $count = (8 - ($pos % 8))) {
1216 my $spaces = ' ' x $count;
1217 $line =~ s/\t/$spaces/;
1221 return $line;
1224 sub project_in_list {
1225 my $project = shift;
1226 my @list = git_get_projects_list();
1227 return @list && scalar(grep { $_->{'path'} eq $project } @list);
1230 ## ----------------------------------------------------------------------
1231 ## HTML aware string manipulation
1233 # Try to chop given string on a word boundary between position
1234 # $len and $len+$add_len. If there is no word boundary there,
1235 # chop at $len+$add_len. Do not chop if chopped part plus ellipsis
1236 # (marking chopped part) would be longer than given string.
1237 sub chop_str {
1238 my $str = shift;
1239 my $len = shift;
1240 my $add_len = shift || 10;
1241 my $where = shift || 'right'; # 'left' | 'center' | 'right'
1243 # Make sure perl knows it is utf8 encoded so we don't
1244 # cut in the middle of a utf8 multibyte char.
1245 $str = to_utf8($str);
1247 # allow only $len chars, but don't cut a word if it would fit in $add_len
1248 # if it doesn't fit, cut it if it's still longer than the dots we would add
1249 # remove chopped character entities entirely
1251 # when chopping in the middle, distribute $len into left and right part
1252 # return early if chopping wouldn't make string shorter
1253 if ($where eq 'center') {
1254 return $str if ($len + 5 >= length($str)); # filler is length 5
1255 $len = int($len/2);
1256 } else {
1257 return $str if ($len + 4 >= length($str)); # filler is length 4
1260 # regexps: ending and beginning with word part up to $add_len
1261 my $endre = qr/.{$len}\w{0,$add_len}/;
1262 my $begre = qr/\w{0,$add_len}.{$len}/;
1264 if ($where eq 'left') {
1265 $str =~ m/^(.*?)($begre)$/;
1266 my ($lead, $body) = ($1, $2);
1267 if (length($lead) > 4) {
1268 $body =~ s/^[^;]*;// if ($lead =~ m/&[^;]*$/);
1269 $lead = " ...";
1271 return "$lead$body";
1273 } elsif ($where eq 'center') {
1274 $str =~ m/^($endre)(.*)$/;
1275 my ($left, $str) = ($1, $2);
1276 $str =~ m/^(.*?)($begre)$/;
1277 my ($mid, $right) = ($1, $2);
1278 if (length($mid) > 5) {
1279 $left =~ s/&[^;]*$//;
1280 $right =~ s/^[^;]*;// if ($mid =~ m/&[^;]*$/);
1281 $mid = " ... ";
1283 return "$left$mid$right";
1285 } else {
1286 $str =~ m/^($endre)(.*)$/;
1287 my $body = $1;
1288 my $tail = $2;
1289 if (length($tail) > 4) {
1290 $body =~ s/&[^;]*$//;
1291 $tail = "... ";
1293 return "$body$tail";
1297 # takes the same arguments as chop_str, but also wraps a <span> around the
1298 # result with a title attribute if it does get chopped. Additionally, the
1299 # string is HTML-escaped.
1300 sub chop_and_escape_str {
1301 my ($str) = @_;
1303 my $chopped = chop_str(@_);
1304 if ($chopped eq $str) {
1305 return esc_html($chopped);
1306 } else {
1307 $str =~ s/[[:cntrl:]]/?/g;
1308 return $cgi->span({-title=>$str}, esc_html($chopped));
1312 ## ----------------------------------------------------------------------
1313 ## functions returning short strings
1315 # CSS class for given age value (in seconds)
1316 sub age_class {
1317 my $age = shift;
1319 if (!defined $age) {
1320 return "noage";
1321 } elsif ($age < 60*60*2) {
1322 return "age0";
1323 } elsif ($age < 60*60*24*2) {
1324 return "age1";
1325 } else {
1326 return "age2";
1330 # convert age in seconds to "nn units ago" string
1331 sub age_string {
1332 my $age = shift;
1333 my $age_str;
1335 if ($age > 60*60*24*365*2) {
1336 $age_str = (int $age/60/60/24/365);
1337 $age_str .= " years ago";
1338 } elsif ($age > 60*60*24*(365/12)*2) {
1339 $age_str = int $age/60/60/24/(365/12);
1340 $age_str .= " months ago";
1341 } elsif ($age > 60*60*24*7*2) {
1342 $age_str = int $age/60/60/24/7;
1343 $age_str .= " weeks ago";
1344 } elsif ($age > 60*60*24*2) {
1345 $age_str = int $age/60/60/24;
1346 $age_str .= " days ago";
1347 } elsif ($age > 60*60*2) {
1348 $age_str = int $age/60/60;
1349 $age_str .= " hours ago";
1350 } elsif ($age > 60*2) {
1351 $age_str = int $age/60;
1352 $age_str .= " min ago";
1353 } elsif ($age > 2) {
1354 $age_str = int $age;
1355 $age_str .= " sec ago";
1356 } else {
1357 $age_str .= " right now";
1359 return $age_str;
1362 use constant {
1363 S_IFINVALID => 0030000,
1364 S_IFGITLINK => 0160000,
1367 # submodule/subproject, a commit object reference
1368 sub S_ISGITLINK {
1369 my $mode = shift;
1371 return (($mode & S_IFMT) == S_IFGITLINK)
1374 # convert file mode in octal to symbolic file mode string
1375 sub mode_str {
1376 my $mode = oct shift;
1378 if (S_ISGITLINK($mode)) {
1379 return 'm---------';
1380 } elsif (S_ISDIR($mode & S_IFMT)) {
1381 return 'drwxr-xr-x';
1382 } elsif (S_ISLNK($mode)) {
1383 return 'lrwxrwxrwx';
1384 } elsif (S_ISREG($mode)) {
1385 # git cares only about the executable bit
1386 if ($mode & S_IXUSR) {
1387 return '-rwxr-xr-x';
1388 } else {
1389 return '-rw-r--r--';
1391 } else {
1392 return '----------';
1396 # convert file mode in octal to file type string
1397 sub file_type {
1398 my $mode = shift;
1400 if ($mode !~ m/^[0-7]+$/) {
1401 return $mode;
1402 } else {
1403 $mode = oct $mode;
1406 if (S_ISGITLINK($mode)) {
1407 return "submodule";
1408 } elsif (S_ISDIR($mode & S_IFMT)) {
1409 return "directory";
1410 } elsif (S_ISLNK($mode)) {
1411 return "symlink";
1412 } elsif (S_ISREG($mode)) {
1413 return "file";
1414 } else {
1415 return "unknown";
1419 # convert file mode in octal to file type description string
1420 sub file_type_long {
1421 my $mode = shift;
1423 if ($mode !~ m/^[0-7]+$/) {
1424 return $mode;
1425 } else {
1426 $mode = oct $mode;
1429 if (S_ISGITLINK($mode)) {
1430 return "submodule";
1431 } elsif (S_ISDIR($mode & S_IFMT)) {
1432 return "directory";
1433 } elsif (S_ISLNK($mode)) {
1434 return "symlink";
1435 } elsif (S_ISREG($mode)) {
1436 if ($mode & S_IXUSR) {
1437 return "executable";
1438 } else {
1439 return "file";
1441 } else {
1442 return "unknown";
1447 ## ----------------------------------------------------------------------
1448 ## functions returning short HTML fragments, or transforming HTML fragments
1449 ## which don't belong to other sections
1451 # format line of commit message.
1452 sub format_log_line_html {
1453 my $line = shift;
1455 $line = esc_html($line, -nbsp=>1);
1456 $line =~ s{\b([0-9a-fA-F]{8,40})\b}{
1457 $cgi->a({-href => href(action=>"object", hash=>$1),
1458 -class => "text"}, $1);
1459 }eg;
1461 return $line;
1464 # format marker of refs pointing to given object
1466 # the destination action is chosen based on object type and current context:
1467 # - for annotated tags, we choose the tag view unless it's the current view
1468 # already, in which case we go to shortlog view
1469 # - for other refs, we keep the current view if we're in history, shortlog or
1470 # log view, and select shortlog otherwise
1471 sub format_ref_marker {
1472 my ($refs, $id) = @_;
1473 my $markers = '';
1475 if (defined $refs->{$id}) {
1476 foreach my $ref (@{$refs->{$id}}) {
1477 # this code exploits the fact that non-lightweight tags are the
1478 # only indirect objects, and that they are the only objects for which
1479 # we want to use tag instead of shortlog as action
1480 my ($type, $name) = qw();
1481 my $indirect = ($ref =~ s/\^\{\}$//);
1482 # e.g. tags/v2.6.11 or heads/next
1483 if ($ref =~ m!^(.*?)s?/(.*)$!) {
1484 $type = $1;
1485 $name = $2;
1486 } else {
1487 $type = "ref";
1488 $name = $ref;
1491 my $class = $type;
1492 $class .= " indirect" if $indirect;
1494 my $dest_action = "shortlog";
1496 if ($indirect) {
1497 $dest_action = "tag" unless $action eq "tag";
1498 } elsif ($action =~ /^(history|(short)?log)$/) {
1499 $dest_action = $action;
1502 my $dest = "";
1503 $dest .= "refs/" unless $ref =~ m!^refs/!;
1504 $dest .= $ref;
1506 my $link = $cgi->a({
1507 -href => href(
1508 action=>$dest_action,
1509 hash=>$dest
1510 )}, $name);
1512 $markers .= " <span class=\"".esc_attr($class)."\" title=\"".esc_attr($ref)."\">" .
1513 $link . "</span>";
1517 if ($markers) {
1518 return ' <span class="refs">'. $markers . '</span>';
1519 } else {
1520 return "";
1524 # format, perhaps shortened and with markers, title line
1525 sub format_subject_html {
1526 my ($long, $short, $href, $extra) = @_;
1527 $extra = '' unless defined($extra);
1529 if (length($short) < length($long)) {
1530 $long =~ s/[[:cntrl:]]/?/g;
1531 return $cgi->a({-href => $href, -class => "list subject",
1532 -title => to_utf8($long)},
1533 esc_html($short)) . $extra;
1534 } else {
1535 return $cgi->a({-href => $href, -class => "list subject"},
1536 esc_html($long)) . $extra;
1540 # Rather than recomputing the url for an email multiple times, we cache it
1541 # after the first hit. This gives a visible benefit in views where the avatar
1542 # for the same email is used repeatedly (e.g. shortlog).
1543 # The cache is shared by all avatar engines (currently gravatar only), which
1544 # are free to use it as preferred. Since only one avatar engine is used for any
1545 # given page, there's no risk for cache conflicts.
1546 our %avatar_cache = ();
1548 # Compute the picon url for a given email, by using the picon search service over at
1549 # http://www.cs.indiana.edu/picons/search.html
1550 sub picon_url {
1551 my $email = lc shift;
1552 if (!$avatar_cache{$email}) {
1553 my ($user, $domain) = split('@', $email);
1554 $avatar_cache{$email} =
1555 "http://www.cs.indiana.edu/cgi-pub/kinzler/piconsearch.cgi/" .
1556 "$domain/$user/" .
1557 "users+domains+unknown/up/single";
1559 return $avatar_cache{$email};
1562 # Compute the gravatar url for a given email, if it's not in the cache already.
1563 # Gravatar stores only the part of the URL before the size, since that's the
1564 # one computationally more expensive. This also allows reuse of the cache for
1565 # different sizes (for this particular engine).
1566 sub gravatar_url {
1567 my $email = lc shift;
1568 my $size = shift;
1569 $avatar_cache{$email} ||=
1570 "http://www.gravatar.com/avatar/" .
1571 Digest::MD5::md5_hex($email) . "?s=";
1572 return $avatar_cache{$email} . $size;
1575 # Insert an avatar for the given $email at the given $size if the feature
1576 # is enabled.
1577 sub git_get_avatar {
1578 my ($email, %opts) = @_;
1579 my $pre_white = ($opts{-pad_before} ? "&nbsp;" : "");
1580 my $post_white = ($opts{-pad_after} ? "&nbsp;" : "");
1581 $opts{-size} ||= 'default';
1582 my $size = $avatar_size{$opts{-size}} || $avatar_size{'default'};
1583 my $url = "";
1584 if ($git_avatar eq 'gravatar') {
1585 $url = gravatar_url($email, $size);
1586 } elsif ($git_avatar eq 'picon') {
1587 $url = picon_url($email);
1589 # Other providers can be added by extending the if chain, defining $url
1590 # as needed. If no variant puts something in $url, we assume avatars
1591 # are completely disabled/unavailable.
1592 if ($url) {
1593 return $pre_white .
1594 "<img width=\"$size\" " .
1595 "class=\"avatar\" " .
1596 "src=\"".esc_url($url)."\" " .
1597 "alt=\"\" " .
1598 "/>" . $post_white;
1599 } else {
1600 return "";
1604 # format the author name of the given commit with the given tag
1605 # the author name is chopped and escaped according to the other
1606 # optional parameters (see chop_str).
1607 sub format_author_html {
1608 my $tag = shift;
1609 my $co = shift;
1610 my $author = chop_and_escape_str($co->{'author_name'}, @_);
1611 return "<$tag class=\"author\">" .
1612 git_get_avatar($co->{'author_email'}, -pad_after => 1) .
1613 $author . "</$tag>";
1616 # format git diff header line, i.e. "diff --(git|combined|cc) ..."
1617 sub format_git_diff_header_line {
1618 my $line = shift;
1619 my $diffinfo = shift;
1620 my ($from, $to) = @_;
1622 if ($diffinfo->{'nparents'}) {
1623 # combined diff
1624 $line =~ s!^(diff (.*?) )"?.*$!$1!;
1625 if ($to->{'href'}) {
1626 $line .= $cgi->a({-href => $to->{'href'}, -class => "path"},
1627 esc_path($to->{'file'}));
1628 } else { # file was deleted (no href)
1629 $line .= esc_path($to->{'file'});
1631 } else {
1632 # "ordinary" diff
1633 $line =~ s!^(diff (.*?) )"?a/.*$!$1!;
1634 if ($from->{'href'}) {
1635 $line .= $cgi->a({-href => $from->{'href'}, -class => "path"},
1636 'a/' . esc_path($from->{'file'}));
1637 } else { # file was added (no href)
1638 $line .= 'a/' . esc_path($from->{'file'});
1640 $line .= ' ';
1641 if ($to->{'href'}) {
1642 $line .= $cgi->a({-href => $to->{'href'}, -class => "path"},
1643 'b/' . esc_path($to->{'file'}));
1644 } else { # file was deleted
1645 $line .= 'b/' . esc_path($to->{'file'});
1649 return "<div class=\"diff header\">$line</div>\n";
1652 # format extended diff header line, before patch itself
1653 sub format_extended_diff_header_line {
1654 my $line = shift;
1655 my $diffinfo = shift;
1656 my ($from, $to) = @_;
1658 # match <path>
1659 if ($line =~ s!^((copy|rename) from ).*$!$1! && $from->{'href'}) {
1660 $line .= $cgi->a({-href=>$from->{'href'}, -class=>"path"},
1661 esc_path($from->{'file'}));
1663 if ($line =~ s!^((copy|rename) to ).*$!$1! && $to->{'href'}) {
1664 $line .= $cgi->a({-href=>$to->{'href'}, -class=>"path"},
1665 esc_path($to->{'file'}));
1667 # match single <mode>
1668 if ($line =~ m/\s(\d{6})$/) {
1669 $line .= '<span class="info"> (' .
1670 file_type_long($1) .
1671 ')</span>';
1673 # match <hash>
1674 if ($line =~ m/^index [0-9a-fA-F]{40},[0-9a-fA-F]{40}/) {
1675 # can match only for combined diff
1676 $line = 'index ';
1677 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
1678 if ($from->{'href'}[$i]) {
1679 $line .= $cgi->a({-href=>$from->{'href'}[$i],
1680 -class=>"hash"},
1681 substr($diffinfo->{'from_id'}[$i],0,7));
1682 } else {
1683 $line .= '0' x 7;
1685 # separator
1686 $line .= ',' if ($i < $diffinfo->{'nparents'} - 1);
1688 $line .= '..';
1689 if ($to->{'href'}) {
1690 $line .= $cgi->a({-href=>$to->{'href'}, -class=>"hash"},
1691 substr($diffinfo->{'to_id'},0,7));
1692 } else {
1693 $line .= '0' x 7;
1696 } elsif ($line =~ m/^index [0-9a-fA-F]{40}..[0-9a-fA-F]{40}/) {
1697 # can match only for ordinary diff
1698 my ($from_link, $to_link);
1699 if ($from->{'href'}) {
1700 $from_link = $cgi->a({-href=>$from->{'href'}, -class=>"hash"},
1701 substr($diffinfo->{'from_id'},0,7));
1702 } else {
1703 $from_link = '0' x 7;
1705 if ($to->{'href'}) {
1706 $to_link = $cgi->a({-href=>$to->{'href'}, -class=>"hash"},
1707 substr($diffinfo->{'to_id'},0,7));
1708 } else {
1709 $to_link = '0' x 7;
1711 my ($from_id, $to_id) = ($diffinfo->{'from_id'}, $diffinfo->{'to_id'});
1712 $line =~ s!$from_id\.\.$to_id!$from_link..$to_link!;
1715 return $line . "<br/>\n";
1718 # format from-file/to-file diff header
1719 sub format_diff_from_to_header {
1720 my ($from_line, $to_line, $diffinfo, $from, $to, @parents) = @_;
1721 my $line;
1722 my $result = '';
1724 $line = $from_line;
1725 #assert($line =~ m/^---/) if DEBUG;
1726 # no extra formatting for "^--- /dev/null"
1727 if (! $diffinfo->{'nparents'}) {
1728 # ordinary (single parent) diff
1729 if ($line =~ m!^--- "?a/!) {
1730 if ($from->{'href'}) {
1731 $line = '--- a/' .
1732 $cgi->a({-href=>$from->{'href'}, -class=>"path"},
1733 esc_path($from->{'file'}));
1734 } else {
1735 $line = '--- a/' .
1736 esc_path($from->{'file'});
1739 $result .= qq!<div class="diff from_file">$line</div>\n!;
1741 } else {
1742 # combined diff (merge commit)
1743 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
1744 if ($from->{'href'}[$i]) {
1745 $line = '--- ' .
1746 $cgi->a({-href=>href(action=>"blobdiff",
1747 hash_parent=>$diffinfo->{'from_id'}[$i],
1748 hash_parent_base=>$parents[$i],
1749 file_parent=>$from->{'file'}[$i],
1750 hash=>$diffinfo->{'to_id'},
1751 hash_base=>$hash,
1752 file_name=>$to->{'file'}),
1753 -class=>"path",
1754 -title=>"diff" . ($i+1)},
1755 $i+1) .
1756 '/' .
1757 $cgi->a({-href=>$from->{'href'}[$i], -class=>"path"},
1758 esc_path($from->{'file'}[$i]));
1759 } else {
1760 $line = '--- /dev/null';
1762 $result .= qq!<div class="diff from_file">$line</div>\n!;
1766 $line = $to_line;
1767 #assert($line =~ m/^\+\+\+/) if DEBUG;
1768 # no extra formatting for "^+++ /dev/null"
1769 if ($line =~ m!^\+\+\+ "?b/!) {
1770 if ($to->{'href'}) {
1771 $line = '+++ b/' .
1772 $cgi->a({-href=>$to->{'href'}, -class=>"path"},
1773 esc_path($to->{'file'}));
1774 } else {
1775 $line = '+++ b/' .
1776 esc_path($to->{'file'});
1779 $result .= qq!<div class="diff to_file">$line</div>\n!;
1781 return $result;
1784 # create note for patch simplified by combined diff
1785 sub format_diff_cc_simplified {
1786 my ($diffinfo, @parents) = @_;
1787 my $result = '';
1789 $result .= "<div class=\"diff header\">" .
1790 "diff --cc ";
1791 if (!is_deleted($diffinfo)) {
1792 $result .= $cgi->a({-href => href(action=>"blob",
1793 hash_base=>$hash,
1794 hash=>$diffinfo->{'to_id'},
1795 file_name=>$diffinfo->{'to_file'}),
1796 -class => "path"},
1797 esc_path($diffinfo->{'to_file'}));
1798 } else {
1799 $result .= esc_path($diffinfo->{'to_file'});
1801 $result .= "</div>\n" . # class="diff header"
1802 "<div class=\"diff nodifferences\">" .
1803 "Simple merge" .
1804 "</div>\n"; # class="diff nodifferences"
1806 return $result;
1809 # format patch (diff) line (not to be used for diff headers)
1810 sub format_diff_line {
1811 my $line = shift;
1812 my ($from, $to) = @_;
1813 my $diff_class = "";
1815 chomp $line;
1817 if ($from && $to && ref($from->{'href'}) eq "ARRAY") {
1818 # combined diff
1819 my $prefix = substr($line, 0, scalar @{$from->{'href'}});
1820 if ($line =~ m/^\@{3}/) {
1821 $diff_class = " chunk_header";
1822 } elsif ($line =~ m/^\\/) {
1823 $diff_class = " incomplete";
1824 } elsif ($prefix =~ tr/+/+/) {
1825 $diff_class = " add";
1826 } elsif ($prefix =~ tr/-/-/) {
1827 $diff_class = " rem";
1829 } else {
1830 # assume ordinary diff
1831 my $char = substr($line, 0, 1);
1832 if ($char eq '+') {
1833 $diff_class = " add";
1834 } elsif ($char eq '-') {
1835 $diff_class = " rem";
1836 } elsif ($char eq '@') {
1837 $diff_class = " chunk_header";
1838 } elsif ($char eq "\\") {
1839 $diff_class = " incomplete";
1842 $line = untabify($line);
1843 if ($from && $to && $line =~ m/^\@{2} /) {
1844 my ($from_text, $from_start, $from_lines, $to_text, $to_start, $to_lines, $section) =
1845 $line =~ m/^\@{2} (-(\d+)(?:,(\d+))?) (\+(\d+)(?:,(\d+))?) \@{2}(.*)$/;
1847 $from_lines = 0 unless defined $from_lines;
1848 $to_lines = 0 unless defined $to_lines;
1850 if ($from->{'href'}) {
1851 $from_text = $cgi->a({-href=>"$from->{'href'}#l$from_start",
1852 -class=>"list"}, $from_text);
1854 if ($to->{'href'}) {
1855 $to_text = $cgi->a({-href=>"$to->{'href'}#l$to_start",
1856 -class=>"list"}, $to_text);
1858 $line = "<span class=\"chunk_info\">@@ $from_text $to_text @@</span>" .
1859 "<span class=\"section\">" . esc_html($section, -nbsp=>1) . "</span>";
1860 return "<div class=\"diff$diff_class\">$line</div>\n";
1861 } elsif ($from && $to && $line =~ m/^\@{3}/) {
1862 my ($prefix, $ranges, $section) = $line =~ m/^(\@+) (.*?) \@+(.*)$/;
1863 my (@from_text, @from_start, @from_nlines, $to_text, $to_start, $to_nlines);
1865 @from_text = split(' ', $ranges);
1866 for (my $i = 0; $i < @from_text; ++$i) {
1867 ($from_start[$i], $from_nlines[$i]) =
1868 (split(',', substr($from_text[$i], 1)), 0);
1871 $to_text = pop @from_text;
1872 $to_start = pop @from_start;
1873 $to_nlines = pop @from_nlines;
1875 $line = "<span class=\"chunk_info\">$prefix ";
1876 for (my $i = 0; $i < @from_text; ++$i) {
1877 if ($from->{'href'}[$i]) {
1878 $line .= $cgi->a({-href=>"$from->{'href'}[$i]#l$from_start[$i]",
1879 -class=>"list"}, $from_text[$i]);
1880 } else {
1881 $line .= $from_text[$i];
1883 $line .= " ";
1885 if ($to->{'href'}) {
1886 $line .= $cgi->a({-href=>"$to->{'href'}#l$to_start",
1887 -class=>"list"}, $to_text);
1888 } else {
1889 $line .= $to_text;
1891 $line .= " $prefix</span>" .
1892 "<span class=\"section\">" . esc_html($section, -nbsp=>1) . "</span>";
1893 return "<div class=\"diff$diff_class\">$line</div>\n";
1895 return "<div class=\"diff$diff_class\">" . esc_html($line, -nbsp=>1) . "</div>\n";
1898 # Generates undef or something like "_snapshot_" or "snapshot (_tbz2_ _zip_)",
1899 # linked. Pass the hash of the tree/commit to snapshot.
1900 sub format_snapshot_links {
1901 my ($hash) = @_;
1902 my $num_fmts = @snapshot_fmts;
1903 if ($num_fmts > 1) {
1904 # A parenthesized list of links bearing format names.
1905 # e.g. "snapshot (_tar.gz_ _zip_)"
1906 return "snapshot (" . join(' ', map
1907 $cgi->a({
1908 -href => href(
1909 action=>"snapshot",
1910 hash=>$hash,
1911 snapshot_format=>$_
1913 }, $known_snapshot_formats{$_}{'display'})
1914 , @snapshot_fmts) . ")";
1915 } elsif ($num_fmts == 1) {
1916 # A single "snapshot" link whose tooltip bears the format name.
1917 # i.e. "_snapshot_"
1918 my ($fmt) = @snapshot_fmts;
1919 return
1920 $cgi->a({
1921 -href => href(
1922 action=>"snapshot",
1923 hash=>$hash,
1924 snapshot_format=>$fmt
1926 -title => "in format: $known_snapshot_formats{$fmt}{'display'}"
1927 }, "snapshot");
1928 } else { # $num_fmts == 0
1929 return undef;
1933 ## ......................................................................
1934 ## functions returning values to be passed, perhaps after some
1935 ## transformation, to other functions; e.g. returning arguments to href()
1937 # returns hash to be passed to href to generate gitweb URL
1938 # in -title key it returns description of link
1939 sub get_feed_info {
1940 my $format = shift || 'Atom';
1941 my %res = (action => lc($format));
1943 # feed links are possible only for project views
1944 return unless (defined $project);
1945 # some views should link to OPML, or to generic project feed,
1946 # or don't have specific feed yet (so they should use generic)
1947 return if ($action =~ /^(?:tags|heads|forks|tag|search)$/x);
1949 my $branch;
1950 # branches refs uses 'refs/heads/' prefix (fullname) to differentiate
1951 # from tag links; this also makes possible to detect branch links
1952 if ((defined $hash_base && $hash_base =~ m!^refs/heads/(.*)$!) ||
1953 (defined $hash && $hash =~ m!^refs/heads/(.*)$!)) {
1954 $branch = $1;
1956 # find log type for feed description (title)
1957 my $type = 'log';
1958 if (defined $file_name) {
1959 $type = "history of $file_name";
1960 $type .= "/" if ($action eq 'tree');
1961 $type .= " on '$branch'" if (defined $branch);
1962 } else {
1963 $type = "log of $branch" if (defined $branch);
1966 $res{-title} = $type;
1967 $res{'hash'} = (defined $branch ? "refs/heads/$branch" : undef);
1968 $res{'file_name'} = $file_name;
1970 return %res;
1973 ## ----------------------------------------------------------------------
1974 ## git utility subroutines, invoking git commands
1976 # returns path to the core git executable and the --git-dir parameter as list
1977 sub git_cmd {
1978 return $GIT, '--git-dir='.$git_dir;
1981 # quote the given arguments for passing them to the shell
1982 # quote_command("command", "arg 1", "arg with ' and ! characters")
1983 # => "'command' 'arg 1' 'arg with '\'' and '\!' characters'"
1984 # Try to avoid using this function wherever possible.
1985 sub quote_command {
1986 return join(' ',
1987 map { my $a = $_; $a =~ s/(['!])/'\\$1'/g; "'$a'" } @_ );
1990 # get HEAD ref of given project as hash
1991 sub git_get_head_hash {
1992 my $project = shift;
1993 my $o_git_dir = $git_dir;
1994 my $retval = undef;
1995 $git_dir = "$projectroot/$project";
1996 if (open my $fd, "-|", git_cmd(), "rev-parse", "--verify", "HEAD") {
1997 my $head = <$fd>;
1998 close $fd;
1999 if (defined $head && $head =~ /^([0-9a-fA-F]{40})$/) {
2000 $retval = $1;
2003 if (defined $o_git_dir) {
2004 $git_dir = $o_git_dir;
2006 return $retval;
2009 # get type of given object
2010 sub git_get_type {
2011 my $hash = shift;
2013 open my $fd, "-|", git_cmd(), "cat-file", '-t', $hash or return;
2014 my $type = <$fd>;
2015 close $fd or return;
2016 chomp $type;
2017 return $type;
2020 # repository configuration
2021 our $config_file = '';
2022 our %config;
2024 # store multiple values for single key as anonymous array reference
2025 # single values stored directly in the hash, not as [ <value> ]
2026 sub hash_set_multi {
2027 my ($hash, $key, $value) = @_;
2029 if (!exists $hash->{$key}) {
2030 $hash->{$key} = $value;
2031 } elsif (!ref $hash->{$key}) {
2032 $hash->{$key} = [ $hash->{$key}, $value ];
2033 } else {
2034 push @{$hash->{$key}}, $value;
2038 # return hash of git project configuration
2039 # optionally limited to some section, e.g. 'gitweb'
2040 sub git_parse_project_config {
2041 my $section_regexp = shift;
2042 my %config;
2044 local $/ = "\0";
2046 open my $fh, "-|", git_cmd(), "config", '-z', '-l',
2047 or return;
2049 while (my $keyval = <$fh>) {
2050 chomp $keyval;
2051 my ($key, $value) = split(/\n/, $keyval, 2);
2053 hash_set_multi(\%config, $key, $value)
2054 if (!defined $section_regexp || $key =~ /^(?:$section_regexp)\./o);
2056 close $fh;
2058 return %config;
2061 # convert config value to boolean: 'true' or 'false'
2062 # no value, number > 0, 'true' and 'yes' values are true
2063 # rest of values are treated as false (never as error)
2064 sub config_to_bool {
2065 my $val = shift;
2067 return 1 if !defined $val; # section.key
2069 # strip leading and trailing whitespace
2070 $val =~ s/^\s+//;
2071 $val =~ s/\s+$//;
2073 return (($val =~ /^\d+$/ && $val) || # section.key = 1
2074 ($val =~ /^(?:true|yes)$/i)); # section.key = true
2077 # convert config value to simple decimal number
2078 # an optional value suffix of 'k', 'm', or 'g' will cause the value
2079 # to be multiplied by 1024, 1048576, or 1073741824
2080 sub config_to_int {
2081 my $val = shift;
2083 # strip leading and trailing whitespace
2084 $val =~ s/^\s+//;
2085 $val =~ s/\s+$//;
2087 if (my ($num, $unit) = ($val =~ /^([0-9]*)([kmg])$/i)) {
2088 $unit = lc($unit);
2089 # unknown unit is treated as 1
2090 return $num * ($unit eq 'g' ? 1073741824 :
2091 $unit eq 'm' ? 1048576 :
2092 $unit eq 'k' ? 1024 : 1);
2094 return $val;
2097 # convert config value to array reference, if needed
2098 sub config_to_multi {
2099 my $val = shift;
2101 return ref($val) ? $val : (defined($val) ? [ $val ] : []);
2104 sub git_get_project_config {
2105 my ($key, $type) = @_;
2107 # key sanity check
2108 return unless ($key);
2109 $key =~ s/^gitweb\.//;
2110 return if ($key =~ m/\W/);
2112 # type sanity check
2113 if (defined $type) {
2114 $type =~ s/^--//;
2115 $type = undef
2116 unless ($type eq 'bool' || $type eq 'int');
2119 # get config
2120 if (!defined $config_file ||
2121 $config_file ne "$git_dir/config") {
2122 %config = git_parse_project_config('gitweb');
2123 $config_file = "$git_dir/config";
2126 # check if config variable (key) exists
2127 return unless exists $config{"gitweb.$key"};
2129 # ensure given type
2130 if (!defined $type) {
2131 return $config{"gitweb.$key"};
2132 } elsif ($type eq 'bool') {
2133 # backward compatibility: 'git config --bool' returns true/false
2134 return config_to_bool($config{"gitweb.$key"}) ? 'true' : 'false';
2135 } elsif ($type eq 'int') {
2136 return config_to_int($config{"gitweb.$key"});
2138 return $config{"gitweb.$key"};
2141 # get hash of given path at given ref
2142 sub git_get_hash_by_path {
2143 my $base = shift;
2144 my $path = shift || return undef;
2145 my $type = shift;
2147 $path =~ s,/+$,,;
2149 open my $fd, "-|", git_cmd(), "ls-tree", $base, "--", $path
2150 or die_error(500, "Open git-ls-tree failed");
2151 my $line = <$fd>;
2152 close $fd or return undef;
2154 if (!defined $line) {
2155 # there is no tree or hash given by $path at $base
2156 return undef;
2159 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'
2160 $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t/;
2161 if (defined $type && $type ne $2) {
2162 # type doesn't match
2163 return undef;
2165 return $3;
2168 # get path of entry with given hash at given tree-ish (ref)
2169 # used to get 'from' filename for combined diff (merge commit) for renames
2170 sub git_get_path_by_hash {
2171 my $base = shift || return;
2172 my $hash = shift || return;
2174 local $/ = "\0";
2176 open my $fd, "-|", git_cmd(), "ls-tree", '-r', '-t', '-z', $base
2177 or return undef;
2178 while (my $line = <$fd>) {
2179 chomp $line;
2181 #'040000 tree 595596a6a9117ddba9fe379b6b012b558bac8423 gitweb'
2182 #'100644 blob e02e90f0429be0d2a69b76571101f20b8f75530f gitweb/README'
2183 if ($line =~ m/(?:[0-9]+) (?:.+) $hash\t(.+)$/) {
2184 close $fd;
2185 return $1;
2188 close $fd;
2189 return undef;
2192 ## ......................................................................
2193 ## git utility functions, directly accessing git repository
2195 sub git_get_project_description {
2196 my $path = shift;
2198 $git_dir = "$projectroot/$path";
2199 open my $fd, '<', "$git_dir/description"
2200 or return git_get_project_config('description');
2201 my $descr = <$fd>;
2202 close $fd;
2203 if (defined $descr) {
2204 chomp $descr;
2206 return $descr;
2209 sub git_get_project_ctags {
2210 my $path = shift;
2211 my $ctags = {};
2213 $git_dir = "$projectroot/$path";
2214 opendir my $dh, "$git_dir/ctags"
2215 or return $ctags;
2216 foreach (grep { -f $_ } map { "$git_dir/ctags/$_" } readdir($dh)) {
2217 open my $ct, '<', $_ or next;
2218 my $val = <$ct>;
2219 chomp $val;
2220 close $ct;
2221 my $ctag = $_; $ctag =~ s#.*/##;
2222 $ctags->{$ctag} = $val;
2224 closedir $dh;
2225 $ctags;
2228 sub git_populate_project_tagcloud {
2229 my $ctags = shift;
2231 # First, merge different-cased tags; tags vote on casing
2232 my %ctags_lc;
2233 foreach (keys %$ctags) {
2234 $ctags_lc{lc $_}->{count} += $ctags->{$_};
2235 if (not $ctags_lc{lc $_}->{topcount}
2236 or $ctags_lc{lc $_}->{topcount} < $ctags->{$_}) {
2237 $ctags_lc{lc $_}->{topcount} = $ctags->{$_};
2238 $ctags_lc{lc $_}->{topname} = $_;
2242 my $cloud;
2243 if (eval { require HTML::TagCloud; 1; }) {
2244 $cloud = HTML::TagCloud->new;
2245 foreach (sort keys %ctags_lc) {
2246 # Pad the title with spaces so that the cloud looks
2247 # less crammed.
2248 my $title = $ctags_lc{$_}->{topname};
2249 $title =~ s/ /&nbsp;/g;
2250 $title =~ s/^/&nbsp;/g;
2251 $title =~ s/$/&nbsp;/g;
2252 $cloud->add($title, $home_link."?by_tag=".$_, $ctags_lc{$_}->{count});
2254 } else {
2255 $cloud = \%ctags_lc;
2257 $cloud;
2260 sub git_show_project_tagcloud {
2261 my ($cloud, $count) = @_;
2262 print STDERR ref($cloud)."..\n";
2263 if (ref $cloud eq 'HTML::TagCloud') {
2264 return $cloud->html_and_css($count);
2265 } else {
2266 my @tags = sort { $cloud->{$a}->{count} <=> $cloud->{$b}->{count} } keys %$cloud;
2267 return '<p align="center">' . join (', ', map {
2268 $cgi->a({-href=>"$home_link?by_tag=$_"}, $cloud->{$_}->{topname})
2269 } splice(@tags, 0, $count)) . '</p>';
2273 sub git_get_project_url_list {
2274 my $path = shift;
2276 $git_dir = "$projectroot/$path";
2277 open my $fd, '<', "$git_dir/cloneurl"
2278 or return wantarray ?
2279 @{ config_to_multi(git_get_project_config('url')) } :
2280 config_to_multi(git_get_project_config('url'));
2281 my @git_project_url_list = map { chomp; $_ } <$fd>;
2282 close $fd;
2284 return wantarray ? @git_project_url_list : \@git_project_url_list;
2287 sub git_get_projects_list {
2288 my ($filter) = @_;
2289 my @list;
2291 $filter ||= '';
2292 $filter =~ s/\.git$//;
2294 my $check_forks = gitweb_check_feature('forks');
2296 if (-d $projects_list) {
2297 # search in directory
2298 my $dir = $projects_list . ($filter ? "/$filter" : '');
2299 # remove the trailing "/"
2300 $dir =~ s!/+$!!;
2301 my $pfxlen = length("$dir");
2302 my $pfxdepth = ($dir =~ tr!/!!);
2304 File::Find::find({
2305 follow_fast => 1, # follow symbolic links
2306 follow_skip => 2, # ignore duplicates
2307 dangling_symlinks => 0, # ignore dangling symlinks, silently
2308 wanted => sub {
2309 # skip project-list toplevel, if we get it.
2310 return if (m!^[/.]$!);
2311 # only directories can be git repositories
2312 return unless (-d $_);
2313 # don't traverse too deep (Find is super slow on os x)
2314 if (($File::Find::name =~ tr!/!!) - $pfxdepth > $project_maxdepth) {
2315 $File::Find::prune = 1;
2316 return;
2319 my $subdir = substr($File::Find::name, $pfxlen + 1);
2320 # we check related file in $projectroot
2321 my $path = ($filter ? "$filter/" : '') . $subdir;
2322 if (check_export_ok("$projectroot/$path")) {
2323 push @list, { path => $path };
2324 $File::Find::prune = 1;
2327 }, "$dir");
2329 } elsif (-f $projects_list) {
2330 # read from file(url-encoded):
2331 # 'git%2Fgit.git Linus+Torvalds'
2332 # 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin'
2333 # 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman'
2334 my %paths;
2335 open my $fd, '<', $projects_list or return;
2336 PROJECT:
2337 while (my $line = <$fd>) {
2338 chomp $line;
2339 my ($path, $owner) = split ' ', $line;
2340 $path = unescape($path);
2341 $owner = unescape($owner);
2342 if (!defined $path) {
2343 next;
2345 if ($filter ne '') {
2346 # looking for forks;
2347 my $pfx = substr($path, 0, length($filter));
2348 if ($pfx ne $filter) {
2349 next PROJECT;
2351 my $sfx = substr($path, length($filter));
2352 if ($sfx !~ /^\/.*\.git$/) {
2353 next PROJECT;
2355 } elsif ($check_forks) {
2356 PATH:
2357 foreach my $filter (keys %paths) {
2358 # looking for forks;
2359 my $pfx = substr($path, 0, length($filter));
2360 if ($pfx ne $filter) {
2361 next PATH;
2363 my $sfx = substr($path, length($filter));
2364 if ($sfx !~ /^\/.*\.git$/) {
2365 next PATH;
2367 # is a fork, don't include it in
2368 # the list
2369 next PROJECT;
2372 if (check_export_ok("$projectroot/$path")) {
2373 my $pr = {
2374 path => $path,
2375 owner => to_utf8($owner),
2377 push @list, $pr;
2378 (my $forks_path = $path) =~ s/\.git$//;
2379 $paths{$forks_path}++;
2382 close $fd;
2384 return @list;
2387 our $gitweb_project_owner = undef;
2388 sub git_get_project_list_from_file {
2390 return if (defined $gitweb_project_owner);
2392 $gitweb_project_owner = {};
2393 # read from file (url-encoded):
2394 # 'git%2Fgit.git Linus+Torvalds'
2395 # 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin'
2396 # 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman'
2397 if (-f $projects_list) {
2398 open(my $fd, '<', $projects_list);
2399 while (my $line = <$fd>) {
2400 chomp $line;
2401 my ($pr, $ow) = split ' ', $line;
2402 $pr = unescape($pr);
2403 $ow = unescape($ow);
2404 $gitweb_project_owner->{$pr} = to_utf8($ow);
2406 close $fd;
2410 sub git_get_project_owner {
2411 my $project = shift;
2412 my $owner;
2414 return undef unless $project;
2415 $git_dir = "$projectroot/$project";
2417 if (!defined $gitweb_project_owner) {
2418 git_get_project_list_from_file();
2421 if (exists $gitweb_project_owner->{$project}) {
2422 $owner = $gitweb_project_owner->{$project};
2424 if (!defined $owner){
2425 $owner = git_get_project_config('owner');
2427 if (!defined $owner) {
2428 $owner = get_file_owner("$git_dir");
2431 return $owner;
2434 sub git_get_last_activity {
2435 my ($path) = @_;
2436 my $fd;
2438 $git_dir = "$projectroot/$path";
2439 open($fd, "-|", git_cmd(), 'for-each-ref',
2440 '--format=%(committer)',
2441 '--sort=-committerdate',
2442 '--count=1',
2443 'refs/heads') or return;
2444 my $most_recent = <$fd>;
2445 close $fd or return;
2446 if (defined $most_recent &&
2447 $most_recent =~ / (\d+) [-+][01]\d\d\d$/) {
2448 my $timestamp = $1;
2449 my $age = time - $timestamp;
2450 return ($age, age_string($age));
2452 return (undef, undef);
2455 sub git_get_references {
2456 my $type = shift || "";
2457 my %refs;
2458 # 5dc01c595e6c6ec9ccda4f6f69c131c0dd945f8c refs/tags/v2.6.11
2459 # c39ae07f393806ccf406ef966e9a15afc43cc36a refs/tags/v2.6.11^{}
2460 open my $fd, "-|", git_cmd(), "show-ref", "--dereference",
2461 ($type ? ("--", "refs/$type") : ()) # use -- <pattern> if $type
2462 or return;
2464 while (my $line = <$fd>) {
2465 chomp $line;
2466 if ($line =~ m!^([0-9a-fA-F]{40})\srefs/($type.*)$!) {
2467 if (defined $refs{$1}) {
2468 push @{$refs{$1}}, $2;
2469 } else {
2470 $refs{$1} = [ $2 ];
2474 close $fd or return;
2475 return \%refs;
2478 sub git_get_rev_name_tags {
2479 my $hash = shift || return undef;
2481 open my $fd, "-|", git_cmd(), "name-rev", "--tags", $hash
2482 or return;
2483 my $name_rev = <$fd>;
2484 close $fd;
2486 if ($name_rev =~ m|^$hash tags/(.*)$|) {
2487 return $1;
2488 } else {
2489 # catches also '$hash undefined' output
2490 return undef;
2494 ## ----------------------------------------------------------------------
2495 ## parse to hash functions
2497 sub parse_date {
2498 my $epoch = shift;
2499 my $tz = shift || "-0000";
2501 my %date;
2502 my @months = ("Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec");
2503 my @days = ("Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat");
2504 my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($epoch);
2505 $date{'hour'} = $hour;
2506 $date{'minute'} = $min;
2507 $date{'mday'} = $mday;
2508 $date{'day'} = $days[$wday];
2509 $date{'month'} = $months[$mon];
2510 $date{'rfc2822'} = sprintf "%s, %d %s %4d %02d:%02d:%02d +0000",
2511 $days[$wday], $mday, $months[$mon], 1900+$year, $hour ,$min, $sec;
2512 $date{'mday-time'} = sprintf "%d %s %02d:%02d",
2513 $mday, $months[$mon], $hour ,$min;
2514 $date{'iso-8601'} = sprintf "%04d-%02d-%02dT%02d:%02d:%02dZ",
2515 1900+$year, 1+$mon, $mday, $hour ,$min, $sec;
2517 $tz =~ m/^([+\-][0-9][0-9])([0-9][0-9])$/;
2518 my $local = $epoch + ((int $1 + ($2/60)) * 3600);
2519 ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($local);
2520 $date{'hour_local'} = $hour;
2521 $date{'minute_local'} = $min;
2522 $date{'tz_local'} = $tz;
2523 $date{'iso-tz'} = sprintf("%04d-%02d-%02d %02d:%02d:%02d %s",
2524 1900+$year, $mon+1, $mday,
2525 $hour, $min, $sec, $tz);
2526 return %date;
2529 sub parse_tag {
2530 my $tag_id = shift;
2531 my %tag;
2532 my @comment;
2534 open my $fd, "-|", git_cmd(), "cat-file", "tag", $tag_id or return;
2535 $tag{'id'} = $tag_id;
2536 while (my $line = <$fd>) {
2537 chomp $line;
2538 if ($line =~ m/^object ([0-9a-fA-F]{40})$/) {
2539 $tag{'object'} = $1;
2540 } elsif ($line =~ m/^type (.+)$/) {
2541 $tag{'type'} = $1;
2542 } elsif ($line =~ m/^tag (.+)$/) {
2543 $tag{'name'} = $1;
2544 } elsif ($line =~ m/^tagger (.*) ([0-9]+) (.*)$/) {
2545 $tag{'author'} = $1;
2546 $tag{'author_epoch'} = $2;
2547 $tag{'author_tz'} = $3;
2548 if ($tag{'author'} =~ m/^([^<]+) <([^>]*)>/) {
2549 $tag{'author_name'} = $1;
2550 $tag{'author_email'} = $2;
2551 } else {
2552 $tag{'author_name'} = $tag{'author'};
2554 } elsif ($line =~ m/--BEGIN/) {
2555 push @comment, $line;
2556 last;
2557 } elsif ($line eq "") {
2558 last;
2561 push @comment, <$fd>;
2562 $tag{'comment'} = \@comment;
2563 close $fd or return;
2564 if (!defined $tag{'name'}) {
2565 return
2567 return %tag
2570 sub parse_commit_text {
2571 my ($commit_text, $withparents) = @_;
2572 my @commit_lines = split '\n', $commit_text;
2573 my %co;
2575 pop @commit_lines; # Remove '\0'
2577 if (! @commit_lines) {
2578 return;
2581 my $header = shift @commit_lines;
2582 if ($header !~ m/^[0-9a-fA-F]{40}/) {
2583 return;
2585 ($co{'id'}, my @parents) = split ' ', $header;
2586 while (my $line = shift @commit_lines) {
2587 last if $line eq "\n";
2588 if ($line =~ m/^tree ([0-9a-fA-F]{40})$/) {
2589 $co{'tree'} = $1;
2590 } elsif ((!defined $withparents) && ($line =~ m/^parent ([0-9a-fA-F]{40})$/)) {
2591 push @parents, $1;
2592 } elsif ($line =~ m/^author (.*) ([0-9]+) (.*)$/) {
2593 $co{'author'} = to_utf8($1);
2594 $co{'author_epoch'} = $2;
2595 $co{'author_tz'} = $3;
2596 if ($co{'author'} =~ m/^([^<]+) <([^>]*)>/) {
2597 $co{'author_name'} = $1;
2598 $co{'author_email'} = $2;
2599 } else {
2600 $co{'author_name'} = $co{'author'};
2602 } elsif ($line =~ m/^committer (.*) ([0-9]+) (.*)$/) {
2603 $co{'committer'} = to_utf8($1);
2604 $co{'committer_epoch'} = $2;
2605 $co{'committer_tz'} = $3;
2606 if ($co{'committer'} =~ m/^([^<]+) <([^>]*)>/) {
2607 $co{'committer_name'} = $1;
2608 $co{'committer_email'} = $2;
2609 } else {
2610 $co{'committer_name'} = $co{'committer'};
2614 if (!defined $co{'tree'}) {
2615 return;
2617 $co{'parents'} = \@parents;
2618 $co{'parent'} = $parents[0];
2620 foreach my $title (@commit_lines) {
2621 $title =~ s/^ //;
2622 if ($title ne "") {
2623 $co{'title'} = chop_str($title, 80, 5);
2624 # remove leading stuff of merges to make the interesting part visible
2625 if (length($title) > 50) {
2626 $title =~ s/^Automatic //;
2627 $title =~ s/^merge (of|with) /Merge ... /i;
2628 if (length($title) > 50) {
2629 $title =~ s/(http|rsync):\/\///;
2631 if (length($title) > 50) {
2632 $title =~ s/(master|www|rsync)\.//;
2634 if (length($title) > 50) {
2635 $title =~ s/kernel.org:?//;
2637 if (length($title) > 50) {
2638 $title =~ s/\/pub\/scm//;
2641 $co{'title_short'} = chop_str($title, 50, 5);
2642 last;
2645 if (! defined $co{'title'} || $co{'title'} eq "") {
2646 $co{'title'} = $co{'title_short'} = '(no commit message)';
2648 # remove added spaces
2649 foreach my $line (@commit_lines) {
2650 $line =~ s/^ //;
2652 $co{'comment'} = \@commit_lines;
2654 my $age = time - $co{'committer_epoch'};
2655 $co{'age'} = $age;
2656 $co{'age_string'} = age_string($age);
2657 my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($co{'committer_epoch'});
2658 if ($age > 60*60*24*7*2) {
2659 $co{'age_string_date'} = sprintf "%4i-%02u-%02i", 1900 + $year, $mon+1, $mday;
2660 $co{'age_string_age'} = $co{'age_string'};
2661 } else {
2662 $co{'age_string_date'} = $co{'age_string'};
2663 $co{'age_string_age'} = sprintf "%4i-%02u-%02i", 1900 + $year, $mon+1, $mday;
2665 return %co;
2668 sub parse_commit {
2669 my ($commit_id) = @_;
2670 my %co;
2672 local $/ = "\0";
2674 open my $fd, "-|", git_cmd(), "rev-list",
2675 "--parents",
2676 "--header",
2677 "--max-count=1",
2678 $commit_id,
2679 "--",
2680 or die_error(500, "Open git-rev-list failed");
2681 %co = parse_commit_text(<$fd>, 1);
2682 close $fd;
2684 return %co;
2687 sub parse_commits {
2688 my ($commit_id, $maxcount, $skip, $filename, @args) = @_;
2689 my @cos;
2691 $maxcount ||= 1;
2692 $skip ||= 0;
2694 local $/ = "\0";
2696 open my $fd, "-|", git_cmd(), "rev-list",
2697 "--header",
2698 @args,
2699 ("--max-count=" . $maxcount),
2700 ("--skip=" . $skip),
2701 @extra_options,
2702 $commit_id,
2703 "--",
2704 ($filename ? ($filename) : ())
2705 or die_error(500, "Open git-rev-list failed");
2706 while (my $line = <$fd>) {
2707 my %co = parse_commit_text($line);
2708 push @cos, \%co;
2710 close $fd;
2712 return wantarray ? @cos : \@cos;
2715 # parse line of git-diff-tree "raw" output
2716 sub parse_difftree_raw_line {
2717 my $line = shift;
2718 my %res;
2720 # ':100644 100644 03b218260e99b78c6df0ed378e59ed9205ccc96d 3b93d5e7cc7f7dd4ebed13a5cc1a4ad976fc94d8 M ls-files.c'
2721 # ':100644 100644 7f9281985086971d3877aca27704f2aaf9c448ce bc190ebc71bbd923f2b728e505408f5e54bd073a M rev-tree.c'
2722 if ($line =~ m/^:([0-7]{6}) ([0-7]{6}) ([0-9a-fA-F]{40}) ([0-9a-fA-F]{40}) (.)([0-9]{0,3})\t(.*)$/) {
2723 $res{'from_mode'} = $1;
2724 $res{'to_mode'} = $2;
2725 $res{'from_id'} = $3;
2726 $res{'to_id'} = $4;
2727 $res{'status'} = $5;
2728 $res{'similarity'} = $6;
2729 if ($res{'status'} eq 'R' || $res{'status'} eq 'C') { # renamed or copied
2730 ($res{'from_file'}, $res{'to_file'}) = map { unquote($_) } split("\t", $7);
2731 } else {
2732 $res{'from_file'} = $res{'to_file'} = $res{'file'} = unquote($7);
2735 # '::100755 100755 100755 60e79ca1b01bc8b057abe17ddab484699a7f5fdb 94067cc5f73388f33722d52ae02f44692bc07490 94067cc5f73388f33722d52ae02f44692bc07490 MR git-gui/git-gui.sh'
2736 # combined diff (for merge commit)
2737 elsif ($line =~ s/^(::+)((?:[0-7]{6} )+)((?:[0-9a-fA-F]{40} )+)([a-zA-Z]+)\t(.*)$//) {
2738 $res{'nparents'} = length($1);
2739 $res{'from_mode'} = [ split(' ', $2) ];
2740 $res{'to_mode'} = pop @{$res{'from_mode'}};
2741 $res{'from_id'} = [ split(' ', $3) ];
2742 $res{'to_id'} = pop @{$res{'from_id'}};
2743 $res{'status'} = [ split('', $4) ];
2744 $res{'to_file'} = unquote($5);
2746 # 'c512b523472485aef4fff9e57b229d9d243c967f'
2747 elsif ($line =~ m/^([0-9a-fA-F]{40})$/) {
2748 $res{'commit'} = $1;
2751 return wantarray ? %res : \%res;
2754 # wrapper: return parsed line of git-diff-tree "raw" output
2755 # (the argument might be raw line, or parsed info)
2756 sub parsed_difftree_line {
2757 my $line_or_ref = shift;
2759 if (ref($line_or_ref) eq "HASH") {
2760 # pre-parsed (or generated by hand)
2761 return $line_or_ref;
2762 } else {
2763 return parse_difftree_raw_line($line_or_ref);
2767 # parse line of git-ls-tree output
2768 sub parse_ls_tree_line {
2769 my $line = shift;
2770 my %opts = @_;
2771 my %res;
2773 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'
2774 $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t(.+)$/s;
2776 $res{'mode'} = $1;
2777 $res{'type'} = $2;
2778 $res{'hash'} = $3;
2779 if ($opts{'-z'}) {
2780 $res{'name'} = $4;
2781 } else {
2782 $res{'name'} = unquote($4);
2785 return wantarray ? %res : \%res;
2788 # generates _two_ hashes, references to which are passed as 2 and 3 argument
2789 sub parse_from_to_diffinfo {
2790 my ($diffinfo, $from, $to, @parents) = @_;
2792 if ($diffinfo->{'nparents'}) {
2793 # combined diff
2794 $from->{'file'} = [];
2795 $from->{'href'} = [];
2796 fill_from_file_info($diffinfo, @parents)
2797 unless exists $diffinfo->{'from_file'};
2798 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
2799 $from->{'file'}[$i] =
2800 defined $diffinfo->{'from_file'}[$i] ?
2801 $diffinfo->{'from_file'}[$i] :
2802 $diffinfo->{'to_file'};
2803 if ($diffinfo->{'status'}[$i] ne "A") { # not new (added) file
2804 $from->{'href'}[$i] = href(action=>"blob",
2805 hash_base=>$parents[$i],
2806 hash=>$diffinfo->{'from_id'}[$i],
2807 file_name=>$from->{'file'}[$i]);
2808 } else {
2809 $from->{'href'}[$i] = undef;
2812 } else {
2813 # ordinary (not combined) diff
2814 $from->{'file'} = $diffinfo->{'from_file'};
2815 if ($diffinfo->{'status'} ne "A") { # not new (added) file
2816 $from->{'href'} = href(action=>"blob", hash_base=>$hash_parent,
2817 hash=>$diffinfo->{'from_id'},
2818 file_name=>$from->{'file'});
2819 } else {
2820 delete $from->{'href'};
2824 $to->{'file'} = $diffinfo->{'to_file'};
2825 if (!is_deleted($diffinfo)) { # file exists in result
2826 $to->{'href'} = href(action=>"blob", hash_base=>$hash,
2827 hash=>$diffinfo->{'to_id'},
2828 file_name=>$to->{'file'});
2829 } else {
2830 delete $to->{'href'};
2834 ## ......................................................................
2835 ## parse to array of hashes functions
2837 sub git_get_heads_list {
2838 my $limit = shift;
2839 my @headslist;
2841 open my $fd, '-|', git_cmd(), 'for-each-ref',
2842 ($limit ? '--count='.($limit+1) : ()), '--sort=-committerdate',
2843 '--format=%(objectname) %(refname) %(subject)%00%(committer)',
2844 'refs/heads'
2845 or return;
2846 while (my $line = <$fd>) {
2847 my %ref_item;
2849 chomp $line;
2850 my ($refinfo, $committerinfo) = split(/\0/, $line);
2851 my ($hash, $name, $title) = split(' ', $refinfo, 3);
2852 my ($committer, $epoch, $tz) =
2853 ($committerinfo =~ /^(.*) ([0-9]+) (.*)$/);
2854 $ref_item{'fullname'} = $name;
2855 $name =~ s!^refs/heads/!!;
2857 $ref_item{'name'} = $name;
2858 $ref_item{'id'} = $hash;
2859 $ref_item{'title'} = $title || '(no commit message)';
2860 $ref_item{'epoch'} = $epoch;
2861 if ($epoch) {
2862 $ref_item{'age'} = age_string(time - $ref_item{'epoch'});
2863 } else {
2864 $ref_item{'age'} = "unknown";
2867 push @headslist, \%ref_item;
2869 close $fd;
2871 return wantarray ? @headslist : \@headslist;
2874 sub git_get_tags_list {
2875 my $limit = shift;
2876 my @tagslist;
2878 open my $fd, '-|', git_cmd(), 'for-each-ref',
2879 ($limit ? '--count='.($limit+1) : ()), '--sort=-creatordate',
2880 '--format=%(objectname) %(objecttype) %(refname) '.
2881 '%(*objectname) %(*objecttype) %(subject)%00%(creator)',
2882 'refs/tags'
2883 or return;
2884 while (my $line = <$fd>) {
2885 my %ref_item;
2887 chomp $line;
2888 my ($refinfo, $creatorinfo) = split(/\0/, $line);
2889 my ($id, $type, $name, $refid, $reftype, $title) = split(' ', $refinfo, 6);
2890 my ($creator, $epoch, $tz) =
2891 ($creatorinfo =~ /^(.*) ([0-9]+) (.*)$/);
2892 $ref_item{'fullname'} = $name;
2893 $name =~ s!^refs/tags/!!;
2895 $ref_item{'type'} = $type;
2896 $ref_item{'id'} = $id;
2897 $ref_item{'name'} = $name;
2898 if ($type eq "tag") {
2899 $ref_item{'subject'} = $title;
2900 $ref_item{'reftype'} = $reftype;
2901 $ref_item{'refid'} = $refid;
2902 } else {
2903 $ref_item{'reftype'} = $type;
2904 $ref_item{'refid'} = $id;
2907 if ($type eq "tag" || $type eq "commit") {
2908 $ref_item{'epoch'} = $epoch;
2909 if ($epoch) {
2910 $ref_item{'age'} = age_string(time - $ref_item{'epoch'});
2911 } else {
2912 $ref_item{'age'} = "unknown";
2916 push @tagslist, \%ref_item;
2918 close $fd;
2920 return wantarray ? @tagslist : \@tagslist;
2923 ## ----------------------------------------------------------------------
2924 ## filesystem-related functions
2926 sub get_file_owner {
2927 my $path = shift;
2929 my ($dev, $ino, $mode, $nlink, $st_uid, $st_gid, $rdev, $size) = stat($path);
2930 my ($name, $passwd, $uid, $gid, $quota, $comment, $gcos, $dir, $shell) = getpwuid($st_uid);
2931 if (!defined $gcos) {
2932 return undef;
2934 my $owner = $gcos;
2935 $owner =~ s/[,;].*$//;
2936 return to_utf8($owner);
2939 # assume that file exists
2940 sub insert_file {
2941 my $filename = shift;
2943 open my $fd, '<', $filename;
2944 print map { to_utf8($_) } <$fd>;
2945 close $fd;
2948 ## ......................................................................
2949 ## mimetype related functions
2951 sub mimetype_guess_file {
2952 my $filename = shift;
2953 my $mimemap = shift;
2954 -r $mimemap or return undef;
2956 my %mimemap;
2957 open(my $mh, '<', $mimemap) or return undef;
2958 while (<$mh>) {
2959 next if m/^#/; # skip comments
2960 my ($mimetype, $exts) = split(/\t+/);
2961 if (defined $exts) {
2962 my @exts = split(/\s+/, $exts);
2963 foreach my $ext (@exts) {
2964 $mimemap{$ext} = $mimetype;
2968 close($mh);
2970 $filename =~ /\.([^.]*)$/;
2971 return $mimemap{$1};
2974 sub mimetype_guess {
2975 my $filename = shift;
2976 my $mime;
2977 $filename =~ /\./ or return undef;
2979 if ($mimetypes_file) {
2980 my $file = $mimetypes_file;
2981 if ($file !~ m!^/!) { # if it is relative path
2982 # it is relative to project
2983 $file = "$projectroot/$project/$file";
2985 $mime = mimetype_guess_file($filename, $file);
2987 $mime ||= mimetype_guess_file($filename, '/etc/mime.types');
2988 return $mime;
2991 sub blob_mimetype {
2992 my $fd = shift;
2993 my $filename = shift;
2995 if ($filename) {
2996 my $mime = mimetype_guess($filename);
2997 $mime and return $mime;
3000 # just in case
3001 return $default_blob_plain_mimetype unless $fd;
3003 if (-T $fd) {
3004 return 'text/plain';
3005 } elsif (! $filename) {
3006 return 'application/octet-stream';
3007 } elsif ($filename =~ m/\.png$/i) {
3008 return 'image/png';
3009 } elsif ($filename =~ m/\.gif$/i) {
3010 return 'image/gif';
3011 } elsif ($filename =~ m/\.jpe?g$/i) {
3012 return 'image/jpeg';
3013 } else {
3014 return 'application/octet-stream';
3018 sub blob_contenttype {
3019 my ($fd, $file_name, $type) = @_;
3021 $type ||= blob_mimetype($fd, $file_name);
3022 if ($type eq 'text/plain' && defined $default_text_plain_charset) {
3023 $type .= "; charset=$default_text_plain_charset";
3026 return $type;
3029 ## ======================================================================
3030 ## functions printing HTML: header, footer, error page
3032 sub git_header_html {
3033 my $status = shift || "200 OK";
3034 my $expires = shift;
3036 my $title = "$site_name";
3037 if (defined $project) {
3038 $title .= " - " . to_utf8($project);
3039 if (defined $action) {
3040 $title .= "/$action";
3041 if (defined $file_name) {
3042 $title .= " - " . esc_path($file_name);
3043 if ($action eq "tree" && $file_name !~ m|/$|) {
3044 $title .= "/";
3049 my $content_type;
3050 # require explicit support from the UA if we are to send the page as
3051 # 'application/xhtml+xml', otherwise send it as plain old 'text/html'.
3052 # we have to do this because MSIE sometimes globs '*/*', pretending to
3053 # support xhtml+xml but choking when it gets what it asked for.
3054 if (defined $cgi->http('HTTP_ACCEPT') &&
3055 $cgi->http('HTTP_ACCEPT') =~ m/(,|;|\s|^)application\/xhtml\+xml(,|;|\s|$)/ &&
3056 $cgi->Accept('application/xhtml+xml') != 0) {
3057 $content_type = 'application/xhtml+xml';
3058 } else {
3059 $content_type = 'text/html';
3061 print $cgi->header(-type=>$content_type, -charset => 'utf-8',
3062 -status=> $status, -expires => $expires);
3063 my $mod_perl_version = $ENV{'MOD_PERL'} ? " $ENV{'MOD_PERL'}" : '';
3064 print <<EOF;
3065 <?xml version="1.0" encoding="utf-8"?>
3066 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
3067 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US" lang="en-US">
3068 <!-- git web interface version $version, (C) 2005-2006, Kay Sievers <kay.sievers\@vrfy.org>, Christian Gierke -->
3069 <!-- git core binaries version $git_version -->
3070 <head>
3071 <meta http-equiv="content-type" content="$content_type; charset=utf-8"/>
3072 <meta name="generator" content="gitweb/$version git/$git_version$mod_perl_version"/>
3073 <meta name="robots" content="index, nofollow"/>
3074 <title>$title</title>
3076 # the stylesheet, favicon etc urls won't work correctly with path_info
3077 # unless we set the appropriate base URL
3078 if ($ENV{'PATH_INFO'}) {
3079 print "<base href=\"".esc_url($base_url)."\" />\n";
3081 # print out each stylesheet that exist, providing backwards capability
3082 # for those people who defined $stylesheet in a config file
3083 if (defined $stylesheet) {
3084 print '<link rel="stylesheet" type="text/css" href="'.esc_url($stylesheet).'"/>'."\n";
3085 } else {
3086 foreach my $stylesheet (@stylesheets) {
3087 next unless $stylesheet;
3088 print '<link rel="stylesheet" type="text/css" href="'.esc_url($stylesheet).'"/>'."\n";
3091 if (defined $project) {
3092 my %href_params = get_feed_info();
3093 if (!exists $href_params{'-title'}) {
3094 $href_params{'-title'} = 'log';
3097 foreach my $format qw(RSS Atom) {
3098 my $type = lc($format);
3099 my %link_attr = (
3100 '-rel' => 'alternate',
3101 '-title' => esc_attr("$project - $href_params{'-title'} - $format feed"),
3102 '-type' => "application/$type+xml"
3105 $href_params{'action'} = $type;
3106 $link_attr{'-href'} = href(%href_params);
3107 print "<link ".
3108 "rel=\"$link_attr{'-rel'}\" ".
3109 "title=\"$link_attr{'-title'}\" ".
3110 "href=\"$link_attr{'-href'}\" ".
3111 "type=\"$link_attr{'-type'}\" ".
3112 "/>\n";
3114 $href_params{'extra_options'} = '--no-merges';
3115 $link_attr{'-href'} = href(%href_params);
3116 $link_attr{'-title'} .= ' (no merges)';
3117 print "<link ".
3118 "rel=\"$link_attr{'-rel'}\" ".
3119 "title=\"$link_attr{'-title'}\" ".
3120 "href=\"$link_attr{'-href'}\" ".
3121 "type=\"$link_attr{'-type'}\" ".
3122 "/>\n";
3125 } else {
3126 printf('<link rel="alternate" title="%s projects list" '.
3127 'href="%s" type="text/plain; charset=utf-8" />'."\n",
3128 esc_attr($site_name), href(project=>undef, action=>"project_index"));
3129 printf('<link rel="alternate" title="%s projects feeds" '.
3130 'href="%s" type="text/x-opml" />'."\n",
3131 esc_attr($site_name), href(project=>undef, action=>"opml"));
3133 if (defined $favicon) {
3134 print qq(<link rel="shortcut icon" href=").esc_url($favicon).qq(" type="image/png" />\n);
3137 print "</head>\n" .
3138 "<body>\n";
3140 if (-f $site_header) {
3141 insert_file($site_header);
3144 print "<div class=\"page_header\">\n" .
3145 $cgi->a({-href => esc_url($logo_url),
3146 -title => $logo_label},
3147 qq(<img src=").esc_url($logo).qq(" width="72" height="27" alt="git" class="logo"/>));
3148 print $cgi->a({-href => esc_url($home_link)}, $home_link_str) . " / ";
3149 if (defined $project) {
3150 print $cgi->a({-href => href(action=>"summary")}, esc_html($project));
3151 if (defined $action) {
3152 print " / $action";
3154 print "\n";
3156 print "</div>\n";
3158 my $have_search = gitweb_check_feature('search');
3159 if (defined $project && $have_search) {
3160 if (!defined $searchtext) {
3161 $searchtext = "";
3163 my $search_hash;
3164 if (defined $hash_base) {
3165 $search_hash = $hash_base;
3166 } elsif (defined $hash) {
3167 $search_hash = $hash;
3168 } else {
3169 $search_hash = "HEAD";
3171 my $action = $my_uri;
3172 my $use_pathinfo = gitweb_check_feature('pathinfo');
3173 if ($use_pathinfo) {
3174 $action .= "/".esc_url($project);
3176 print $cgi->startform(-method => "get", -action => $action) .
3177 "<div class=\"search\">\n" .
3178 (!$use_pathinfo &&
3179 $cgi->input({-name=>"p", -value=>$project, -type=>"hidden"}) . "\n") .
3180 $cgi->input({-name=>"a", -value=>"search", -type=>"hidden"}) . "\n" .
3181 $cgi->input({-name=>"h", -value=>$search_hash, -type=>"hidden"}) . "\n" .
3182 $cgi->popup_menu(-name => 'st', -default => 'commit',
3183 -values => ['commit', 'grep', 'author', 'committer', 'pickaxe']) .
3184 $cgi->sup($cgi->a({-href => href(action=>"search_help")}, "?")) .
3185 " search:\n",
3186 $cgi->textfield(-name => "s", -value => $searchtext) . "\n" .
3187 "<span title=\"Extended regular expression\">" .
3188 $cgi->checkbox(-name => 'sr', -value => 1, -label => 're',
3189 -checked => $search_use_regexp) .
3190 "</span>" .
3191 "</div>" .
3192 $cgi->end_form() . "\n";
3196 sub git_footer_html {
3197 my $feed_class = 'rss_logo';
3199 print "<div class=\"page_footer\">\n";
3200 if (defined $project) {
3201 my $descr = git_get_project_description($project);
3202 if (defined $descr) {
3203 print "<div class=\"page_footer_text\">" . esc_html($descr) . "</div>\n";
3206 my %href_params = get_feed_info();
3207 if (!%href_params) {
3208 $feed_class .= ' generic';
3210 $href_params{'-title'} ||= 'log';
3212 foreach my $format qw(RSS Atom) {
3213 $href_params{'action'} = lc($format);
3214 print $cgi->a({-href => href(%href_params),
3215 -title => "$href_params{'-title'} $format feed",
3216 -class => $feed_class}, $format)."\n";
3219 } else {
3220 print $cgi->a({-href => href(project=>undef, action=>"opml"),
3221 -class => $feed_class}, "OPML") . " ";
3222 print $cgi->a({-href => href(project=>undef, action=>"project_index"),
3223 -class => $feed_class}, "TXT") . "\n";
3225 print "</div>\n"; # class="page_footer"
3227 if (-f $site_footer) {
3228 insert_file($site_footer);
3231 print "</body>\n" .
3232 "</html>";
3235 # die_error(<http_status_code>, <error_message>)
3236 # Example: die_error(404, 'Hash not found')
3237 # By convention, use the following status codes (as defined in RFC 2616):
3238 # 400: Invalid or missing CGI parameters, or
3239 # requested object exists but has wrong type.
3240 # 403: Requested feature (like "pickaxe" or "snapshot") not enabled on
3241 # this server or project.
3242 # 404: Requested object/revision/project doesn't exist.
3243 # 500: The server isn't configured properly, or
3244 # an internal error occurred (e.g. failed assertions caused by bugs), or
3245 # an unknown error occurred (e.g. the git binary died unexpectedly).
3246 sub die_error {
3247 my $status = shift || 500;
3248 my $error = shift || "Internal server error";
3250 my %http_responses = (400 => '400 Bad Request',
3251 403 => '403 Forbidden',
3252 404 => '404 Not Found',
3253 500 => '500 Internal Server Error');
3254 git_header_html($http_responses{$status});
3255 print <<EOF;
3256 <div class="page_body">
3257 <br /><br />
3258 $status - $error
3259 <br />
3260 </div>
3262 git_footer_html();
3263 exit;
3266 ## ----------------------------------------------------------------------
3267 ## functions printing or outputting HTML: navigation
3269 sub git_print_page_nav {
3270 my ($current, $suppress, $head, $treehead, $treebase, $extra) = @_;
3271 $extra = '' if !defined $extra; # pager or formats
3273 my @navs = qw(summary shortlog log commit commitdiff tree);
3274 if ($suppress) {
3275 @navs = grep { $_ ne $suppress } @navs;
3278 my %arg = map { $_ => {action=>$_} } @navs;
3279 if (defined $head) {
3280 for (qw(commit commitdiff)) {
3281 $arg{$_}{'hash'} = $head;
3283 if ($current =~ m/^(tree | log | shortlog | commit | commitdiff | search)$/x) {
3284 for (qw(shortlog log)) {
3285 $arg{$_}{'hash'} = $head;
3290 $arg{'tree'}{'hash'} = $treehead if defined $treehead;
3291 $arg{'tree'}{'hash_base'} = $treebase if defined $treebase;
3293 my @actions = gitweb_get_feature('actions');
3294 my %repl = (
3295 '%' => '%',
3296 'n' => $project, # project name
3297 'f' => $git_dir, # project path within filesystem
3298 'h' => $treehead || '', # current hash ('h' parameter)
3299 'b' => $treebase || '', # hash base ('hb' parameter)
3301 while (@actions) {
3302 my ($label, $link, $pos) = splice(@actions,0,3);
3303 # insert
3304 @navs = map { $_ eq $pos ? ($_, $label) : $_ } @navs;
3305 # munch munch
3306 $link =~ s/%([%nfhb])/$repl{$1}/g;
3307 $arg{$label}{'_href'} = $link;
3310 print "<div class=\"page_nav\">\n" .
3311 (join " | ",
3312 map { $_ eq $current ?
3313 $_ : $cgi->a({-href => ($arg{$_}{_href} ? $arg{$_}{_href} : href(%{$arg{$_}}))}, "$_")
3314 } @navs);
3315 print "<br/>\n$extra<br/>\n" .
3316 "</div>\n";
3319 sub format_paging_nav {
3320 my ($action, $hash, $head, $page, $has_next_link) = @_;
3321 my $paging_nav;
3324 if ($hash ne $head || $page) {
3325 $paging_nav .= $cgi->a({-href => href(action=>$action)}, "HEAD");
3326 } else {
3327 $paging_nav .= "HEAD";
3330 if ($page > 0) {
3331 $paging_nav .= " &sdot; " .
3332 $cgi->a({-href => href(-replay=>1, page=>$page-1),
3333 -accesskey => "p", -title => "Alt-p"}, "prev");
3334 } else {
3335 $paging_nav .= " &sdot; prev";
3338 if ($has_next_link) {
3339 $paging_nav .= " &sdot; " .
3340 $cgi->a({-href => href(-replay=>1, page=>$page+1),
3341 -accesskey => "n", -title => "Alt-n"}, "next");
3342 } else {
3343 $paging_nav .= " &sdot; next";
3346 return $paging_nav;
3349 ## ......................................................................
3350 ## functions printing or outputting HTML: div
3352 sub git_print_header_div {
3353 my ($action, $title, $hash, $hash_base) = @_;
3354 my %args = ();
3356 $args{'action'} = $action;
3357 $args{'hash'} = $hash if $hash;
3358 $args{'hash_base'} = $hash_base if $hash_base;
3360 print "<div class=\"header\">\n" .
3361 $cgi->a({-href => href(%args), -class => "title"},
3362 $title ? $title : $action) .
3363 "\n</div>\n";
3366 sub print_local_time {
3367 my %date = @_;
3368 if ($date{'hour_local'} < 6) {
3369 printf(" (<span class=\"atnight\">%02d:%02d</span> %s)",
3370 $date{'hour_local'}, $date{'minute_local'}, $date{'tz_local'});
3371 } else {
3372 printf(" (%02d:%02d %s)",
3373 $date{'hour_local'}, $date{'minute_local'}, $date{'tz_local'});
3377 # Outputs the author name and date in long form
3378 sub git_print_authorship {
3379 my $co = shift;
3380 my %opts = @_;
3381 my $tag = $opts{-tag} || 'div';
3383 my %ad = parse_date($co->{'author_epoch'}, $co->{'author_tz'});
3384 print "<$tag class=\"author_date\">" .
3385 esc_html($co->{'author_name'}) .
3386 " [$ad{'rfc2822'}";
3387 print_local_time(%ad) if ($opts{-localtime});
3388 print "]" . git_get_avatar($co->{'author_email'}, -pad_before => 1)
3389 . "</$tag>\n";
3392 # Outputs table rows containing the full author or committer information,
3393 # in the format expected for 'commit' view (& similia).
3394 # Parameters are a commit hash reference, followed by the list of people
3395 # to output information for. If the list is empty it defalts to both
3396 # author and committer.
3397 sub git_print_authorship_rows {
3398 my $co = shift;
3399 # too bad we can't use @people = @_ || ('author', 'committer')
3400 my @people = @_;
3401 @people = ('author', 'committer') unless @people;
3402 foreach my $who (@people) {
3403 my %wd = parse_date($co->{"${who}_epoch"}, $co->{"${who}_tz"});
3404 print "<tr><td>$who</td><td>" . esc_html($co->{$who}) . "</td>" .
3405 "<td rowspan=\"2\">" .
3406 git_get_avatar($co->{"${who}_email"}, -size => 'double') .
3407 "</td></tr>\n" .
3408 "<tr>" .
3409 "<td></td><td> $wd{'rfc2822'}";
3410 print_local_time(%wd);
3411 print "</td>" .
3412 "</tr>\n";
3416 sub git_print_page_path {
3417 my $name = shift;
3418 my $type = shift;
3419 my $hb = shift;
3422 print "<div class=\"page_path\">";
3423 print $cgi->a({-href => href(action=>"tree", hash_base=>$hb),
3424 -title => 'tree root'}, to_utf8("[$project]"));
3425 print " / ";
3426 if (defined $name) {
3427 my @dirname = split '/', $name;
3428 my $basename = pop @dirname;
3429 my $fullname = '';
3431 foreach my $dir (@dirname) {
3432 $fullname .= ($fullname ? '/' : '') . $dir;
3433 print $cgi->a({-href => href(action=>"tree", file_name=>$fullname,
3434 hash_base=>$hb),
3435 -title => $fullname}, esc_path($dir));
3436 print " / ";
3438 if (defined $type && $type eq 'blob') {
3439 print $cgi->a({-href => href(action=>"blob_plain", file_name=>$file_name,
3440 hash_base=>$hb),
3441 -title => $name}, esc_path($basename));
3442 } elsif (defined $type && $type eq 'tree') {
3443 print $cgi->a({-href => href(action=>"tree", file_name=>$file_name,
3444 hash_base=>$hb),
3445 -title => $name}, esc_path($basename));
3446 print " / ";
3447 } else {
3448 print esc_path($basename);
3451 print "<br/></div>\n";
3454 sub git_print_log {
3455 my $log = shift;
3456 my %opts = @_;
3458 if ($opts{'-remove_title'}) {
3459 # remove title, i.e. first line of log
3460 shift @$log;
3462 # remove leading empty lines
3463 while (defined $log->[0] && $log->[0] eq "") {
3464 shift @$log;
3467 # print log
3468 my $signoff = 0;
3469 my $empty = 0;
3470 foreach my $line (@$log) {
3471 if ($line =~ m/^ *(signed[ \-]off[ \-]by[ :]|acked[ \-]by[ :]|cc[ :])/i) {
3472 $signoff = 1;
3473 $empty = 0;
3474 if (! $opts{'-remove_signoff'}) {
3475 print "<span class=\"signoff\">" . esc_html($line) . "</span><br/>\n";
3476 next;
3477 } else {
3478 # remove signoff lines
3479 next;
3481 } else {
3482 $signoff = 0;
3485 # print only one empty line
3486 # do not print empty line after signoff
3487 if ($line eq "") {
3488 next if ($empty || $signoff);
3489 $empty = 1;
3490 } else {
3491 $empty = 0;
3494 print format_log_line_html($line) . "<br/>\n";
3497 if ($opts{'-final_empty_line'}) {
3498 # end with single empty line
3499 print "<br/>\n" unless $empty;
3503 # return link target (what link points to)
3504 sub git_get_link_target {
3505 my $hash = shift;
3506 my $link_target;
3508 # read link
3509 open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
3510 or return;
3512 local $/ = undef;
3513 $link_target = <$fd>;
3515 close $fd
3516 or return;
3518 return $link_target;
3521 # given link target, and the directory (basedir) the link is in,
3522 # return target of link relative to top directory (top tree);
3523 # return undef if it is not possible (including absolute links).
3524 sub normalize_link_target {
3525 my ($link_target, $basedir) = @_;
3527 # absolute symlinks (beginning with '/') cannot be normalized
3528 return if (substr($link_target, 0, 1) eq '/');
3530 # normalize link target to path from top (root) tree (dir)
3531 my $path;
3532 if ($basedir) {
3533 $path = $basedir . '/' . $link_target;
3534 } else {
3535 # we are in top (root) tree (dir)
3536 $path = $link_target;
3539 # remove //, /./, and /../
3540 my @path_parts;
3541 foreach my $part (split('/', $path)) {
3542 # discard '.' and ''
3543 next if (!$part || $part eq '.');
3544 # handle '..'
3545 if ($part eq '..') {
3546 if (@path_parts) {
3547 pop @path_parts;
3548 } else {
3549 # link leads outside repository (outside top dir)
3550 return;
3552 } else {
3553 push @path_parts, $part;
3556 $path = join('/', @path_parts);
3558 return $path;
3561 # print tree entry (row of git_tree), but without encompassing <tr> element
3562 sub git_print_tree_entry {
3563 my ($t, $basedir, $hash_base, $have_blame) = @_;
3565 my %base_key = ();
3566 $base_key{'hash_base'} = $hash_base if defined $hash_base;
3568 # The format of a table row is: mode list link. Where mode is
3569 # the mode of the entry, list is the name of the entry, an href,
3570 # and link is the action links of the entry.
3572 print "<td class=\"mode\">" . mode_str($t->{'mode'}) . "</td>\n";
3573 if ($t->{'type'} eq "blob") {
3574 print "<td class=\"list\">" .
3575 $cgi->a({-href => href(action=>"blob", hash=>$t->{'hash'},
3576 file_name=>"$basedir$t->{'name'}", %base_key),
3577 -class => "list"}, esc_path($t->{'name'}));
3578 if (S_ISLNK(oct $t->{'mode'})) {
3579 my $link_target = git_get_link_target($t->{'hash'});
3580 if ($link_target) {
3581 my $norm_target = normalize_link_target($link_target, $basedir);
3582 if (defined $norm_target) {
3583 print " -> " .
3584 $cgi->a({-href => href(action=>"object", hash_base=>$hash_base,
3585 file_name=>$norm_target),
3586 -title => $norm_target}, esc_path($link_target));
3587 } else {
3588 print " -> " . esc_path($link_target);
3592 print "</td>\n";
3593 print "<td class=\"link\">";
3594 print $cgi->a({-href => href(action=>"blob", hash=>$t->{'hash'},
3595 file_name=>"$basedir$t->{'name'}", %base_key)},
3596 "blob");
3597 if ($have_blame) {
3598 print " | " .
3599 $cgi->a({-href => href(action=>"blame", hash=>$t->{'hash'},
3600 file_name=>"$basedir$t->{'name'}", %base_key)},
3601 "blame");
3603 if (defined $hash_base) {
3604 print " | " .
3605 $cgi->a({-href => href(action=>"history", hash_base=>$hash_base,
3606 hash=>$t->{'hash'}, file_name=>"$basedir$t->{'name'}")},
3607 "history");
3609 print " | " .
3610 $cgi->a({-href => href(action=>"blob_plain", hash_base=>$hash_base,
3611 file_name=>"$basedir$t->{'name'}")},
3612 "raw");
3613 print "</td>\n";
3615 } elsif ($t->{'type'} eq "tree") {
3616 print "<td class=\"list\">";
3617 print $cgi->a({-href => href(action=>"tree", hash=>$t->{'hash'},
3618 file_name=>"$basedir$t->{'name'}", %base_key)},
3619 esc_path($t->{'name'}));
3620 print "</td>\n";
3621 print "<td class=\"link\">";
3622 print $cgi->a({-href => href(action=>"tree", hash=>$t->{'hash'},
3623 file_name=>"$basedir$t->{'name'}", %base_key)},
3624 "tree");
3625 if (defined $hash_base) {
3626 print " | " .
3627 $cgi->a({-href => href(action=>"history", hash_base=>$hash_base,
3628 file_name=>"$basedir$t->{'name'}")},
3629 "history");
3631 print "</td>\n";
3632 } else {
3633 # unknown object: we can only present history for it
3634 # (this includes 'commit' object, i.e. submodule support)
3635 print "<td class=\"list\">" .
3636 esc_path($t->{'name'}) .
3637 "</td>\n";
3638 print "<td class=\"link\">";
3639 if (defined $hash_base) {
3640 print $cgi->a({-href => href(action=>"history",
3641 hash_base=>$hash_base,
3642 file_name=>"$basedir$t->{'name'}")},
3643 "history");
3645 print "</td>\n";
3649 ## ......................................................................
3650 ## functions printing large fragments of HTML
3652 # get pre-image filenames for merge (combined) diff
3653 sub fill_from_file_info {
3654 my ($diff, @parents) = @_;
3656 $diff->{'from_file'} = [ ];
3657 $diff->{'from_file'}[$diff->{'nparents'} - 1] = undef;
3658 for (my $i = 0; $i < $diff->{'nparents'}; $i++) {
3659 if ($diff->{'status'}[$i] eq 'R' ||
3660 $diff->{'status'}[$i] eq 'C') {
3661 $diff->{'from_file'}[$i] =
3662 git_get_path_by_hash($parents[$i], $diff->{'from_id'}[$i]);
3666 return $diff;
3669 # is current raw difftree line of file deletion
3670 sub is_deleted {
3671 my $diffinfo = shift;
3673 return $diffinfo->{'to_id'} eq ('0' x 40);
3676 # does patch correspond to [previous] difftree raw line
3677 # $diffinfo - hashref of parsed raw diff format
3678 # $patchinfo - hashref of parsed patch diff format
3679 # (the same keys as in $diffinfo)
3680 sub is_patch_split {
3681 my ($diffinfo, $patchinfo) = @_;
3683 return defined $diffinfo && defined $patchinfo
3684 && $diffinfo->{'to_file'} eq $patchinfo->{'to_file'};
3688 sub git_difftree_body {
3689 my ($difftree, $hash, @parents) = @_;
3690 my ($parent) = $parents[0];
3691 my $have_blame = gitweb_check_feature('blame');
3692 print "<div class=\"list_head\">\n";
3693 if ($#{$difftree} > 10) {
3694 print(($#{$difftree} + 1) . " files changed:\n");
3696 print "</div>\n";
3698 print "<table class=\"" .
3699 (@parents > 1 ? "combined " : "") .
3700 "diff_tree\">\n";
3702 # header only for combined diff in 'commitdiff' view
3703 my $has_header = @$difftree && @parents > 1 && $action eq 'commitdiff';
3704 if ($has_header) {
3705 # table header
3706 print "<thead><tr>\n" .
3707 "<th></th><th></th>\n"; # filename, patchN link
3708 for (my $i = 0; $i < @parents; $i++) {
3709 my $par = $parents[$i];
3710 print "<th>" .
3711 $cgi->a({-href => href(action=>"commitdiff",
3712 hash=>$hash, hash_parent=>$par),
3713 -title => 'commitdiff to parent number ' .
3714 ($i+1) . ': ' . substr($par,0,7)},
3715 $i+1) .
3716 "&nbsp;</th>\n";
3718 print "</tr></thead>\n<tbody>\n";
3721 my $alternate = 1;
3722 my $patchno = 0;
3723 foreach my $line (@{$difftree}) {
3724 my $diff = parsed_difftree_line($line);
3726 if ($alternate) {
3727 print "<tr class=\"dark\">\n";
3728 } else {
3729 print "<tr class=\"light\">\n";
3731 $alternate ^= 1;
3733 if (exists $diff->{'nparents'}) { # combined diff
3735 fill_from_file_info($diff, @parents)
3736 unless exists $diff->{'from_file'};
3738 if (!is_deleted($diff)) {
3739 # file exists in the result (child) commit
3740 print "<td>" .
3741 $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3742 file_name=>$diff->{'to_file'},
3743 hash_base=>$hash),
3744 -class => "list"}, esc_path($diff->{'to_file'})) .
3745 "</td>\n";
3746 } else {
3747 print "<td>" .
3748 esc_path($diff->{'to_file'}) .
3749 "</td>\n";
3752 if ($action eq 'commitdiff') {
3753 # link to patch
3754 $patchno++;
3755 print "<td class=\"link\">" .
3756 $cgi->a({-href => "#patch$patchno"}, "patch") .
3757 " | " .
3758 "</td>\n";
3761 my $has_history = 0;
3762 my $not_deleted = 0;
3763 for (my $i = 0; $i < $diff->{'nparents'}; $i++) {
3764 my $hash_parent = $parents[$i];
3765 my $from_hash = $diff->{'from_id'}[$i];
3766 my $from_path = $diff->{'from_file'}[$i];
3767 my $status = $diff->{'status'}[$i];
3769 $has_history ||= ($status ne 'A');
3770 $not_deleted ||= ($status ne 'D');
3772 if ($status eq 'A') {
3773 print "<td class=\"link\" align=\"right\"> | </td>\n";
3774 } elsif ($status eq 'D') {
3775 print "<td class=\"link\">" .
3776 $cgi->a({-href => href(action=>"blob",
3777 hash_base=>$hash,
3778 hash=>$from_hash,
3779 file_name=>$from_path)},
3780 "blob" . ($i+1)) .
3781 " | </td>\n";
3782 } else {
3783 if ($diff->{'to_id'} eq $from_hash) {
3784 print "<td class=\"link nochange\">";
3785 } else {
3786 print "<td class=\"link\">";
3788 print $cgi->a({-href => href(action=>"blobdiff",
3789 hash=>$diff->{'to_id'},
3790 hash_parent=>$from_hash,
3791 hash_base=>$hash,
3792 hash_parent_base=>$hash_parent,
3793 file_name=>$diff->{'to_file'},
3794 file_parent=>$from_path)},
3795 "diff" . ($i+1)) .
3796 " | </td>\n";
3800 print "<td class=\"link\">";
3801 if ($not_deleted) {
3802 print $cgi->a({-href => href(action=>"blob",
3803 hash=>$diff->{'to_id'},
3804 file_name=>$diff->{'to_file'},
3805 hash_base=>$hash)},
3806 "blob");
3807 print " | " if ($has_history);
3809 if ($has_history) {
3810 print $cgi->a({-href => href(action=>"history",
3811 file_name=>$diff->{'to_file'},
3812 hash_base=>$hash)},
3813 "history");
3815 print "</td>\n";
3817 print "</tr>\n";
3818 next; # instead of 'else' clause, to avoid extra indent
3820 # else ordinary diff
3822 my ($to_mode_oct, $to_mode_str, $to_file_type);
3823 my ($from_mode_oct, $from_mode_str, $from_file_type);
3824 if ($diff->{'to_mode'} ne ('0' x 6)) {
3825 $to_mode_oct = oct $diff->{'to_mode'};
3826 if (S_ISREG($to_mode_oct)) { # only for regular file
3827 $to_mode_str = sprintf("%04o", $to_mode_oct & 0777); # permission bits
3829 $to_file_type = file_type($diff->{'to_mode'});
3831 if ($diff->{'from_mode'} ne ('0' x 6)) {
3832 $from_mode_oct = oct $diff->{'from_mode'};
3833 if (S_ISREG($to_mode_oct)) { # only for regular file
3834 $from_mode_str = sprintf("%04o", $from_mode_oct & 0777); # permission bits
3836 $from_file_type = file_type($diff->{'from_mode'});
3839 if ($diff->{'status'} eq "A") { # created
3840 my $mode_chng = "<span class=\"file_status new\">[new $to_file_type";
3841 $mode_chng .= " with mode: $to_mode_str" if $to_mode_str;
3842 $mode_chng .= "]</span>";
3843 print "<td>";
3844 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3845 hash_base=>$hash, file_name=>$diff->{'file'}),
3846 -class => "list"}, esc_path($diff->{'file'}));
3847 print "</td>\n";
3848 print "<td>$mode_chng</td>\n";
3849 print "<td class=\"link\">";
3850 if ($action eq 'commitdiff') {
3851 # link to patch
3852 $patchno++;
3853 print $cgi->a({-href => "#patch$patchno"}, "patch");
3854 print " | ";
3856 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3857 hash_base=>$hash, file_name=>$diff->{'file'})},
3858 "blob");
3859 print "</td>\n";
3861 } elsif ($diff->{'status'} eq "D") { # deleted
3862 my $mode_chng = "<span class=\"file_status deleted\">[deleted $from_file_type]</span>";
3863 print "<td>";
3864 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'from_id'},
3865 hash_base=>$parent, file_name=>$diff->{'file'}),
3866 -class => "list"}, esc_path($diff->{'file'}));
3867 print "</td>\n";
3868 print "<td>$mode_chng</td>\n";
3869 print "<td class=\"link\">";
3870 if ($action eq 'commitdiff') {
3871 # link to patch
3872 $patchno++;
3873 print $cgi->a({-href => "#patch$patchno"}, "patch");
3874 print " | ";
3876 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'from_id'},
3877 hash_base=>$parent, file_name=>$diff->{'file'})},
3878 "blob") . " | ";
3879 if ($have_blame) {
3880 print $cgi->a({-href => href(action=>"blame", hash_base=>$parent,
3881 file_name=>$diff->{'file'})},
3882 "blame") . " | ";
3884 print $cgi->a({-href => href(action=>"history", hash_base=>$parent,
3885 file_name=>$diff->{'file'})},
3886 "history");
3887 print "</td>\n";
3889 } elsif ($diff->{'status'} eq "M" || $diff->{'status'} eq "T") { # modified, or type changed
3890 my $mode_chnge = "";
3891 if ($diff->{'from_mode'} != $diff->{'to_mode'}) {
3892 $mode_chnge = "<span class=\"file_status mode_chnge\">[changed";
3893 if ($from_file_type ne $to_file_type) {
3894 $mode_chnge .= " from $from_file_type to $to_file_type";
3896 if (($from_mode_oct & 0777) != ($to_mode_oct & 0777)) {
3897 if ($from_mode_str && $to_mode_str) {
3898 $mode_chnge .= " mode: $from_mode_str->$to_mode_str";
3899 } elsif ($to_mode_str) {
3900 $mode_chnge .= " mode: $to_mode_str";
3903 $mode_chnge .= "]</span>\n";
3905 print "<td>";
3906 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3907 hash_base=>$hash, file_name=>$diff->{'file'}),
3908 -class => "list"}, esc_path($diff->{'file'}));
3909 print "</td>\n";
3910 print "<td>$mode_chnge</td>\n";
3911 print "<td class=\"link\">";
3912 if ($action eq 'commitdiff') {
3913 # link to patch
3914 $patchno++;
3915 print $cgi->a({-href => "#patch$patchno"}, "patch") .
3916 " | ";
3917 } elsif ($diff->{'to_id'} ne $diff->{'from_id'}) {
3918 # "commit" view and modified file (not onlu mode changed)
3919 print $cgi->a({-href => href(action=>"blobdiff",
3920 hash=>$diff->{'to_id'}, hash_parent=>$diff->{'from_id'},
3921 hash_base=>$hash, hash_parent_base=>$parent,
3922 file_name=>$diff->{'file'})},
3923 "diff") .
3924 " | ";
3926 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3927 hash_base=>$hash, file_name=>$diff->{'file'})},
3928 "blob") . " | ";
3929 if ($have_blame) {
3930 print $cgi->a({-href => href(action=>"blame", hash_base=>$hash,
3931 file_name=>$diff->{'file'})},
3932 "blame") . " | ";
3934 print $cgi->a({-href => href(action=>"history", hash_base=>$hash,
3935 file_name=>$diff->{'file'})},
3936 "history");
3937 print "</td>\n";
3939 } elsif ($diff->{'status'} eq "R" || $diff->{'status'} eq "C") { # renamed or copied
3940 my %status_name = ('R' => 'moved', 'C' => 'copied');
3941 my $nstatus = $status_name{$diff->{'status'}};
3942 my $mode_chng = "";
3943 if ($diff->{'from_mode'} != $diff->{'to_mode'}) {
3944 # mode also for directories, so we cannot use $to_mode_str
3945 $mode_chng = sprintf(", mode: %04o", $to_mode_oct & 0777);
3947 print "<td>" .
3948 $cgi->a({-href => href(action=>"blob", hash_base=>$hash,
3949 hash=>$diff->{'to_id'}, file_name=>$diff->{'to_file'}),
3950 -class => "list"}, esc_path($diff->{'to_file'})) . "</td>\n" .
3951 "<td><span class=\"file_status $nstatus\">[$nstatus from " .
3952 $cgi->a({-href => href(action=>"blob", hash_base=>$parent,
3953 hash=>$diff->{'from_id'}, file_name=>$diff->{'from_file'}),
3954 -class => "list"}, esc_path($diff->{'from_file'})) .
3955 " with " . (int $diff->{'similarity'}) . "% similarity$mode_chng]</span></td>\n" .
3956 "<td class=\"link\">";
3957 if ($action eq 'commitdiff') {
3958 # link to patch
3959 $patchno++;
3960 print $cgi->a({-href => "#patch$patchno"}, "patch") .
3961 " | ";
3962 } elsif ($diff->{'to_id'} ne $diff->{'from_id'}) {
3963 # "commit" view and modified file (not only pure rename or copy)
3964 print $cgi->a({-href => href(action=>"blobdiff",
3965 hash=>$diff->{'to_id'}, hash_parent=>$diff->{'from_id'},
3966 hash_base=>$hash, hash_parent_base=>$parent,
3967 file_name=>$diff->{'to_file'}, file_parent=>$diff->{'from_file'})},
3968 "diff") .
3969 " | ";
3971 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3972 hash_base=>$parent, file_name=>$diff->{'to_file'})},
3973 "blob") . " | ";
3974 if ($have_blame) {
3975 print $cgi->a({-href => href(action=>"blame", hash_base=>$hash,
3976 file_name=>$diff->{'to_file'})},
3977 "blame") . " | ";
3979 print $cgi->a({-href => href(action=>"history", hash_base=>$hash,
3980 file_name=>$diff->{'to_file'})},
3981 "history");
3982 print "</td>\n";
3984 } # we should not encounter Unmerged (U) or Unknown (X) status
3985 print "</tr>\n";
3987 print "</tbody>" if $has_header;
3988 print "</table>\n";
3991 sub git_patchset_body {
3992 my ($fd, $difftree, $hash, @hash_parents) = @_;
3993 my ($hash_parent) = $hash_parents[0];
3995 my $is_combined = (@hash_parents > 1);
3996 my $patch_idx = 0;
3997 my $patch_number = 0;
3998 my $patch_line;
3999 my $diffinfo;
4000 my $to_name;
4001 my (%from, %to);
4003 print "<div class=\"patchset\">\n";
4005 # skip to first patch
4006 while ($patch_line = <$fd>) {
4007 chomp $patch_line;
4009 last if ($patch_line =~ m/^diff /);
4012 PATCH:
4013 while ($patch_line) {
4015 # parse "git diff" header line
4016 if ($patch_line =~ m/^diff --git (\"(?:[^\\\"]*(?:\\.[^\\\"]*)*)\"|[^ "]*) (.*)$/) {
4017 # $1 is from_name, which we do not use
4018 $to_name = unquote($2);
4019 $to_name =~ s!^b/!!;
4020 } elsif ($patch_line =~ m/^diff --(cc|combined) ("?.*"?)$/) {
4021 # $1 is 'cc' or 'combined', which we do not use
4022 $to_name = unquote($2);
4023 } else {
4024 $to_name = undef;
4027 # check if current patch belong to current raw line
4028 # and parse raw git-diff line if needed
4029 if (is_patch_split($diffinfo, { 'to_file' => $to_name })) {
4030 # this is continuation of a split patch
4031 print "<div class=\"patch cont\">\n";
4032 } else {
4033 # advance raw git-diff output if needed
4034 $patch_idx++ if defined $diffinfo;
4036 # read and prepare patch information
4037 $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
4039 # compact combined diff output can have some patches skipped
4040 # find which patch (using pathname of result) we are at now;
4041 if ($is_combined) {
4042 while ($to_name ne $diffinfo->{'to_file'}) {
4043 print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n" .
4044 format_diff_cc_simplified($diffinfo, @hash_parents) .
4045 "</div>\n"; # class="patch"
4047 $patch_idx++;
4048 $patch_number++;
4050 last if $patch_idx > $#$difftree;
4051 $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
4055 # modifies %from, %to hashes
4056 parse_from_to_diffinfo($diffinfo, \%from, \%to, @hash_parents);
4058 # this is first patch for raw difftree line with $patch_idx index
4059 # we index @$difftree array from 0, but number patches from 1
4060 print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n";
4063 # git diff header
4064 #assert($patch_line =~ m/^diff /) if DEBUG;
4065 #assert($patch_line !~ m!$/$!) if DEBUG; # is chomp-ed
4066 $patch_number++;
4067 # print "git diff" header
4068 print format_git_diff_header_line($patch_line, $diffinfo,
4069 \%from, \%to);
4071 # print extended diff header
4072 print "<div class=\"diff extended_header\">\n";
4073 EXTENDED_HEADER:
4074 while ($patch_line = <$fd>) {
4075 chomp $patch_line;
4077 last EXTENDED_HEADER if ($patch_line =~ m/^--- |^diff /);
4079 print format_extended_diff_header_line($patch_line, $diffinfo,
4080 \%from, \%to);
4082 print "</div>\n"; # class="diff extended_header"
4084 # from-file/to-file diff header
4085 if (! $patch_line) {
4086 print "</div>\n"; # class="patch"
4087 last PATCH;
4089 next PATCH if ($patch_line =~ m/^diff /);
4090 #assert($patch_line =~ m/^---/) if DEBUG;
4092 my $last_patch_line = $patch_line;
4093 $patch_line = <$fd>;
4094 chomp $patch_line;
4095 #assert($patch_line =~ m/^\+\+\+/) if DEBUG;
4097 print format_diff_from_to_header($last_patch_line, $patch_line,
4098 $diffinfo, \%from, \%to,
4099 @hash_parents);
4101 # the patch itself
4102 LINE:
4103 while ($patch_line = <$fd>) {
4104 chomp $patch_line;
4106 next PATCH if ($patch_line =~ m/^diff /);
4108 print format_diff_line($patch_line, \%from, \%to);
4111 } continue {
4112 print "</div>\n"; # class="patch"
4115 # for compact combined (--cc) format, with chunk and patch simpliciaction
4116 # patchset might be empty, but there might be unprocessed raw lines
4117 for (++$patch_idx if $patch_number > 0;
4118 $patch_idx < @$difftree;
4119 ++$patch_idx) {
4120 # read and prepare patch information
4121 $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
4123 # generate anchor for "patch" links in difftree / whatchanged part
4124 print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n" .
4125 format_diff_cc_simplified($diffinfo, @hash_parents) .
4126 "</div>\n"; # class="patch"
4128 $patch_number++;
4131 if ($patch_number == 0) {
4132 if (@hash_parents > 1) {
4133 print "<div class=\"diff nodifferences\">Trivial merge</div>\n";
4134 } else {
4135 print "<div class=\"diff nodifferences\">No differences found</div>\n";
4139 print "</div>\n"; # class="patchset"
4142 # . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
4144 # fills project list info (age, description, owner, forks) for each
4145 # project in the list, removing invalid projects from returned list
4146 # NOTE: modifies $projlist, but does not remove entries from it
4147 sub fill_project_list_info {
4148 my ($projlist, $check_forks) = @_;
4149 my @projects;
4151 my $show_ctags = gitweb_check_feature('ctags');
4152 PROJECT:
4153 foreach my $pr (@$projlist) {
4154 my (@activity) = git_get_last_activity($pr->{'path'});
4155 unless (@activity) {
4156 next PROJECT;
4158 ($pr->{'age'}, $pr->{'age_string'}) = @activity;
4159 if (!defined $pr->{'descr'}) {
4160 my $descr = git_get_project_description($pr->{'path'}) || "";
4161 $descr = to_utf8($descr);
4162 $pr->{'descr_long'} = $descr;
4163 $pr->{'descr'} = chop_str($descr, $projects_list_description_width, 5);
4165 if (!defined $pr->{'owner'}) {
4166 $pr->{'owner'} = git_get_project_owner("$pr->{'path'}") || "";
4168 if ($check_forks) {
4169 my $pname = $pr->{'path'};
4170 if (($pname =~ s/\.git$//) &&
4171 ($pname !~ /\/$/) &&
4172 (-d "$projectroot/$pname")) {
4173 $pr->{'forks'} = "-d $projectroot/$pname";
4174 } else {
4175 $pr->{'forks'} = 0;
4178 $show_ctags and $pr->{'ctags'} = git_get_project_ctags($pr->{'path'});
4179 push @projects, $pr;
4182 return @projects;
4185 # print 'sort by' <th> element, generating 'sort by $name' replay link
4186 # if that order is not selected
4187 sub print_sort_th {
4188 my ($name, $order, $header) = @_;
4189 $header ||= ucfirst($name);
4191 if ($order eq $name) {
4192 print "<th>$header</th>\n";
4193 } else {
4194 print "<th>" .
4195 $cgi->a({-href => href(-replay=>1, order=>$name),
4196 -class => "header"}, $header) .
4197 "</th>\n";
4201 sub git_project_list_body {
4202 # actually uses global variable $project
4203 my ($projlist, $order, $from, $to, $extra, $no_header) = @_;
4205 my $check_forks = gitweb_check_feature('forks');
4206 my @projects = fill_project_list_info($projlist, $check_forks);
4208 $order ||= $default_projects_order;
4209 $from = 0 unless defined $from;
4210 $to = $#projects if (!defined $to || $#projects < $to);
4212 my %order_info = (
4213 project => { key => 'path', type => 'str' },
4214 descr => { key => 'descr_long', type => 'str' },
4215 owner => { key => 'owner', type => 'str' },
4216 age => { key => 'age', type => 'num' }
4218 my $oi = $order_info{$order};
4219 if ($oi->{'type'} eq 'str') {
4220 @projects = sort {$a->{$oi->{'key'}} cmp $b->{$oi->{'key'}}} @projects;
4221 } else {
4222 @projects = sort {$a->{$oi->{'key'}} <=> $b->{$oi->{'key'}}} @projects;
4225 my $show_ctags = gitweb_check_feature('ctags');
4226 if ($show_ctags) {
4227 my %ctags;
4228 foreach my $p (@projects) {
4229 foreach my $ct (keys %{$p->{'ctags'}}) {
4230 $ctags{$ct} += $p->{'ctags'}->{$ct};
4233 my $cloud = git_populate_project_tagcloud(\%ctags);
4234 print git_show_project_tagcloud($cloud, 64);
4237 print "<table class=\"project_list\">\n";
4238 unless ($no_header) {
4239 print "<tr>\n";
4240 if ($check_forks) {
4241 print "<th></th>\n";
4243 print_sort_th('project', $order, 'Project');
4244 print_sort_th('descr', $order, 'Description');
4245 print_sort_th('owner', $order, 'Owner');
4246 print_sort_th('age', $order, 'Last Change');
4247 print "<th></th>\n" . # for links
4248 "</tr>\n";
4250 my $alternate = 1;
4251 my $tagfilter = $cgi->param('by_tag');
4252 for (my $i = $from; $i <= $to; $i++) {
4253 my $pr = $projects[$i];
4255 next if $tagfilter and $show_ctags and not grep { lc $_ eq lc $tagfilter } keys %{$pr->{'ctags'}};
4256 next if $searchtext and not $pr->{'path'} =~ /$searchtext/
4257 and not $pr->{'descr_long'} =~ /$searchtext/;
4258 # Weed out forks or non-matching entries of search
4259 if ($check_forks) {
4260 my $forkbase = $project; $forkbase ||= ''; $forkbase =~ s#\.git$#/#;
4261 $forkbase="^$forkbase" if $forkbase;
4262 next if not $searchtext and not $tagfilter and $show_ctags
4263 and $pr->{'path'} =~ m#$forkbase.*/.*#; # regexp-safe
4266 if ($alternate) {
4267 print "<tr class=\"dark\">\n";
4268 } else {
4269 print "<tr class=\"light\">\n";
4271 $alternate ^= 1;
4272 if ($check_forks) {
4273 print "<td>";
4274 if ($pr->{'forks'}) {
4275 print "<!-- $pr->{'forks'} -->\n";
4276 print $cgi->a({-href => href(project=>$pr->{'path'}, action=>"forks")}, "+");
4278 print "</td>\n";
4280 print "<td>" . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary"),
4281 -class => "list"}, esc_html($pr->{'path'})) . "</td>\n" .
4282 "<td>" . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary"),
4283 -class => "list", -title => $pr->{'descr_long'}},
4284 esc_html($pr->{'descr'})) . "</td>\n" .
4285 "<td><i>" . chop_and_escape_str($pr->{'owner'}, 15) . "</i></td>\n";
4286 print "<td class=\"". age_class($pr->{'age'}) . "\">" .
4287 (defined $pr->{'age_string'} ? $pr->{'age_string'} : "No commits") . "</td>\n" .
4288 "<td class=\"link\">" .
4289 $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary")}, "summary") . " | " .
4290 $cgi->a({-href => href(project=>$pr->{'path'}, action=>"shortlog")}, "shortlog") . " | " .
4291 $cgi->a({-href => href(project=>$pr->{'path'}, action=>"log")}, "log") . " | " .
4292 $cgi->a({-href => href(project=>$pr->{'path'}, action=>"tree")}, "tree") .
4293 ($pr->{'forks'} ? " | " . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"forks")}, "forks") : '') .
4294 "</td>\n" .
4295 "</tr>\n";
4297 if (defined $extra) {
4298 print "<tr>\n";
4299 if ($check_forks) {
4300 print "<td></td>\n";
4302 print "<td colspan=\"5\">$extra</td>\n" .
4303 "</tr>\n";
4305 print "</table>\n";
4308 sub git_shortlog_body {
4309 # uses global variable $project
4310 my ($commitlist, $from, $to, $refs, $extra) = @_;
4312 $from = 0 unless defined $from;
4313 $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
4315 print "<table class=\"shortlog\">\n";
4316 my $alternate = 1;
4317 for (my $i = $from; $i <= $to; $i++) {
4318 my %co = %{$commitlist->[$i]};
4319 my $commit = $co{'id'};
4320 my $ref = format_ref_marker($refs, $commit);
4321 if ($alternate) {
4322 print "<tr class=\"dark\">\n";
4323 } else {
4324 print "<tr class=\"light\">\n";
4326 $alternate ^= 1;
4327 # git_summary() used print "<td><i>$co{'age_string'}</i></td>\n" .
4328 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
4329 format_author_html('td', \%co, 10) . "<td>";
4330 print format_subject_html($co{'title'}, $co{'title_short'},
4331 href(action=>"commit", hash=>$commit), $ref);
4332 print "</td>\n" .
4333 "<td class=\"link\">" .
4334 $cgi->a({-href => href(action=>"commit", hash=>$commit)}, "commit") . " | " .
4335 $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff") . " | " .
4336 $cgi->a({-href => href(action=>"tree", hash=>$commit, hash_base=>$commit)}, "tree");
4337 my $snapshot_links = format_snapshot_links($commit);
4338 if (defined $snapshot_links) {
4339 print " | " . $snapshot_links;
4341 print "</td>\n" .
4342 "</tr>\n";
4344 if (defined $extra) {
4345 print "<tr>\n" .
4346 "<td colspan=\"4\">$extra</td>\n" .
4347 "</tr>\n";
4349 print "</table>\n";
4352 sub git_history_body {
4353 # Warning: assumes constant type (blob or tree) during history
4354 my ($commitlist, $from, $to, $refs, $hash_base, $ftype, $extra) = @_;
4356 $from = 0 unless defined $from;
4357 $to = $#{$commitlist} unless (defined $to && $to <= $#{$commitlist});
4359 print "<table class=\"history\">\n";
4360 my $alternate = 1;
4361 for (my $i = $from; $i <= $to; $i++) {
4362 my %co = %{$commitlist->[$i]};
4363 if (!%co) {
4364 next;
4366 my $commit = $co{'id'};
4368 my $ref = format_ref_marker($refs, $commit);
4370 if ($alternate) {
4371 print "<tr class=\"dark\">\n";
4372 } else {
4373 print "<tr class=\"light\">\n";
4375 $alternate ^= 1;
4376 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
4377 # shortlog: format_author_html('td', \%co, 10)
4378 format_author_html('td', \%co, 15, 3) . "<td>";
4379 # originally git_history used chop_str($co{'title'}, 50)
4380 print format_subject_html($co{'title'}, $co{'title_short'},
4381 href(action=>"commit", hash=>$commit), $ref);
4382 print "</td>\n" .
4383 "<td class=\"link\">" .
4384 $cgi->a({-href => href(action=>$ftype, hash_base=>$commit, file_name=>$file_name)}, $ftype) . " | " .
4385 $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff");
4387 if ($ftype eq 'blob') {
4388 my $blob_current = git_get_hash_by_path($hash_base, $file_name);
4389 my $blob_parent = git_get_hash_by_path($commit, $file_name);
4390 if (defined $blob_current && defined $blob_parent &&
4391 $blob_current ne $blob_parent) {
4392 print " | " .
4393 $cgi->a({-href => href(action=>"blobdiff",
4394 hash=>$blob_current, hash_parent=>$blob_parent,
4395 hash_base=>$hash_base, hash_parent_base=>$commit,
4396 file_name=>$file_name)},
4397 "diff to current");
4400 print "</td>\n" .
4401 "</tr>\n";
4403 if (defined $extra) {
4404 print "<tr>\n" .
4405 "<td colspan=\"4\">$extra</td>\n" .
4406 "</tr>\n";
4408 print "</table>\n";
4411 sub git_tags_body {
4412 # uses global variable $project
4413 my ($taglist, $from, $to, $extra) = @_;
4414 $from = 0 unless defined $from;
4415 $to = $#{$taglist} if (!defined $to || $#{$taglist} < $to);
4417 print "<table class=\"tags\">\n";
4418 my $alternate = 1;
4419 for (my $i = $from; $i <= $to; $i++) {
4420 my $entry = $taglist->[$i];
4421 my %tag = %$entry;
4422 my $comment = $tag{'subject'};
4423 my $comment_short;
4424 if (defined $comment) {
4425 $comment_short = chop_str($comment, 30, 5);
4427 if ($alternate) {
4428 print "<tr class=\"dark\">\n";
4429 } else {
4430 print "<tr class=\"light\">\n";
4432 $alternate ^= 1;
4433 if (defined $tag{'age'}) {
4434 print "<td><i>$tag{'age'}</i></td>\n";
4435 } else {
4436 print "<td></td>\n";
4438 print "<td>" .
4439 $cgi->a({-href => href(action=>$tag{'reftype'}, hash=>$tag{'refid'}),
4440 -class => "list name"}, esc_html($tag{'name'})) .
4441 "</td>\n" .
4442 "<td>";
4443 if (defined $comment) {
4444 print format_subject_html($comment, $comment_short,
4445 href(action=>"tag", hash=>$tag{'id'}));
4447 print "</td>\n" .
4448 "<td class=\"selflink\">";
4449 if ($tag{'type'} eq "tag") {
4450 print $cgi->a({-href => href(action=>"tag", hash=>$tag{'id'})}, "tag");
4451 } else {
4452 print "&nbsp;";
4454 print "</td>\n" .
4455 "<td class=\"link\">" . " | " .
4456 $cgi->a({-href => href(action=>$tag{'reftype'}, hash=>$tag{'refid'})}, $tag{'reftype'});
4457 if ($tag{'reftype'} eq "commit") {
4458 print " | " . $cgi->a({-href => href(action=>"shortlog", hash=>$tag{'fullname'})}, "shortlog") .
4459 " | " . $cgi->a({-href => href(action=>"log", hash=>$tag{'fullname'})}, "log");
4460 } elsif ($tag{'reftype'} eq "blob") {
4461 print " | " . $cgi->a({-href => href(action=>"blob_plain", hash=>$tag{'refid'})}, "raw");
4463 print "</td>\n" .
4464 "</tr>";
4466 if (defined $extra) {
4467 print "<tr>\n" .
4468 "<td colspan=\"5\">$extra</td>\n" .
4469 "</tr>\n";
4471 print "</table>\n";
4474 sub git_heads_body {
4475 # uses global variable $project
4476 my ($headlist, $head, $from, $to, $extra) = @_;
4477 $from = 0 unless defined $from;
4478 $to = $#{$headlist} if (!defined $to || $#{$headlist} < $to);
4480 print "<table class=\"heads\">\n";
4481 my $alternate = 1;
4482 for (my $i = $from; $i <= $to; $i++) {
4483 my $entry = $headlist->[$i];
4484 my %ref = %$entry;
4485 my $curr = $ref{'id'} eq $head;
4486 if ($alternate) {
4487 print "<tr class=\"dark\">\n";
4488 } else {
4489 print "<tr class=\"light\">\n";
4491 $alternate ^= 1;
4492 print "<td><i>$ref{'age'}</i></td>\n" .
4493 ($curr ? "<td class=\"current_head\">" : "<td>") .
4494 $cgi->a({-href => href(action=>"shortlog", hash=>$ref{'fullname'}),
4495 -class => "list name"},esc_html($ref{'name'})) .
4496 "</td>\n" .
4497 "<td class=\"link\">" .
4498 $cgi->a({-href => href(action=>"shortlog", hash=>$ref{'fullname'})}, "shortlog") . " | " .
4499 $cgi->a({-href => href(action=>"log", hash=>$ref{'fullname'})}, "log") . " | " .
4500 $cgi->a({-href => href(action=>"tree", hash=>$ref{'fullname'}, hash_base=>$ref{'name'})}, "tree") .
4501 "</td>\n" .
4502 "</tr>";
4504 if (defined $extra) {
4505 print "<tr>\n" .
4506 "<td colspan=\"3\">$extra</td>\n" .
4507 "</tr>\n";
4509 print "</table>\n";
4512 sub git_search_grep_body {
4513 my ($commitlist, $from, $to, $extra) = @_;
4514 $from = 0 unless defined $from;
4515 $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
4517 print "<table class=\"commit_search\">\n";
4518 my $alternate = 1;
4519 for (my $i = $from; $i <= $to; $i++) {
4520 my %co = %{$commitlist->[$i]};
4521 if (!%co) {
4522 next;
4524 my $commit = $co{'id'};
4525 if ($alternate) {
4526 print "<tr class=\"dark\">\n";
4527 } else {
4528 print "<tr class=\"light\">\n";
4530 $alternate ^= 1;
4531 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
4532 format_author_html('td', \%co, 15, 5) .
4533 "<td>" .
4534 $cgi->a({-href => href(action=>"commit", hash=>$co{'id'}),
4535 -class => "list subject"},
4536 chop_and_escape_str($co{'title'}, 50) . "<br/>");
4537 my $comment = $co{'comment'};
4538 foreach my $line (@$comment) {
4539 if ($line =~ m/^(.*?)($search_regexp)(.*)$/i) {
4540 my ($lead, $match, $trail) = ($1, $2, $3);
4541 $match = chop_str($match, 70, 5, 'center');
4542 my $contextlen = int((80 - length($match))/2);
4543 $contextlen = 30 if ($contextlen > 30);
4544 $lead = chop_str($lead, $contextlen, 10, 'left');
4545 $trail = chop_str($trail, $contextlen, 10, 'right');
4547 $lead = esc_html($lead);
4548 $match = esc_html($match);
4549 $trail = esc_html($trail);
4551 print "$lead<span class=\"match\">$match</span>$trail<br />";
4554 print "</td>\n" .
4555 "<td class=\"link\">" .
4556 $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") .
4557 " | " .
4558 $cgi->a({-href => href(action=>"commitdiff", hash=>$co{'id'})}, "commitdiff") .
4559 " | " .
4560 $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree");
4561 print "</td>\n" .
4562 "</tr>\n";
4564 if (defined $extra) {
4565 print "<tr>\n" .
4566 "<td colspan=\"3\">$extra</td>\n" .
4567 "</tr>\n";
4569 print "</table>\n";
4572 ## ======================================================================
4573 ## ======================================================================
4574 ## actions
4576 sub git_project_list {
4577 my $order = $input_params{'order'};
4578 if (defined $order && $order !~ m/none|project|descr|owner|age/) {
4579 die_error(400, "Unknown order parameter");
4582 my @list = git_get_projects_list();
4583 if (!@list) {
4584 die_error(404, "No projects found");
4587 git_header_html();
4588 if (-f $home_text) {
4589 print "<div class=\"index_include\">\n";
4590 insert_file($home_text);
4591 print "</div>\n";
4593 print $cgi->startform(-method => "get") .
4594 "<p class=\"projsearch\">Search:\n" .
4595 $cgi->textfield(-name => "s", -value => $searchtext) . "\n" .
4596 "</p>" .
4597 $cgi->end_form() . "\n";
4598 git_project_list_body(\@list, $order);
4599 git_footer_html();
4602 sub git_forks {
4603 my $order = $input_params{'order'};
4604 if (defined $order && $order !~ m/none|project|descr|owner|age/) {
4605 die_error(400, "Unknown order parameter");
4608 my @list = git_get_projects_list($project);
4609 if (!@list) {
4610 die_error(404, "No forks found");
4613 git_header_html();
4614 git_print_page_nav('','');
4615 git_print_header_div('summary', "$project forks");
4616 git_project_list_body(\@list, $order);
4617 git_footer_html();
4620 sub git_project_index {
4621 my @projects = git_get_projects_list($project);
4623 print $cgi->header(
4624 -type => 'text/plain',
4625 -charset => 'utf-8',
4626 -content_disposition => 'inline; filename="index.aux"');
4628 foreach my $pr (@projects) {
4629 if (!exists $pr->{'owner'}) {
4630 $pr->{'owner'} = git_get_project_owner("$pr->{'path'}");
4633 my ($path, $owner) = ($pr->{'path'}, $pr->{'owner'});
4634 # quote as in CGI::Util::encode, but keep the slash, and use '+' for ' '
4635 $path =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf("%%%02X", ord($1))/eg;
4636 $owner =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf("%%%02X", ord($1))/eg;
4637 $path =~ s/ /\+/g;
4638 $owner =~ s/ /\+/g;
4640 print "$path $owner\n";
4644 sub git_summary {
4645 my $descr = git_get_project_description($project) || "none";
4646 my %co = parse_commit("HEAD");
4647 my %cd = %co ? parse_date($co{'committer_epoch'}, $co{'committer_tz'}) : ();
4648 my $head = $co{'id'};
4650 my $owner = git_get_project_owner($project);
4652 my $refs = git_get_references();
4653 # These get_*_list functions return one more to allow us to see if
4654 # there are more ...
4655 my @taglist = git_get_tags_list(16);
4656 my @headlist = git_get_heads_list(16);
4657 my @forklist;
4658 my $check_forks = gitweb_check_feature('forks');
4660 if ($check_forks) {
4661 @forklist = git_get_projects_list($project);
4664 git_header_html();
4665 git_print_page_nav('summary','', $head);
4667 print "<div class=\"title\">&nbsp;</div>\n";
4668 print "<table class=\"projects_list\">\n" .
4669 "<tr id=\"metadata_desc\"><td>description</td><td>" . esc_html($descr) . "</td></tr>\n" .
4670 "<tr id=\"metadata_owner\"><td>owner</td><td>" . esc_html($owner) . "</td></tr>\n";
4671 if (defined $cd{'rfc2822'}) {
4672 print "<tr id=\"metadata_lchange\"><td>last change</td><td>$cd{'rfc2822'}</td></tr>\n";
4675 # use per project git URL list in $projectroot/$project/cloneurl
4676 # or make project git URL from git base URL and project name
4677 my $url_tag = "URL";
4678 my @url_list = git_get_project_url_list($project);
4679 @url_list = map { "$_/$project" } @git_base_url_list unless @url_list;
4680 foreach my $git_url (@url_list) {
4681 next unless $git_url;
4682 print "<tr class=\"metadata_url\"><td>$url_tag</td><td>$git_url</td></tr>\n";
4683 $url_tag = "";
4686 # Tag cloud
4687 my $show_ctags = gitweb_check_feature('ctags');
4688 if ($show_ctags) {
4689 my $ctags = git_get_project_ctags($project);
4690 my $cloud = git_populate_project_tagcloud($ctags);
4691 print "<tr id=\"metadata_ctags\"><td>Content tags:<br />";
4692 print "</td>\n<td>" unless %$ctags;
4693 print "<form action=\"$show_ctags\" method=\"post\"><input type=\"hidden\" name=\"p\" value=\"$project\" />Add: <input type=\"text\" name=\"t\" size=\"8\" /></form>";
4694 print "</td>\n<td>" if %$ctags;
4695 print git_show_project_tagcloud($cloud, 48);
4696 print "</td></tr>";
4699 print "</table>\n";
4701 # If XSS prevention is on, we don't include README.html.
4702 # TODO: Allow a readme in some safe format.
4703 if (!$prevent_xss && -s "$projectroot/$project/README.html") {
4704 print "<div class=\"title\">readme</div>\n" .
4705 "<div class=\"readme\">\n";
4706 insert_file("$projectroot/$project/README.html");
4707 print "\n</div>\n"; # class="readme"
4710 # we need to request one more than 16 (0..15) to check if
4711 # those 16 are all
4712 my @commitlist = $head ? parse_commits($head, 17) : ();
4713 if (@commitlist) {
4714 git_print_header_div('shortlog');
4715 git_shortlog_body(\@commitlist, 0, 15, $refs,
4716 $#commitlist <= 15 ? undef :
4717 $cgi->a({-href => href(action=>"shortlog")}, "..."));
4720 if (@taglist) {
4721 git_print_header_div('tags');
4722 git_tags_body(\@taglist, 0, 15,
4723 $#taglist <= 15 ? undef :
4724 $cgi->a({-href => href(action=>"tags")}, "..."));
4727 if (@headlist) {
4728 git_print_header_div('heads');
4729 git_heads_body(\@headlist, $head, 0, 15,
4730 $#headlist <= 15 ? undef :
4731 $cgi->a({-href => href(action=>"heads")}, "..."));
4734 if (@forklist) {
4735 git_print_header_div('forks');
4736 git_project_list_body(\@forklist, 'age', 0, 15,
4737 $#forklist <= 15 ? undef :
4738 $cgi->a({-href => href(action=>"forks")}, "..."),
4739 'no_header');
4742 git_footer_html();
4745 sub git_tag {
4746 my $head = git_get_head_hash($project);
4747 git_header_html();
4748 git_print_page_nav('','', $head,undef,$head);
4749 my %tag = parse_tag($hash);
4751 if (! %tag) {
4752 die_error(404, "Unknown tag object");
4755 git_print_header_div('commit', esc_html($tag{'name'}), $hash);
4756 print "<div class=\"title_text\">\n" .
4757 "<table class=\"object_header\">\n" .
4758 "<tr>\n" .
4759 "<td>object</td>\n" .
4760 "<td>" . $cgi->a({-class => "list", -href => href(action=>$tag{'type'}, hash=>$tag{'object'})},
4761 $tag{'object'}) . "</td>\n" .
4762 "<td class=\"link\">" . $cgi->a({-href => href(action=>$tag{'type'}, hash=>$tag{'object'})},
4763 $tag{'type'}) . "</td>\n" .
4764 "</tr>\n";
4765 if (defined($tag{'author'})) {
4766 git_print_authorship_rows(\%tag, 'author');
4768 print "</table>\n\n" .
4769 "</div>\n";
4770 print "<div class=\"page_body\">";
4771 my $comment = $tag{'comment'};
4772 foreach my $line (@$comment) {
4773 chomp $line;
4774 print esc_html($line, -nbsp=>1) . "<br/>\n";
4776 print "</div>\n";
4777 git_footer_html();
4780 sub git_blame {
4781 # permissions
4782 gitweb_check_feature('blame')
4783 or die_error(403, "Blame view not allowed");
4785 # error checking
4786 die_error(400, "No file name given") unless $file_name;
4787 $hash_base ||= git_get_head_hash($project);
4788 die_error(404, "Couldn't find base commit") unless $hash_base;
4789 my %co = parse_commit($hash_base)
4790 or die_error(404, "Commit not found");
4791 my $ftype = "blob";
4792 if (!defined $hash) {
4793 $hash = git_get_hash_by_path($hash_base, $file_name, "blob")
4794 or die_error(404, "Error looking up file");
4795 } else {
4796 $ftype = git_get_type($hash);
4797 if ($ftype !~ "blob") {
4798 die_error(400, "Object is not a blob");
4802 # run git-blame --porcelain
4803 open my $fd, "-|", git_cmd(), "blame", '-p',
4804 $hash_base, '--', $file_name
4805 or die_error(500, "Open git-blame failed");
4807 # page header
4808 git_header_html();
4809 my $formats_nav =
4810 $cgi->a({-href => href(action=>"blob", -replay=>1)},
4811 "blob") .
4812 " | " .
4813 $cgi->a({-href => href(action=>"history", -replay=>1)},
4814 "history") .
4815 " | " .
4816 $cgi->a({-href => href(action=>"blame", file_name=>$file_name)},
4817 "HEAD");
4818 git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
4819 git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
4820 git_print_page_path($file_name, $ftype, $hash_base);
4822 # page body
4823 my @rev_color = qw(light dark);
4824 my $num_colors = scalar(@rev_color);
4825 my $current_color = 0;
4826 my %metainfo = ();
4828 print <<HTML;
4829 <div class="page_body">
4830 <table class="blame">
4831 <tr><th>Commit</th><th>Line</th><th>Data</th></tr>
4832 HTML
4833 LINE:
4834 while (my $line = <$fd>) {
4835 chomp $line;
4836 # the header: <SHA-1> <src lineno> <dst lineno> [<lines in group>]
4837 # no <lines in group> for subsequent lines in group of lines
4838 my ($full_rev, $orig_lineno, $lineno, $group_size) =
4839 ($line =~ /^([0-9a-f]{40}) (\d+) (\d+)(?: (\d+))?$/);
4840 if (!exists $metainfo{$full_rev}) {
4841 $metainfo{$full_rev} = { 'nprevious' => 0 };
4843 my $meta = $metainfo{$full_rev};
4844 my $data;
4845 while ($data = <$fd>) {
4846 chomp $data;
4847 last if ($data =~ s/^\t//); # contents of line
4848 if ($data =~ /^(\S+)(?: (.*))?$/) {
4849 $meta->{$1} = $2 unless exists $meta->{$1};
4851 if ($data =~ /^previous /) {
4852 $meta->{'nprevious'}++;
4855 my $short_rev = substr($full_rev, 0, 8);
4856 my $author = $meta->{'author'};
4857 my %date =
4858 parse_date($meta->{'author-time'}, $meta->{'author-tz'});
4859 my $date = $date{'iso-tz'};
4860 if ($group_size) {
4861 $current_color = ($current_color + 1) % $num_colors;
4863 my $tr_class = $rev_color[$current_color];
4864 $tr_class .= ' boundary' if (exists $meta->{'boundary'});
4865 $tr_class .= ' no-previous' if ($meta->{'nprevious'} == 0);
4866 $tr_class .= ' multiple-previous' if ($meta->{'nprevious'} > 1);
4867 print "<tr id=\"l$lineno\" class=\"$tr_class\">\n";
4868 if ($group_size) {
4869 print "<td class=\"sha1\"";
4870 print " title=\"". esc_html($author) . ", $date\"";
4871 print " rowspan=\"$group_size\"" if ($group_size > 1);
4872 print ">";
4873 print $cgi->a({-href => href(action=>"commit",
4874 hash=>$full_rev,
4875 file_name=>$file_name)},
4876 esc_html($short_rev));
4877 if ($group_size >= 2) {
4878 my @author_initials = ($author =~ /\b([[:upper:]])\B/g);
4879 if (@author_initials) {
4880 print "<br />" .
4881 esc_html(join('', @author_initials));
4882 # or join('.', ...)
4885 print "</td>\n";
4887 # 'previous' <sha1 of parent commit> <filename at commit>
4888 if (exists $meta->{'previous'} &&
4889 $meta->{'previous'} =~ /^([a-fA-F0-9]{40}) (.*)$/) {
4890 $meta->{'parent'} = $1;
4891 $meta->{'file_parent'} = unquote($2);
4893 my $linenr_commit =
4894 exists($meta->{'parent'}) ?
4895 $meta->{'parent'} : $full_rev;
4896 my $linenr_filename =
4897 exists($meta->{'file_parent'}) ?
4898 $meta->{'file_parent'} : unquote($meta->{'filename'});
4899 my $blamed = href(action => 'blame',
4900 file_name => $linenr_filename,
4901 hash_base => $linenr_commit);
4902 print "<td class=\"linenr\">";
4903 print $cgi->a({ -href => "$blamed#l$orig_lineno",
4904 -class => "linenr" },
4905 esc_html($lineno));
4906 print "</td>";
4907 print "<td class=\"pre\">" . esc_html($data) . "</td>\n";
4908 print "</tr>\n";
4910 print "</table>\n";
4911 print "</div>";
4912 close $fd
4913 or print "Reading blob failed\n";
4915 # page footer
4916 git_footer_html();
4919 sub git_tags {
4920 my $head = git_get_head_hash($project);
4921 git_header_html();
4922 git_print_page_nav('','', $head,undef,$head);
4923 git_print_header_div('summary', $project);
4925 my @tagslist = git_get_tags_list();
4926 if (@tagslist) {
4927 git_tags_body(\@tagslist);
4929 git_footer_html();
4932 sub git_heads {
4933 my $head = git_get_head_hash($project);
4934 git_header_html();
4935 git_print_page_nav('','', $head,undef,$head);
4936 git_print_header_div('summary', $project);
4938 my @headslist = git_get_heads_list();
4939 if (@headslist) {
4940 git_heads_body(\@headslist, $head);
4942 git_footer_html();
4945 sub git_blob_plain {
4946 my $type = shift;
4947 my $expires;
4949 if (!defined $hash) {
4950 if (defined $file_name) {
4951 my $base = $hash_base || git_get_head_hash($project);
4952 $hash = git_get_hash_by_path($base, $file_name, "blob")
4953 or die_error(404, "Cannot find file");
4954 } else {
4955 die_error(400, "No file name defined");
4957 } elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) {
4958 # blobs defined by non-textual hash id's can be cached
4959 $expires = "+1d";
4962 open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
4963 or die_error(500, "Open git-cat-file blob '$hash' failed");
4965 # content-type (can include charset)
4966 $type = blob_contenttype($fd, $file_name, $type);
4968 # "save as" filename, even when no $file_name is given
4969 my $save_as = "$hash";
4970 if (defined $file_name) {
4971 $save_as = $file_name;
4972 } elsif ($type =~ m/^text\//) {
4973 $save_as .= '.txt';
4976 # With XSS prevention on, blobs of all types except a few known safe
4977 # ones are served with "Content-Disposition: attachment" to make sure
4978 # they don't run in our security domain. For certain image types,
4979 # blob view writes an <img> tag referring to blob_plain view, and we
4980 # want to be sure not to break that by serving the image as an
4981 # attachment (though Firefox 3 doesn't seem to care).
4982 my $sandbox = $prevent_xss &&
4983 $type !~ m!^(?:text/plain|image/(?:gif|png|jpeg))$!;
4985 print $cgi->header(
4986 -type => $type,
4987 -expires => $expires,
4988 -content_disposition =>
4989 ($sandbox ? 'attachment' : 'inline')
4990 . '; filename="' . $save_as . '"');
4991 local $/ = undef;
4992 binmode STDOUT, ':raw';
4993 print <$fd>;
4994 binmode STDOUT, ':utf8'; # as set at the beginning of gitweb.cgi
4995 close $fd;
4998 sub git_blob {
4999 my $expires;
5001 if (!defined $hash) {
5002 if (defined $file_name) {
5003 my $base = $hash_base || git_get_head_hash($project);
5004 $hash = git_get_hash_by_path($base, $file_name, "blob")
5005 or die_error(404, "Cannot find file");
5006 } else {
5007 die_error(400, "No file name defined");
5009 } elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) {
5010 # blobs defined by non-textual hash id's can be cached
5011 $expires = "+1d";
5014 my $have_blame = gitweb_check_feature('blame');
5015 open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
5016 or die_error(500, "Couldn't cat $file_name, $hash");
5017 my $mimetype = blob_mimetype($fd, $file_name);
5018 if ($mimetype !~ m!^(?:text/|image/(?:gif|png|jpeg)$)! && -B $fd) {
5019 close $fd;
5020 return git_blob_plain($mimetype);
5022 # we can have blame only for text/* mimetype
5023 $have_blame &&= ($mimetype =~ m!^text/!);
5025 git_header_html(undef, $expires);
5026 my $formats_nav = '';
5027 if (defined $hash_base && (my %co = parse_commit($hash_base))) {
5028 if (defined $file_name) {
5029 if ($have_blame) {
5030 $formats_nav .=
5031 $cgi->a({-href => href(action=>"blame", -replay=>1)},
5032 "blame") .
5033 " | ";
5035 $formats_nav .=
5036 $cgi->a({-href => href(action=>"history", -replay=>1)},
5037 "history") .
5038 " | " .
5039 $cgi->a({-href => href(action=>"blob_plain", -replay=>1)},
5040 "raw") .
5041 " | " .
5042 $cgi->a({-href => href(action=>"blob",
5043 hash_base=>"HEAD", file_name=>$file_name)},
5044 "HEAD");
5045 } else {
5046 $formats_nav .=
5047 $cgi->a({-href => href(action=>"blob_plain", -replay=>1)},
5048 "raw");
5050 git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
5051 git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
5052 } else {
5053 print "<div class=\"page_nav\">\n" .
5054 "<br/><br/></div>\n" .
5055 "<div class=\"title\">".esc_html($hash)."</div>\n";
5057 git_print_page_path($file_name, "blob", $hash_base);
5058 print "<div class=\"page_body\">\n";
5059 if ($mimetype =~ m!^image/!) {
5060 print qq!<img type="!.esc_attr($mimetype).qq!"!;
5061 if ($file_name) {
5062 print qq! alt="!.esc_attr($file_name).qq!" title="!.esc_attr($file_name).qq!"!;
5064 print qq! src="! .
5065 href(action=>"blob_plain", hash=>$hash,
5066 hash_base=>$hash_base, file_name=>$file_name) .
5067 qq!" />\n!;
5068 } else {
5069 my $nr;
5070 while (my $line = <$fd>) {
5071 chomp $line;
5072 $nr++;
5073 $line = untabify($line);
5074 printf "<div class=\"pre\"><a id=\"l%i\" href=\""
5075 . esc_attr(href(-replay => 1))
5076 . "#l%i\" class=\"linenr\">%4i</a> %s</div>\n",
5077 $nr, $nr, $nr, esc_html($line, -nbsp=>1);
5080 close $fd
5081 or print "Reading blob failed.\n";
5082 print "</div>";
5083 git_footer_html();
5086 sub git_tree {
5087 if (!defined $hash_base) {
5088 $hash_base = "HEAD";
5090 if (!defined $hash) {
5091 if (defined $file_name) {
5092 $hash = git_get_hash_by_path($hash_base, $file_name, "tree");
5093 } else {
5094 $hash = $hash_base;
5097 die_error(404, "No such tree") unless defined($hash);
5099 my @entries = ();
5101 local $/ = "\0";
5102 open my $fd, "-|", git_cmd(), "ls-tree", '-z', $hash
5103 or die_error(500, "Open git-ls-tree failed");
5104 @entries = map { chomp; $_ } <$fd>;
5105 close $fd
5106 or die_error(404, "Reading tree failed");
5109 my $refs = git_get_references();
5110 my $ref = format_ref_marker($refs, $hash_base);
5111 git_header_html();
5112 my $basedir = '';
5113 my $have_blame = gitweb_check_feature('blame');
5114 if (defined $hash_base && (my %co = parse_commit($hash_base))) {
5115 my @views_nav = ();
5116 if (defined $file_name) {
5117 push @views_nav,
5118 $cgi->a({-href => href(action=>"history", -replay=>1)},
5119 "history"),
5120 $cgi->a({-href => href(action=>"tree",
5121 hash_base=>"HEAD", file_name=>$file_name)},
5122 "HEAD"),
5124 my $snapshot_links = format_snapshot_links($hash);
5125 if (defined $snapshot_links) {
5126 # FIXME: Should be available when we have no hash base as well.
5127 push @views_nav, $snapshot_links;
5129 git_print_page_nav('tree','', $hash_base, undef, undef, join(' | ', @views_nav));
5130 git_print_header_div('commit', esc_html($co{'title'}) . $ref, $hash_base);
5131 } else {
5132 undef $hash_base;
5133 print "<div class=\"page_nav\">\n";
5134 print "<br/><br/></div>\n";
5135 print "<div class=\"title\">".esc_html($hash)."</div>\n";
5137 if (defined $file_name) {
5138 $basedir = $file_name;
5139 if ($basedir ne '' && substr($basedir, -1) ne '/') {
5140 $basedir .= '/';
5142 git_print_page_path($file_name, 'tree', $hash_base);
5144 print "<div class=\"page_body\">\n";
5145 print "<table class=\"tree\">\n";
5146 my $alternate = 1;
5147 # '..' (top directory) link if possible
5148 if (defined $hash_base &&
5149 defined $file_name && $file_name =~ m![^/]+$!) {
5150 if ($alternate) {
5151 print "<tr class=\"dark\">\n";
5152 } else {
5153 print "<tr class=\"light\">\n";
5155 $alternate ^= 1;
5157 my $up = $file_name;
5158 $up =~ s!/?[^/]+$!!;
5159 undef $up unless $up;
5160 # based on git_print_tree_entry
5161 print '<td class="mode">' . mode_str('040000') . "</td>\n";
5162 print '<td class="list">';
5163 print $cgi->a({-href => href(action=>"tree", hash_base=>$hash_base,
5164 file_name=>$up)},
5165 "..");
5166 print "</td>\n";
5167 print "<td class=\"link\"></td>\n";
5169 print "</tr>\n";
5171 foreach my $line (@entries) {
5172 my %t = parse_ls_tree_line($line, -z => 1);
5174 if ($alternate) {
5175 print "<tr class=\"dark\">\n";
5176 } else {
5177 print "<tr class=\"light\">\n";
5179 $alternate ^= 1;
5181 git_print_tree_entry(\%t, $basedir, $hash_base, $have_blame);
5183 print "</tr>\n";
5185 print "</table>\n" .
5186 "</div>";
5187 git_footer_html();
5190 sub git_snapshot {
5191 my $format = $input_params{'snapshot_format'};
5192 if (!@snapshot_fmts) {
5193 die_error(403, "Snapshots not allowed");
5195 # default to first supported snapshot format
5196 $format ||= $snapshot_fmts[0];
5197 if ($format !~ m/^[a-z0-9]+$/) {
5198 die_error(400, "Invalid snapshot format parameter");
5199 } elsif (!exists($known_snapshot_formats{$format})) {
5200 die_error(400, "Unknown snapshot format");
5201 } elsif ($known_snapshot_formats{$format}{'disabled'}) {
5202 die_error(403, "Snapshot format not allowed");
5203 } elsif (!grep($_ eq $format, @snapshot_fmts)) {
5204 die_error(403, "Unsupported snapshot format");
5207 if (!defined $hash) {
5208 $hash = git_get_head_hash($project);
5211 my $name = $project;
5212 $name =~ s,([^/])/*\.git$,$1,;
5213 $name = basename($name);
5214 my $filename = to_utf8($name);
5215 $name =~ s/\047/\047\\\047\047/g;
5216 my $cmd;
5217 $filename .= "-$hash$known_snapshot_formats{$format}{'suffix'}";
5218 $cmd = quote_command(
5219 git_cmd(), 'archive',
5220 "--format=$known_snapshot_formats{$format}{'format'}",
5221 "--prefix=$name/", $hash);
5222 if (exists $known_snapshot_formats{$format}{'compressor'}) {
5223 $cmd .= ' | ' . quote_command(@{$known_snapshot_formats{$format}{'compressor'}});
5226 print $cgi->header(
5227 -type => $known_snapshot_formats{$format}{'type'},
5228 -content_disposition => 'inline; filename="' . "$filename" . '"',
5229 -status => '200 OK');
5231 open my $fd, "-|", $cmd
5232 or die_error(500, "Execute git-archive failed");
5233 binmode STDOUT, ':raw';
5234 print <$fd>;
5235 binmode STDOUT, ':utf8'; # as set at the beginning of gitweb.cgi
5236 close $fd;
5239 sub git_log {
5240 my $head = git_get_head_hash($project);
5241 if (!defined $hash) {
5242 $hash = $head;
5244 if (!defined $page) {
5245 $page = 0;
5247 my $refs = git_get_references();
5249 my @commitlist = parse_commits($hash, 101, (100 * $page));
5251 my $paging_nav = format_paging_nav('log', $hash, $head, $page, $#commitlist >= 100);
5253 my ($patch_max) = gitweb_get_feature('patches');
5254 if ($patch_max) {
5255 if ($patch_max < 0 || @commitlist <= $patch_max) {
5256 $paging_nav .= " &sdot; " .
5257 $cgi->a({-href => href(action=>"patches", -replay=>1)},
5258 "patches");
5262 git_header_html();
5263 git_print_page_nav('log','', $hash,undef,undef, $paging_nav);
5265 if (!@commitlist) {
5266 my %co = parse_commit($hash);
5268 git_print_header_div('summary', $project);
5269 print "<div class=\"page_body\"> Last change $co{'age_string'}.<br/><br/></div>\n";
5271 my $to = ($#commitlist >= 99) ? (99) : ($#commitlist);
5272 for (my $i = 0; $i <= $to; $i++) {
5273 my %co = %{$commitlist[$i]};
5274 next if !%co;
5275 my $commit = $co{'id'};
5276 my $ref = format_ref_marker($refs, $commit);
5277 my %ad = parse_date($co{'author_epoch'});
5278 git_print_header_div('commit',
5279 "<span class=\"age\">$co{'age_string'}</span>" .
5280 esc_html($co{'title'}) . $ref,
5281 $commit);
5282 print "<div class=\"title_text\">\n" .
5283 "<div class=\"log_link\">\n" .
5284 $cgi->a({-href => href(action=>"commit", hash=>$commit)}, "commit") .
5285 " | " .
5286 $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff") .
5287 " | " .
5288 $cgi->a({-href => href(action=>"tree", hash=>$commit, hash_base=>$commit)}, "tree") .
5289 "<br/>\n" .
5290 "</div>\n";
5291 git_print_authorship(\%co, -tag => 'span');
5292 print "<br/>\n</div>\n";
5294 print "<div class=\"log_body\">\n";
5295 git_print_log($co{'comment'}, -final_empty_line=> 1);
5296 print "</div>\n";
5298 if ($#commitlist >= 100) {
5299 print "<div class=\"page_nav\">\n";
5300 print $cgi->a({-href => href(-replay=>1, page=>$page+1),
5301 -accesskey => "n", -title => "Alt-n"}, "next");
5302 print "</div>\n";
5304 git_footer_html();
5307 sub git_commit {
5308 $hash ||= $hash_base || "HEAD";
5309 my %co = parse_commit($hash)
5310 or die_error(404, "Unknown commit object");
5312 my $parent = $co{'parent'};
5313 my $parents = $co{'parents'}; # listref
5315 # we need to prepare $formats_nav before any parameter munging
5316 my $formats_nav;
5317 if (!defined $parent) {
5318 # --root commitdiff
5319 $formats_nav .= '(initial)';
5320 } elsif (@$parents == 1) {
5321 # single parent commit
5322 $formats_nav .=
5323 '(parent: ' .
5324 $cgi->a({-href => href(action=>"commit",
5325 hash=>$parent)},
5326 esc_html(substr($parent, 0, 7))) .
5327 ')';
5328 } else {
5329 # merge commit
5330 $formats_nav .=
5331 '(merge: ' .
5332 join(' ', map {
5333 $cgi->a({-href => href(action=>"commit",
5334 hash=>$_)},
5335 esc_html(substr($_, 0, 7)));
5336 } @$parents ) .
5337 ')';
5339 if (gitweb_check_feature('patches') && @$parents <= 1) {
5340 $formats_nav .= " | " .
5341 $cgi->a({-href => href(action=>"patch", -replay=>1)},
5342 "patch");
5345 if (!defined $parent) {
5346 $parent = "--root";
5348 my @difftree;
5349 open my $fd, "-|", git_cmd(), "diff-tree", '-r', "--no-commit-id",
5350 @diff_opts,
5351 (@$parents <= 1 ? $parent : '-c'),
5352 $hash, "--"
5353 or die_error(500, "Open git-diff-tree failed");
5354 @difftree = map { chomp; $_ } <$fd>;
5355 close $fd or die_error(404, "Reading git-diff-tree failed");
5357 # non-textual hash id's can be cached
5358 my $expires;
5359 if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
5360 $expires = "+1d";
5362 my $refs = git_get_references();
5363 my $ref = format_ref_marker($refs, $co{'id'});
5365 git_header_html(undef, $expires);
5366 git_print_page_nav('commit', '',
5367 $hash, $co{'tree'}, $hash,
5368 $formats_nav);
5370 if (defined $co{'parent'}) {
5371 git_print_header_div('commitdiff', esc_html($co{'title'}) . $ref, $hash);
5372 } else {
5373 git_print_header_div('tree', esc_html($co{'title'}) . $ref, $co{'tree'}, $hash);
5375 print "<div class=\"title_text\">\n" .
5376 "<table class=\"object_header\">\n";
5377 git_print_authorship_rows(\%co);
5378 print "<tr><td>commit</td><td class=\"sha1\">$co{'id'}</td></tr>\n";
5379 print "<tr>" .
5380 "<td>tree</td>" .
5381 "<td class=\"sha1\">" .
5382 $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$hash),
5383 class => "list"}, $co{'tree'}) .
5384 "</td>" .
5385 "<td class=\"link\">" .
5386 $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$hash)},
5387 "tree");
5388 my $snapshot_links = format_snapshot_links($hash);
5389 if (defined $snapshot_links) {
5390 print " | " . $snapshot_links;
5392 print "</td>" .
5393 "</tr>\n";
5395 foreach my $par (@$parents) {
5396 print "<tr>" .
5397 "<td>parent</td>" .
5398 "<td class=\"sha1\">" .
5399 $cgi->a({-href => href(action=>"commit", hash=>$par),
5400 class => "list"}, $par) .
5401 "</td>" .
5402 "<td class=\"link\">" .
5403 $cgi->a({-href => href(action=>"commit", hash=>$par)}, "commit") .
5404 " | " .
5405 $cgi->a({-href => href(action=>"commitdiff", hash=>$hash, hash_parent=>$par)}, "diff") .
5406 "</td>" .
5407 "</tr>\n";
5409 print "</table>".
5410 "</div>\n";
5412 print "<div class=\"page_body\">\n";
5413 git_print_log($co{'comment'});
5414 print "</div>\n";
5416 git_difftree_body(\@difftree, $hash, @$parents);
5418 git_footer_html();
5421 sub git_object {
5422 # object is defined by:
5423 # - hash or hash_base alone
5424 # - hash_base and file_name
5425 my $type;
5427 # - hash or hash_base alone
5428 if ($hash || ($hash_base && !defined $file_name)) {
5429 my $object_id = $hash || $hash_base;
5431 open my $fd, "-|", quote_command(
5432 git_cmd(), 'cat-file', '-t', $object_id) . ' 2> /dev/null'
5433 or die_error(404, "Object does not exist");
5434 $type = <$fd>;
5435 chomp $type;
5436 close $fd
5437 or die_error(404, "Object does not exist");
5439 # - hash_base and file_name
5440 } elsif ($hash_base && defined $file_name) {
5441 $file_name =~ s,/+$,,;
5443 system(git_cmd(), "cat-file", '-e', $hash_base) == 0
5444 or die_error(404, "Base object does not exist");
5446 # here errors should not hapen
5447 open my $fd, "-|", git_cmd(), "ls-tree", $hash_base, "--", $file_name
5448 or die_error(500, "Open git-ls-tree failed");
5449 my $line = <$fd>;
5450 close $fd;
5452 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'
5453 unless ($line && $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t/) {
5454 die_error(404, "File or directory for given base does not exist");
5456 $type = $2;
5457 $hash = $3;
5458 } else {
5459 die_error(400, "Not enough information to find object");
5462 print $cgi->redirect(-uri => href(action=>$type, -full=>1,
5463 hash=>$hash, hash_base=>$hash_base,
5464 file_name=>$file_name),
5465 -status => '302 Found');
5468 sub git_blobdiff {
5469 my $format = shift || 'html';
5471 my $fd;
5472 my @difftree;
5473 my %diffinfo;
5474 my $expires;
5476 # preparing $fd and %diffinfo for git_patchset_body
5477 # new style URI
5478 if (defined $hash_base && defined $hash_parent_base) {
5479 if (defined $file_name) {
5480 # read raw output
5481 open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
5482 $hash_parent_base, $hash_base,
5483 "--", (defined $file_parent ? $file_parent : ()), $file_name
5484 or die_error(500, "Open git-diff-tree failed");
5485 @difftree = map { chomp; $_ } <$fd>;
5486 close $fd
5487 or die_error(404, "Reading git-diff-tree failed");
5488 @difftree
5489 or die_error(404, "Blob diff not found");
5491 } elsif (defined $hash &&
5492 $hash =~ /[0-9a-fA-F]{40}/) {
5493 # try to find filename from $hash
5495 # read filtered raw output
5496 open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
5497 $hash_parent_base, $hash_base, "--"
5498 or die_error(500, "Open git-diff-tree failed");
5499 @difftree =
5500 # ':100644 100644 03b21826... 3b93d5e7... M ls-files.c'
5501 # $hash == to_id
5502 grep { /^:[0-7]{6} [0-7]{6} [0-9a-fA-F]{40} $hash/ }
5503 map { chomp; $_ } <$fd>;
5504 close $fd
5505 or die_error(404, "Reading git-diff-tree failed");
5506 @difftree
5507 or die_error(404, "Blob diff not found");
5509 } else {
5510 die_error(400, "Missing one of the blob diff parameters");
5513 if (@difftree > 1) {
5514 die_error(400, "Ambiguous blob diff specification");
5517 %diffinfo = parse_difftree_raw_line($difftree[0]);
5518 $file_parent ||= $diffinfo{'from_file'} || $file_name;
5519 $file_name ||= $diffinfo{'to_file'};
5521 $hash_parent ||= $diffinfo{'from_id'};
5522 $hash ||= $diffinfo{'to_id'};
5524 # non-textual hash id's can be cached
5525 if ($hash_base =~ m/^[0-9a-fA-F]{40}$/ &&
5526 $hash_parent_base =~ m/^[0-9a-fA-F]{40}$/) {
5527 $expires = '+1d';
5530 # open patch output
5531 open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
5532 '-p', ($format eq 'html' ? "--full-index" : ()),
5533 $hash_parent_base, $hash_base,
5534 "--", (defined $file_parent ? $file_parent : ()), $file_name
5535 or die_error(500, "Open git-diff-tree failed");
5538 # old/legacy style URI -- not generated anymore since 1.4.3.
5539 if (!%diffinfo) {
5540 die_error('404 Not Found', "Missing one of the blob diff parameters")
5543 # header
5544 if ($format eq 'html') {
5545 my $formats_nav =
5546 $cgi->a({-href => href(action=>"blobdiff_plain", -replay=>1)},
5547 "raw");
5548 git_header_html(undef, $expires);
5549 if (defined $hash_base && (my %co = parse_commit($hash_base))) {
5550 git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
5551 git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
5552 } else {
5553 print "<div class=\"page_nav\"><br/>$formats_nav<br/></div>\n";
5554 print "<div class=\"title\">".esc_html("$hash vs $hash_parent")."</div>\n";
5556 if (defined $file_name) {
5557 git_print_page_path($file_name, "blob", $hash_base);
5558 } else {
5559 print "<div class=\"page_path\"></div>\n";
5562 } elsif ($format eq 'plain') {
5563 print $cgi->header(
5564 -type => 'text/plain',
5565 -charset => 'utf-8',
5566 -expires => $expires,
5567 -content_disposition => 'inline; filename="' . "$file_name" . '.patch"');
5569 print "X-Git-Url: " . $cgi->self_url() . "\n\n";
5571 } else {
5572 die_error(400, "Unknown blobdiff format");
5575 # patch
5576 if ($format eq 'html') {
5577 print "<div class=\"page_body\">\n";
5579 git_patchset_body($fd, [ \%diffinfo ], $hash_base, $hash_parent_base);
5580 close $fd;
5582 print "</div>\n"; # class="page_body"
5583 git_footer_html();
5585 } else {
5586 while (my $line = <$fd>) {
5587 $line =~ s!a/($hash|$hash_parent)!'a/'.esc_path($diffinfo{'from_file'})!eg;
5588 $line =~ s!b/($hash|$hash_parent)!'b/'.esc_path($diffinfo{'to_file'})!eg;
5590 print $line;
5592 last if $line =~ m!^\+\+\+!;
5594 local $/ = undef;
5595 print <$fd>;
5596 close $fd;
5600 sub git_blobdiff_plain {
5601 git_blobdiff('plain');
5604 sub git_commitdiff {
5605 my %params = @_;
5606 my $format = $params{-format} || 'html';
5608 my ($patch_max) = gitweb_get_feature('patches');
5609 if ($format eq 'patch') {
5610 die_error(403, "Patch view not allowed") unless $patch_max;
5613 $hash ||= $hash_base || "HEAD";
5614 my %co = parse_commit($hash)
5615 or die_error(404, "Unknown commit object");
5617 # choose format for commitdiff for merge
5618 if (! defined $hash_parent && @{$co{'parents'}} > 1) {
5619 $hash_parent = '--cc';
5621 # we need to prepare $formats_nav before almost any parameter munging
5622 my $formats_nav;
5623 if ($format eq 'html') {
5624 $formats_nav =
5625 $cgi->a({-href => href(action=>"commitdiff_plain", -replay=>1)},
5626 "raw");
5627 if ($patch_max && @{$co{'parents'}} <= 1) {
5628 $formats_nav .= " | " .
5629 $cgi->a({-href => href(action=>"patch", -replay=>1)},
5630 "patch");
5633 if (defined $hash_parent &&
5634 $hash_parent ne '-c' && $hash_parent ne '--cc') {
5635 # commitdiff with two commits given
5636 my $hash_parent_short = $hash_parent;
5637 if ($hash_parent =~ m/^[0-9a-fA-F]{40}$/) {
5638 $hash_parent_short = substr($hash_parent, 0, 7);
5640 $formats_nav .=
5641 ' (from';
5642 for (my $i = 0; $i < @{$co{'parents'}}; $i++) {
5643 if ($co{'parents'}[$i] eq $hash_parent) {
5644 $formats_nav .= ' parent ' . ($i+1);
5645 last;
5648 $formats_nav .= ': ' .
5649 $cgi->a({-href => href(action=>"commitdiff",
5650 hash=>$hash_parent)},
5651 esc_html($hash_parent_short)) .
5652 ')';
5653 } elsif (!$co{'parent'}) {
5654 # --root commitdiff
5655 $formats_nav .= ' (initial)';
5656 } elsif (scalar @{$co{'parents'}} == 1) {
5657 # single parent commit
5658 $formats_nav .=
5659 ' (parent: ' .
5660 $cgi->a({-href => href(action=>"commitdiff",
5661 hash=>$co{'parent'})},
5662 esc_html(substr($co{'parent'}, 0, 7))) .
5663 ')';
5664 } else {
5665 # merge commit
5666 if ($hash_parent eq '--cc') {
5667 $formats_nav .= ' | ' .
5668 $cgi->a({-href => href(action=>"commitdiff",
5669 hash=>$hash, hash_parent=>'-c')},
5670 'combined');
5671 } else { # $hash_parent eq '-c'
5672 $formats_nav .= ' | ' .
5673 $cgi->a({-href => href(action=>"commitdiff",
5674 hash=>$hash, hash_parent=>'--cc')},
5675 'compact');
5677 $formats_nav .=
5678 ' (merge: ' .
5679 join(' ', map {
5680 $cgi->a({-href => href(action=>"commitdiff",
5681 hash=>$_)},
5682 esc_html(substr($_, 0, 7)));
5683 } @{$co{'parents'}} ) .
5684 ')';
5688 my $hash_parent_param = $hash_parent;
5689 if (!defined $hash_parent_param) {
5690 # --cc for multiple parents, --root for parentless
5691 $hash_parent_param =
5692 @{$co{'parents'}} > 1 ? '--cc' : $co{'parent'} || '--root';
5695 # read commitdiff
5696 my $fd;
5697 my @difftree;
5698 if ($format eq 'html') {
5699 open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
5700 "--no-commit-id", "--patch-with-raw", "--full-index",
5701 $hash_parent_param, $hash, "--"
5702 or die_error(500, "Open git-diff-tree failed");
5704 while (my $line = <$fd>) {
5705 chomp $line;
5706 # empty line ends raw part of diff-tree output
5707 last unless $line;
5708 push @difftree, scalar parse_difftree_raw_line($line);
5711 } elsif ($format eq 'plain') {
5712 open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
5713 '-p', $hash_parent_param, $hash, "--"
5714 or die_error(500, "Open git-diff-tree failed");
5715 } elsif ($format eq 'patch') {
5716 # For commit ranges, we limit the output to the number of
5717 # patches specified in the 'patches' feature.
5718 # For single commits, we limit the output to a single patch,
5719 # diverging from the git-format-patch default.
5720 my @commit_spec = ();
5721 if ($hash_parent) {
5722 if ($patch_max > 0) {
5723 push @commit_spec, "-$patch_max";
5725 push @commit_spec, '-n', "$hash_parent..$hash";
5726 } else {
5727 if ($params{-single}) {
5728 push @commit_spec, '-1';
5729 } else {
5730 if ($patch_max > 0) {
5731 push @commit_spec, "-$patch_max";
5733 push @commit_spec, "-n";
5735 push @commit_spec, '--root', $hash;
5737 open $fd, "-|", git_cmd(), "format-patch", '--encoding=utf8',
5738 '--stdout', @commit_spec
5739 or die_error(500, "Open git-format-patch failed");
5740 } else {
5741 die_error(400, "Unknown commitdiff format");
5744 # non-textual hash id's can be cached
5745 my $expires;
5746 if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
5747 $expires = "+1d";
5750 # write commit message
5751 if ($format eq 'html') {
5752 my $refs = git_get_references();
5753 my $ref = format_ref_marker($refs, $co{'id'});
5755 git_header_html(undef, $expires);
5756 git_print_page_nav('commitdiff','', $hash,$co{'tree'},$hash, $formats_nav);
5757 git_print_header_div('commit', esc_html($co{'title'}) . $ref, $hash);
5758 print "<div class=\"title_text\">\n" .
5759 "<table class=\"object_header\">\n";
5760 git_print_authorship_rows(\%co);
5761 print "</table>".
5762 "</div>\n";
5763 print "<div class=\"page_body\">\n";
5764 if (@{$co{'comment'}} > 1) {
5765 print "<div class=\"log\">\n";
5766 git_print_log($co{'comment'}, -final_empty_line=> 1, -remove_title => 1);
5767 print "</div>\n"; # class="log"
5770 } elsif ($format eq 'plain') {
5771 my $refs = git_get_references("tags");
5772 my $tagname = git_get_rev_name_tags($hash);
5773 my $filename = basename($project) . "-$hash.patch";
5775 print $cgi->header(
5776 -type => 'text/plain',
5777 -charset => 'utf-8',
5778 -expires => $expires,
5779 -content_disposition => 'inline; filename="' . "$filename" . '"');
5780 my %ad = parse_date($co{'author_epoch'}, $co{'author_tz'});
5781 print "From: " . to_utf8($co{'author'}) . "\n";
5782 print "Date: $ad{'rfc2822'} ($ad{'tz_local'})\n";
5783 print "Subject: " . to_utf8($co{'title'}) . "\n";
5785 print "X-Git-Tag: $tagname\n" if $tagname;
5786 print "X-Git-Url: " . $cgi->self_url() . "\n\n";
5788 foreach my $line (@{$co{'comment'}}) {
5789 print to_utf8($line) . "\n";
5791 print "---\n\n";
5792 } elsif ($format eq 'patch') {
5793 my $filename = basename($project) . "-$hash.patch";
5795 print $cgi->header(
5796 -type => 'text/plain',
5797 -charset => 'utf-8',
5798 -expires => $expires,
5799 -content_disposition => 'inline; filename="' . "$filename" . '"');
5802 # write patch
5803 if ($format eq 'html') {
5804 my $use_parents = !defined $hash_parent ||
5805 $hash_parent eq '-c' || $hash_parent eq '--cc';
5806 git_difftree_body(\@difftree, $hash,
5807 $use_parents ? @{$co{'parents'}} : $hash_parent);
5808 print "<br/>\n";
5810 git_patchset_body($fd, \@difftree, $hash,
5811 $use_parents ? @{$co{'parents'}} : $hash_parent);
5812 close $fd;
5813 print "</div>\n"; # class="page_body"
5814 git_footer_html();
5816 } elsif ($format eq 'plain') {
5817 local $/ = undef;
5818 print <$fd>;
5819 close $fd
5820 or print "Reading git-diff-tree failed\n";
5821 } elsif ($format eq 'patch') {
5822 local $/ = undef;
5823 print <$fd>;
5824 close $fd
5825 or print "Reading git-format-patch failed\n";
5829 sub git_commitdiff_plain {
5830 git_commitdiff(-format => 'plain');
5833 # format-patch-style patches
5834 sub git_patch {
5835 git_commitdiff(-format => 'patch', -single => 1);
5838 sub git_patches {
5839 git_commitdiff(-format => 'patch');
5842 sub git_history {
5843 if (!defined $hash_base) {
5844 $hash_base = git_get_head_hash($project);
5846 if (!defined $page) {
5847 $page = 0;
5849 my $ftype;
5850 my %co = parse_commit($hash_base)
5851 or die_error(404, "Unknown commit object");
5853 my $refs = git_get_references();
5854 my $limit = sprintf("--max-count=%i", (100 * ($page+1)));
5856 my @commitlist = parse_commits($hash_base, 101, (100 * $page),
5857 $file_name, "--full-history")
5858 or die_error(404, "No such file or directory on given branch");
5860 if (!defined $hash && defined $file_name) {
5861 # some commits could have deleted file in question,
5862 # and not have it in tree, but one of them has to have it
5863 for (my $i = 0; $i <= @commitlist; $i++) {
5864 $hash = git_get_hash_by_path($commitlist[$i]{'id'}, $file_name);
5865 last if defined $hash;
5868 if (defined $hash) {
5869 $ftype = git_get_type($hash);
5871 if (!defined $ftype) {
5872 die_error(500, "Unknown type of object");
5875 my $paging_nav = '';
5876 if ($page > 0) {
5877 $paging_nav .=
5878 $cgi->a({-href => href(action=>"history", hash=>$hash, hash_base=>$hash_base,
5879 file_name=>$file_name)},
5880 "first");
5881 $paging_nav .= " &sdot; " .
5882 $cgi->a({-href => href(-replay=>1, page=>$page-1),
5883 -accesskey => "p", -title => "Alt-p"}, "prev");
5884 } else {
5885 $paging_nav .= "first";
5886 $paging_nav .= " &sdot; prev";
5888 my $next_link = '';
5889 if ($#commitlist >= 100) {
5890 $next_link =
5891 $cgi->a({-href => href(-replay=>1, page=>$page+1),
5892 -accesskey => "n", -title => "Alt-n"}, "next");
5893 $paging_nav .= " &sdot; $next_link";
5894 } else {
5895 $paging_nav .= " &sdot; next";
5898 git_header_html();
5899 git_print_page_nav('history','', $hash_base,$co{'tree'},$hash_base, $paging_nav);
5900 git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
5901 git_print_page_path($file_name, $ftype, $hash_base);
5903 git_history_body(\@commitlist, 0, 99,
5904 $refs, $hash_base, $ftype, $next_link);
5906 git_footer_html();
5909 sub git_search {
5910 gitweb_check_feature('search') or die_error(403, "Search is disabled");
5911 if (!defined $searchtext) {
5912 die_error(400, "Text field is empty");
5914 if (!defined $hash) {
5915 $hash = git_get_head_hash($project);
5917 my %co = parse_commit($hash);
5918 if (!%co) {
5919 die_error(404, "Unknown commit object");
5921 if (!defined $page) {
5922 $page = 0;
5925 $searchtype ||= 'commit';
5926 if ($searchtype eq 'pickaxe') {
5927 # pickaxe may take all resources of your box and run for several minutes
5928 # with every query - so decide by yourself how public you make this feature
5929 gitweb_check_feature('pickaxe')
5930 or die_error(403, "Pickaxe is disabled");
5932 if ($searchtype eq 'grep') {
5933 gitweb_check_feature('grep')
5934 or die_error(403, "Grep is disabled");
5937 git_header_html();
5939 if ($searchtype eq 'commit' or $searchtype eq 'author' or $searchtype eq 'committer') {
5940 my $greptype;
5941 if ($searchtype eq 'commit') {
5942 $greptype = "--grep=";
5943 } elsif ($searchtype eq 'author') {
5944 $greptype = "--author=";
5945 } elsif ($searchtype eq 'committer') {
5946 $greptype = "--committer=";
5948 $greptype .= $searchtext;
5949 my @commitlist = parse_commits($hash, 101, (100 * $page), undef,
5950 $greptype, '--regexp-ignore-case',
5951 $search_use_regexp ? '--extended-regexp' : '--fixed-strings');
5953 my $paging_nav = '';
5954 if ($page > 0) {
5955 $paging_nav .=
5956 $cgi->a({-href => href(action=>"search", hash=>$hash,
5957 searchtext=>$searchtext,
5958 searchtype=>$searchtype)},
5959 "first");
5960 $paging_nav .= " &sdot; " .
5961 $cgi->a({-href => href(-replay=>1, page=>$page-1),
5962 -accesskey => "p", -title => "Alt-p"}, "prev");
5963 } else {
5964 $paging_nav .= "first";
5965 $paging_nav .= " &sdot; prev";
5967 my $next_link = '';
5968 if ($#commitlist >= 100) {
5969 $next_link =
5970 $cgi->a({-href => href(-replay=>1, page=>$page+1),
5971 -accesskey => "n", -title => "Alt-n"}, "next");
5972 $paging_nav .= " &sdot; $next_link";
5973 } else {
5974 $paging_nav .= " &sdot; next";
5977 if ($#commitlist >= 100) {
5980 git_print_page_nav('','', $hash,$co{'tree'},$hash, $paging_nav);
5981 git_print_header_div('commit', esc_html($co{'title'}), $hash);
5982 git_search_grep_body(\@commitlist, 0, 99, $next_link);
5985 if ($searchtype eq 'pickaxe') {
5986 git_print_page_nav('','', $hash,$co{'tree'},$hash);
5987 git_print_header_div('commit', esc_html($co{'title'}), $hash);
5989 print "<table class=\"pickaxe search\">\n";
5990 my $alternate = 1;
5991 local $/ = "\n";
5992 open my $fd, '-|', git_cmd(), '--no-pager', 'log', @diff_opts,
5993 '--pretty=format:%H', '--no-abbrev', '--raw', "-S$searchtext",
5994 ($search_use_regexp ? '--pickaxe-regex' : ());
5995 undef %co;
5996 my @files;
5997 while (my $line = <$fd>) {
5998 chomp $line;
5999 next unless $line;
6001 my %set = parse_difftree_raw_line($line);
6002 if (defined $set{'commit'}) {
6003 # finish previous commit
6004 if (%co) {
6005 print "</td>\n" .
6006 "<td class=\"link\">" .
6007 $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") .
6008 " | " .
6009 $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree");
6010 print "</td>\n" .
6011 "</tr>\n";
6014 if ($alternate) {
6015 print "<tr class=\"dark\">\n";
6016 } else {
6017 print "<tr class=\"light\">\n";
6019 $alternate ^= 1;
6020 %co = parse_commit($set{'commit'});
6021 my $author = chop_and_escape_str($co{'author_name'}, 15, 5);
6022 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
6023 "<td><i>$author</i></td>\n" .
6024 "<td>" .
6025 $cgi->a({-href => href(action=>"commit", hash=>$co{'id'}),
6026 -class => "list subject"},
6027 chop_and_escape_str($co{'title'}, 50) . "<br/>");
6028 } elsif (defined $set{'to_id'}) {
6029 next if ($set{'to_id'} =~ m/^0{40}$/);
6031 print $cgi->a({-href => href(action=>"blob", hash_base=>$co{'id'},
6032 hash=>$set{'to_id'}, file_name=>$set{'to_file'}),
6033 -class => "list"},
6034 "<span class=\"match\">" . esc_path($set{'file'}) . "</span>") .
6035 "<br/>\n";
6038 close $fd;
6040 # finish last commit (warning: repetition!)
6041 if (%co) {
6042 print "</td>\n" .
6043 "<td class=\"link\">" .
6044 $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") .
6045 " | " .
6046 $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree");
6047 print "</td>\n" .
6048 "</tr>\n";
6051 print "</table>\n";
6054 if ($searchtype eq 'grep') {
6055 git_print_page_nav('','', $hash,$co{'tree'},$hash);
6056 git_print_header_div('commit', esc_html($co{'title'}), $hash);
6058 print "<table class=\"grep_search\">\n";
6059 my $alternate = 1;
6060 my $matches = 0;
6061 local $/ = "\n";
6062 open my $fd, "-|", git_cmd(), 'grep', '-n',
6063 $search_use_regexp ? ('-E', '-i') : '-F',
6064 $searchtext, $co{'tree'};
6065 my $lastfile = '';
6066 while (my $line = <$fd>) {
6067 chomp $line;
6068 my ($file, $lno, $ltext, $binary);
6069 last if ($matches++ > 1000);
6070 if ($line =~ /^Binary file (.+) matches$/) {
6071 $file = $1;
6072 $binary = 1;
6073 } else {
6074 (undef, $file, $lno, $ltext) = split(/:/, $line, 4);
6076 if ($file ne $lastfile) {
6077 $lastfile and print "</td></tr>\n";
6078 if ($alternate++) {
6079 print "<tr class=\"dark\">\n";
6080 } else {
6081 print "<tr class=\"light\">\n";
6083 print "<td class=\"list\">".
6084 $cgi->a({-href => href(action=>"blob", hash=>$co{'hash'},
6085 file_name=>"$file"),
6086 -class => "list"}, esc_path($file));
6087 print "</td><td>\n";
6088 $lastfile = $file;
6090 if ($binary) {
6091 print "<div class=\"binary\">Binary file</div>\n";
6092 } else {
6093 $ltext = untabify($ltext);
6094 if ($ltext =~ m/^(.*)($search_regexp)(.*)$/i) {
6095 $ltext = esc_html($1, -nbsp=>1);
6096 $ltext .= '<span class="match">';
6097 $ltext .= esc_html($2, -nbsp=>1);
6098 $ltext .= '</span>';
6099 $ltext .= esc_html($3, -nbsp=>1);
6100 } else {
6101 $ltext = esc_html($ltext, -nbsp=>1);
6103 print "<div class=\"pre\">" .
6104 $cgi->a({-href => href(action=>"blob", hash=>$co{'hash'},
6105 file_name=>"$file").'#l'.$lno,
6106 -class => "linenr"}, sprintf('%4i', $lno))
6107 . ' ' . $ltext . "</div>\n";
6110 if ($lastfile) {
6111 print "</td></tr>\n";
6112 if ($matches > 1000) {
6113 print "<div class=\"diff nodifferences\">Too many matches, listing trimmed</div>\n";
6115 } else {
6116 print "<div class=\"diff nodifferences\">No matches found</div>\n";
6118 close $fd;
6120 print "</table>\n";
6122 git_footer_html();
6125 sub git_search_help {
6126 git_header_html();
6127 git_print_page_nav('','', $hash,$hash,$hash);
6128 print <<EOT;
6129 <p><strong>Pattern</strong> is by default a normal string that is matched precisely (but without
6130 regard to case, except in the case of pickaxe). However, when you check the <em>re</em> checkbox,
6131 the pattern entered is recognized as the POSIX extended
6132 <a href="http://en.wikipedia.org/wiki/Regular_expression">regular expression</a> (also case
6133 insensitive).</p>
6134 <dl>
6135 <dt><b>commit</b></dt>
6136 <dd>The commit messages and authorship information will be scanned for the given pattern.</dd>
6138 my $have_grep = gitweb_check_feature('grep');
6139 if ($have_grep) {
6140 print <<EOT;
6141 <dt><b>grep</b></dt>
6142 <dd>All files in the currently selected tree (HEAD unless you are explicitly browsing
6143 a different one) are searched for the given pattern. On large trees, this search can take
6144 a while and put some strain on the server, so please use it with some consideration. Note that
6145 due to git-grep peculiarity, currently if regexp mode is turned off, the matches are
6146 case-sensitive.</dd>
6149 print <<EOT;
6150 <dt><b>author</b></dt>
6151 <dd>Name and e-mail of the change author and date of birth of the patch will be scanned for the given pattern.</dd>
6152 <dt><b>committer</b></dt>
6153 <dd>Name and e-mail of the committer and date of commit will be scanned for the given pattern.</dd>
6155 my $have_pickaxe = gitweb_check_feature('pickaxe');
6156 if ($have_pickaxe) {
6157 print <<EOT;
6158 <dt><b>pickaxe</b></dt>
6159 <dd>All commits that caused the string to appear or disappear from any file (changes that
6160 added, removed or "modified" the string) will be listed. This search can take a while and
6161 takes a lot of strain on the server, so please use it wisely. Note that since you may be
6162 interested even in changes just changing the case as well, this search is case sensitive.</dd>
6165 print "</dl>\n";
6166 git_footer_html();
6169 sub git_shortlog {
6170 my $head = git_get_head_hash($project);
6171 if (!defined $hash) {
6172 $hash = $head;
6174 if (!defined $page) {
6175 $page = 0;
6177 my $refs = git_get_references();
6179 my $commit_hash = $hash;
6180 if (defined $hash_parent) {
6181 $commit_hash = "$hash_parent..$hash";
6183 my @commitlist = parse_commits($commit_hash, 101, (100 * $page));
6185 my $paging_nav = format_paging_nav('shortlog', $hash, $head, $page, $#commitlist >= 100);
6186 my $next_link = '';
6187 if ($#commitlist >= 100) {
6188 $next_link =
6189 $cgi->a({-href => href(-replay=>1, page=>$page+1),
6190 -accesskey => "n", -title => "Alt-n"}, "next");
6192 my $patch_max = gitweb_check_feature('patches');
6193 if ($patch_max) {
6194 if ($patch_max < 0 || @commitlist <= $patch_max) {
6195 $paging_nav .= " &sdot; " .
6196 $cgi->a({-href => href(action=>"patches", -replay=>1)},
6197 "patches");
6201 git_header_html();
6202 git_print_page_nav('shortlog','', $hash,$hash,$hash, $paging_nav);
6203 git_print_header_div('summary', $project);
6205 git_shortlog_body(\@commitlist, 0, 99, $refs, $next_link);
6207 git_footer_html();
6210 ## ......................................................................
6211 ## feeds (RSS, Atom; OPML)
6213 sub git_feed {
6214 my $format = shift || 'atom';
6215 my $have_blame = gitweb_check_feature('blame');
6217 # Atom: http://www.atomenabled.org/developers/syndication/
6218 # RSS: http://www.notestips.com/80256B3A007F2692/1/NAMO5P9UPQ
6219 if ($format ne 'rss' && $format ne 'atom') {
6220 die_error(400, "Unknown web feed format");
6223 # log/feed of current (HEAD) branch, log of given branch, history of file/directory
6224 my $head = $hash || 'HEAD';
6225 my @commitlist = parse_commits($head, 150, 0, $file_name);
6227 my %latest_commit;
6228 my %latest_date;
6229 my $content_type = "application/$format+xml";
6230 if (defined $cgi->http('HTTP_ACCEPT') &&
6231 $cgi->Accept('text/xml') > $cgi->Accept($content_type)) {
6232 # browser (feed reader) prefers text/xml
6233 $content_type = 'text/xml';
6235 if (defined($commitlist[0])) {
6236 %latest_commit = %{$commitlist[0]};
6237 my $latest_epoch = $latest_commit{'committer_epoch'};
6238 %latest_date = parse_date($latest_epoch);
6239 my $if_modified = $cgi->http('IF_MODIFIED_SINCE');
6240 if (defined $if_modified) {
6241 my $since;
6242 if (eval { require HTTP::Date; 1; }) {
6243 $since = HTTP::Date::str2time($if_modified);
6244 } elsif (eval { require Time::ParseDate; 1; }) {
6245 $since = Time::ParseDate::parsedate($if_modified, GMT => 1);
6247 if (defined $since && $latest_epoch <= $since) {
6248 print $cgi->header(
6249 -type => $content_type,
6250 -charset => 'utf-8',
6251 -last_modified => $latest_date{'rfc2822'},
6252 -status => '304 Not Modified');
6253 return;
6256 print $cgi->header(
6257 -type => $content_type,
6258 -charset => 'utf-8',
6259 -last_modified => $latest_date{'rfc2822'});
6260 } else {
6261 print $cgi->header(
6262 -type => $content_type,
6263 -charset => 'utf-8');
6266 # Optimization: skip generating the body if client asks only
6267 # for Last-Modified date.
6268 return if ($cgi->request_method() eq 'HEAD');
6270 # header variables
6271 my $title = "$site_name - $project/$action";
6272 my $feed_type = 'log';
6273 if (defined $hash) {
6274 $title .= " - '$hash'";
6275 $feed_type = 'branch log';
6276 if (defined $file_name) {
6277 $title .= " :: $file_name";
6278 $feed_type = 'history';
6280 } elsif (defined $file_name) {
6281 $title .= " - $file_name";
6282 $feed_type = 'history';
6284 $title .= " $feed_type";
6285 my $descr = git_get_project_description($project);
6286 if (defined $descr) {
6287 $descr = esc_html($descr);
6288 } else {
6289 $descr = "$project " .
6290 ($format eq 'rss' ? 'RSS' : 'Atom') .
6291 " feed";
6293 my $owner = git_get_project_owner($project);
6294 $owner = esc_html($owner);
6296 #header
6297 my $alt_url;
6298 if (defined $file_name) {
6299 $alt_url = href(-full=>1, action=>"history", hash=>$hash, file_name=>$file_name);
6300 } elsif (defined $hash) {
6301 $alt_url = href(-full=>1, action=>"log", hash=>$hash);
6302 } else {
6303 $alt_url = href(-full=>1, action=>"summary");
6305 print qq!<?xml version="1.0" encoding="utf-8"?>\n!;
6306 if ($format eq 'rss') {
6307 print <<XML;
6308 <rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
6309 <channel>
6311 print "<title>$title</title>\n" .
6312 "<link>$alt_url</link>\n" .
6313 "<description>$descr</description>\n" .
6314 "<language>en</language>\n" .
6315 # project owner is responsible for 'editorial' content
6316 "<managingEditor>$owner</managingEditor>\n";
6317 if (defined $logo || defined $favicon) {
6318 # prefer the logo to the favicon, since RSS
6319 # doesn't allow both
6320 my $img = esc_url($logo || $favicon);
6321 print "<image>\n" .
6322 "<url>$img</url>\n" .
6323 "<title>$title</title>\n" .
6324 "<link>$alt_url</link>\n" .
6325 "</image>\n";
6327 if (%latest_date) {
6328 print "<pubDate>$latest_date{'rfc2822'}</pubDate>\n";
6329 print "<lastBuildDate>$latest_date{'rfc2822'}</lastBuildDate>\n";
6331 print "<generator>gitweb v.$version/$git_version</generator>\n";
6332 } elsif ($format eq 'atom') {
6333 print <<XML;
6334 <feed xmlns="http://www.w3.org/2005/Atom">
6336 print "<title>$title</title>\n" .
6337 "<subtitle>$descr</subtitle>\n" .
6338 '<link rel="alternate" type="text/html" href="' .
6339 $alt_url . '" />' . "\n" .
6340 '<link rel="self" type="' . $content_type . '" href="' .
6341 $cgi->self_url() . '" />' . "\n" .
6342 "<id>" . href(-full=>1) . "</id>\n" .
6343 # use project owner for feed author
6344 "<author><name>$owner</name></author>\n";
6345 if (defined $favicon) {
6346 print "<icon>" . esc_url($favicon) . "</icon>\n";
6348 if (defined $logo_url) {
6349 # not twice as wide as tall: 72 x 27 pixels
6350 print "<logo>" . esc_url($logo) . "</logo>\n";
6352 if (! %latest_date) {
6353 # dummy date to keep the feed valid until commits trickle in:
6354 print "<updated>1970-01-01T00:00:00Z</updated>\n";
6355 } else {
6356 print "<updated>$latest_date{'iso-8601'}</updated>\n";
6358 print "<generator version='$version/$git_version'>gitweb</generator>\n";
6361 # contents
6362 for (my $i = 0; $i <= $#commitlist; $i++) {
6363 my %co = %{$commitlist[$i]};
6364 my $commit = $co{'id'};
6365 # we read 150, we always show 30 and the ones more recent than 48 hours
6366 if (($i >= 20) && ((time - $co{'author_epoch'}) > 48*60*60)) {
6367 last;
6369 my %cd = parse_date($co{'author_epoch'});
6371 # get list of changed files
6372 open my $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
6373 $co{'parent'} || "--root",
6374 $co{'id'}, "--", (defined $file_name ? $file_name : ())
6375 or next;
6376 my @difftree = map { chomp; $_ } <$fd>;
6377 close $fd
6378 or next;
6380 # print element (entry, item)
6381 my $co_url = href(-full=>1, action=>"commitdiff", hash=>$commit);
6382 if ($format eq 'rss') {
6383 print "<item>\n" .
6384 "<title>" . esc_html($co{'title'}) . "</title>\n" .
6385 "<author>" . esc_html($co{'author'}) . "</author>\n" .
6386 "<pubDate>$cd{'rfc2822'}</pubDate>\n" .
6387 "<guid isPermaLink=\"true\">$co_url</guid>\n" .
6388 "<link>$co_url</link>\n" .
6389 "<description>" . esc_html($co{'title'}) . "</description>\n" .
6390 "<content:encoded>" .
6391 "<![CDATA[\n";
6392 } elsif ($format eq 'atom') {
6393 print "<entry>\n" .
6394 "<title type=\"html\">" . esc_html($co{'title'}) . "</title>\n" .
6395 "<updated>$cd{'iso-8601'}</updated>\n" .
6396 "<author>\n" .
6397 " <name>" . esc_html($co{'author_name'}) . "</name>\n";
6398 if ($co{'author_email'}) {
6399 print " <email>" . esc_html($co{'author_email'}) . "</email>\n";
6401 print "</author>\n" .
6402 # use committer for contributor
6403 "<contributor>\n" .
6404 " <name>" . esc_html($co{'committer_name'}) . "</name>\n";
6405 if ($co{'committer_email'}) {
6406 print " <email>" . esc_html($co{'committer_email'}) . "</email>\n";
6408 print "</contributor>\n" .
6409 "<published>$cd{'iso-8601'}</published>\n" .
6410 "<link rel=\"alternate\" type=\"text/html\" href=\"$co_url\" />\n" .
6411 "<id>$co_url</id>\n" .
6412 "<content type=\"xhtml\" xml:base=\"" . esc_url($my_url) . "\">\n" .
6413 "<div xmlns=\"http://www.w3.org/1999/xhtml\">\n";
6415 my $comment = $co{'comment'};
6416 print "<pre>\n";
6417 foreach my $line (@$comment) {
6418 $line = esc_html($line);
6419 print "$line\n";
6421 print "</pre><ul>\n";
6422 foreach my $difftree_line (@difftree) {
6423 my %difftree = parse_difftree_raw_line($difftree_line);
6424 next if !$difftree{'from_id'};
6426 my $file = $difftree{'file'} || $difftree{'to_file'};
6428 print "<li>" .
6429 "[" .
6430 $cgi->a({-href => href(-full=>1, action=>"blobdiff",
6431 hash=>$difftree{'to_id'}, hash_parent=>$difftree{'from_id'},
6432 hash_base=>$co{'id'}, hash_parent_base=>$co{'parent'},
6433 file_name=>$file, file_parent=>$difftree{'from_file'}),
6434 -title => "diff"}, 'D');
6435 if ($have_blame) {
6436 print $cgi->a({-href => href(-full=>1, action=>"blame",
6437 file_name=>$file, hash_base=>$commit),
6438 -title => "blame"}, 'B');
6440 # if this is not a feed of a file history
6441 if (!defined $file_name || $file_name ne $file) {
6442 print $cgi->a({-href => href(-full=>1, action=>"history",
6443 file_name=>$file, hash=>$commit),
6444 -title => "history"}, 'H');
6446 $file = esc_path($file);
6447 print "] ".
6448 "$file</li>\n";
6450 if ($format eq 'rss') {
6451 print "</ul>]]>\n" .
6452 "</content:encoded>\n" .
6453 "</item>\n";
6454 } elsif ($format eq 'atom') {
6455 print "</ul>\n</div>\n" .
6456 "</content>\n" .
6457 "</entry>\n";
6461 # end of feed
6462 if ($format eq 'rss') {
6463 print "</channel>\n</rss>\n";
6464 } elsif ($format eq 'atom') {
6465 print "</feed>\n";
6469 sub git_rss {
6470 git_feed('rss');
6473 sub git_atom {
6474 git_feed('atom');
6477 sub git_opml {
6478 my @list = git_get_projects_list();
6480 print $cgi->header(
6481 -type => 'text/xml',
6482 -charset => 'utf-8',
6483 -content_disposition => 'inline; filename="opml.xml"');
6485 print <<XML;
6486 <?xml version="1.0" encoding="utf-8"?>
6487 <opml version="1.0">
6488 <head>
6489 <title>$site_name OPML Export</title>
6490 </head>
6491 <body>
6492 <outline text="git RSS feeds">
6495 foreach my $pr (@list) {
6496 my %proj = %$pr;
6497 my $head = git_get_head_hash($proj{'path'});
6498 if (!defined $head) {
6499 next;
6501 $git_dir = "$projectroot/$proj{'path'}";
6502 my %co = parse_commit($head);
6503 if (!%co) {
6504 next;
6507 my $path = esc_html(chop_str($proj{'path'}, 25, 5));
6508 my $rss = href('project' => $proj{'path'}, 'action' => 'rss', -full => 1);
6509 my $html = href('project' => $proj{'path'}, 'action' => 'summary', -full => 1);
6510 print "<outline type=\"rss\" text=\"$path\" title=\"$path\" xmlUrl=\"$rss\" htmlUrl=\"$html\"/>\n";
6512 print <<XML;
6513 </outline>
6514 </body>
6515 </opml>