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
12 use CGI qw(:standard :escapeHTML -nosticky);
13 use CGI::Util qw(unescape);
14 use CGI::Carp qw(fatalsToBrowser);
18 use File::Basename qw(basename);
19 binmode STDOUT, ':utf8';
22 if (eval { require Time::HiRes; 1; }) {
23 $t0 = [Time::HiRes::gettimeofday()];
25 our $number_of_git_cmds = 0;
28 CGI->compile() if $ENV{'MOD_PERL'};
31 our $version = "++GIT_VERSION++";
33 our ($my_url, $my_uri, $base_url, $path_info, $home_link);
37 our $my_url = $cgi->url();
38 our $my_uri = $cgi->url(-absolute => 1);
40 # Base URL for relative URLs in gitweb ($logo, $favicon, ...),
41 # needed and used only for URLs with nonempty PATH_INFO
42 our $base_url = $my_url;
44 # When the script is used as DirectoryIndex, the URL does not contain the name
45 # of the script file itself, and $cgi->url() fails to strip PATH_INFO, so we
46 # have to do it ourselves. We make $path_info global because it's also used
49 # Another issue with the script being the DirectoryIndex is that the resulting
50 # $my_url data is not the full script URL: this is good, because we want
51 # generated links to keep implying the script name if it wasn't explicitly
52 # indicated in the URL we're handling, but it means that $my_url cannot be used
54 # Therefore, if we needed to strip PATH_INFO, then we know that we have
55 # to build the base URL ourselves:
56 our $path_info = $ENV{"PATH_INFO"};
58 if ($my_url =~ s,\Q$path_info\E$,, &&
59 $my_uri =~ s,\Q$path_info\E$,, &&
60 defined $ENV{'SCRIPT_NAME'}) {
61 $base_url = $cgi->url(-base => 1) . $ENV{'SCRIPT_NAME'};
65 # target of the home link on top of all pages
66 our $home_link = $my_uri || "/";
69 # core git executable to use
70 # this can just be "git" if your webserver has a sensible PATH
71 our $GIT = "++GIT_BINDIR++/git";
73 # absolute fs-path which will be prepended to the project path
74 #our $projectroot = "/pub/scm";
75 our $projectroot = "++GITWEB_PROJECTROOT++";
77 # fs traversing limit for getting project list
78 # the number is relative to the projectroot
79 our $project_maxdepth = "++GITWEB_PROJECT_MAXDEPTH++";
81 # string of the home link on top of all pages
82 our $home_link_str = "++GITWEB_HOME_LINK_STR++";
84 # name of your site or organization to appear in page titles
85 # replace this with something more descriptive for clearer bookmarks
86 our $site_name = "++GITWEB_SITENAME++"
87 || ($ENV{'SERVER_NAME'} || "Untitled") . " Git";
89 # filename of html text to include at top of each page
90 our $site_header = "++GITWEB_SITE_HEADER++";
91 # html text to include at home page
92 our $home_text = "++GITWEB_HOMETEXT++";
93 # filename of html text to include at bottom of each page
94 our $site_footer = "++GITWEB_SITE_FOOTER++";
97 our @stylesheets = ("++GITWEB_CSS++");
98 # URI of a single stylesheet, which can be overridden in GITWEB_CONFIG.
99 our $stylesheet = undef;
100 # URI of GIT logo (72x27 size)
101 our $logo = "++GITWEB_LOGO++";
102 # URI of GIT favicon, assumed to be image/png type
103 our $favicon = "++GITWEB_FAVICON++";
104 # URI of gitweb.js (JavaScript code for gitweb)
105 our $javascript = "++GITWEB_JS++";
107 # URI and label (title) of GIT logo link
108 #our $logo_url = "http://www.kernel.org/pub/software/scm/git/docs/";
109 #our $logo_label = "git documentation";
110 our $logo_url = "http://git-scm.com/";
111 our $logo_label = "git homepage";
113 # source of projects list
114 our $projects_list = "++GITWEB_LIST++";
116 # the width (in characters) of the projects list "Description" column
117 our $projects_list_description_width = 25;
119 # default order of projects list
120 # valid values are none, project, descr, owner, and age
121 our $default_projects_order = "project";
123 # show repository only if this file exists
124 # (only effective if this variable evaluates to true)
125 our $export_ok = "++GITWEB_EXPORT_OK++";
127 # show repository only if this subroutine returns true
128 # when given the path to the project, for example:
129 # sub { return -e "$_[0]/git-daemon-export-ok"; }
130 our $export_auth_hook = undef;
132 # only allow viewing of repositories also shown on the overview page
133 our $strict_export = "++GITWEB_STRICT_EXPORT++";
135 # list of git base URLs used for URL to where fetch project from,
136 # i.e. full URL is "$git_base_url/$project"
137 our @git_base_url_list = grep { $_ ne '' } ("++GITWEB_BASE_URL++");
139 # default blob_plain mimetype and default charset for text/plain blob
140 our $default_blob_plain_mimetype = 'text/plain';
141 our $default_text_plain_charset = undef;
143 # file to use for guessing MIME types before trying /etc/mime.types
144 # (relative to the current git repository)
145 our $mimetypes_file = undef;
147 # assume this charset if line contains non-UTF-8 characters;
148 # it should be valid encoding (see Encoding::Supported(3pm) for list),
149 # for which encoding all byte sequences are valid, for example
150 # 'iso-8859-1' aka 'latin1' (it is decoded without checking, so it
151 # could be even 'utf-8' for the old behavior)
152 our $fallback_encoding = 'latin1';
154 # rename detection options for git-diff and git-diff-tree
155 # - default is '-M', with the cost proportional to
156 # (number of removed files) * (number of new files).
157 # - more costly is '-C' (which implies '-M'), with the cost proportional to
158 # (number of changed files + number of removed files) * (number of new files)
159 # - even more costly is '-C', '--find-copies-harder' with cost
160 # (number of files in the original tree) * (number of new files)
161 # - one might want to include '-B' option, e.g. '-B', '-M'
162 our @diff_opts = ('-M'); # taken from git_commit
164 # Disables features that would allow repository owners to inject script into
166 our $prevent_xss = 0;
168 # information about snapshot formats that gitweb is capable of serving
169 our %known_snapshot_formats = (
171 # 'display' => display name,
172 # 'type' => mime type,
173 # 'suffix' => filename suffix,
174 # 'format' => --format for git-archive,
175 # 'compressor' => [compressor command and arguments]
176 # (array reference, optional)
177 # 'disabled' => boolean (optional)}
180 'display' => 'tar.gz',
181 'type' => 'application/x-gzip',
182 'suffix' => '.tar.gz',
184 'compressor' => ['gzip']},
187 'display' => 'tar.bz2',
188 'type' => 'application/x-bzip2',
189 'suffix' => '.tar.bz2',
191 'compressor' => ['bzip2']},
194 'display' => 'tar.xz',
195 'type' => 'application/x-xz',
196 'suffix' => '.tar.xz',
198 'compressor' => ['xz'],
203 'type' => 'application/x-zip',
208 # Aliases so we understand old gitweb.snapshot values in repository
210 our %known_snapshot_format_aliases = (
215 # backward compatibility: legacy gitweb config support
216 'x-gzip' => undef, 'gz' => undef,
217 'x-bzip2' => undef, 'bz2' => undef,
218 'x-zip' => undef, '' => undef,
221 # Pixel sizes for icons and avatars. If the default font sizes or lineheights
222 # are changed, it may be appropriate to change these values too via
229 # Used to set the maximum load that we will still respond to gitweb queries.
230 # If server load exceed this value then return "503 server busy" error.
231 # If gitweb cannot determined server load, it is taken to be 0.
232 # Leave it undefined (or set to 'undef') to turn off load checking.
235 # You define site-wide feature defaults here; override them with
236 # $GITWEB_CONFIG as necessary.
239 # 'sub' => feature-sub (subroutine),
240 # 'override' => allow-override (boolean),
241 # 'default' => [ default options...] (array reference)}
243 # if feature is overridable (it means that allow-override has true value),
244 # then feature-sub will be called with default options as parameters;
245 # return value of feature-sub indicates if to enable specified feature
247 # if there is no 'sub' key (no feature-sub), then feature cannot be
250 # use gitweb_get_feature(<feature>) to retrieve the <feature> value
251 # (an array) or gitweb_check_feature(<feature>) to check if <feature>
254 # Enable the 'blame' blob view, showing the last commit that modified
255 # each line in the file. This can be very CPU-intensive.
257 # To enable system wide have in $GITWEB_CONFIG
258 # $feature{'blame'}{'default'} = [1];
259 # To have project specific config enable override in $GITWEB_CONFIG
260 # $feature{'blame'}{'override'} = 1;
261 # and in project config gitweb.blame = 0|1;
263 'sub' => sub { feature_bool('blame', @_) },
267 # Enable the 'snapshot' link, providing a compressed archive of any
268 # tree. This can potentially generate high traffic if you have large
271 # Value is a list of formats defined in %known_snapshot_formats that
273 # To disable system wide have in $GITWEB_CONFIG
274 # $feature{'snapshot'}{'default'} = [];
275 # To have project specific config enable override in $GITWEB_CONFIG
276 # $feature{'snapshot'}{'override'} = 1;
277 # and in project config, a comma-separated list of formats or "none"
278 # to disable. Example: gitweb.snapshot = tbz2,zip;
280 'sub' => \&feature_snapshot,
282 'default' => ['tgz']},
284 # Enable text search, which will list the commits which match author,
285 # committer or commit text to a given string. Enabled by default.
286 # Project specific override is not supported.
291 # Enable grep search, which will list the files in currently selected
292 # tree containing the given string. Enabled by default. This can be
293 # potentially CPU-intensive, of course.
295 # To enable system wide have in $GITWEB_CONFIG
296 # $feature{'grep'}{'default'} = [1];
297 # To have project specific config enable override in $GITWEB_CONFIG
298 # $feature{'grep'}{'override'} = 1;
299 # and in project config gitweb.grep = 0|1;
301 'sub' => sub { feature_bool('grep', @_) },
305 # Enable the pickaxe search, which will list the commits that modified
306 # a given string in a file. This can be practical and quite faster
307 # alternative to 'blame', but still potentially CPU-intensive.
309 # To enable system wide have in $GITWEB_CONFIG
310 # $feature{'pickaxe'}{'default'} = [1];
311 # To have project specific config enable override in $GITWEB_CONFIG
312 # $feature{'pickaxe'}{'override'} = 1;
313 # and in project config gitweb.pickaxe = 0|1;
315 'sub' => sub { feature_bool('pickaxe', @_) },
319 # Enable showing size of blobs in a 'tree' view, in a separate
320 # column, similar to what 'ls -l' does. This cost a bit of IO.
322 # To disable system wide have in $GITWEB_CONFIG
323 # $feature{'show-sizes'}{'default'} = [0];
324 # To have project specific config enable override in $GITWEB_CONFIG
325 # $feature{'show-sizes'}{'override'} = 1;
326 # and in project config gitweb.showsizes = 0|1;
328 'sub' => sub { feature_bool('showsizes', @_) },
332 # Make gitweb use an alternative format of the URLs which can be
333 # more readable and natural-looking: project name is embedded
334 # directly in the path and the query string contains other
335 # auxiliary information. All gitweb installations recognize
336 # URL in either format; this configures in which formats gitweb
339 # To enable system wide have in $GITWEB_CONFIG
340 # $feature{'pathinfo'}{'default'} = [1];
341 # Project specific override is not supported.
343 # Note that you will need to change the default location of CSS,
344 # favicon, logo and possibly other files to an absolute URL. Also,
345 # if gitweb.cgi serves as your indexfile, you will need to force
346 # $my_uri to contain the script name in your $GITWEB_CONFIG.
351 # Make gitweb consider projects in project root subdirectories
352 # to be forks of existing projects. Given project $projname.git,
353 # projects matching $projname/*.git will not be shown in the main
354 # projects list, instead a '+' mark will be added to $projname
355 # there and a 'forks' view will be enabled for the project, listing
356 # all the forks. If project list is taken from a file, forks have
357 # to be listed after the main project.
359 # To enable system wide have in $GITWEB_CONFIG
360 # $feature{'forks'}{'default'} = [1];
361 # Project specific override is not supported.
366 # Insert custom links to the action bar of all project pages.
367 # This enables you mainly to link to third-party scripts integrating
368 # into gitweb; e.g. git-browser for graphical history representation
369 # or custom web-based repository administration interface.
371 # The 'default' value consists of a list of triplets in the form
372 # (label, link, position) where position is the label after which
373 # to insert the link and link is a format string where %n expands
374 # to the project name, %f to the project path within the filesystem,
375 # %h to the current hash (h gitweb parameter) and %b to the current
376 # hash base (hb gitweb parameter); %% expands to %.
378 # To enable system wide have in $GITWEB_CONFIG e.g.
379 # $feature{'actions'}{'default'} = [('graphiclog',
380 # '/git-browser/by-commit.html?r=%n', 'summary')];
381 # Project specific override is not supported.
386 # Allow gitweb scan project content tags described in ctags/
387 # of project repository, and display the popular Web 2.0-ish
388 # "tag cloud" near the project list. Note that this is something
389 # COMPLETELY different from the normal Git tags.
391 # gitweb by itself can show existing tags, but it does not handle
392 # tagging itself; you need an external application for that.
393 # For an example script, check Girocco's cgi/tagproj.cgi.
394 # You may want to install the HTML::TagCloud Perl module to get
395 # a pretty tag cloud instead of just a list of tags.
397 # To enable system wide have in $GITWEB_CONFIG
398 # $feature{'ctags'}{'default'} = ['path_to_tag_script'];
399 # Project specific override is not supported.
404 # The maximum number of patches in a patchset generated in patch
405 # view. Set this to 0 or undef to disable patch view, or to a
406 # negative number to remove any limit.
408 # To disable system wide have in $GITWEB_CONFIG
409 # $feature{'patches'}{'default'} = [0];
410 # To have project specific config enable override in $GITWEB_CONFIG
411 # $feature{'patches'}{'override'} = 1;
412 # and in project config gitweb.patches = 0|n;
413 # where n is the maximum number of patches allowed in a patchset.
415 'sub' => \&feature_patches,
419 # Avatar support. When this feature is enabled, views such as
420 # shortlog or commit will display an avatar associated with
421 # the email of the committer(s) and/or author(s).
423 # Currently available providers are gravatar and picon.
424 # If an unknown provider is specified, the feature is disabled.
426 # Gravatar depends on Digest::MD5.
427 # Picon currently relies on the indiana.edu database.
429 # To enable system wide have in $GITWEB_CONFIG
430 # $feature{'avatar'}{'default'} = ['<provider>'];
431 # where <provider> is either gravatar or picon.
432 # To have project specific config enable override in $GITWEB_CONFIG
433 # $feature{'avatar'}{'override'} = 1;
434 # and in project config gitweb.avatar = <provider>;
436 'sub' => \&feature_avatar,
440 # Enable displaying how much time and how many git commands
441 # it took to generate and display page. Disabled by default.
442 # Project specific override is not supported.
447 # Enable turning some links into links to actions which require
448 # JavaScript to run (like 'blame_incremental'). Not enabled by
449 # default. Project specific override is currently not supported.
450 'javascript-actions' => {
455 sub gitweb_get_feature {
457 return unless exists $feature{$name};
458 my ($sub, $override, @defaults) = (
459 $feature{$name}{'sub'},
460 $feature{$name}{'override'},
461 @{$feature{$name}{'default'}});
462 # project specific override is possible only if we have project
463 our $git_dir; # global variable, declared later
464 if (!$override || !defined $git_dir) {
468 warn "feature $name is not overridable";
471 return $sub->(@defaults);
474 # A wrapper to check if a given feature is enabled.
475 # With this, you can say
477 # my $bool_feat = gitweb_check_feature('bool_feat');
478 # gitweb_check_feature('bool_feat') or somecode;
482 # my ($bool_feat) = gitweb_get_feature('bool_feat');
483 # (gitweb_get_feature('bool_feat'))[0] or somecode;
485 sub gitweb_check_feature {
486 return (gitweb_get_feature(@_))[0];
492 my ($val) = git_get_project_config($key, '--bool');
496 } elsif ($val eq 'true') {
498 } elsif ($val eq 'false') {
503 sub feature_snapshot {
506 my ($val) = git_get_project_config('snapshot');
509 @fmts = ($val eq 'none' ? () : split /\s*[,\s]\s*/, $val);
515 sub feature_patches {
516 my @val = (git_get_project_config('patches', '--int'));
526 my @val = (git_get_project_config('avatar'));
528 return @val ? @val : @_;
531 # checking HEAD file with -e is fragile if the repository was
532 # initialized long time ago (i.e. symlink HEAD) and was pack-ref'ed
534 sub check_head_link {
536 my $headfile = "$dir/HEAD";
537 return ((-e $headfile) ||
538 (-l $headfile && readlink($headfile) =~ /^refs\/heads\//));
541 sub check_export_ok {
543 return (check_head_link($dir) &&
544 (!$export_ok || -e "$dir/$export_ok") &&
545 (!$export_auth_hook || $export_auth_hook->($dir)));
548 # process alternate names for backward compatibility
549 # filter out unsupported (unknown) snapshot formats
550 sub filter_snapshot_fmts {
554 exists $known_snapshot_format_aliases{$_} ?
555 $known_snapshot_format_aliases{$_} : $_} @fmts;
557 exists $known_snapshot_formats{$_} &&
558 !$known_snapshot_formats{$_}{'disabled'}} @fmts;
561 our ($GITWEB_CONFIG, $GITWEB_CONFIG_SYSTEM);
562 sub evaluate_gitweb_config {
563 our $GITWEB_CONFIG = $ENV{'GITWEB_CONFIG'} || "++GITWEB_CONFIG++";
564 our $GITWEB_CONFIG_SYSTEM = $ENV{'GITWEB_CONFIG_SYSTEM'} || "++GITWEB_CONFIG_SYSTEM++";
565 # die if there are errors parsing config file
566 if (-e $GITWEB_CONFIG) {
569 } elsif (-e $GITWEB_CONFIG_SYSTEM) {
570 do $GITWEB_CONFIG_SYSTEM;
575 # Get loadavg of system, to compare against $maxload.
576 # Currently it requires '/proc/loadavg' present to get loadavg;
577 # if it is not present it returns 0, which means no load checking.
579 if( -e '/proc/loadavg' ){
580 open my $fd, '<', '/proc/loadavg'
582 my @load = split(/\s+/, scalar <$fd>);
585 # The first three columns measure CPU and IO utilization of the last one,
586 # five, and 10 minute periods. The fourth column shows the number of
587 # currently running processes and the total number of processes in the m/n
588 # format. The last column displays the last process ID used.
589 return $load[0] || 0;
591 # additional checks for load average should go here for things that don't export
597 # version of the core git binary
599 sub evaluate_git_version {
600 our $git_version = qx("$GIT" --version) =~ m/git version (.*)$/ ? $1 : "unknown";
601 $number_of_git_cmds++;
605 if (defined $maxload && get_loadavg() > $maxload) {
606 die_error(503, "The load average on the server is too high");
610 # ======================================================================
611 # input validation and dispatch
613 # input parameters can be collected from a variety of sources (presently, CGI
614 # and PATH_INFO), so we define an %input_params hash that collects them all
615 # together during validation: this allows subsequent uses (e.g. href()) to be
616 # agnostic of the parameter origin
618 our %input_params = ();
620 # input parameters are stored with the long parameter name as key. This will
621 # also be used in the href subroutine to convert parameters to their CGI
622 # equivalent, and since the href() usage is the most frequent one, we store
623 # the name -> CGI key mapping here, instead of the reverse.
625 # XXX: Warning: If you touch this, check the search form for updating,
628 our @cgi_param_mapping = (
636 hash_parent_base => "hpb",
641 snapshot_format => "sf",
642 extra_options => "opt",
643 search_use_regexp => "sr",
644 # this must be last entry (for manipulation from JavaScript)
647 our %cgi_param_mapping = @cgi_param_mapping;
649 # we will also need to know the possible actions, for validation
651 "blame" => \&git_blame,
652 "blame_incremental" => \&git_blame_incremental,
653 "blame_data" => \&git_blame_data,
654 "blobdiff" => \&git_blobdiff,
655 "blobdiff_plain" => \&git_blobdiff_plain,
656 "blob" => \&git_blob,
657 "blob_plain" => \&git_blob_plain,
658 "commitdiff" => \&git_commitdiff,
659 "commitdiff_plain" => \&git_commitdiff_plain,
660 "commit" => \&git_commit,
661 "forks" => \&git_forks,
662 "heads" => \&git_heads,
663 "history" => \&git_history,
665 "patch" => \&git_patch,
666 "patches" => \&git_patches,
668 "atom" => \&git_atom,
669 "search" => \&git_search,
670 "search_help" => \&git_search_help,
671 "shortlog" => \&git_shortlog,
672 "summary" => \&git_summary,
674 "tags" => \&git_tags,
675 "tree" => \&git_tree,
676 "snapshot" => \&git_snapshot,
677 "object" => \&git_object,
678 # those below don't need $project
679 "opml" => \&git_opml,
680 "project_list" => \&git_project_list,
681 "project_index" => \&git_project_index,
684 # finally, we have the hash of allowed extra_options for the commands that
686 our %allowed_options = (
687 "--no-merges" => [ qw(rss atom log shortlog history) ],
690 # fill %input_params with the CGI parameters. All values except for 'opt'
691 # should be single values, but opt can be an array. We should probably
692 # build an array of parameters that can be multi-valued, but since for the time
693 # being it's only this one, we just single it out
694 sub evaluate_query_params {
697 while (my ($name, $symbol) = each %cgi_param_mapping) {
698 if ($symbol eq 'opt') {
699 $input_params{$name} = [ $cgi->param($symbol) ];
701 $input_params{$name} = $cgi->param($symbol);
706 # now read PATH_INFO and update the parameter list for missing parameters
707 sub evaluate_path_info {
708 return if defined $input_params{'project'};
709 return if !$path_info;
710 $path_info =~ s,^/+,,;
711 return if !$path_info;
713 # find which part of PATH_INFO is project
714 my $project = $path_info;
716 while ($project && !check_head_link("$projectroot/$project")) {
717 $project =~ s,/*[^/]*$,,;
719 return unless $project;
720 $input_params{'project'} = $project;
722 # do not change any parameters if an action is given using the query string
723 return if $input_params{'action'};
724 $path_info =~ s,^\Q$project\E/*,,;
726 # next, check if we have an action
727 my $action = $path_info;
729 if (exists $actions{$action}) {
730 $path_info =~ s,^$action/*,,;
731 $input_params{'action'} = $action;
734 # list of actions that want hash_base instead of hash, but can have no
735 # pathname (f) parameter
742 # [$hash_parent_base[:$file_parent]..]$hash_parent[:$file_name]
743 my ($parentrefname, $parentpathname, $refname, $pathname) =
744 ($path_info =~ /^(?:(.+?)(?::(.+))?\.\.)?(.+?)(?::(.+))?$/);
746 # first, analyze the 'current' part
747 if (defined $pathname) {
748 # we got "branch:filename" or "branch:dir/"
749 # we could use git_get_type(branch:pathname), but:
750 # - it needs $git_dir
751 # - it does a git() call
752 # - the convention of terminating directories with a slash
753 # makes it superfluous
754 # - embedding the action in the PATH_INFO would make it even
756 $pathname =~ s,^/+,,;
757 if (!$pathname || substr($pathname, -1) eq "/") {
758 $input_params{'action'} ||= "tree";
761 # the default action depends on whether we had parent info
763 if ($parentrefname) {
764 $input_params{'action'} ||= "blobdiff_plain";
766 $input_params{'action'} ||= "blob_plain";
769 $input_params{'hash_base'} ||= $refname;
770 $input_params{'file_name'} ||= $pathname;
771 } elsif (defined $refname) {
772 # we got "branch". In this case we have to choose if we have to
773 # set hash or hash_base.
775 # Most of the actions without a pathname only want hash to be
776 # set, except for the ones specified in @wants_base that want
777 # hash_base instead. It should also be noted that hand-crafted
778 # links having 'history' as an action and no pathname or hash
779 # set will fail, but that happens regardless of PATH_INFO.
780 $input_params{'action'} ||= "shortlog";
781 if (grep { $_ eq $input_params{'action'} } @wants_base) {
782 $input_params{'hash_base'} ||= $refname;
784 $input_params{'hash'} ||= $refname;
788 # next, handle the 'parent' part, if present
789 if (defined $parentrefname) {
790 # a missing pathspec defaults to the 'current' filename, allowing e.g.
791 # someproject/blobdiff/oldrev..newrev:/filename
792 if ($parentpathname) {
793 $parentpathname =~ s,^/+,,;
794 $parentpathname =~ s,/$,,;
795 $input_params{'file_parent'} ||= $parentpathname;
797 $input_params{'file_parent'} ||= $input_params{'file_name'};
799 # we assume that hash_parent_base is wanted if a path was specified,
800 # or if the action wants hash_base instead of hash
801 if (defined $input_params{'file_parent'} ||
802 grep { $_ eq $input_params{'action'} } @wants_base) {
803 $input_params{'hash_parent_base'} ||= $parentrefname;
805 $input_params{'hash_parent'} ||= $parentrefname;
809 # for the snapshot action, we allow URLs in the form
810 # $project/snapshot/$hash.ext
811 # where .ext determines the snapshot and gets removed from the
812 # passed $refname to provide the $hash.
814 # To be able to tell that $refname includes the format extension, we
815 # require the following two conditions to be satisfied:
816 # - the hash input parameter MUST have been set from the $refname part
817 # of the URL (i.e. they must be equal)
818 # - the snapshot format MUST NOT have been defined already (e.g. from
820 # It's also useless to try any matching unless $refname has a dot,
821 # so we check for that too
822 if (defined $input_params{'action'} &&
823 $input_params{'action'} eq 'snapshot' &&
824 defined $refname && index($refname, '.') != -1 &&
825 $refname eq $input_params{'hash'} &&
826 !defined $input_params{'snapshot_format'}) {
827 # We loop over the known snapshot formats, checking for
828 # extensions. Allowed extensions are both the defined suffix
829 # (which includes the initial dot already) and the snapshot
830 # format key itself, with a prepended dot
831 while (my ($fmt, $opt) = each %known_snapshot_formats) {
833 unless ($hash =~ s/(\Q$opt->{'suffix'}\E|\Q.$fmt\E)$//) {
837 # a valid suffix was found, so set the snapshot format
838 # and reset the hash parameter
839 $input_params{'snapshot_format'} = $fmt;
840 $input_params{'hash'} = $hash;
841 # we also set the format suffix to the one requested
842 # in the URL: this way a request for e.g. .tgz returns
843 # a .tgz instead of a .tar.gz
844 $known_snapshot_formats{$fmt}{'suffix'} = $sfx;
850 our ($action, $project, $file_name, $file_parent, $hash, $hash_parent, $hash_base,
851 $hash_parent_base, @extra_options, $page, $searchtype, $search_use_regexp,
852 $searchtext, $search_regexp);
853 sub evaluate_and_validate_params {
854 our $action = $input_params{'action'};
855 if (defined $action) {
856 if (!validate_action($action)) {
857 die_error(400, "Invalid action parameter");
861 # parameters which are pathnames
862 our $project = $input_params{'project'};
863 if (defined $project) {
864 if (!validate_project($project)) {
866 die_error(404, "No such project");
870 our $file_name = $input_params{'file_name'};
871 if (defined $file_name) {
872 if (!validate_pathname($file_name)) {
873 die_error(400, "Invalid file parameter");
877 our $file_parent = $input_params{'file_parent'};
878 if (defined $file_parent) {
879 if (!validate_pathname($file_parent)) {
880 die_error(400, "Invalid file parent parameter");
884 # parameters which are refnames
885 our $hash = $input_params{'hash'};
887 if (!validate_refname($hash)) {
888 die_error(400, "Invalid hash parameter");
892 our $hash_parent = $input_params{'hash_parent'};
893 if (defined $hash_parent) {
894 if (!validate_refname($hash_parent)) {
895 die_error(400, "Invalid hash parent parameter");
899 our $hash_base = $input_params{'hash_base'};
900 if (defined $hash_base) {
901 if (!validate_refname($hash_base)) {
902 die_error(400, "Invalid hash base parameter");
906 our @extra_options = @{$input_params{'extra_options'}};
907 # @extra_options is always defined, since it can only be (currently) set from
908 # CGI, and $cgi->param() returns the empty array in array context if the param
910 foreach my $opt (@extra_options) {
911 if (not exists $allowed_options{$opt}) {
912 die_error(400, "Invalid option parameter");
914 if (not grep(/^$action$/, @{$allowed_options{$opt}})) {
915 die_error(400, "Invalid option parameter for this action");
919 our $hash_parent_base = $input_params{'hash_parent_base'};
920 if (defined $hash_parent_base) {
921 if (!validate_refname($hash_parent_base)) {
922 die_error(400, "Invalid hash parent base parameter");
927 our $page = $input_params{'page'};
929 if ($page =~ m/[^0-9]/) {
930 die_error(400, "Invalid page parameter");
934 our $searchtype = $input_params{'searchtype'};
935 if (defined $searchtype) {
936 if ($searchtype =~ m/[^a-z]/) {
937 die_error(400, "Invalid searchtype parameter");
941 our $search_use_regexp = $input_params{'search_use_regexp'};
943 our $searchtext = $input_params{'searchtext'};
945 if (defined $searchtext) {
946 if (length($searchtext) < 2) {
947 die_error(403, "At least two characters are required for search parameter");
949 $search_regexp = $search_use_regexp ? $searchtext : quotemeta $searchtext;
953 # path to the current git repository
955 sub evaluate_git_dir {
956 our $git_dir = "$projectroot/$project" if $project;
959 our (@snapshot_fmts, $git_avatar);
960 sub configure_gitweb_features {
961 # list of supported snapshot formats
962 our @snapshot_fmts = gitweb_get_feature('snapshot');
963 @snapshot_fmts = filter_snapshot_fmts(@snapshot_fmts);
965 # check that the avatar feature is set to a known provider name,
966 # and for each provider check if the dependencies are satisfied.
967 # if the provider name is invalid or the dependencies are not met,
968 # reset $git_avatar to the empty string.
969 our ($git_avatar) = gitweb_get_feature('avatar');
970 if ($git_avatar eq 'gravatar') {
971 $git_avatar = '' unless (eval { require Digest::MD5; 1; });
972 } elsif ($git_avatar eq 'picon') {
981 if (!defined $action) {
983 $action = git_get_type($hash);
984 } elsif (defined $hash_base && defined $file_name) {
985 $action = git_get_type("$hash_base:$file_name");
986 } elsif (defined $project) {
989 $action = 'project_list';
992 if (!defined($actions{$action})) {
993 die_error(400, "Unknown action");
995 if ($action !~ m/^(?:opml|project_list|project_index)$/ &&
997 die_error(400, "Project needed");
999 $actions{$action}->();
1003 our $t0 = [Time::HiRes::gettimeofday()]
1007 evaluate_gitweb_config();
1008 evaluate_git_version();
1011 # $projectroot and $projects_list might be set in gitweb config file
1012 $projects_list ||= $projectroot;
1014 evaluate_query_params();
1015 evaluate_path_info();
1016 evaluate_and_validate_params();
1019 configure_gitweb_features();
1024 our $is_last_request = sub { 1 };
1025 our ($pre_dispatch_hook, $post_dispatch_hook, $pre_listen_hook);
1029 return unless (@ARGV);
1031 require Getopt::Long;
1032 Getopt::Long::GetOptions(
1033 'fastcgi|fcgi|f' => sub {
1035 our $CGI = 'CGI::Fast';
1037 my $request_number = 0;
1038 # let each child service 100 requests
1039 our $is_last_request = sub { ++$request_number > 100 };
1041 'nproc|n=i' => sub {
1042 my ($arg, $val) = @_;
1043 return unless eval { require FCGI::ProcManager; 1; };
1044 my $proc_manager = FCGI::ProcManager->new({
1045 n_processes => $val,
1047 our $pre_listen_hook = sub { $proc_manager->pm_manage() };
1048 our $pre_dispatch_hook = sub { $proc_manager->pm_pre_dispatch() };
1049 our $post_dispatch_hook = sub { $proc_manager->pm_post_dispatch() };
1057 $pre_listen_hook->()
1058 if $pre_listen_hook;
1061 while ($cgi = $CGI->new()) {
1062 $pre_dispatch_hook->()
1063 if $pre_dispatch_hook;
1067 $pre_dispatch_hook->()
1068 if $post_dispatch_hook;
1070 last REQUEST if ($is_last_request->());
1079 ## ======================================================================
1082 # possible values of extra options
1083 # -full => 0|1 - use absolute/full URL ($my_uri/$my_url as base)
1084 # -replay => 1 - start from a current view (replay with modifications)
1085 # -path_info => 0|1 - don't use/use path_info URL (if possible)
1088 # default is to use -absolute url() i.e. $my_uri
1089 my $href = $params{-full} ? $my_url : $my_uri;
1091 $params{'project'} = $project unless exists $params{'project'};
1093 if ($params{-replay}) {
1094 while (my ($name, $symbol) = each %cgi_param_mapping) {
1095 if (!exists $params{$name}) {
1096 $params{$name} = $input_params{$name};
1101 my $use_pathinfo = gitweb_check_feature('pathinfo');
1102 if (defined $params{'project'} &&
1103 (exists $params{-path_info} ? $params{-path_info} : $use_pathinfo)) {
1104 # try to put as many parameters as possible in PATH_INFO:
1107 # - hash_parent or hash_parent_base:/file_parent
1108 # - hash or hash_base:/filename
1109 # - the snapshot_format as an appropriate suffix
1111 # When the script is the root DirectoryIndex for the domain,
1112 # $href here would be something like http://gitweb.example.com/
1113 # Thus, we strip any trailing / from $href, to spare us double
1114 # slashes in the final URL
1117 # Then add the project name, if present
1118 $href .= "/".esc_url($params{'project'});
1119 delete $params{'project'};
1121 # since we destructively absorb parameters, we keep this
1122 # boolean that remembers if we're handling a snapshot
1123 my $is_snapshot = $params{'action'} eq 'snapshot';
1125 # Summary just uses the project path URL, any other action is
1127 if (defined $params{'action'}) {
1128 $href .= "/".esc_url($params{'action'}) unless $params{'action'} eq 'summary';
1129 delete $params{'action'};
1132 # Next, we put hash_parent_base:/file_parent..hash_base:/file_name,
1133 # stripping nonexistent or useless pieces
1134 $href .= "/" if ($params{'hash_base'} || $params{'hash_parent_base'}
1135 || $params{'hash_parent'} || $params{'hash'});
1136 if (defined $params{'hash_base'}) {
1137 if (defined $params{'hash_parent_base'}) {
1138 $href .= esc_url($params{'hash_parent_base'});
1139 # skip the file_parent if it's the same as the file_name
1140 if (defined $params{'file_parent'}) {
1141 if (defined $params{'file_name'} && $params{'file_parent'} eq $params{'file_name'}) {
1142 delete $params{'file_parent'};
1143 } elsif ($params{'file_parent'} !~ /\.\./) {
1144 $href .= ":/".esc_url($params{'file_parent'});
1145 delete $params{'file_parent'};
1149 delete $params{'hash_parent'};
1150 delete $params{'hash_parent_base'};
1151 } elsif (defined $params{'hash_parent'}) {
1152 $href .= esc_url($params{'hash_parent'}). "..";
1153 delete $params{'hash_parent'};
1156 $href .= esc_url($params{'hash_base'});
1157 if (defined $params{'file_name'} && $params{'file_name'} !~ /\.\./) {
1158 $href .= ":/".esc_url($params{'file_name'});
1159 delete $params{'file_name'};
1161 delete $params{'hash'};
1162 delete $params{'hash_base'};
1163 } elsif (defined $params{'hash'}) {
1164 $href .= esc_url($params{'hash'});
1165 delete $params{'hash'};
1168 # If the action was a snapshot, we can absorb the
1169 # snapshot_format parameter too
1171 my $fmt = $params{'snapshot_format'};
1172 # snapshot_format should always be defined when href()
1173 # is called, but just in case some code forgets, we
1174 # fall back to the default
1175 $fmt ||= $snapshot_fmts[0];
1176 $href .= $known_snapshot_formats{$fmt}{'suffix'};
1177 delete $params{'snapshot_format'};
1181 # now encode the parameters explicitly
1183 for (my $i = 0; $i < @cgi_param_mapping; $i += 2) {
1184 my ($name, $symbol) = ($cgi_param_mapping[$i], $cgi_param_mapping[$i+1]);
1185 if (defined $params{$name}) {
1186 if (ref($params{$name}) eq "ARRAY") {
1187 foreach my $par (@{$params{$name}}) {
1188 push @result, $symbol . "=" . esc_param($par);
1191 push @result, $symbol . "=" . esc_param($params{$name});
1195 $href .= "?" . join(';', @result) if scalar @result;
1201 ## ======================================================================
1202 ## validation, quoting/unquoting and escaping
1204 sub validate_action {
1205 my $input = shift || return undef;
1206 return undef unless exists $actions{$input};
1210 sub validate_project {
1211 my $input = shift || return undef;
1212 if (!validate_pathname($input) ||
1213 !(-d "$projectroot/$input") ||
1214 !check_export_ok("$projectroot/$input") ||
1215 ($strict_export && !project_in_list($input))) {
1222 sub validate_pathname {
1223 my $input = shift || return undef;
1225 # no '.' or '..' as elements of path, i.e. no '.' nor '..'
1226 # at the beginning, at the end, and between slashes.
1227 # also this catches doubled slashes
1228 if ($input =~ m!(^|/)(|\.|\.\.)(/|$)!) {
1231 # no null characters
1232 if ($input =~ m!\0!) {
1238 sub validate_refname {
1239 my $input = shift || return undef;
1241 # textual hashes are O.K.
1242 if ($input =~ m/^[0-9a-fA-F]{40}$/) {
1245 # it must be correct pathname
1246 $input = validate_pathname($input)
1248 # restrictions on ref name according to git-check-ref-format
1249 if ($input =~ m!(/\.|\.\.|[\000-\040\177 ~^:?*\[]|/$)!) {
1255 # decode sequences of octets in utf8 into Perl's internal form,
1256 # which is utf-8 with utf8 flag set if needed. gitweb writes out
1257 # in utf-8 thanks to "binmode STDOUT, ':utf8'" at beginning
1260 return undef unless defined $str;
1261 if (utf8::valid($str)) {
1265 return decode($fallback_encoding, $str, Encode::FB_DEFAULT);
1269 # quote unsafe chars, but keep the slash, even when it's not
1270 # correct, but quoted slashes look too horrible in bookmarks
1273 return undef unless defined $str;
1274 $str =~ s/([^A-Za-z0-9\-_.~()\/:@ ]+)/CGI::escape($1)/eg;
1279 # quote unsafe chars in whole URL, so some charactrs cannot be quoted
1282 return undef unless defined $str;
1283 $str =~ s/([^A-Za-z0-9\-_.~();\/;?:@&=])/sprintf("%%%02X", ord($1))/eg;
1289 # replace invalid utf8 character with SUBSTITUTION sequence
1294 return undef unless defined $str;
1296 $str = to_utf8($str);
1297 $str = $cgi->escapeHTML($str);
1298 if ($opts{'-nbsp'}) {
1299 $str =~ s/ / /g;
1301 $str =~ s|([[:cntrl:]])|(($1 ne "\t") ? quot_cec($1) : $1)|eg;
1305 # quote control characters and escape filename to HTML
1310 return undef unless defined $str;
1312 $str = to_utf8($str);
1313 $str = $cgi->escapeHTML($str);
1314 if ($opts{'-nbsp'}) {
1315 $str =~ s/ / /g;
1317 $str =~ s|([[:cntrl:]])|quot_cec($1)|eg;
1321 # Make control characters "printable", using character escape codes (CEC)
1325 my %es = ( # character escape codes, aka escape sequences
1326 "\t" => '\t', # tab (HT)
1327 "\n" => '\n', # line feed (LF)
1328 "\r" => '\r', # carrige return (CR)
1329 "\f" => '\f', # form feed (FF)
1330 "\b" => '\b', # backspace (BS)
1331 "\a" => '\a', # alarm (bell) (BEL)
1332 "\e" => '\e', # escape (ESC)
1333 "\013" => '\v', # vertical tab (VT)
1334 "\000" => '\0', # nul character (NUL)
1336 my $chr = ( (exists $es{$cntrl})
1338 : sprintf('\%2x', ord($cntrl)) );
1339 if ($opts{-nohtml}) {
1342 return "<span class=\"cntrl\">$chr</span>";
1346 # Alternatively use unicode control pictures codepoints,
1347 # Unicode "printable representation" (PR)
1352 my $chr = sprintf('&#%04d;', 0x2400+ord($cntrl));
1353 if ($opts{-nohtml}) {
1356 return "<span class=\"cntrl\">$chr</span>";
1360 # git may return quoted and escaped filenames
1366 my %es = ( # character escape codes, aka escape sequences
1367 't' => "\t", # tab (HT, TAB)
1368 'n' => "\n", # newline (NL)
1369 'r' => "\r", # return (CR)
1370 'f' => "\f", # form feed (FF)
1371 'b' => "\b", # backspace (BS)
1372 'a' => "\a", # alarm (bell) (BEL)
1373 'e' => "\e", # escape (ESC)
1374 'v' => "\013", # vertical tab (VT)
1377 if ($seq =~ m/^[0-7]{1,3}$/) {
1378 # octal char sequence
1379 return chr(oct($seq));
1380 } elsif (exists $es{$seq}) {
1381 # C escape sequence, aka character escape code
1384 # quoted ordinary character
1388 if ($str =~ m/^"(.*)"$/) {
1391 $str =~ s/\\([^0-7]|[0-7]{1,3})/unq($1)/eg;
1396 # escape tabs (convert tabs to spaces)
1400 while ((my $pos = index($line, "\t")) != -1) {
1401 if (my $count = (8 - ($pos % 8))) {
1402 my $spaces = ' ' x $count;
1403 $line =~ s/\t/$spaces/;
1410 sub project_in_list {
1411 my $project = shift;
1412 my @list = git_get_projects_list();
1413 return @list && scalar(grep { $_->{'path'} eq $project } @list);
1416 ## ----------------------------------------------------------------------
1417 ## HTML aware string manipulation
1419 # Try to chop given string on a word boundary between position
1420 # $len and $len+$add_len. If there is no word boundary there,
1421 # chop at $len+$add_len. Do not chop if chopped part plus ellipsis
1422 # (marking chopped part) would be longer than given string.
1426 my $add_len = shift || 10;
1427 my $where = shift || 'right'; # 'left' | 'center' | 'right'
1429 # Make sure perl knows it is utf8 encoded so we don't
1430 # cut in the middle of a utf8 multibyte char.
1431 $str = to_utf8($str);
1433 # allow only $len chars, but don't cut a word if it would fit in $add_len
1434 # if it doesn't fit, cut it if it's still longer than the dots we would add
1435 # remove chopped character entities entirely
1437 # when chopping in the middle, distribute $len into left and right part
1438 # return early if chopping wouldn't make string shorter
1439 if ($where eq 'center') {
1440 return $str if ($len + 5 >= length($str)); # filler is length 5
1443 return $str if ($len + 4 >= length($str)); # filler is length 4
1446 # regexps: ending and beginning with word part up to $add_len
1447 my $endre = qr/.{$len}\w{0,$add_len}/;
1448 my $begre = qr/\w{0,$add_len}.{$len}/;
1450 if ($where eq 'left') {
1451 $str =~ m/^(.*?)($begre)$/;
1452 my ($lead, $body) = ($1, $2);
1453 if (length($lead) > 4) {
1456 return "$lead$body";
1458 } elsif ($where eq 'center') {
1459 $str =~ m/^($endre)(.*)$/;
1460 my ($left, $str) = ($1, $2);
1461 $str =~ m/^(.*?)($begre)$/;
1462 my ($mid, $right) = ($1, $2);
1463 if (length($mid) > 5) {
1466 return "$left$mid$right";
1469 $str =~ m/^($endre)(.*)$/;
1472 if (length($tail) > 4) {
1475 return "$body$tail";
1479 # takes the same arguments as chop_str, but also wraps a <span> around the
1480 # result with a title attribute if it does get chopped. Additionally, the
1481 # string is HTML-escaped.
1482 sub chop_and_escape_str {
1485 my $chopped = chop_str(@_);
1486 if ($chopped eq $str) {
1487 return esc_html($chopped);
1489 $str =~ s/[[:cntrl:]]/?/g;
1490 return $cgi->span({-title=>$str}, esc_html($chopped));
1494 ## ----------------------------------------------------------------------
1495 ## functions returning short strings
1497 # CSS class for given age value (in seconds)
1501 if (!defined $age) {
1503 } elsif ($age < 60*60*2) {
1505 } elsif ($age < 60*60*24*2) {
1512 # convert age in seconds to "nn units ago" string
1517 if ($age > 60*60*24*365*2) {
1518 $age_str = (int $age/60/60/24/365);
1519 $age_str .= " years ago";
1520 } elsif ($age > 60*60*24*(365/12)*2) {
1521 $age_str = int $age/60/60/24/(365/12);
1522 $age_str .= " months ago";
1523 } elsif ($age > 60*60*24*7*2) {
1524 $age_str = int $age/60/60/24/7;
1525 $age_str .= " weeks ago";
1526 } elsif ($age > 60*60*24*2) {
1527 $age_str = int $age/60/60/24;
1528 $age_str .= " days ago";
1529 } elsif ($age > 60*60*2) {
1530 $age_str = int $age/60/60;
1531 $age_str .= " hours ago";
1532 } elsif ($age > 60*2) {
1533 $age_str = int $age/60;
1534 $age_str .= " min ago";
1535 } elsif ($age > 2) {
1536 $age_str = int $age;
1537 $age_str .= " sec ago";
1539 $age_str .= " right now";
1545 S_IFINVALID => 0030000,
1546 S_IFGITLINK => 0160000,
1549 # submodule/subproject, a commit object reference
1553 return (($mode & S_IFMT) == S_IFGITLINK)
1556 # convert file mode in octal to symbolic file mode string
1558 my $mode = oct shift;
1560 if (S_ISGITLINK($mode)) {
1561 return 'm---------';
1562 } elsif (S_ISDIR($mode & S_IFMT)) {
1563 return 'drwxr-xr-x';
1564 } elsif (S_ISLNK($mode)) {
1565 return 'lrwxrwxrwx';
1566 } elsif (S_ISREG($mode)) {
1567 # git cares only about the executable bit
1568 if ($mode & S_IXUSR) {
1569 return '-rwxr-xr-x';
1571 return '-rw-r--r--';
1574 return '----------';
1578 # convert file mode in octal to file type string
1582 if ($mode !~ m/^[0-7]+$/) {
1588 if (S_ISGITLINK($mode)) {
1590 } elsif (S_ISDIR($mode & S_IFMT)) {
1592 } elsif (S_ISLNK($mode)) {
1594 } elsif (S_ISREG($mode)) {
1601 # convert file mode in octal to file type description string
1602 sub file_type_long {
1605 if ($mode !~ m/^[0-7]+$/) {
1611 if (S_ISGITLINK($mode)) {
1613 } elsif (S_ISDIR($mode & S_IFMT)) {
1615 } elsif (S_ISLNK($mode)) {
1617 } elsif (S_ISREG($mode)) {
1618 if ($mode & S_IXUSR) {
1619 return "executable";
1629 ## ----------------------------------------------------------------------
1630 ## functions returning short HTML fragments, or transforming HTML fragments
1631 ## which don't belong to other sections
1633 # format line of commit message.
1634 sub format_log_line_html {
1637 $line = esc_html($line, -nbsp=>1);
1638 $line =~ s{\b([0-9a-fA-F]{8,40})\b}{
1639 $cgi->a({-href => href(action=>"object", hash=>$1),
1640 -class => "text"}, $1);
1646 # format marker of refs pointing to given object
1648 # the destination action is chosen based on object type and current context:
1649 # - for annotated tags, we choose the tag view unless it's the current view
1650 # already, in which case we go to shortlog view
1651 # - for other refs, we keep the current view if we're in history, shortlog or
1652 # log view, and select shortlog otherwise
1653 sub format_ref_marker {
1654 my ($refs, $id) = @_;
1657 if (defined $refs->{$id}) {
1658 foreach my $ref (@{$refs->{$id}}) {
1659 # this code exploits the fact that non-lightweight tags are the
1660 # only indirect objects, and that they are the only objects for which
1661 # we want to use tag instead of shortlog as action
1662 my ($type, $name) = qw();
1663 my $indirect = ($ref =~ s/\^\{\}$//);
1664 # e.g. tags/v2.6.11 or heads/next
1665 if ($ref =~ m!^(.*?)s?/(.*)$!) {
1674 $class .= " indirect" if $indirect;
1676 my $dest_action = "shortlog";
1679 $dest_action = "tag" unless $action eq "tag";
1680 } elsif ($action =~ /^(history|(short)?log)$/) {
1681 $dest_action = $action;
1685 $dest .= "refs/" unless $ref =~ m!^refs/!;
1688 my $link = $cgi->a({
1690 action=>$dest_action,
1694 $markers .= " <span class=\"$class\" title=\"$ref\">" .
1700 return ' <span class="refs">'. $markers . '</span>';
1706 # format, perhaps shortened and with markers, title line
1707 sub format_subject_html {
1708 my ($long, $short, $href, $extra) = @_;
1709 $extra = '' unless defined($extra);
1711 if (length($short) < length($long)) {
1712 $long =~ s/[[:cntrl:]]/?/g;
1713 return $cgi->a({-href => $href, -class => "list subject",
1714 -title => to_utf8($long)},
1715 esc_html($short)) . $extra;
1717 return $cgi->a({-href => $href, -class => "list subject"},
1718 esc_html($long)) . $extra;
1722 # Rather than recomputing the url for an email multiple times, we cache it
1723 # after the first hit. This gives a visible benefit in views where the avatar
1724 # for the same email is used repeatedly (e.g. shortlog).
1725 # The cache is shared by all avatar engines (currently gravatar only), which
1726 # are free to use it as preferred. Since only one avatar engine is used for any
1727 # given page, there's no risk for cache conflicts.
1728 our %avatar_cache = ();
1730 # Compute the picon url for a given email, by using the picon search service over at
1731 # http://www.cs.indiana.edu/picons/search.html
1733 my $email = lc shift;
1734 if (!$avatar_cache{$email}) {
1735 my ($user, $domain) = split('@', $email);
1736 $avatar_cache{$email} =
1737 "http://www.cs.indiana.edu/cgi-pub/kinzler/piconsearch.cgi/" .
1739 "users+domains+unknown/up/single";
1741 return $avatar_cache{$email};
1744 # Compute the gravatar url for a given email, if it's not in the cache already.
1745 # Gravatar stores only the part of the URL before the size, since that's the
1746 # one computationally more expensive. This also allows reuse of the cache for
1747 # different sizes (for this particular engine).
1749 my $email = lc shift;
1751 $avatar_cache{$email} ||=
1752 "http://www.gravatar.com/avatar/" .
1753 Digest::MD5::md5_hex($email) . "?s=";
1754 return $avatar_cache{$email} . $size;
1757 # Insert an avatar for the given $email at the given $size if the feature
1759 sub git_get_avatar {
1760 my ($email, %opts) = @_;
1761 my $pre_white = ($opts{-pad_before} ? " " : "");
1762 my $post_white = ($opts{-pad_after} ? " " : "");
1763 $opts{-size} ||= 'default';
1764 my $size = $avatar_size{$opts{-size}} || $avatar_size{'default'};
1766 if ($git_avatar eq 'gravatar') {
1767 $url = gravatar_url($email, $size);
1768 } elsif ($git_avatar eq 'picon') {
1769 $url = picon_url($email);
1771 # Other providers can be added by extending the if chain, defining $url
1772 # as needed. If no variant puts something in $url, we assume avatars
1773 # are completely disabled/unavailable.
1776 "<img width=\"$size\" " .
1777 "class=\"avatar\" " .
1786 sub format_search_author {
1787 my ($author, $searchtype, $displaytext) = @_;
1788 my $have_search = gitweb_check_feature('search');
1792 if ($searchtype eq 'author') {
1793 $performed = "authored";
1794 } elsif ($searchtype eq 'committer') {
1795 $performed = "committed";
1798 return $cgi->a({-href => href(action=>"search", hash=>$hash,
1799 searchtext=>$author,
1800 searchtype=>$searchtype), class=>"list",
1801 title=>"Search for commits $performed by $author"},
1805 return $displaytext;
1809 # format the author name of the given commit with the given tag
1810 # the author name is chopped and escaped according to the other
1811 # optional parameters (see chop_str).
1812 sub format_author_html {
1815 my $author = chop_and_escape_str($co->{'author_name'}, @_);
1816 return "<$tag class=\"author\">" .
1817 format_search_author($co->{'author_name'}, "author",
1818 git_get_avatar($co->{'author_email'}, -pad_after => 1) .
1823 # format git diff header line, i.e. "diff --(git|combined|cc) ..."
1824 sub format_git_diff_header_line {
1826 my $diffinfo = shift;
1827 my ($from, $to) = @_;
1829 if ($diffinfo->{'nparents'}) {
1831 $line =~ s!^(diff (.*?) )"?.*$!$1!;
1832 if ($to->{'href'}) {
1833 $line .= $cgi->a({-href => $to->{'href'}, -class => "path"},
1834 esc_path($to->{'file'}));
1835 } else { # file was deleted (no href)
1836 $line .= esc_path($to->{'file'});
1840 $line =~ s!^(diff (.*?) )"?a/.*$!$1!;
1841 if ($from->{'href'}) {
1842 $line .= $cgi->a({-href => $from->{'href'}, -class => "path"},
1843 'a/' . esc_path($from->{'file'}));
1844 } else { # file was added (no href)
1845 $line .= 'a/' . esc_path($from->{'file'});
1848 if ($to->{'href'}) {
1849 $line .= $cgi->a({-href => $to->{'href'}, -class => "path"},
1850 'b/' . esc_path($to->{'file'}));
1851 } else { # file was deleted
1852 $line .= 'b/' . esc_path($to->{'file'});
1856 return "<div class=\"diff header\">$line</div>\n";
1859 # format extended diff header line, before patch itself
1860 sub format_extended_diff_header_line {
1862 my $diffinfo = shift;
1863 my ($from, $to) = @_;
1866 if ($line =~ s!^((copy|rename) from ).*$!$1! && $from->{'href'}) {
1867 $line .= $cgi->a({-href=>$from->{'href'}, -class=>"path"},
1868 esc_path($from->{'file'}));
1870 if ($line =~ s!^((copy|rename) to ).*$!$1! && $to->{'href'}) {
1871 $line .= $cgi->a({-href=>$to->{'href'}, -class=>"path"},
1872 esc_path($to->{'file'}));
1874 # match single <mode>
1875 if ($line =~ m/\s(\d{6})$/) {
1876 $line .= '<span class="info"> (' .
1877 file_type_long($1) .
1881 if ($line =~ m/^index [0-9a-fA-F]{40},[0-9a-fA-F]{40}/) {
1882 # can match only for combined diff
1884 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
1885 if ($from->{'href'}[$i]) {
1886 $line .= $cgi->a({-href=>$from->{'href'}[$i],
1888 substr($diffinfo->{'from_id'}[$i],0,7));
1893 $line .= ',' if ($i < $diffinfo->{'nparents'} - 1);
1896 if ($to->{'href'}) {
1897 $line .= $cgi->a({-href=>$to->{'href'}, -class=>"hash"},
1898 substr($diffinfo->{'to_id'},0,7));
1903 } elsif ($line =~ m/^index [0-9a-fA-F]{40}..[0-9a-fA-F]{40}/) {
1904 # can match only for ordinary diff
1905 my ($from_link, $to_link);
1906 if ($from->{'href'}) {
1907 $from_link = $cgi->a({-href=>$from->{'href'}, -class=>"hash"},
1908 substr($diffinfo->{'from_id'},0,7));
1910 $from_link = '0' x 7;
1912 if ($to->{'href'}) {
1913 $to_link = $cgi->a({-href=>$to->{'href'}, -class=>"hash"},
1914 substr($diffinfo->{'to_id'},0,7));
1918 my ($from_id, $to_id) = ($diffinfo->{'from_id'}, $diffinfo->{'to_id'});
1919 $line =~ s!$from_id\.\.$to_id!$from_link..$to_link!;
1922 return $line . "<br/>\n";
1925 # format from-file/to-file diff header
1926 sub format_diff_from_to_header {
1927 my ($from_line, $to_line, $diffinfo, $from, $to, @parents) = @_;
1932 #assert($line =~ m/^---/) if DEBUG;
1933 # no extra formatting for "^--- /dev/null"
1934 if (! $diffinfo->{'nparents'}) {
1935 # ordinary (single parent) diff
1936 if ($line =~ m!^--- "?a/!) {
1937 if ($from->{'href'}) {
1939 $cgi->a({-href=>$from->{'href'}, -class=>"path"},
1940 esc_path($from->{'file'}));
1943 esc_path($from->{'file'});
1946 $result .= qq!<div class="diff from_file">$line</div>\n!;
1949 # combined diff (merge commit)
1950 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
1951 if ($from->{'href'}[$i]) {
1953 $cgi->a({-href=>href(action=>"blobdiff",
1954 hash_parent=>$diffinfo->{'from_id'}[$i],
1955 hash_parent_base=>$parents[$i],
1956 file_parent=>$from->{'file'}[$i],
1957 hash=>$diffinfo->{'to_id'},
1959 file_name=>$to->{'file'}),
1961 -title=>"diff" . ($i+1)},
1964 $cgi->a({-href=>$from->{'href'}[$i], -class=>"path"},
1965 esc_path($from->{'file'}[$i]));
1967 $line = '--- /dev/null';
1969 $result .= qq!<div class="diff from_file">$line</div>\n!;
1974 #assert($line =~ m/^\+\+\+/) if DEBUG;
1975 # no extra formatting for "^+++ /dev/null"
1976 if ($line =~ m!^\+\+\+ "?b/!) {
1977 if ($to->{'href'}) {
1979 $cgi->a({-href=>$to->{'href'}, -class=>"path"},
1980 esc_path($to->{'file'}));
1983 esc_path($to->{'file'});
1986 $result .= qq!<div class="diff to_file">$line</div>\n!;
1991 # create note for patch simplified by combined diff
1992 sub format_diff_cc_simplified {
1993 my ($diffinfo, @parents) = @_;
1996 $result .= "<div class=\"diff header\">" .
1998 if (!is_deleted($diffinfo)) {
1999 $result .= $cgi->a({-href => href(action=>"blob",
2001 hash=>$diffinfo->{'to_id'},
2002 file_name=>$diffinfo->{'to_file'}),
2004 esc_path($diffinfo->{'to_file'}));
2006 $result .= esc_path($diffinfo->{'to_file'});
2008 $result .= "</div>\n" . # class="diff header"
2009 "<div class=\"diff nodifferences\">" .
2011 "</div>\n"; # class="diff nodifferences"
2016 # format patch (diff) line (not to be used for diff headers)
2017 sub format_diff_line {
2019 my ($from, $to) = @_;
2020 my $diff_class = "";
2024 if ($from && $to && ref($from->{'href'}) eq "ARRAY") {
2026 my $prefix = substr($line, 0, scalar @{$from->{'href'}});
2027 if ($line =~ m/^\@{3}/) {
2028 $diff_class = " chunk_header";
2029 } elsif ($line =~ m/^\\/) {
2030 $diff_class = " incomplete";
2031 } elsif ($prefix =~ tr/+/+/) {
2032 $diff_class = " add";
2033 } elsif ($prefix =~ tr/-/-/) {
2034 $diff_class = " rem";
2037 # assume ordinary diff
2038 my $char = substr($line, 0, 1);
2040 $diff_class = " add";
2041 } elsif ($char eq '-') {
2042 $diff_class = " rem";
2043 } elsif ($char eq '@') {
2044 $diff_class = " chunk_header";
2045 } elsif ($char eq "\\") {
2046 $diff_class = " incomplete";
2049 $line = untabify($line);
2050 if ($from && $to && $line =~ m/^\@{2} /) {
2051 my ($from_text, $from_start, $from_lines, $to_text, $to_start, $to_lines, $section) =
2052 $line =~ m/^\@{2} (-(\d+)(?:,(\d+))?) (\+(\d+)(?:,(\d+))?) \@{2}(.*)$/;
2054 $from_lines = 0 unless defined $from_lines;
2055 $to_lines = 0 unless defined $to_lines;
2057 if ($from->{'href'}) {
2058 $from_text = $cgi->a({-href=>"$from->{'href'}#l$from_start",
2059 -class=>"list"}, $from_text);
2061 if ($to->{'href'}) {
2062 $to_text = $cgi->a({-href=>"$to->{'href'}#l$to_start",
2063 -class=>"list"}, $to_text);
2065 $line = "<span class=\"chunk_info\">@@ $from_text $to_text @@</span>" .
2066 "<span class=\"section\">" . esc_html($section, -nbsp=>1) . "</span>";
2067 return "<div class=\"diff$diff_class\">$line</div>\n";
2068 } elsif ($from && $to && $line =~ m/^\@{3}/) {
2069 my ($prefix, $ranges, $section) = $line =~ m/^(\@+) (.*?) \@+(.*)$/;
2070 my (@from_text, @from_start, @from_nlines, $to_text, $to_start, $to_nlines);
2072 @from_text = split(' ', $ranges);
2073 for (my $i = 0; $i < @from_text; ++$i) {
2074 ($from_start[$i], $from_nlines[$i]) =
2075 (split(',', substr($from_text[$i], 1)), 0);
2078 $to_text = pop @from_text;
2079 $to_start = pop @from_start;
2080 $to_nlines = pop @from_nlines;
2082 $line = "<span class=\"chunk_info\">$prefix ";
2083 for (my $i = 0; $i < @from_text; ++$i) {
2084 if ($from->{'href'}[$i]) {
2085 $line .= $cgi->a({-href=>"$from->{'href'}[$i]#l$from_start[$i]",
2086 -class=>"list"}, $from_text[$i]);
2088 $line .= $from_text[$i];
2092 if ($to->{'href'}) {
2093 $line .= $cgi->a({-href=>"$to->{'href'}#l$to_start",
2094 -class=>"list"}, $to_text);
2098 $line .= " $prefix</span>" .
2099 "<span class=\"section\">" . esc_html($section, -nbsp=>1) . "</span>";
2100 return "<div class=\"diff$diff_class\">$line</div>\n";
2102 return "<div class=\"diff$diff_class\">" . esc_html($line, -nbsp=>1) . "</div>\n";
2105 # Generates undef or something like "_snapshot_" or "snapshot (_tbz2_ _zip_)",
2106 # linked. Pass the hash of the tree/commit to snapshot.
2107 sub format_snapshot_links {
2109 my $num_fmts = @snapshot_fmts;
2110 if ($num_fmts > 1) {
2111 # A parenthesized list of links bearing format names.
2112 # e.g. "snapshot (_tar.gz_ _zip_)"
2113 return "snapshot (" . join(' ', map
2120 }, $known_snapshot_formats{$_}{'display'})
2121 , @snapshot_fmts) . ")";
2122 } elsif ($num_fmts == 1) {
2123 # A single "snapshot" link whose tooltip bears the format name.
2125 my ($fmt) = @snapshot_fmts;
2131 snapshot_format=>$fmt
2133 -title => "in format: $known_snapshot_formats{$fmt}{'display'}"
2135 } else { # $num_fmts == 0
2140 ## ......................................................................
2141 ## functions returning values to be passed, perhaps after some
2142 ## transformation, to other functions; e.g. returning arguments to href()
2144 # returns hash to be passed to href to generate gitweb URL
2145 # in -title key it returns description of link
2147 my $format = shift || 'Atom';
2148 my %res = (action => lc($format));
2150 # feed links are possible only for project views
2151 return unless (defined $project);
2152 # some views should link to OPML, or to generic project feed,
2153 # or don't have specific feed yet (so they should use generic)
2154 return if ($action =~ /^(?:tags|heads|forks|tag|search)$/x);
2157 # branches refs uses 'refs/heads/' prefix (fullname) to differentiate
2158 # from tag links; this also makes possible to detect branch links
2159 if ((defined $hash_base && $hash_base =~ m!^refs/heads/(.*)$!) ||
2160 (defined $hash && $hash =~ m!^refs/heads/(.*)$!)) {
2163 # find log type for feed description (title)
2165 if (defined $file_name) {
2166 $type = "history of $file_name";
2167 $type .= "/" if ($action eq 'tree');
2168 $type .= " on '$branch'" if (defined $branch);
2170 $type = "log of $branch" if (defined $branch);
2173 $res{-title} = $type;
2174 $res{'hash'} = (defined $branch ? "refs/heads/$branch" : undef);
2175 $res{'file_name'} = $file_name;
2180 ## ----------------------------------------------------------------------
2181 ## git utility subroutines, invoking git commands
2183 # returns path to the core git executable and the --git-dir parameter as list
2185 $number_of_git_cmds++;
2186 return $GIT, '--git-dir='.$git_dir;
2189 # quote the given arguments for passing them to the shell
2190 # quote_command("command", "arg 1", "arg with ' and ! characters")
2191 # => "'command' 'arg 1' 'arg with '\'' and '\!' characters'"
2192 # Try to avoid using this function wherever possible.
2195 map { my $a = $_; $a =~ s/(['!])/'\\$1'/g; "'$a'" } @_ );
2198 # get HEAD ref of given project as hash
2199 sub git_get_head_hash {
2200 return git_get_full_hash(shift, 'HEAD');
2203 sub git_get_full_hash {
2204 return git_get_hash(@_);
2207 sub git_get_short_hash {
2208 return git_get_hash(@_, '--short=7');
2212 my ($project, $hash, @options) = @_;
2213 my $o_git_dir = $git_dir;
2215 $git_dir = "$projectroot/$project";
2216 if (open my $fd, '-|', git_cmd(), 'rev-parse',
2217 '--verify', '-q', @options, $hash) {
2219 chomp $retval if defined $retval;
2222 if (defined $o_git_dir) {
2223 $git_dir = $o_git_dir;
2228 # get type of given object
2232 open my $fd, "-|", git_cmd(), "cat-file", '-t', $hash or return;
2234 close $fd or return;
2239 # repository configuration
2240 our $config_file = '';
2243 # store multiple values for single key as anonymous array reference
2244 # single values stored directly in the hash, not as [ <value> ]
2245 sub hash_set_multi {
2246 my ($hash, $key, $value) = @_;
2248 if (!exists $hash->{$key}) {
2249 $hash->{$key} = $value;
2250 } elsif (!ref $hash->{$key}) {
2251 $hash->{$key} = [ $hash->{$key}, $value ];
2253 push @{$hash->{$key}}, $value;
2257 # return hash of git project configuration
2258 # optionally limited to some section, e.g. 'gitweb'
2259 sub git_parse_project_config {
2260 my $section_regexp = shift;
2265 open my $fh, "-|", git_cmd(), "config", '-z', '-l',
2268 while (my $keyval = <$fh>) {
2270 my ($key, $value) = split(/\n/, $keyval, 2);
2272 hash_set_multi(\%config, $key, $value)
2273 if (!defined $section_regexp || $key =~ /^(?:$section_regexp)\./o);
2280 # convert config value to boolean: 'true' or 'false'
2281 # no value, number > 0, 'true' and 'yes' values are true
2282 # rest of values are treated as false (never as error)
2283 sub config_to_bool {
2286 return 1 if !defined $val; # section.key
2288 # strip leading and trailing whitespace
2292 return (($val =~ /^\d+$/ && $val) || # section.key = 1
2293 ($val =~ /^(?:true|yes)$/i)); # section.key = true
2296 # convert config value to simple decimal number
2297 # an optional value suffix of 'k', 'm', or 'g' will cause the value
2298 # to be multiplied by 1024, 1048576, or 1073741824
2302 # strip leading and trailing whitespace
2306 if (my ($num, $unit) = ($val =~ /^([0-9]*)([kmg])$/i)) {
2308 # unknown unit is treated as 1
2309 return $num * ($unit eq 'g' ? 1073741824 :
2310 $unit eq 'm' ? 1048576 :
2311 $unit eq 'k' ? 1024 : 1);
2316 # convert config value to array reference, if needed
2317 sub config_to_multi {
2320 return ref($val) ? $val : (defined($val) ? [ $val ] : []);
2323 sub git_get_project_config {
2324 my ($key, $type) = @_;
2326 return unless defined $git_dir;
2329 return unless ($key);
2330 $key =~ s/^gitweb\.//;
2331 return if ($key =~ m/\W/);
2334 if (defined $type) {
2337 unless ($type eq 'bool' || $type eq 'int');
2341 if (!defined $config_file ||
2342 $config_file ne "$git_dir/config") {
2343 %config = git_parse_project_config('gitweb');
2344 $config_file = "$git_dir/config";
2347 # check if config variable (key) exists
2348 return unless exists $config{"gitweb.$key"};
2351 if (!defined $type) {
2352 return $config{"gitweb.$key"};
2353 } elsif ($type eq 'bool') {
2354 # backward compatibility: 'git config --bool' returns true/false
2355 return config_to_bool($config{"gitweb.$key"}) ? 'true' : 'false';
2356 } elsif ($type eq 'int') {
2357 return config_to_int($config{"gitweb.$key"});
2359 return $config{"gitweb.$key"};
2362 # get hash of given path at given ref
2363 sub git_get_hash_by_path {
2365 my $path = shift || return undef;
2370 open my $fd, "-|", git_cmd(), "ls-tree", $base, "--", $path
2371 or die_error(500, "Open git-ls-tree failed");
2373 close $fd or return undef;
2375 if (!defined $line) {
2376 # there is no tree or hash given by $path at $base
2380 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'
2381 $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t/;
2382 if (defined $type && $type ne $2) {
2383 # type doesn't match
2389 # get path of entry with given hash at given tree-ish (ref)
2390 # used to get 'from' filename for combined diff (merge commit) for renames
2391 sub git_get_path_by_hash {
2392 my $base = shift || return;
2393 my $hash = shift || return;
2397 open my $fd, "-|", git_cmd(), "ls-tree", '-r', '-t', '-z', $base
2399 while (my $line = <$fd>) {
2402 #'040000 tree 595596a6a9117ddba9fe379b6b012b558bac8423 gitweb'
2403 #'100644 blob e02e90f0429be0d2a69b76571101f20b8f75530f gitweb/README'
2404 if ($line =~ m/(?:[0-9]+) (?:.+) $hash\t(.+)$/) {
2413 ## ......................................................................
2414 ## git utility functions, directly accessing git repository
2416 sub git_get_project_description {
2419 $git_dir = "$projectroot/$path";
2420 open my $fd, '<', "$git_dir/description"
2421 or return git_get_project_config('description');
2424 if (defined $descr) {
2430 sub git_get_project_ctags {
2434 $git_dir = "$projectroot/$path";
2435 opendir my $dh, "$git_dir/ctags"
2437 foreach (grep { -f $_ } map { "$git_dir/ctags/$_" } readdir($dh)) {
2438 open my $ct, '<', $_ or next;
2442 my $ctag = $_; $ctag =~ s#.*/##;
2443 $ctags->{$ctag} = $val;
2449 sub git_populate_project_tagcloud {
2452 # First, merge different-cased tags; tags vote on casing
2454 foreach (keys %$ctags) {
2455 $ctags_lc{lc $_}->{count} += $ctags->{$_};
2456 if (not $ctags_lc{lc $_}->{topcount}
2457 or $ctags_lc{lc $_}->{topcount} < $ctags->{$_}) {
2458 $ctags_lc{lc $_}->{topcount} = $ctags->{$_};
2459 $ctags_lc{lc $_}->{topname} = $_;
2464 if (eval { require HTML::TagCloud; 1; }) {
2465 $cloud = HTML::TagCloud->new;
2466 foreach (sort keys %ctags_lc) {
2467 # Pad the title with spaces so that the cloud looks
2469 my $title = $ctags_lc{$_}->{topname};
2470 $title =~ s/ / /g;
2471 $title =~ s/^/ /g;
2472 $title =~ s/$/ /g;
2473 $cloud->add($title, $home_link."?by_tag=".$_, $ctags_lc{$_}->{count});
2476 $cloud = \%ctags_lc;
2481 sub git_show_project_tagcloud {
2482 my ($cloud, $count) = @_;
2483 print STDERR ref($cloud)."..\n";
2484 if (ref $cloud eq 'HTML::TagCloud') {
2485 return $cloud->html_and_css($count);
2487 my @tags = sort { $cloud->{$a}->{count} <=> $cloud->{$b}->{count} } keys %$cloud;
2488 return '<p align="center">' . join (', ', map {
2489 "<a href=\"$home_link?by_tag=$_\">$cloud->{$_}->{topname}</a>"
2490 } splice(@tags, 0, $count)) . '</p>';
2494 sub git_get_project_url_list {
2497 $git_dir = "$projectroot/$path";
2498 open my $fd, '<', "$git_dir/cloneurl"
2499 or return wantarray ?
2500 @{ config_to_multi(git_get_project_config('url')) } :
2501 config_to_multi(git_get_project_config('url'));
2502 my @git_project_url_list = map { chomp; $_ } <$fd>;
2505 return wantarray ? @git_project_url_list : \@git_project_url_list;
2508 sub git_get_projects_list {
2513 $filter =~ s/\.git$//;
2515 my $check_forks = gitweb_check_feature('forks');
2517 if (-d $projects_list) {
2518 # search in directory
2519 my $dir = $projects_list . ($filter ? "/$filter" : '');
2520 # remove the trailing "/"
2522 my $pfxlen = length("$dir");
2523 my $pfxdepth = ($dir =~ tr!/!!);
2526 follow_fast => 1, # follow symbolic links
2527 follow_skip => 2, # ignore duplicates
2528 dangling_symlinks => 0, # ignore dangling symlinks, silently
2530 # skip project-list toplevel, if we get it.
2531 return if (m!^[/.]$!);
2532 # only directories can be git repositories
2533 return unless (-d $_);
2534 # don't traverse too deep (Find is super slow on os x)
2535 if (($File::Find::name =~ tr!/!!) - $pfxdepth > $project_maxdepth) {
2536 $File::Find::prune = 1;
2540 my $subdir = substr($File::Find::name, $pfxlen + 1);
2541 # we check related file in $projectroot
2542 my $path = ($filter ? "$filter/" : '') . $subdir;
2543 if (check_export_ok("$projectroot/$path")) {
2544 push @list, { path => $path };
2545 $File::Find::prune = 1;
2550 } elsif (-f $projects_list) {
2551 # read from file(url-encoded):
2552 # 'git%2Fgit.git Linus+Torvalds'
2553 # 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin'
2554 # 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman'
2556 open my $fd, '<', $projects_list or return;
2558 while (my $line = <$fd>) {
2560 my ($path, $owner) = split ' ', $line;
2561 $path = unescape($path);
2562 $owner = unescape($owner);
2563 if (!defined $path) {
2566 if ($filter ne '') {
2567 # looking for forks;
2568 my $pfx = substr($path, 0, length($filter));
2569 if ($pfx ne $filter) {
2572 my $sfx = substr($path, length($filter));
2573 if ($sfx !~ /^\/.*\.git$/) {
2576 } elsif ($check_forks) {
2578 foreach my $filter (keys %paths) {
2579 # looking for forks;
2580 my $pfx = substr($path, 0, length($filter));
2581 if ($pfx ne $filter) {
2584 my $sfx = substr($path, length($filter));
2585 if ($sfx !~ /^\/.*\.git$/) {
2588 # is a fork, don't include it in
2593 if (check_export_ok("$projectroot/$path")) {
2596 owner => to_utf8($owner),
2599 (my $forks_path = $path) =~ s/\.git$//;
2600 $paths{$forks_path}++;
2608 our $gitweb_project_owner = undef;
2609 sub git_get_project_list_from_file {
2611 return if (defined $gitweb_project_owner);
2613 $gitweb_project_owner = {};
2614 # read from file (url-encoded):
2615 # 'git%2Fgit.git Linus+Torvalds'
2616 # 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin'
2617 # 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman'
2618 if (-f $projects_list) {
2619 open(my $fd, '<', $projects_list);
2620 while (my $line = <$fd>) {
2622 my ($pr, $ow) = split ' ', $line;
2623 $pr = unescape($pr);
2624 $ow = unescape($ow);
2625 $gitweb_project_owner->{$pr} = to_utf8($ow);
2631 sub git_get_project_owner {
2632 my $project = shift;
2635 return undef unless $project;
2636 $git_dir = "$projectroot/$project";
2638 if (!defined $gitweb_project_owner) {
2639 git_get_project_list_from_file();
2642 if (exists $gitweb_project_owner->{$project}) {
2643 $owner = $gitweb_project_owner->{$project};
2645 if (!defined $owner){
2646 $owner = git_get_project_config('owner');
2648 if (!defined $owner) {
2649 $owner = get_file_owner("$git_dir");
2655 sub git_get_last_activity {
2659 $git_dir = "$projectroot/$path";
2660 open($fd, "-|", git_cmd(), 'for-each-ref',
2661 '--format=%(committer)',
2662 '--sort=-committerdate',
2664 'refs/heads') or return;
2665 my $most_recent = <$fd>;
2666 close $fd or return;
2667 if (defined $most_recent &&
2668 $most_recent =~ / (\d+) [-+][01]\d\d\d$/) {
2670 my $age = time - $timestamp;
2671 return ($age, age_string($age));
2673 return (undef, undef);
2676 sub git_get_references {
2677 my $type = shift || "";
2679 # 5dc01c595e6c6ec9ccda4f6f69c131c0dd945f8c refs/tags/v2.6.11
2680 # c39ae07f393806ccf406ef966e9a15afc43cc36a refs/tags/v2.6.11^{}
2681 open my $fd, "-|", git_cmd(), "show-ref", "--dereference",
2682 ($type ? ("--", "refs/$type") : ()) # use -- <pattern> if $type
2685 while (my $line = <$fd>) {
2687 if ($line =~ m!^([0-9a-fA-F]{40})\srefs/($type.*)$!) {
2688 if (defined $refs{$1}) {
2689 push @{$refs{$1}}, $2;
2695 close $fd or return;
2699 sub git_get_rev_name_tags {
2700 my $hash = shift || return undef;
2702 open my $fd, "-|", git_cmd(), "name-rev", "--tags", $hash
2704 my $name_rev = <$fd>;
2707 if ($name_rev =~ m|^$hash tags/(.*)$|) {
2710 # catches also '$hash undefined' output
2715 ## ----------------------------------------------------------------------
2716 ## parse to hash functions
2720 my $tz = shift || "-0000";
2723 my @months = ("Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec");
2724 my @days = ("Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat");
2725 my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($epoch);
2726 $date{'hour'} = $hour;
2727 $date{'minute'} = $min;
2728 $date{'mday'} = $mday;
2729 $date{'day'} = $days[$wday];
2730 $date{'month'} = $months[$mon];
2731 $date{'rfc2822'} = sprintf "%s, %d %s %4d %02d:%02d:%02d +0000",
2732 $days[$wday], $mday, $months[$mon], 1900+$year, $hour ,$min, $sec;
2733 $date{'mday-time'} = sprintf "%d %s %02d:%02d",
2734 $mday, $months[$mon], $hour ,$min;
2735 $date{'iso-8601'} = sprintf "%04d-%02d-%02dT%02d:%02d:%02dZ",
2736 1900+$year, 1+$mon, $mday, $hour ,$min, $sec;
2738 $tz =~ m/^([+\-][0-9][0-9])([0-9][0-9])$/;
2739 my $local = $epoch + ((int $1 + ($2/60)) * 3600);
2740 ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($local);
2741 $date{'hour_local'} = $hour;
2742 $date{'minute_local'} = $min;
2743 $date{'tz_local'} = $tz;
2744 $date{'iso-tz'} = sprintf("%04d-%02d-%02d %02d:%02d:%02d %s",
2745 1900+$year, $mon+1, $mday,
2746 $hour, $min, $sec, $tz);
2755 open my $fd, "-|", git_cmd(), "cat-file", "tag", $tag_id or return;
2756 $tag{'id'} = $tag_id;
2757 while (my $line = <$fd>) {
2759 if ($line =~ m/^object ([0-9a-fA-F]{40})$/) {
2760 $tag{'object'} = $1;
2761 } elsif ($line =~ m/^type (.+)$/) {
2763 } elsif ($line =~ m/^tag (.+)$/) {
2765 } elsif ($line =~ m/^tagger (.*) ([0-9]+) (.*)$/) {
2766 $tag{'author'} = $1;
2767 $tag{'author_epoch'} = $2;
2768 $tag{'author_tz'} = $3;
2769 if ($tag{'author'} =~ m/^([^<]+) <([^>]*)>/) {
2770 $tag{'author_name'} = $1;
2771 $tag{'author_email'} = $2;
2773 $tag{'author_name'} = $tag{'author'};
2775 } elsif ($line =~ m/--BEGIN/) {
2776 push @comment, $line;
2778 } elsif ($line eq "") {
2782 push @comment, <$fd>;
2783 $tag{'comment'} = \@comment;
2784 close $fd or return;
2785 if (!defined $tag{'name'}) {
2791 sub parse_commit_text {
2792 my ($commit_text, $withparents) = @_;
2793 my @commit_lines = split '\n', $commit_text;
2796 pop @commit_lines; # Remove '\0'
2798 if (! @commit_lines) {
2802 my $header = shift @commit_lines;
2803 if ($header !~ m/^[0-9a-fA-F]{40}/) {
2806 ($co{'id'}, my @parents) = split ' ', $header;
2807 while (my $line = shift @commit_lines) {
2808 last if $line eq "\n";
2809 if ($line =~ m/^tree ([0-9a-fA-F]{40})$/) {
2811 } elsif ((!defined $withparents) && ($line =~ m/^parent ([0-9a-fA-F]{40})$/)) {
2813 } elsif ($line =~ m/^author (.*) ([0-9]+) (.*)$/) {
2814 $co{'author'} = to_utf8($1);
2815 $co{'author_epoch'} = $2;
2816 $co{'author_tz'} = $3;
2817 if ($co{'author'} =~ m/^([^<]+) <([^>]*)>/) {
2818 $co{'author_name'} = $1;
2819 $co{'author_email'} = $2;
2821 $co{'author_name'} = $co{'author'};
2823 } elsif ($line =~ m/^committer (.*) ([0-9]+) (.*)$/) {
2824 $co{'committer'} = to_utf8($1);
2825 $co{'committer_epoch'} = $2;
2826 $co{'committer_tz'} = $3;
2827 if ($co{'committer'} =~ m/^([^<]+) <([^>]*)>/) {
2828 $co{'committer_name'} = $1;
2829 $co{'committer_email'} = $2;
2831 $co{'committer_name'} = $co{'committer'};
2835 if (!defined $co{'tree'}) {
2838 $co{'parents'} = \@parents;
2839 $co{'parent'} = $parents[0];
2841 foreach my $title (@commit_lines) {
2844 $co{'title'} = chop_str($title, 80, 5);
2845 # remove leading stuff of merges to make the interesting part visible
2846 if (length($title) > 50) {
2847 $title =~ s/^Automatic //;
2848 $title =~ s/^merge (of|with) /Merge ... /i;
2849 if (length($title) > 50) {
2850 $title =~ s/(http|rsync):\/\///;
2852 if (length($title) > 50) {
2853 $title =~ s/(master|www|rsync)\.//;
2855 if (length($title) > 50) {
2856 $title =~ s/kernel.org:?//;
2858 if (length($title) > 50) {
2859 $title =~ s/\/pub\/scm//;
2862 $co{'title_short'} = chop_str($title, 50, 5);
2866 if (! defined $co{'title'} || $co{'title'} eq "") {
2867 $co{'title'} = $co{'title_short'} = '(no commit message)';
2869 # remove added spaces
2870 foreach my $line (@commit_lines) {
2873 $co{'comment'} = \@commit_lines;
2875 my $age = time - $co{'committer_epoch'};
2877 $co{'age_string'} = age_string($age);
2878 my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($co{'committer_epoch'});
2879 if ($age > 60*60*24*7*2) {
2880 $co{'age_string_date'} = sprintf "%4i-%02u-%02i", 1900 + $year, $mon+1, $mday;
2881 $co{'age_string_age'} = $co{'age_string'};
2883 $co{'age_string_date'} = $co{'age_string'};
2884 $co{'age_string_age'} = sprintf "%4i-%02u-%02i", 1900 + $year, $mon+1, $mday;
2890 my ($commit_id) = @_;
2895 open my $fd, "-|", git_cmd(), "rev-list",
2901 or die_error(500, "Open git-rev-list failed");
2902 %co = parse_commit_text(<$fd>, 1);
2909 my ($commit_id, $maxcount, $skip, $filename, @args) = @_;
2917 open my $fd, "-|", git_cmd(), "rev-list",
2920 ("--max-count=" . $maxcount),
2921 ("--skip=" . $skip),
2925 ($filename ? ($filename) : ())
2926 or die_error(500, "Open git-rev-list failed");
2927 while (my $line = <$fd>) {
2928 my %co = parse_commit_text($line);
2933 return wantarray ? @cos : \@cos;
2936 # parse line of git-diff-tree "raw" output
2937 sub parse_difftree_raw_line {
2941 # ':100644 100644 03b218260e99b78c6df0ed378e59ed9205ccc96d 3b93d5e7cc7f7dd4ebed13a5cc1a4ad976fc94d8 M ls-files.c'
2942 # ':100644 100644 7f9281985086971d3877aca27704f2aaf9c448ce bc190ebc71bbd923f2b728e505408f5e54bd073a M rev-tree.c'
2943 if ($line =~ m/^:([0-7]{6}) ([0-7]{6}) ([0-9a-fA-F]{40}) ([0-9a-fA-F]{40}) (.)([0-9]{0,3})\t(.*)$/) {
2944 $res{'from_mode'} = $1;
2945 $res{'to_mode'} = $2;
2946 $res{'from_id'} = $3;
2948 $res{'status'} = $5;
2949 $res{'similarity'} = $6;
2950 if ($res{'status'} eq 'R' || $res{'status'} eq 'C') { # renamed or copied
2951 ($res{'from_file'}, $res{'to_file'}) = map { unquote($_) } split("\t", $7);
2953 $res{'from_file'} = $res{'to_file'} = $res{'file'} = unquote($7);
2956 # '::100755 100755 100755 60e79ca1b01bc8b057abe17ddab484699a7f5fdb 94067cc5f73388f33722d52ae02f44692bc07490 94067cc5f73388f33722d52ae02f44692bc07490 MR git-gui/git-gui.sh'
2957 # combined diff (for merge commit)
2958 elsif ($line =~ s/^(::+)((?:[0-7]{6} )+)((?:[0-9a-fA-F]{40} )+)([a-zA-Z]+)\t(.*)$//) {
2959 $res{'nparents'} = length($1);
2960 $res{'from_mode'} = [ split(' ', $2) ];
2961 $res{'to_mode'} = pop @{$res{'from_mode'}};
2962 $res{'from_id'} = [ split(' ', $3) ];
2963 $res{'to_id'} = pop @{$res{'from_id'}};
2964 $res{'status'} = [ split('', $4) ];
2965 $res{'to_file'} = unquote($5);
2967 # 'c512b523472485aef4fff9e57b229d9d243c967f'
2968 elsif ($line =~ m/^([0-9a-fA-F]{40})$/) {
2969 $res{'commit'} = $1;
2972 return wantarray ? %res : \%res;
2975 # wrapper: return parsed line of git-diff-tree "raw" output
2976 # (the argument might be raw line, or parsed info)
2977 sub parsed_difftree_line {
2978 my $line_or_ref = shift;
2980 if (ref($line_or_ref) eq "HASH") {
2981 # pre-parsed (or generated by hand)
2982 return $line_or_ref;
2984 return parse_difftree_raw_line($line_or_ref);
2988 # parse line of git-ls-tree output
2989 sub parse_ls_tree_line {
2995 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa 16717 panic.c'
2996 $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40}) +(-|[0-9]+)\t(.+)$/s;
3005 $res{'name'} = unquote($5);
3008 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'
3009 $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t(.+)$/s;
3017 $res{'name'} = unquote($4);
3021 return wantarray ? %res : \%res;
3024 # generates _two_ hashes, references to which are passed as 2 and 3 argument
3025 sub parse_from_to_diffinfo {
3026 my ($diffinfo, $from, $to, @parents) = @_;
3028 if ($diffinfo->{'nparents'}) {
3030 $from->{'file'} = [];
3031 $from->{'href'} = [];
3032 fill_from_file_info($diffinfo, @parents)
3033 unless exists $diffinfo->{'from_file'};
3034 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
3035 $from->{'file'}[$i] =
3036 defined $diffinfo->{'from_file'}[$i] ?
3037 $diffinfo->{'from_file'}[$i] :
3038 $diffinfo->{'to_file'};
3039 if ($diffinfo->{'status'}[$i] ne "A") { # not new (added) file
3040 $from->{'href'}[$i] = href(action=>"blob",
3041 hash_base=>$parents[$i],
3042 hash=>$diffinfo->{'from_id'}[$i],
3043 file_name=>$from->{'file'}[$i]);
3045 $from->{'href'}[$i] = undef;
3049 # ordinary (not combined) diff
3050 $from->{'file'} = $diffinfo->{'from_file'};
3051 if ($diffinfo->{'status'} ne "A") { # not new (added) file
3052 $from->{'href'} = href(action=>"blob", hash_base=>$hash_parent,
3053 hash=>$diffinfo->{'from_id'},
3054 file_name=>$from->{'file'});
3056 delete $from->{'href'};
3060 $to->{'file'} = $diffinfo->{'to_file'};
3061 if (!is_deleted($diffinfo)) { # file exists in result
3062 $to->{'href'} = href(action=>"blob", hash_base=>$hash,
3063 hash=>$diffinfo->{'to_id'},
3064 file_name=>$to->{'file'});
3066 delete $to->{'href'};
3070 ## ......................................................................
3071 ## parse to array of hashes functions
3073 sub git_get_heads_list {
3077 open my $fd, '-|', git_cmd(), 'for-each-ref',
3078 ($limit ? '--count='.($limit+1) : ()), '--sort=-committerdate',
3079 '--format=%(objectname) %(refname) %(subject)%00%(committer)',
3082 while (my $line = <$fd>) {
3086 my ($refinfo, $committerinfo) = split(/\0/, $line);
3087 my ($hash, $name, $title) = split(' ', $refinfo, 3);
3088 my ($committer, $epoch, $tz) =
3089 ($committerinfo =~ /^(.*) ([0-9]+) (.*)$/);
3090 $ref_item{'fullname'} = $name;
3091 $name =~ s!^refs/heads/!!;
3093 $ref_item{'name'} = $name;
3094 $ref_item{'id'} = $hash;
3095 $ref_item{'title'} = $title || '(no commit message)';
3096 $ref_item{'epoch'} = $epoch;
3098 $ref_item{'age'} = age_string(time - $ref_item{'epoch'});
3100 $ref_item{'age'} = "unknown";
3103 push @headslist, \%ref_item;
3107 return wantarray ? @headslist : \@headslist;
3110 sub git_get_tags_list {
3114 open my $fd, '-|', git_cmd(), 'for-each-ref',
3115 ($limit ? '--count='.($limit+1) : ()), '--sort=-creatordate',
3116 '--format=%(objectname) %(objecttype) %(refname) '.
3117 '%(*objectname) %(*objecttype) %(subject)%00%(creator)',
3120 while (my $line = <$fd>) {
3124 my ($refinfo, $creatorinfo) = split(/\0/, $line);
3125 my ($id, $type, $name, $refid, $reftype, $title) = split(' ', $refinfo, 6);
3126 my ($creator, $epoch, $tz) =
3127 ($creatorinfo =~ /^(.*) ([0-9]+) (.*)$/);
3128 $ref_item{'fullname'} = $name;
3129 $name =~ s!^refs/tags/!!;
3131 $ref_item{'type'} = $type;
3132 $ref_item{'id'} = $id;
3133 $ref_item{'name'} = $name;
3134 if ($type eq "tag") {
3135 $ref_item{'subject'} = $title;
3136 $ref_item{'reftype'} = $reftype;
3137 $ref_item{'refid'} = $refid;
3139 $ref_item{'reftype'} = $type;
3140 $ref_item{'refid'} = $id;
3143 if ($type eq "tag" || $type eq "commit") {
3144 $ref_item{'epoch'} = $epoch;
3146 $ref_item{'age'} = age_string(time - $ref_item{'epoch'});
3148 $ref_item{'age'} = "unknown";
3152 push @tagslist, \%ref_item;
3156 return wantarray ? @tagslist : \@tagslist;
3159 ## ----------------------------------------------------------------------
3160 ## filesystem-related functions
3162 sub get_file_owner {
3165 my ($dev, $ino, $mode, $nlink, $st_uid, $st_gid, $rdev, $size) = stat($path);
3166 my ($name, $passwd, $uid, $gid, $quota, $comment, $gcos, $dir, $shell) = getpwuid($st_uid);
3167 if (!defined $gcos) {
3171 $owner =~ s/[,;].*$//;
3172 return to_utf8($owner);
3175 # assume that file exists
3177 my $filename = shift;
3179 open my $fd, '<', $filename;
3180 print map { to_utf8($_) } <$fd>;
3184 ## ......................................................................
3185 ## mimetype related functions
3187 sub mimetype_guess_file {
3188 my $filename = shift;
3189 my $mimemap = shift;
3190 -r $mimemap or return undef;
3193 open(my $mh, '<', $mimemap) or return undef;
3195 next if m/^#/; # skip comments
3196 my ($mimetype, $exts) = split(/\t+/);
3197 if (defined $exts) {
3198 my @exts = split(/\s+/, $exts);
3199 foreach my $ext (@exts) {
3200 $mimemap{$ext} = $mimetype;
3206 $filename =~ /\.([^.]*)$/;
3207 return $mimemap{$1};
3210 sub mimetype_guess {
3211 my $filename = shift;
3213 $filename =~ /\./ or return undef;
3215 if ($mimetypes_file) {
3216 my $file = $mimetypes_file;
3217 if ($file !~ m!^/!) { # if it is relative path
3218 # it is relative to project
3219 $file = "$projectroot/$project/$file";
3221 $mime = mimetype_guess_file($filename, $file);
3223 $mime ||= mimetype_guess_file($filename, '/etc/mime.types');
3229 my $filename = shift;
3232 my $mime = mimetype_guess($filename);
3233 $mime and return $mime;
3237 return $default_blob_plain_mimetype unless $fd;
3240 return 'text/plain';
3241 } elsif (! $filename) {
3242 return 'application/octet-stream';
3243 } elsif ($filename =~ m/\.png$/i) {
3245 } elsif ($filename =~ m/\.gif$/i) {
3247 } elsif ($filename =~ m/\.jpe?g$/i) {
3248 return 'image/jpeg';
3250 return 'application/octet-stream';
3254 sub blob_contenttype {
3255 my ($fd, $file_name, $type) = @_;
3257 $type ||= blob_mimetype($fd, $file_name);
3258 if ($type eq 'text/plain' && defined $default_text_plain_charset) {
3259 $type .= "; charset=$default_text_plain_charset";
3265 ## ======================================================================
3266 ## functions printing HTML: header, footer, error page
3268 sub git_header_html {
3269 my $status = shift || "200 OK";
3270 my $expires = shift;
3272 my $title = "$site_name";
3273 if (defined $project) {
3274 $title .= " - " . to_utf8($project);
3275 if (defined $action) {
3276 $title .= "/$action";
3277 if (defined $file_name) {
3278 $title .= " - " . esc_path($file_name);
3279 if ($action eq "tree" && $file_name !~ m|/$|) {
3286 # require explicit support from the UA if we are to send the page as
3287 # 'application/xhtml+xml', otherwise send it as plain old 'text/html'.
3288 # we have to do this because MSIE sometimes globs '*/*', pretending to
3289 # support xhtml+xml but choking when it gets what it asked for.
3290 if (defined $cgi->http('HTTP_ACCEPT') &&
3291 $cgi->http('HTTP_ACCEPT') =~ m/(,|;|\s|^)application\/xhtml\+xml(,|;|\s|$)/ &&
3292 $cgi->Accept('application/xhtml+xml') != 0) {
3293 $content_type = 'application/xhtml+xml';
3295 $content_type = 'text/html';
3297 print $cgi->header(-type=>$content_type, -charset => 'utf-8',
3298 -status=> $status, -expires => $expires);
3299 my $mod_perl_version = $ENV{'MOD_PERL'} ? " $ENV{'MOD_PERL'}" : '';
3301 <?xml version="1.0" encoding="utf-8"?>
3302 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
3303 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US" lang="en-US">
3304 <!-- git web interface version $version, (C) 2005-2006, Kay Sievers <kay.sievers\@vrfy.org>, Christian Gierke -->
3305 <!-- git core binaries version $git_version -->
3307 <meta http-equiv="content-type" content="$content_type; charset=utf-8"/>
3308 <meta name="generator" content="gitweb/$version git/$git_version$mod_perl_version"/>
3309 <meta name="robots" content="index, nofollow"/>
3310 <title>$title</title>
3312 # the stylesheet, favicon etc urls won't work correctly with path_info
3313 # unless we set the appropriate base URL
3314 if ($ENV{'PATH_INFO'}) {
3315 print "<base href=\"".esc_url($base_url)."\" />\n";
3317 # print out each stylesheet that exist, providing backwards capability
3318 # for those people who defined $stylesheet in a config file
3319 if (defined $stylesheet) {
3320 print '<link rel="stylesheet" type="text/css" href="'.$stylesheet.'"/>'."\n";
3322 foreach my $stylesheet (@stylesheets) {
3323 next unless $stylesheet;
3324 print '<link rel="stylesheet" type="text/css" href="'.$stylesheet.'"/>'."\n";
3327 if (defined $project) {
3328 my %href_params = get_feed_info();
3329 if (!exists $href_params{'-title'}) {
3330 $href_params{'-title'} = 'log';
3333 foreach my $format qw(RSS Atom) {
3334 my $type = lc($format);
3336 '-rel' => 'alternate',
3337 '-title' => "$project - $href_params{'-title'} - $format feed",
3338 '-type' => "application/$type+xml"
3341 $href_params{'action'} = $type;
3342 $link_attr{'-href'} = href(%href_params);
3344 "rel=\"$link_attr{'-rel'}\" ".
3345 "title=\"$link_attr{'-title'}\" ".
3346 "href=\"$link_attr{'-href'}\" ".
3347 "type=\"$link_attr{'-type'}\" ".
3350 $href_params{'extra_options'} = '--no-merges';
3351 $link_attr{'-href'} = href(%href_params);
3352 $link_attr{'-title'} .= ' (no merges)';
3354 "rel=\"$link_attr{'-rel'}\" ".
3355 "title=\"$link_attr{'-title'}\" ".
3356 "href=\"$link_attr{'-href'}\" ".
3357 "type=\"$link_attr{'-type'}\" ".
3362 printf('<link rel="alternate" title="%s projects list" '.
3363 'href="%s" type="text/plain; charset=utf-8" />'."\n",
3364 $site_name, href(project=>undef, action=>"project_index"));
3365 printf('<link rel="alternate" title="%s projects feeds" '.
3366 'href="%s" type="text/x-opml" />'."\n",
3367 $site_name, href(project=>undef, action=>"opml"));
3369 if (defined $favicon) {
3370 print qq(<link rel="shortcut icon" href="$favicon" type="image/png" />\n);
3376 if (defined $site_header && -f $site_header) {
3377 insert_file($site_header);
3380 print "<div class=\"page_header\">\n" .
3381 $cgi->a({-href => esc_url($logo_url),
3382 -title => $logo_label},
3383 qq(<img src="$logo" width="72" height="27" alt="git" class="logo"/>));
3384 print $cgi->a({-href => esc_url($home_link)}, $home_link_str) . " / ";
3385 if (defined $project) {
3386 print $cgi->a({-href => href(action=>"summary")}, esc_html($project));
3387 if (defined $action) {
3394 my $have_search = gitweb_check_feature('search');
3395 if (defined $project && $have_search) {
3396 if (!defined $searchtext) {
3400 if (defined $hash_base) {
3401 $search_hash = $hash_base;
3402 } elsif (defined $hash) {
3403 $search_hash = $hash;
3405 $search_hash = "HEAD";
3407 my $action = $my_uri;
3408 my $use_pathinfo = gitweb_check_feature('pathinfo');
3409 if ($use_pathinfo) {
3410 $action .= "/".esc_url($project);
3412 print $cgi->startform(-method => "get", -action => $action) .
3413 "<div class=\"search\">\n" .
3415 $cgi->input({-name=>"p", -value=>$project, -type=>"hidden"}) . "\n") .
3416 $cgi->input({-name=>"a", -value=>"search", -type=>"hidden"}) . "\n" .
3417 $cgi->input({-name=>"h", -value=>$search_hash, -type=>"hidden"}) . "\n" .
3418 $cgi->popup_menu(-name => 'st', -default => 'commit',
3419 -values => ['commit', 'grep', 'author', 'committer', 'pickaxe']) .
3420 $cgi->sup($cgi->a({-href => href(action=>"search_help")}, "?")) .
3422 $cgi->textfield(-name => "s", -value => $searchtext) . "\n" .
3423 "<span title=\"Extended regular expression\">" .
3424 $cgi->checkbox(-name => 'sr', -value => 1, -label => 're',
3425 -checked => $search_use_regexp) .
3428 $cgi->end_form() . "\n";
3432 sub git_footer_html {
3433 my $feed_class = 'rss_logo';
3435 print "<div class=\"page_footer\">\n";
3436 if (defined $project) {
3437 my $descr = git_get_project_description($project);
3438 if (defined $descr) {
3439 print "<div class=\"page_footer_text\">" . esc_html($descr) . "</div>\n";
3442 my %href_params = get_feed_info();
3443 if (!%href_params) {
3444 $feed_class .= ' generic';
3446 $href_params{'-title'} ||= 'log';
3448 foreach my $format qw(RSS Atom) {
3449 $href_params{'action'} = lc($format);
3450 print $cgi->a({-href => href(%href_params),
3451 -title => "$href_params{'-title'} $format feed",
3452 -class => $feed_class}, $format)."\n";
3456 print $cgi->a({-href => href(project=>undef, action=>"opml"),
3457 -class => $feed_class}, "OPML") . " ";
3458 print $cgi->a({-href => href(project=>undef, action=>"project_index"),
3459 -class => $feed_class}, "TXT") . "\n";
3461 print "</div>\n"; # class="page_footer"
3463 if (defined $t0 && gitweb_check_feature('timed')) {
3464 print "<div id=\"generating_info\">\n";
3465 print 'This page took '.
3466 '<span id="generating_time" class="time_span">'.
3467 Time::HiRes::tv_interval($t0, [Time::HiRes::gettimeofday()]).
3470 '<span id="generating_cmd">'.
3471 $number_of_git_cmds.
3472 '</span> git commands '.
3474 print "</div>\n"; # class="page_footer"
3477 if (defined $site_footer && -f $site_footer) {
3478 insert_file($site_footer);
3481 print qq!<script type="text/javascript" src="$javascript"></script>\n!;
3482 if (defined $action &&
3483 $action eq 'blame_incremental') {
3484 print qq!<script type="text/javascript">\n!.
3485 qq!startBlame("!. href(action=>"blame_data", -replay=>1) .qq!",\n!.
3486 qq! "!. href() .qq!");\n!.
3488 } elsif (gitweb_check_feature('javascript-actions')) {
3489 print qq!<script type="text/javascript">\n!.
3490 qq!window.onload = fixLinks;\n!.
3498 # die_error(<http_status_code>, <error_message>[, <detailed_html_description>])
3499 # Example: die_error(404, 'Hash not found')
3500 # By convention, use the following status codes (as defined in RFC 2616):
3501 # 400: Invalid or missing CGI parameters, or
3502 # requested object exists but has wrong type.
3503 # 403: Requested feature (like "pickaxe" or "snapshot") not enabled on
3504 # this server or project.
3505 # 404: Requested object/revision/project doesn't exist.
3506 # 500: The server isn't configured properly, or
3507 # an internal error occurred (e.g. failed assertions caused by bugs), or
3508 # an unknown error occurred (e.g. the git binary died unexpectedly).
3509 # 503: The server is currently unavailable (because it is overloaded,
3510 # or down for maintenance). Generally, this is a temporary state.
3512 my $status = shift || 500;
3513 my $error = esc_html(shift) || "Internal Server Error";
3516 my %http_responses = (
3517 400 => '400 Bad Request',
3518 403 => '403 Forbidden',
3519 404 => '404 Not Found',
3520 500 => '500 Internal Server Error',
3521 503 => '503 Service Unavailable',
3523 git_header_html($http_responses{$status});
3525 <div class="page_body">
3530 if (defined $extra) {
3540 ## ----------------------------------------------------------------------
3541 ## functions printing or outputting HTML: navigation
3543 sub git_print_page_nav {
3544 my ($current, $suppress, $head, $treehead, $treebase, $extra) = @_;
3545 $extra = '' if !defined $extra; # pager or formats
3547 my @navs = qw(summary shortlog log commit commitdiff tree);
3549 @navs = grep { $_ ne $suppress } @navs;
3552 my %arg = map { $_ => {action=>$_} } @navs;
3553 if (defined $head) {
3554 for (qw(commit commitdiff)) {
3555 $arg{$_}{'hash'} = $head;
3557 if ($current =~ m/^(tree | log | shortlog | commit | commitdiff | search)$/x) {
3558 for (qw(shortlog log)) {
3559 $arg{$_}{'hash'} = $head;
3564 $arg{'tree'}{'hash'} = $treehead if defined $treehead;
3565 $arg{'tree'}{'hash_base'} = $treebase if defined $treebase;
3567 my @actions = gitweb_get_feature('actions');
3570 'n' => $project, # project name
3571 'f' => $git_dir, # project path within filesystem
3572 'h' => $treehead || '', # current hash ('h' parameter)
3573 'b' => $treebase || '', # hash base ('hb' parameter)
3576 my ($label, $link, $pos) = splice(@actions,0,3);
3578 @navs = map { $_ eq $pos ? ($_, $label) : $_ } @navs;
3580 $link =~ s/%([%nfhb])/$repl{$1}/g;
3581 $arg{$label}{'_href'} = $link;
3584 print "<div class=\"page_nav\">\n" .
3586 map { $_ eq $current ?
3587 $_ : $cgi->a({-href => ($arg{$_}{_href} ? $arg{$_}{_href} : href(%{$arg{$_}}))}, "$_")
3589 print "<br/>\n$extra<br/>\n" .
3593 sub format_paging_nav {
3594 my ($action, $page, $has_next_link) = @_;
3600 $cgi->a({-href => href(-replay=>1, page=>undef)}, "first") .
3602 $cgi->a({-href => href(-replay=>1, page=>$page-1),
3603 -accesskey => "p", -title => "Alt-p"}, "prev");
3605 $paging_nav .= "first ⋅ prev";
3608 if ($has_next_link) {
3609 $paging_nav .= " ⋅ " .
3610 $cgi->a({-href => href(-replay=>1, page=>$page+1),
3611 -accesskey => "n", -title => "Alt-n"}, "next");
3613 $paging_nav .= " ⋅ next";
3619 ## ......................................................................
3620 ## functions printing or outputting HTML: div
3622 sub git_print_header_div {
3623 my ($action, $title, $hash, $hash_base) = @_;
3626 $args{'action'} = $action;
3627 $args{'hash'} = $hash if $hash;
3628 $args{'hash_base'} = $hash_base if $hash_base;
3630 print "<div class=\"header\">\n" .
3631 $cgi->a({-href => href(%args), -class => "title"},
3632 $title ? $title : $action) .
3636 sub print_local_time {
3637 print format_local_time(@_);
3640 sub format_local_time {
3643 if ($date{'hour_local'} < 6) {
3644 $localtime .= sprintf(" (<span class=\"atnight\">%02d:%02d</span> %s)",
3645 $date{'hour_local'}, $date{'minute_local'}, $date{'tz_local'});
3647 $localtime .= sprintf(" (%02d:%02d %s)",
3648 $date{'hour_local'}, $date{'minute_local'}, $date{'tz_local'});
3654 # Outputs the author name and date in long form
3655 sub git_print_authorship {
3658 my $tag = $opts{-tag} || 'div';
3659 my $author = $co->{'author_name'};
3661 my %ad = parse_date($co->{'author_epoch'}, $co->{'author_tz'});
3662 print "<$tag class=\"author_date\">" .
3663 format_search_author($author, "author", esc_html($author)) .
3665 print_local_time(%ad) if ($opts{-localtime});
3666 print "]" . git_get_avatar($co->{'author_email'}, -pad_before => 1)
3670 # Outputs table rows containing the full author or committer information,
3671 # in the format expected for 'commit' view (& similia).
3672 # Parameters are a commit hash reference, followed by the list of people
3673 # to output information for. If the list is empty it defalts to both
3674 # author and committer.
3675 sub git_print_authorship_rows {
3677 # too bad we can't use @people = @_ || ('author', 'committer')
3679 @people = ('author', 'committer') unless @people;
3680 foreach my $who (@people) {
3681 my %wd = parse_date($co->{"${who}_epoch"}, $co->{"${who}_tz"});
3682 print "<tr><td>$who</td><td>" .
3683 format_search_author($co->{"${who}_name"}, $who,
3684 esc_html($co->{"${who}_name"})) . " " .
3685 format_search_author($co->{"${who}_email"}, $who,
3686 esc_html("<" . $co->{"${who}_email"} . ">")) .
3687 "</td><td rowspan=\"2\">" .
3688 git_get_avatar($co->{"${who}_email"}, -size => 'double') .
3691 "<td></td><td> $wd{'rfc2822'}";
3692 print_local_time(%wd);
3698 sub git_print_page_path {
3704 print "<div class=\"page_path\">";
3705 print $cgi->a({-href => href(action=>"tree", hash_base=>$hb),
3706 -title => 'tree root'}, to_utf8("[$project]"));
3708 if (defined $name) {
3709 my @dirname = split '/', $name;
3710 my $basename = pop @dirname;
3713 foreach my $dir (@dirname) {
3714 $fullname .= ($fullname ? '/' : '') . $dir;
3715 print $cgi->a({-href => href(action=>"tree", file_name=>$fullname,
3717 -title => $fullname}, esc_path($dir));
3720 if (defined $type && $type eq 'blob') {
3721 print $cgi->a({-href => href(action=>"blob_plain", file_name=>$file_name,
3723 -title => $name}, esc_path($basename));
3724 } elsif (defined $type && $type eq 'tree') {
3725 print $cgi->a({-href => href(action=>"tree", file_name=>$file_name,
3727 -title => $name}, esc_path($basename));
3730 print esc_path($basename);
3733 print "<br/></div>\n";
3740 if ($opts{'-remove_title'}) {
3741 # remove title, i.e. first line of log
3744 # remove leading empty lines
3745 while (defined $log->[0] && $log->[0] eq "") {
3752 foreach my $line (@$log) {
3753 if ($line =~ m/^ *(signed[ \-]off[ \-]by[ :]|acked[ \-]by[ :]|cc[ :])/i) {
3756 if (! $opts{'-remove_signoff'}) {
3757 print "<span class=\"signoff\">" . esc_html($line) . "</span><br/>\n";
3760 # remove signoff lines
3767 # print only one empty line
3768 # do not print empty line after signoff
3770 next if ($empty || $signoff);
3776 print format_log_line_html($line) . "<br/>\n";
3779 if ($opts{'-final_empty_line'}) {
3780 # end with single empty line
3781 print "<br/>\n" unless $empty;
3785 # return link target (what link points to)
3786 sub git_get_link_target {
3791 open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
3795 $link_target = <$fd>;
3800 return $link_target;
3803 # given link target, and the directory (basedir) the link is in,
3804 # return target of link relative to top directory (top tree);
3805 # return undef if it is not possible (including absolute links).
3806 sub normalize_link_target {
3807 my ($link_target, $basedir) = @_;
3809 # absolute symlinks (beginning with '/') cannot be normalized
3810 return if (substr($link_target, 0, 1) eq '/');
3812 # normalize link target to path from top (root) tree (dir)
3815 $path = $basedir . '/' . $link_target;
3817 # we are in top (root) tree (dir)
3818 $path = $link_target;
3821 # remove //, /./, and /../
3823 foreach my $part (split('/', $path)) {
3824 # discard '.' and ''
3825 next if (!$part || $part eq '.');
3827 if ($part eq '..') {
3831 # link leads outside repository (outside top dir)
3835 push @path_parts, $part;
3838 $path = join('/', @path_parts);
3843 # print tree entry (row of git_tree), but without encompassing <tr> element
3844 sub git_print_tree_entry {
3845 my ($t, $basedir, $hash_base, $have_blame) = @_;
3848 $base_key{'hash_base'} = $hash_base if defined $hash_base;
3850 # The format of a table row is: mode list link. Where mode is
3851 # the mode of the entry, list is the name of the entry, an href,
3852 # and link is the action links of the entry.
3854 print "<td class=\"mode\">" . mode_str($t->{'mode'}) . "</td>\n";
3855 if (exists $t->{'size'}) {
3856 print "<td class=\"size\">$t->{'size'}</td>\n";
3858 if ($t->{'type'} eq "blob") {
3859 print "<td class=\"list\">" .
3860 $cgi->a({-href => href(action=>"blob", hash=>$t->{'hash'},
3861 file_name=>"$basedir$t->{'name'}", %base_key),
3862 -class => "list"}, esc_path($t->{'name'}));
3863 if (S_ISLNK(oct $t->{'mode'})) {
3864 my $link_target = git_get_link_target($t->{'hash'});
3866 my $norm_target = normalize_link_target($link_target, $basedir);
3867 if (defined $norm_target) {
3869 $cgi->a({-href => href(action=>"object", hash_base=>$hash_base,
3870 file_name=>$norm_target),
3871 -title => $norm_target}, esc_path($link_target));
3873 print " -> " . esc_path($link_target);
3878 print "<td class=\"link\">";
3879 print $cgi->a({-href => href(action=>"blob", hash=>$t->{'hash'},
3880 file_name=>"$basedir$t->{'name'}", %base_key)},
3884 $cgi->a({-href => href(action=>"blame", hash=>$t->{'hash'},
3885 file_name=>"$basedir$t->{'name'}", %base_key)},
3888 if (defined $hash_base) {
3890 $cgi->a({-href => href(action=>"history", hash_base=>$hash_base,
3891 hash=>$t->{'hash'}, file_name=>"$basedir$t->{'name'}")},
3895 $cgi->a({-href => href(action=>"blob_plain", hash_base=>$hash_base,
3896 file_name=>"$basedir$t->{'name'}")},
3900 } elsif ($t->{'type'} eq "tree") {
3901 print "<td class=\"list\">";
3902 print $cgi->a({-href => href(action=>"tree", hash=>$t->{'hash'},
3903 file_name=>"$basedir$t->{'name'}",
3905 esc_path($t->{'name'}));
3907 print "<td class=\"link\">";
3908 print $cgi->a({-href => href(action=>"tree", hash=>$t->{'hash'},
3909 file_name=>"$basedir$t->{'name'}",
3912 if (defined $hash_base) {
3914 $cgi->a({-href => href(action=>"history", hash_base=>$hash_base,
3915 file_name=>"$basedir$t->{'name'}")},
3920 # unknown object: we can only present history for it
3921 # (this includes 'commit' object, i.e. submodule support)
3922 print "<td class=\"list\">" .
3923 esc_path($t->{'name'}) .
3925 print "<td class=\"link\">";
3926 if (defined $hash_base) {
3927 print $cgi->a({-href => href(action=>"history",
3928 hash_base=>$hash_base,
3929 file_name=>"$basedir$t->{'name'}")},
3936 ## ......................................................................
3937 ## functions printing large fragments of HTML
3939 # get pre-image filenames for merge (combined) diff
3940 sub fill_from_file_info {
3941 my ($diff, @parents) = @_;
3943 $diff->{'from_file'} = [ ];
3944 $diff->{'from_file'}[$diff->{'nparents'} - 1] = undef;
3945 for (my $i = 0; $i < $diff->{'nparents'}; $i++) {
3946 if ($diff->{'status'}[$i] eq 'R' ||
3947 $diff->{'status'}[$i] eq 'C') {
3948 $diff->{'from_file'}[$i] =
3949 git_get_path_by_hash($parents[$i], $diff->{'from_id'}[$i]);
3956 # is current raw difftree line of file deletion
3958 my $diffinfo = shift;
3960 return $diffinfo->{'to_id'} eq ('0' x 40);
3963 # does patch correspond to [previous] difftree raw line
3964 # $diffinfo - hashref of parsed raw diff format
3965 # $patchinfo - hashref of parsed patch diff format
3966 # (the same keys as in $diffinfo)
3967 sub is_patch_split {
3968 my ($diffinfo, $patchinfo) = @_;
3970 return defined $diffinfo && defined $patchinfo
3971 && $diffinfo->{'to_file'} eq $patchinfo->{'to_file'};
3975 sub git_difftree_body {
3976 my ($difftree, $hash, @parents) = @_;
3977 my ($parent) = $parents[0];
3978 my $have_blame = gitweb_check_feature('blame');
3979 print "<div class=\"list_head\">\n";
3980 if ($#{$difftree} > 10) {
3981 print(($#{$difftree} + 1) . " files changed:\n");
3985 print "<table class=\"" .
3986 (@parents > 1 ? "combined " : "") .
3989 # header only for combined diff in 'commitdiff' view
3990 my $has_header = @$difftree && @parents > 1 && $action eq 'commitdiff';
3993 print "<thead><tr>\n" .
3994 "<th></th><th></th>\n"; # filename, patchN link
3995 for (my $i = 0; $i < @parents; $i++) {
3996 my $par = $parents[$i];
3998 $cgi->a({-href => href(action=>"commitdiff",
3999 hash=>$hash, hash_parent=>$par),
4000 -title => 'commitdiff to parent number ' .
4001 ($i+1) . ': ' . substr($par,0,7)},
4005 print "</tr></thead>\n<tbody>\n";
4010 foreach my $line (@{$difftree}) {
4011 my $diff = parsed_difftree_line($line);
4014 print "<tr class=\"dark\">\n";
4016 print "<tr class=\"light\">\n";
4020 if (exists $diff->{'nparents'}) { # combined diff
4022 fill_from_file_info($diff, @parents)
4023 unless exists $diff->{'from_file'};
4025 if (!is_deleted($diff)) {
4026 # file exists in the result (child) commit
4028 $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
4029 file_name=>$diff->{'to_file'},
4031 -class => "list"}, esc_path($diff->{'to_file'})) .
4035 esc_path($diff->{'to_file'}) .
4039 if ($action eq 'commitdiff') {
4042 print "<td class=\"link\">" .
4043 $cgi->a({-href => "#patch$patchno"}, "patch") .
4048 my $has_history = 0;
4049 my $not_deleted = 0;
4050 for (my $i = 0; $i < $diff->{'nparents'}; $i++) {
4051 my $hash_parent = $parents[$i];
4052 my $from_hash = $diff->{'from_id'}[$i];
4053 my $from_path = $diff->{'from_file'}[$i];
4054 my $status = $diff->{'status'}[$i];
4056 $has_history ||= ($status ne 'A');
4057 $not_deleted ||= ($status ne 'D');
4059 if ($status eq 'A') {
4060 print "<td class=\"link\" align=\"right\"> | </td>\n";
4061 } elsif ($status eq 'D') {
4062 print "<td class=\"link\">" .
4063 $cgi->a({-href => href(action=>"blob",
4066 file_name=>$from_path)},
4070 if ($diff->{'to_id'} eq $from_hash) {
4071 print "<td class=\"link nochange\">";
4073 print "<td class=\"link\">";
4075 print $cgi->a({-href => href(action=>"blobdiff",
4076 hash=>$diff->{'to_id'},
4077 hash_parent=>$from_hash,
4079 hash_parent_base=>$hash_parent,
4080 file_name=>$diff->{'to_file'},
4081 file_parent=>$from_path)},
4087 print "<td class=\"link\">";
4089 print $cgi->a({-href => href(action=>"blob",
4090 hash=>$diff->{'to_id'},
4091 file_name=>$diff->{'to_file'},
4094 print " | " if ($has_history);
4097 print $cgi->a({-href => href(action=>"history",
4098 file_name=>$diff->{'to_file'},
4105 next; # instead of 'else' clause, to avoid extra indent
4107 # else ordinary diff
4109 my ($to_mode_oct, $to_mode_str, $to_file_type);
4110 my ($from_mode_oct, $from_mode_str, $from_file_type);
4111 if ($diff->{'to_mode'} ne ('0' x 6)) {
4112 $to_mode_oct = oct $diff->{'to_mode'};
4113 if (S_ISREG($to_mode_oct)) { # only for regular file
4114 $to_mode_str = sprintf("%04o", $to_mode_oct & 0777); # permission bits
4116 $to_file_type = file_type($diff->{'to_mode'});
4118 if ($diff->{'from_mode'} ne ('0' x 6)) {
4119 $from_mode_oct = oct $diff->{'from_mode'};
4120 if (S_ISREG($to_mode_oct)) { # only for regular file
4121 $from_mode_str = sprintf("%04o", $from_mode_oct & 0777); # permission bits
4123 $from_file_type = file_type($diff->{'from_mode'});
4126 if ($diff->{'status'} eq "A") { # created
4127 my $mode_chng = "<span class=\"file_status new\">[new $to_file_type";
4128 $mode_chng .= " with mode: $to_mode_str" if $to_mode_str;
4129 $mode_chng .= "]</span>";
4131 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
4132 hash_base=>$hash, file_name=>$diff->{'file'}),
4133 -class => "list"}, esc_path($diff->{'file'}));
4135 print "<td>$mode_chng</td>\n";
4136 print "<td class=\"link\">";
4137 if ($action eq 'commitdiff') {
4140 print $cgi->a({-href => "#patch$patchno"}, "patch");
4143 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
4144 hash_base=>$hash, file_name=>$diff->{'file'})},
4148 } elsif ($diff->{'status'} eq "D") { # deleted
4149 my $mode_chng = "<span class=\"file_status deleted\">[deleted $from_file_type]</span>";
4151 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'from_id'},
4152 hash_base=>$parent, file_name=>$diff->{'file'}),
4153 -class => "list"}, esc_path($diff->{'file'}));
4155 print "<td>$mode_chng</td>\n";
4156 print "<td class=\"link\">";
4157 if ($action eq 'commitdiff') {
4160 print $cgi->a({-href => "#patch$patchno"}, "patch");
4163 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'from_id'},
4164 hash_base=>$parent, file_name=>$diff->{'file'})},
4167 print $cgi->a({-href => href(action=>"blame", hash_base=>$parent,
4168 file_name=>$diff->{'file'})},
4171 print $cgi->a({-href => href(action=>"history", hash_base=>$parent,
4172 file_name=>$diff->{'file'})},
4176 } elsif ($diff->{'status'} eq "M" || $diff->{'status'} eq "T") { # modified, or type changed
4177 my $mode_chnge = "";
4178 if ($diff->{'from_mode'} != $diff->{'to_mode'}) {
4179 $mode_chnge = "<span class=\"file_status mode_chnge\">[changed";
4180 if ($from_file_type ne $to_file_type) {
4181 $mode_chnge .= " from $from_file_type to $to_file_type";
4183 if (($from_mode_oct & 0777) != ($to_mode_oct & 0777)) {
4184 if ($from_mode_str && $to_mode_str) {
4185 $mode_chnge .= " mode: $from_mode_str->$to_mode_str";
4186 } elsif ($to_mode_str) {
4187 $mode_chnge .= " mode: $to_mode_str";
4190 $mode_chnge .= "]</span>\n";
4193 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
4194 hash_base=>$hash, file_name=>$diff->{'file'}),
4195 -class => "list"}, esc_path($diff->{'file'}));
4197 print "<td>$mode_chnge</td>\n";
4198 print "<td class=\"link\">";
4199 if ($action eq 'commitdiff') {
4202 print $cgi->a({-href => "#patch$patchno"}, "patch") .
4204 } elsif ($diff->{'to_id'} ne $diff->{'from_id'}) {
4205 # "commit" view and modified file (not onlu mode changed)
4206 print $cgi->a({-href => href(action=>"blobdiff",
4207 hash=>$diff->{'to_id'}, hash_parent=>$diff->{'from_id'},
4208 hash_base=>$hash, hash_parent_base=>$parent,
4209 file_name=>$diff->{'file'})},
4213 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
4214 hash_base=>$hash, file_name=>$diff->{'file'})},
4217 print $cgi->a({-href => href(action=>"blame", hash_base=>$hash,
4218 file_name=>$diff->{'file'})},
4221 print $cgi->a({-href => href(action=>"history", hash_base=>$hash,
4222 file_name=>$diff->{'file'})},
4226 } elsif ($diff->{'status'} eq "R" || $diff->{'status'} eq "C") { # renamed or copied
4227 my %status_name = ('R' => 'moved', 'C' => 'copied');
4228 my $nstatus = $status_name{$diff->{'status'}};
4230 if ($diff->{'from_mode'} != $diff->{'to_mode'}) {
4231 # mode also for directories, so we cannot use $to_mode_str
4232 $mode_chng = sprintf(", mode: %04o", $to_mode_oct & 0777);
4235 $cgi->a({-href => href(action=>"blob", hash_base=>$hash,
4236 hash=>$diff->{'to_id'}, file_name=>$diff->{'to_file'}),
4237 -class => "list"}, esc_path($diff->{'to_file'})) . "</td>\n" .
4238 "<td><span class=\"file_status $nstatus\">[$nstatus from " .
4239 $cgi->a({-href => href(action=>"blob", hash_base=>$parent,
4240 hash=>$diff->{'from_id'}, file_name=>$diff->{'from_file'}),
4241 -class => "list"}, esc_path($diff->{'from_file'})) .
4242 " with " . (int $diff->{'similarity'}) . "% similarity$mode_chng]</span></td>\n" .
4243 "<td class=\"link\">";
4244 if ($action eq 'commitdiff') {
4247 print $cgi->a({-href => "#patch$patchno"}, "patch") .
4249 } elsif ($diff->{'to_id'} ne $diff->{'from_id'}) {
4250 # "commit" view and modified file (not only pure rename or copy)
4251 print $cgi->a({-href => href(action=>"blobdiff",
4252 hash=>$diff->{'to_id'}, hash_parent=>$diff->{'from_id'},
4253 hash_base=>$hash, hash_parent_base=>$parent,
4254 file_name=>$diff->{'to_file'}, file_parent=>$diff->{'from_file'})},
4258 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
4259 hash_base=>$parent, file_name=>$diff->{'to_file'})},
4262 print $cgi->a({-href => href(action=>"blame", hash_base=>$hash,
4263 file_name=>$diff->{'to_file'})},
4266 print $cgi->a({-href => href(action=>"history", hash_base=>$hash,
4267 file_name=>$diff->{'to_file'})},
4271 } # we should not encounter Unmerged (U) or Unknown (X) status
4274 print "</tbody>" if $has_header;
4278 sub git_patchset_body {
4279 my ($fd, $difftree, $hash, @hash_parents) = @_;
4280 my ($hash_parent) = $hash_parents[0];
4282 my $is_combined = (@hash_parents > 1);
4284 my $patch_number = 0;
4290 print "<div class=\"patchset\">\n";
4292 # skip to first patch
4293 while ($patch_line = <$fd>) {
4296 last if ($patch_line =~ m/^diff /);
4300 while ($patch_line) {
4302 # parse "git diff" header line
4303 if ($patch_line =~ m/^diff --git (\"(?:[^\\\"]*(?:\\.[^\\\"]*)*)\"|[^ "]*) (.*)$/) {
4304 # $1 is from_name, which we do not use
4305 $to_name = unquote($2);
4306 $to_name =~ s!^b/!!;
4307 } elsif ($patch_line =~ m/^diff --(cc|combined) ("?.*"?)$/) {
4308 # $1 is 'cc' or 'combined', which we do not use
4309 $to_name = unquote($2);
4314 # check if current patch belong to current raw line
4315 # and parse raw git-diff line if needed
4316 if (is_patch_split($diffinfo, { 'to_file' => $to_name })) {
4317 # this is continuation of a split patch
4318 print "<div class=\"patch cont\">\n";
4320 # advance raw git-diff output if needed
4321 $patch_idx++ if defined $diffinfo;
4323 # read and prepare patch information
4324 $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
4326 # compact combined diff output can have some patches skipped
4327 # find which patch (using pathname of result) we are at now;
4329 while ($to_name ne $diffinfo->{'to_file'}) {
4330 print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n" .
4331 format_diff_cc_simplified($diffinfo, @hash_parents) .
4332 "</div>\n"; # class="patch"
4337 last if $patch_idx > $#$difftree;
4338 $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
4342 # modifies %from, %to hashes
4343 parse_from_to_diffinfo($diffinfo, \%from, \%to, @hash_parents);
4345 # this is first patch for raw difftree line with $patch_idx index
4346 # we index @$difftree array from 0, but number patches from 1
4347 print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n";
4351 #assert($patch_line =~ m/^diff /) if DEBUG;
4352 #assert($patch_line !~ m!$/$!) if DEBUG; # is chomp-ed
4354 # print "git diff" header
4355 print format_git_diff_header_line($patch_line, $diffinfo,
4358 # print extended diff header
4359 print "<div class=\"diff extended_header\">\n";
4361 while ($patch_line = <$fd>) {
4364 last EXTENDED_HEADER if ($patch_line =~ m/^--- |^diff /);
4366 print format_extended_diff_header_line($patch_line, $diffinfo,
4369 print "</div>\n"; # class="diff extended_header"
4371 # from-file/to-file diff header
4372 if (! $patch_line) {
4373 print "</div>\n"; # class="patch"
4376 next PATCH if ($patch_line =~ m/^diff /);
4377 #assert($patch_line =~ m/^---/) if DEBUG;
4379 my $last_patch_line = $patch_line;
4380 $patch_line = <$fd>;
4382 #assert($patch_line =~ m/^\+\+\+/) if DEBUG;
4384 print format_diff_from_to_header($last_patch_line, $patch_line,
4385 $diffinfo, \%from, \%to,
4390 while ($patch_line = <$fd>) {
4393 next PATCH if ($patch_line =~ m/^diff /);
4395 print format_diff_line($patch_line, \%from, \%to);
4399 print "</div>\n"; # class="patch"
4402 # for compact combined (--cc) format, with chunk and patch simpliciaction
4403 # patchset might be empty, but there might be unprocessed raw lines
4404 for (++$patch_idx if $patch_number > 0;
4405 $patch_idx < @$difftree;
4407 # read and prepare patch information
4408 $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
4410 # generate anchor for "patch" links in difftree / whatchanged part
4411 print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n" .
4412 format_diff_cc_simplified($diffinfo, @hash_parents) .
4413 "</div>\n"; # class="patch"
4418 if ($patch_number == 0) {
4419 if (@hash_parents > 1) {
4420 print "<div class=\"diff nodifferences\">Trivial merge</div>\n";
4422 print "<div class=\"diff nodifferences\">No differences found</div>\n";
4426 print "</div>\n"; # class="patchset"
4429 # . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
4431 # fills project list info (age, description, owner, forks) for each
4432 # project in the list, removing invalid projects from returned list
4433 # NOTE: modifies $projlist, but does not remove entries from it
4434 sub fill_project_list_info {
4435 my ($projlist, $check_forks) = @_;
4438 my $show_ctags = gitweb_check_feature('ctags');
4440 foreach my $pr (@$projlist) {
4441 my (@activity) = git_get_last_activity($pr->{'path'});
4442 unless (@activity) {
4445 ($pr->{'age'}, $pr->{'age_string'}) = @activity;
4446 if (!defined $pr->{'descr'}) {
4447 my $descr = git_get_project_description($pr->{'path'}) || "";
4448 $descr = to_utf8($descr);
4449 $pr->{'descr_long'} = $descr;
4450 $pr->{'descr'} = chop_str($descr, $projects_list_description_width, 5);
4452 if (!defined $pr->{'owner'}) {
4453 $pr->{'owner'} = git_get_project_owner("$pr->{'path'}") || "";
4456 my $pname = $pr->{'path'};
4457 if (($pname =~ s/\.git$//) &&
4458 ($pname !~ /\/$/) &&
4459 (-d "$projectroot/$pname")) {
4460 $pr->{'forks'} = "-d $projectroot/$pname";
4465 $show_ctags and $pr->{'ctags'} = git_get_project_ctags($pr->{'path'});
4466 push @projects, $pr;
4472 # print 'sort by' <th> element, generating 'sort by $name' replay link
4473 # if that order is not selected
4475 print format_sort_th(@_);
4478 sub format_sort_th {
4479 my ($name, $order, $header) = @_;
4481 $header ||= ucfirst($name);
4483 if ($order eq $name) {
4484 $sort_th .= "<th>$header</th>\n";
4486 $sort_th .= "<th>" .
4487 $cgi->a({-href => href(-replay=>1, order=>$name),
4488 -class => "header"}, $header) .
4495 sub git_project_list_body {
4496 # actually uses global variable $project
4497 my ($projlist, $order, $from, $to, $extra, $no_header) = @_;
4499 my $check_forks = gitweb_check_feature('forks');
4500 my @projects = fill_project_list_info($projlist, $check_forks);
4502 $order ||= $default_projects_order;
4503 $from = 0 unless defined $from;
4504 $to = $#projects if (!defined $to || $#projects < $to);
4507 project => { key => 'path', type => 'str' },
4508 descr => { key => 'descr_long', type => 'str' },
4509 owner => { key => 'owner', type => 'str' },
4510 age => { key => 'age', type => 'num' }
4512 my $oi = $order_info{$order};
4513 if ($oi->{'type'} eq 'str') {
4514 @projects = sort {$a->{$oi->{'key'}} cmp $b->{$oi->{'key'}}} @projects;
4516 @projects = sort {$a->{$oi->{'key'}} <=> $b->{$oi->{'key'}}} @projects;
4519 my $show_ctags = gitweb_check_feature('ctags');
4522 foreach my $p (@projects) {
4523 foreach my $ct (keys %{$p->{'ctags'}}) {
4524 $ctags{$ct} += $p->{'ctags'}->{$ct};
4527 my $cloud = git_populate_project_tagcloud(\%ctags);
4528 print git_show_project_tagcloud($cloud, 64);
4531 print "<table class=\"project_list\">\n";
4532 unless ($no_header) {
4535 print "<th></th>\n";
4537 print_sort_th('project', $order, 'Project');
4538 print_sort_th('descr', $order, 'Description');
4539 print_sort_th('owner', $order, 'Owner');
4540 print_sort_th('age', $order, 'Last Change');
4541 print "<th></th>\n" . # for links
4545 my $tagfilter = $cgi->param('by_tag');
4546 for (my $i = $from; $i <= $to; $i++) {
4547 my $pr = $projects[$i];
4549 next if $tagfilter and $show_ctags and not grep { lc $_ eq lc $tagfilter } keys %{$pr->{'ctags'}};
4550 next if $searchtext and not $pr->{'path'} =~ /$searchtext/
4551 and not $pr->{'descr_long'} =~ /$searchtext/;
4552 # Weed out forks or non-matching entries of search
4554 my $forkbase = $project; $forkbase ||= ''; $forkbase =~ s#\.git$#/#;
4555 $forkbase="^$forkbase" if $forkbase;
4556 next if not $searchtext and not $tagfilter and $show_ctags
4557 and $pr->{'path'} =~ m#$forkbase.*/.*#; # regexp-safe
4561 print "<tr class=\"dark\">\n";
4563 print "<tr class=\"light\">\n";
4568 if ($pr->{'forks'}) {
4569 print "<!-- $pr->{'forks'} -->\n";
4570 print $cgi->a({-href => href(project=>$pr->{'path'}, action=>"forks")}, "+");
4574 print "<td>" . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary"),
4575 -class => "list"}, esc_html($pr->{'path'})) . "</td>\n" .
4576 "<td>" . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary"),
4577 -class => "list", -title => $pr->{'descr_long'}},
4578 esc_html($pr->{'descr'})) . "</td>\n" .
4579 "<td><i>" . chop_and_escape_str($pr->{'owner'}, 15) . "</i></td>\n";
4580 print "<td class=\"". age_class($pr->{'age'}) . "\">" .
4581 (defined $pr->{'age_string'} ? $pr->{'age_string'} : "No commits") . "</td>\n" .
4582 "<td class=\"link\">" .
4583 $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary")}, "summary") . " | " .
4584 $cgi->a({-href => href(project=>$pr->{'path'}, action=>"shortlog")}, "shortlog") . " | " .
4585 $cgi->a({-href => href(project=>$pr->{'path'}, action=>"log")}, "log") . " | " .
4586 $cgi->a({-href => href(project=>$pr->{'path'}, action=>"tree")}, "tree") .
4587 ($pr->{'forks'} ? " | " . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"forks")}, "forks") : '') .
4591 if (defined $extra) {
4594 print "<td></td>\n";
4596 print "<td colspan=\"5\">$extra</td>\n" .
4603 # uses global variable $project
4604 my ($commitlist, $from, $to, $refs, $extra) = @_;
4606 $from = 0 unless defined $from;
4607 $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
4609 for (my $i = 0; $i <= $to; $i++) {
4610 my %co = %{$commitlist->[$i]};
4612 my $commit = $co{'id'};
4613 my $ref = format_ref_marker($refs, $commit);
4614 my %ad = parse_date($co{'author_epoch'});
4615 git_print_header_div('commit',
4616 "<span class=\"age\">$co{'age_string'}</span>" .
4617 esc_html($co{'title'}) . $ref,
4619 print "<div class=\"title_text\">\n" .
4620 "<div class=\"log_link\">\n" .
4621 $cgi->a({-href => href(action=>"commit", hash=>$commit)}, "commit") .
4623 $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff") .
4625 $cgi->a({-href => href(action=>"tree", hash=>$commit, hash_base=>$commit)}, "tree") .
4628 git_print_authorship(\%co, -tag => 'span');
4629 print "<br/>\n</div>\n";
4631 print "<div class=\"log_body\">\n";
4632 git_print_log($co{'comment'}, -final_empty_line=> 1);
4636 print "<div class=\"page_nav\">\n";
4642 sub git_shortlog_body {
4643 # uses global variable $project
4644 my ($commitlist, $from, $to, $refs, $extra) = @_;
4646 $from = 0 unless defined $from;
4647 $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
4649 print "<table class=\"shortlog\">\n";
4651 for (my $i = $from; $i <= $to; $i++) {
4652 my %co = %{$commitlist->[$i]};
4653 my $commit = $co{'id'};
4654 my $ref = format_ref_marker($refs, $commit);
4656 print "<tr class=\"dark\">\n";
4658 print "<tr class=\"light\">\n";
4661 # git_summary() used print "<td><i>$co{'age_string'}</i></td>\n" .
4662 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
4663 format_author_html('td', \%co, 10) . "<td>";
4664 print format_subject_html($co{'title'}, $co{'title_short'},
4665 href(action=>"commit", hash=>$commit), $ref);
4667 "<td class=\"link\">" .
4668 $cgi->a({-href => href(action=>"commit", hash=>$commit)}, "commit") . " | " .
4669 $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff") . " | " .
4670 $cgi->a({-href => href(action=>"tree", hash=>$commit, hash_base=>$commit)}, "tree");
4671 my $snapshot_links = format_snapshot_links($commit);
4672 if (defined $snapshot_links) {
4673 print " | " . $snapshot_links;
4678 if (defined $extra) {
4680 "<td colspan=\"4\">$extra</td>\n" .
4686 sub git_history_body {
4687 # Warning: assumes constant type (blob or tree) during history
4688 my ($commitlist, $from, $to, $refs, $extra,
4689 $file_name, $file_hash, $ftype) = @_;
4691 $from = 0 unless defined $from;
4692 $to = $#{$commitlist} unless (defined $to && $to <= $#{$commitlist});
4694 print "<table class=\"history\">\n";
4696 for (my $i = $from; $i <= $to; $i++) {
4697 my %co = %{$commitlist->[$i]};
4701 my $commit = $co{'id'};
4703 my $ref = format_ref_marker($refs, $commit);
4706 print "<tr class=\"dark\">\n";
4708 print "<tr class=\"light\">\n";
4711 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
4712 # shortlog: format_author_html('td', \%co, 10)
4713 format_author_html('td', \%co, 15, 3) . "<td>";
4714 # originally git_history used chop_str($co{'title'}, 50)
4715 print format_subject_html($co{'title'}, $co{'title_short'},
4716 href(action=>"commit", hash=>$commit), $ref);
4718 "<td class=\"link\">" .
4719 $cgi->a({-href => href(action=>$ftype, hash_base=>$commit, file_name=>$file_name)}, $ftype) . " | " .
4720 $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff");
4722 if ($ftype eq 'blob') {
4723 my $blob_current = $file_hash;
4724 my $blob_parent = git_get_hash_by_path($commit, $file_name);
4725 if (defined $blob_current && defined $blob_parent &&
4726 $blob_current ne $blob_parent) {
4728 $cgi->a({-href => href(action=>"blobdiff",
4729 hash=>$blob_current, hash_parent=>$blob_parent,
4730 hash_base=>$hash_base, hash_parent_base=>$commit,
4731 file_name=>$file_name)},
4738 if (defined $extra) {
4740 "<td colspan=\"4\">$extra</td>\n" .
4747 # uses global variable $project
4748 my ($taglist, $from, $to, $extra) = @_;
4749 $from = 0 unless defined $from;
4750 $to = $#{$taglist} if (!defined $to || $#{$taglist} < $to);
4752 print "<table class=\"tags\">\n";
4754 for (my $i = $from; $i <= $to; $i++) {
4755 my $entry = $taglist->[$i];
4757 my $comment = $tag{'subject'};
4759 if (defined $comment) {
4760 $comment_short = chop_str($comment, 30, 5);
4763 print "<tr class=\"dark\">\n";
4765 print "<tr class=\"light\">\n";
4768 if (defined $tag{'age'}) {
4769 print "<td><i>$tag{'age'}</i></td>\n";
4771 print "<td></td>\n";
4774 $cgi->a({-href => href(action=>$tag{'reftype'}, hash=>$tag{'refid'}),
4775 -class => "list name"}, esc_html($tag{'name'})) .
4778 if (defined $comment) {
4779 print format_subject_html($comment, $comment_short,
4780 href(action=>"tag", hash=>$tag{'id'}));
4783 "<td class=\"selflink\">";
4784 if ($tag{'type'} eq "tag") {
4785 print $cgi->a({-href => href(action=>"tag", hash=>$tag{'id'})}, "tag");
4790 "<td class=\"link\">" . " | " .
4791 $cgi->a({-href => href(action=>$tag{'reftype'}, hash=>$tag{'refid'})}, $tag{'reftype'});
4792 if ($tag{'reftype'} eq "commit") {
4793 print " | " . $cgi->a({-href => href(action=>"shortlog", hash=>$tag{'fullname'})}, "shortlog") .
4794 " | " . $cgi->a({-href => href(action=>"log", hash=>$tag{'fullname'})}, "log");
4795 } elsif ($tag{'reftype'} eq "blob") {
4796 print " | " . $cgi->a({-href => href(action=>"blob_plain", hash=>$tag{'refid'})}, "raw");
4801 if (defined $extra) {
4803 "<td colspan=\"5\">$extra</td>\n" .
4809 sub git_heads_body {
4810 # uses global variable $project
4811 my ($headlist, $head, $from, $to, $extra) = @_;
4812 $from = 0 unless defined $from;
4813 $to = $#{$headlist} if (!defined $to || $#{$headlist} < $to);
4815 print "<table class=\"heads\">\n";
4817 for (my $i = $from; $i <= $to; $i++) {
4818 my $entry = $headlist->[$i];
4820 my $curr = $ref{'id'} eq $head;
4822 print "<tr class=\"dark\">\n";
4824 print "<tr class=\"light\">\n";
4827 print "<td><i>$ref{'age'}</i></td>\n" .
4828 ($curr ? "<td class=\"current_head\">" : "<td>") .
4829 $cgi->a({-href => href(action=>"shortlog", hash=>$ref{'fullname'}),
4830 -class => "list name"},esc_html($ref{'name'})) .
4832 "<td class=\"link\">" .
4833 $cgi->a({-href => href(action=>"shortlog", hash=>$ref{'fullname'})}, "shortlog") . " | " .
4834 $cgi->a({-href => href(action=>"log", hash=>$ref{'fullname'})}, "log") . " | " .
4835 $cgi->a({-href => href(action=>"tree", hash=>$ref{'fullname'}, hash_base=>$ref{'name'})}, "tree") .
4839 if (defined $extra) {
4841 "<td colspan=\"3\">$extra</td>\n" .
4847 sub git_search_grep_body {
4848 my ($commitlist, $from, $to, $extra) = @_;
4849 $from = 0 unless defined $from;
4850 $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
4852 print "<table class=\"commit_search\">\n";
4854 for (my $i = $from; $i <= $to; $i++) {
4855 my %co = %{$commitlist->[$i]};
4859 my $commit = $co{'id'};
4861 print "<tr class=\"dark\">\n";
4863 print "<tr class=\"light\">\n";
4866 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
4867 format_author_html('td', \%co, 15, 5) .
4869 $cgi->a({-href => href(action=>"commit", hash=>$co{'id'}),
4870 -class => "list subject"},
4871 chop_and_escape_str($co{'title'}, 50) . "<br/>");
4872 my $comment = $co{'comment'};
4873 foreach my $line (@$comment) {
4874 if ($line =~ m/^(.*?)($search_regexp)(.*)$/i) {
4875 my ($lead, $match, $trail) = ($1, $2, $3);
4876 $match = chop_str($match, 70, 5, 'center');
4877 my $contextlen = int((80 - length($match))/2);
4878 $contextlen = 30 if ($contextlen > 30);
4879 $lead = chop_str($lead, $contextlen, 10, 'left');
4880 $trail = chop_str($trail, $contextlen, 10, 'right');
4882 $lead = esc_html($lead);
4883 $match = esc_html($match);
4884 $trail = esc_html($trail);
4886 print "$lead<span class=\"match\">$match</span>$trail<br />";
4890 "<td class=\"link\">" .
4891 $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") .
4893 $cgi->a({-href => href(action=>"commitdiff", hash=>$co{'id'})}, "commitdiff") .
4895 $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree");
4899 if (defined $extra) {
4901 "<td colspan=\"3\">$extra</td>\n" .
4907 ## ======================================================================
4908 ## ======================================================================
4911 sub git_project_list {
4912 my $order = $input_params{'order'};
4913 if (defined $order && $order !~ m/none|project|descr|owner|age/) {
4914 die_error(400, "Unknown order parameter");
4917 my @list = git_get_projects_list();
4919 die_error(404, "No projects found");
4923 if (defined $home_text && -f $home_text) {
4924 print "<div class=\"index_include\">\n";
4925 insert_file($home_text);
4928 print $cgi->startform(-method => "get") .
4929 "<p class=\"projsearch\">Search:\n" .
4930 $cgi->textfield(-name => "s", -value => $searchtext) . "\n" .
4932 $cgi->end_form() . "\n";
4933 git_project_list_body(\@list, $order);
4938 my $order = $input_params{'order'};
4939 if (defined $order && $order !~ m/none|project|descr|owner|age/) {
4940 die_error(400, "Unknown order parameter");
4943 my @list = git_get_projects_list($project);
4945 die_error(404, "No forks found");
4949 git_print_page_nav('','');
4950 git_print_header_div('summary', "$project forks");
4951 git_project_list_body(\@list, $order);
4955 sub git_project_index {
4956 my @projects = git_get_projects_list($project);
4959 -type => 'text/plain',
4960 -charset => 'utf-8',
4961 -content_disposition => 'inline; filename="index.aux"');
4963 foreach my $pr (@projects) {
4964 if (!exists $pr->{'owner'}) {
4965 $pr->{'owner'} = git_get_project_owner("$pr->{'path'}");
4968 my ($path, $owner) = ($pr->{'path'}, $pr->{'owner'});
4969 # quote as in CGI::Util::encode, but keep the slash, and use '+' for ' '
4970 $path =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf("%%%02X", ord($1))/eg;
4971 $owner =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf("%%%02X", ord($1))/eg;
4975 print "$path $owner\n";
4980 my $descr = git_get_project_description($project) || "none";
4981 my %co = parse_commit("HEAD");
4982 my %cd = %co ? parse_date($co{'committer_epoch'}, $co{'committer_tz'}) : ();
4983 my $head = $co{'id'};
4985 my $owner = git_get_project_owner($project);
4987 my $refs = git_get_references();
4988 # These get_*_list functions return one more to allow us to see if
4989 # there are more ...
4990 my @taglist = git_get_tags_list(16);
4991 my @headlist = git_get_heads_list(16);
4993 my $check_forks = gitweb_check_feature('forks');
4996 @forklist = git_get_projects_list($project);
5000 git_print_page_nav('summary','', $head);
5002 print "<div class=\"title\"> </div>\n";
5003 print "<table class=\"projects_list\">\n" .
5004 "<tr id=\"metadata_desc\"><td>description</td><td>" . esc_html($descr) . "</td></tr>\n" .
5005 "<tr id=\"metadata_owner\"><td>owner</td><td>" . esc_html($owner) . "</td></tr>\n";
5006 if (defined $cd{'rfc2822'}) {
5007 print "<tr id=\"metadata_lchange\"><td>last change</td><td>$cd{'rfc2822'}</td></tr>\n";
5010 # use per project git URL list in $projectroot/$project/cloneurl
5011 # or make project git URL from git base URL and project name
5012 my $url_tag = "URL";
5013 my @url_list = git_get_project_url_list($project);
5014 @url_list = map { "$_/$project" } @git_base_url_list unless @url_list;
5015 foreach my $git_url (@url_list) {
5016 next unless $git_url;
5017 print "<tr class=\"metadata_url\"><td>$url_tag</td><td>$git_url</td></tr>\n";
5022 my $show_ctags = gitweb_check_feature('ctags');
5024 my $ctags = git_get_project_ctags($project);
5025 my $cloud = git_populate_project_tagcloud($ctags);
5026 print "<tr id=\"metadata_ctags\"><td>Content tags:<br />";
5027 print "</td>\n<td>" unless %$ctags;
5028 print "<form action=\"$show_ctags\" method=\"post\"><input type=\"hidden\" name=\"p\" value=\"$project\" />Add: <input type=\"text\" name=\"t\" size=\"8\" /></form>";
5029 print "</td>\n<td>" if %$ctags;
5030 print git_show_project_tagcloud($cloud, 48);
5036 # If XSS prevention is on, we don't include README.html.
5037 # TODO: Allow a readme in some safe format.
5038 if (!$prevent_xss && -s "$projectroot/$project/README.html") {
5039 print "<div class=\"title\">readme</div>\n" .
5040 "<div class=\"readme\">\n";
5041 insert_file("$projectroot/$project/README.html");
5042 print "\n</div>\n"; # class="readme"
5045 # we need to request one more than 16 (0..15) to check if
5047 my @commitlist = $head ? parse_commits($head, 17) : ();
5049 git_print_header_div('shortlog');
5050 git_shortlog_body(\@commitlist, 0, 15, $refs,
5051 $#commitlist <= 15 ? undef :
5052 $cgi->a({-href => href(action=>"shortlog")}, "..."));
5056 git_print_header_div('tags');
5057 git_tags_body(\@taglist, 0, 15,
5058 $#taglist <= 15 ? undef :
5059 $cgi->a({-href => href(action=>"tags")}, "..."));
5063 git_print_header_div('heads');
5064 git_heads_body(\@headlist, $head, 0, 15,
5065 $#headlist <= 15 ? undef :
5066 $cgi->a({-href => href(action=>"heads")}, "..."));
5070 git_print_header_div('forks');
5071 git_project_list_body(\@forklist, 'age', 0, 15,
5072 $#forklist <= 15 ? undef :
5073 $cgi->a({-href => href(action=>"forks")}, "..."),
5081 my $head = git_get_head_hash($project);
5083 git_print_page_nav('','', $head,undef,$head);
5084 my %tag = parse_tag($hash);
5087 die_error(404, "Unknown tag object");
5090 git_print_header_div('commit', esc_html($tag{'name'}), $hash);
5091 print "<div class=\"title_text\">\n" .
5092 "<table class=\"object_header\">\n" .
5094 "<td>object</td>\n" .
5095 "<td>" . $cgi->a({-class => "list", -href => href(action=>$tag{'type'}, hash=>$tag{'object'})},
5096 $tag{'object'}) . "</td>\n" .
5097 "<td class=\"link\">" . $cgi->a({-href => href(action=>$tag{'type'}, hash=>$tag{'object'})},
5098 $tag{'type'}) . "</td>\n" .
5100 if (defined($tag{'author'})) {
5101 git_print_authorship_rows(\%tag, 'author');
5103 print "</table>\n\n" .
5105 print "<div class=\"page_body\">";
5106 my $comment = $tag{'comment'};
5107 foreach my $line (@$comment) {
5109 print esc_html($line, -nbsp=>1) . "<br/>\n";
5115 sub git_blame_common {
5116 my $format = shift || 'porcelain';
5117 if ($format eq 'porcelain' && $cgi->param('js')) {
5118 $format = 'incremental';
5119 $action = 'blame_incremental'; # for page title etc
5123 gitweb_check_feature('blame')
5124 or die_error(403, "Blame view not allowed");
5127 die_error(400, "No file name given") unless $file_name;
5128 $hash_base ||= git_get_head_hash($project);
5129 die_error(404, "Couldn't find base commit") unless $hash_base;
5130 my %co = parse_commit($hash_base)
5131 or die_error(404, "Commit not found");
5133 if (!defined $hash) {
5134 $hash = git_get_hash_by_path($hash_base, $file_name, "blob")
5135 or die_error(404, "Error looking up file");
5137 $ftype = git_get_type($hash);
5138 if ($ftype !~ "blob") {
5139 die_error(400, "Object is not a blob");
5144 if ($format eq 'incremental') {
5145 # get file contents (as base)
5146 open $fd, "-|", git_cmd(), 'cat-file', 'blob', $hash
5147 or die_error(500, "Open git-cat-file failed");
5148 } elsif ($format eq 'data') {
5149 # run git-blame --incremental
5150 open $fd, "-|", git_cmd(), "blame", "--incremental",
5151 $hash_base, "--", $file_name
5152 or die_error(500, "Open git-blame --incremental failed");
5154 # run git-blame --porcelain
5155 open $fd, "-|", git_cmd(), "blame", '-p',
5156 $hash_base, '--', $file_name
5157 or die_error(500, "Open git-blame --porcelain failed");
5160 # incremental blame data returns early
5161 if ($format eq 'data') {
5163 -type=>"text/plain", -charset => "utf-8",
5164 -status=> "200 OK");
5165 local $| = 1; # output autoflush
5168 or print "ERROR $!\n";
5171 if (defined $t0 && gitweb_check_feature('timed')) {
5173 Time::HiRes::tv_interval($t0, [Time::HiRes::gettimeofday()]).
5174 ' '.$number_of_git_cmds;
5184 $cgi->a({-href => href(action=>"blob", -replay=>1)},
5187 if ($format eq 'incremental') {
5189 $cgi->a({-href => href(action=>"blame", javascript=>0, -replay=>1)},
5190 "blame") . " (non-incremental)";
5193 $cgi->a({-href => href(action=>"blame_incremental", -replay=>1)},
5194 "blame") . " (incremental)";
5198 $cgi->a({-href => href(action=>"history", -replay=>1)},
5201 $cgi->a({-href => href(action=>$action, file_name=>$file_name)},
5203 git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
5204 git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
5205 git_print_page_path($file_name, $ftype, $hash_base);
5208 if ($format eq 'incremental') {
5209 print "<noscript>\n<div class=\"error\"><center><b>\n".
5210 "This page requires JavaScript to run.\n Use ".
5211 $cgi->a({-href => href(action=>'blame',javascript=>0,-replay=>1)},
5214 "</b></center></div>\n</noscript>\n";
5216 print qq!<div id="progress_bar" style="width: 100%; background-color: yellow"></div>\n!;
5219 print qq!<div class="page_body">\n!;
5220 print qq!<div id="progress_info">... / ...</div>\n!
5221 if ($format eq 'incremental');
5222 print qq!<table id="blame_table" class="blame" width="100%">\n!.
5223 #qq!<col width="5.5em" /><col width="2.5em" /><col width="*" />\n!.
5225 qq!<tr><th>Commit</th><th>Line</th><th>Data</th></tr>\n!.
5229 my @rev_color = qw(light dark);
5230 my $num_colors = scalar(@rev_color);
5231 my $current_color = 0;
5233 if ($format eq 'incremental') {
5234 my $color_class = $rev_color[$current_color];
5239 while (my $line = <$fd>) {
5243 print qq!<tr id="l$linenr" class="$color_class">!.
5244 qq!<td class="sha1"><a href=""> </a></td>!.
5245 qq!<td class="linenr">!.
5246 qq!<a class="linenr" href="">$linenr</a></td>!;
5247 print qq!<td class="pre">! . esc_html($line) . "</td>\n";
5251 } else { # porcelain, i.e. ordinary blame
5252 my %metainfo = (); # saves information about commits
5256 while (my $line = <$fd>) {
5258 # the header: <SHA-1> <src lineno> <dst lineno> [<lines in group>]
5259 # no <lines in group> for subsequent lines in group of lines
5260 my ($full_rev, $orig_lineno, $lineno, $group_size) =
5261 ($line =~ /^([0-9a-f]{40}) (\d+) (\d+)(?: (\d+))?$/);
5262 if (!exists $metainfo{$full_rev}) {
5263 $metainfo{$full_rev} = { 'nprevious' => 0 };
5265 my $meta = $metainfo{$full_rev};
5267 while ($data = <$fd>) {
5269 last if ($data =~ s/^\t//); # contents of line
5270 if ($data =~ /^(\S+)(?: (.*))?$/) {
5271 $meta->{$1} = $2 unless exists $meta->{$1};
5273 if ($data =~ /^previous /) {
5274 $meta->{'nprevious'}++;
5277 my $short_rev = substr($full_rev, 0, 8);
5278 my $author = $meta->{'author'};
5280 parse_date($meta->{'author-time'}, $meta->{'author-tz'});
5281 my $date = $date{'iso-tz'};
5283 $current_color = ($current_color + 1) % $num_colors;
5285 my $tr_class = $rev_color[$current_color];
5286 $tr_class .= ' boundary' if (exists $meta->{'boundary'});
5287 $tr_class .= ' no-previous' if ($meta->{'nprevious'} == 0);
5288 $tr_class .= ' multiple-previous' if ($meta->{'nprevious'} > 1);
5289 print "<tr id=\"l$lineno\" class=\"$tr_class\">\n";
5291 print "<td class=\"sha1\"";
5292 print " title=\"". esc_html($author) . ", $date\"";
5293 print " rowspan=\"$group_size\"" if ($group_size > 1);
5295 print $cgi->a({-href => href(action=>"commit",
5297 file_name=>$file_name)},
5298 esc_html($short_rev));
5299 if ($group_size >= 2) {
5300 my @author_initials = ($author =~ /\b([[:upper:]])\B/g);
5301 if (@author_initials) {
5303 esc_html(join('', @author_initials));
5309 # 'previous' <sha1 of parent commit> <filename at commit>
5310 if (exists $meta->{'previous'} &&
5311 $meta->{'previous'} =~ /^([a-fA-F0-9]{40}) (.*)$/) {
5312 $meta->{'parent'} = $1;
5313 $meta->{'file_parent'} = unquote($2);
5316 exists($meta->{'parent'}) ?
5317 $meta->{'parent'} : $full_rev;
5318 my $linenr_filename =
5319 exists($meta->{'file_parent'}) ?
5320 $meta->{'file_parent'} : unquote($meta->{'filename'});
5321 my $blamed = href(action => 'blame',
5322 file_name => $linenr_filename,
5323 hash_base => $linenr_commit);
5324 print "<td class=\"linenr\">";
5325 print $cgi->a({ -href => "$blamed#l$orig_lineno",
5326 -class => "linenr" },
5329 print "<td class=\"pre\">" . esc_html($data) . "</td>\n";
5337 "</table>\n"; # class="blame"
5338 print "</div>\n"; # class="blame_body"
5340 or print "Reading blob failed\n";
5349 sub git_blame_incremental {
5350 git_blame_common('incremental');
5353 sub git_blame_data {
5354 git_blame_common('data');
5358 my $head = git_get_head_hash($project);
5360 git_print_page_nav('','', $head,undef,$head);
5361 git_print_header_div('summary', $project);
5363 my @tagslist = git_get_tags_list();
5365 git_tags_body(\@tagslist);
5371 my $head = git_get_head_hash($project);
5373 git_print_page_nav('','', $head,undef,$head);
5374 git_print_header_div('summary', $project);
5376 my @headslist = git_get_heads_list();
5378 git_heads_body(\@headslist, $head);
5383 sub git_blob_plain {
5387 if (!defined $hash) {
5388 if (defined $file_name) {
5389 my $base = $hash_base || git_get_head_hash($project);
5390 $hash = git_get_hash_by_path($base, $file_name, "blob")
5391 or die_error(404, "Cannot find file");
5393 die_error(400, "No file name defined");
5395 } elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) {
5396 # blobs defined by non-textual hash id's can be cached
5400 open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
5401 or die_error(500, "Open git-cat-file blob '$hash' failed");
5403 # content-type (can include charset)
5404 $type = blob_contenttype($fd, $file_name, $type);
5406 # "save as" filename, even when no $file_name is given
5407 my $save_as = "$hash";
5408 if (defined $file_name) {
5409 $save_as = $file_name;
5410 } elsif ($type =~ m/^text\//) {
5414 # With XSS prevention on, blobs of all types except a few known safe
5415 # ones are served with "Content-Disposition: attachment" to make sure
5416 # they don't run in our security domain. For certain image types,
5417 # blob view writes an <img> tag referring to blob_plain view, and we
5418 # want to be sure not to break that by serving the image as an
5419 # attachment (though Firefox 3 doesn't seem to care).
5420 my $sandbox = $prevent_xss &&
5421 $type !~ m!^(?:text/plain|image/(?:gif|png|jpeg))$!;
5425 -expires => $expires,
5426 -content_disposition =>
5427 ($sandbox ? 'attachment' : 'inline')
5428 . '; filename="' . $save_as . '"');
5430 binmode STDOUT, ':raw';
5432 binmode STDOUT, ':utf8'; # as set at the beginning of gitweb.cgi
5439 if (!defined $hash) {
5440 if (defined $file_name) {
5441 my $base = $hash_base || git_get_head_hash($project);
5442 $hash = git_get_hash_by_path($base, $file_name, "blob")
5443 or die_error(404, "Cannot find file");
5445 die_error(400, "No file name defined");
5447 } elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) {
5448 # blobs defined by non-textual hash id's can be cached
5452 my $have_blame = gitweb_check_feature('blame');
5453 open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
5454 or die_error(500, "Couldn't cat $file_name, $hash");
5455 my $mimetype = blob_mimetype($fd, $file_name);
5456 if ($mimetype !~ m!^(?:text/|image/(?:gif|png|jpeg)$)! && -B $fd) {
5458 return git_blob_plain($mimetype);
5460 # we can have blame only for text/* mimetype
5461 $have_blame &&= ($mimetype =~ m!^text/!);
5463 git_header_html(undef, $expires);
5464 my $formats_nav = '';
5465 if (defined $hash_base && (my %co = parse_commit($hash_base))) {
5466 if (defined $file_name) {
5469 $cgi->a({-href => href(action=>"blame", -replay=>1)},
5474 $cgi->a({-href => href(action=>"history", -replay=>1)},
5477 $cgi->a({-href => href(action=>"blob_plain", -replay=>1)},
5480 $cgi->a({-href => href(action=>"blob",
5481 hash_base=>"HEAD", file_name=>$file_name)},
5485 $cgi->a({-href => href(action=>"blob_plain", -replay=>1)},
5488 git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
5489 git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
5491 print "<div class=\"page_nav\">\n" .
5492 "<br/><br/></div>\n" .
5493 "<div class=\"title\">$hash</div>\n";
5495 git_print_page_path($file_name, "blob", $hash_base);
5496 print "<div class=\"page_body\">\n";
5497 if ($mimetype =~ m!^image/!) {
5498 print qq!<img type="$mimetype"!;
5500 print qq! alt="$file_name" title="$file_name"!;
5503 href(action=>"blob_plain", hash=>$hash,
5504 hash_base=>$hash_base, file_name=>$file_name) .
5508 while (my $line = <$fd>) {
5511 $line = untabify($line);
5512 printf "<div class=\"pre\"><a id=\"l%i\" href=\"" . href(-replay => 1)
5513 . "#l%i\" class=\"linenr\">%4i</a> %s</div>\n",
5514 $nr, $nr, $nr, esc_html($line, -nbsp=>1);
5518 or print "Reading blob failed.\n";
5524 if (!defined $hash_base) {
5525 $hash_base = "HEAD";
5527 if (!defined $hash) {
5528 if (defined $file_name) {
5529 $hash = git_get_hash_by_path($hash_base, $file_name, "tree");
5534 die_error(404, "No such tree") unless defined($hash);
5536 my $show_sizes = gitweb_check_feature('show-sizes');
5537 my $have_blame = gitweb_check_feature('blame');
5542 open my $fd, "-|", git_cmd(), "ls-tree", '-z',
5543 ($show_sizes ? '-l' : ()), @extra_options, $hash
5544 or die_error(500, "Open git-ls-tree failed");
5545 @entries = map { chomp; $_ } <$fd>;
5547 or die_error(404, "Reading tree failed");
5550 my $refs = git_get_references();
5551 my $ref = format_ref_marker($refs, $hash_base);
5554 if (defined $hash_base && (my %co = parse_commit($hash_base))) {
5556 if (defined $file_name) {
5558 $cgi->a({-href => href(action=>"history", -replay=>1)},
5560 $cgi->a({-href => href(action=>"tree",
5561 hash_base=>"HEAD", file_name=>$file_name)},
5564 my $snapshot_links = format_snapshot_links($hash);
5565 if (defined $snapshot_links) {
5566 # FIXME: Should be available when we have no hash base as well.
5567 push @views_nav, $snapshot_links;
5569 git_print_page_nav('tree','', $hash_base, undef, undef,
5570 join(' | ', @views_nav));
5571 git_print_header_div('commit', esc_html($co{'title'}) . $ref, $hash_base);
5574 print "<div class=\"page_nav\">\n";
5575 print "<br/><br/></div>\n";
5576 print "<div class=\"title\">$hash</div>\n";
5578 if (defined $file_name) {
5579 $basedir = $file_name;
5580 if ($basedir ne '' && substr($basedir, -1) ne '/') {
5583 git_print_page_path($file_name, 'tree', $hash_base);
5585 print "<div class=\"page_body\">\n";
5586 print "<table class=\"tree\">\n";
5588 # '..' (top directory) link if possible
5589 if (defined $hash_base &&
5590 defined $file_name && $file_name =~ m![^/]+$!) {
5592 print "<tr class=\"dark\">\n";
5594 print "<tr class=\"light\">\n";
5598 my $up = $file_name;
5599 $up =~ s!/?[^/]+$!!;
5600 undef $up unless $up;
5601 # based on git_print_tree_entry
5602 print '<td class="mode">' . mode_str('040000') . "</td>\n";
5603 print '<td class="size"> </td>'."\n" if $show_sizes;
5604 print '<td class="list">';
5605 print $cgi->a({-href => href(action=>"tree",
5606 hash_base=>$hash_base,
5610 print "<td class=\"link\"></td>\n";
5614 foreach my $line (@entries) {
5615 my %t = parse_ls_tree_line($line, -z => 1, -l => $show_sizes);
5618 print "<tr class=\"dark\">\n";
5620 print "<tr class=\"light\">\n";
5624 git_print_tree_entry(\%t, $basedir, $hash_base, $have_blame);
5628 print "</table>\n" .
5634 my ($project, $hash) = @_;
5636 # path/to/project.git -> project
5637 # path/to/project/.git -> project
5638 my $name = to_utf8($project);
5639 $name =~ s,([^/])/*\.git$,$1,;
5640 $name = basename($name);
5642 $name =~ s/[[:cntrl:]]/?/g;
5645 if ($hash =~ /^[0-9a-fA-F]+$/) {
5646 # shorten SHA-1 hash
5647 my $full_hash = git_get_full_hash($project, $hash);
5648 if ($full_hash =~ /^$hash/ && length($hash) > 7) {
5649 $ver = git_get_short_hash($project, $hash);
5651 } elsif ($hash =~ m!^refs/tags/(.*)$!) {
5652 # tags don't need shortened SHA-1 hash
5655 # branches and other need shortened SHA-1 hash
5656 if ($hash =~ m!^refs/(?:heads|remotes)/(.*)$!) {
5659 $ver .= '-' . git_get_short_hash($project, $hash);
5661 # in case of hierarchical branch names
5664 # name = project-version_string
5665 $name = "$name-$ver";
5667 return wantarray ? ($name, $name) : $name;
5671 my $format = $input_params{'snapshot_format'};
5672 if (!@snapshot_fmts) {
5673 die_error(403, "Snapshots not allowed");
5675 # default to first supported snapshot format
5676 $format ||= $snapshot_fmts[0];
5677 if ($format !~ m/^[a-z0-9]+$/) {
5678 die_error(400, "Invalid snapshot format parameter");
5679 } elsif (!exists($known_snapshot_formats{$format})) {
5680 die_error(400, "Unknown snapshot format");
5681 } elsif ($known_snapshot_formats{$format}{'disabled'}) {
5682 die_error(403, "Snapshot format not allowed");
5683 } elsif (!grep($_ eq $format, @snapshot_fmts)) {
5684 die_error(403, "Unsupported snapshot format");
5687 my $type = git_get_type("$hash^{}");
5689 die_error(404, 'Object does not exist');
5690 } elsif ($type eq 'blob') {
5691 die_error(400, 'Object is not a tree-ish');
5694 my ($name, $prefix) = snapshot_name($project, $hash);
5695 my $filename = "$name$known_snapshot_formats{$format}{'suffix'}";
5696 my $cmd = quote_command(
5697 git_cmd(), 'archive',
5698 "--format=$known_snapshot_formats{$format}{'format'}",
5699 "--prefix=$prefix/", $hash);
5700 if (exists $known_snapshot_formats{$format}{'compressor'}) {
5701 $cmd .= ' | ' . quote_command(@{$known_snapshot_formats{$format}{'compressor'}});
5704 $filename =~ s/(["\\])/\\$1/g;
5706 -type => $known_snapshot_formats{$format}{'type'},
5707 -content_disposition => 'inline; filename="' . $filename . '"',
5708 -status => '200 OK');
5710 open my $fd, "-|", $cmd
5711 or die_error(500, "Execute git-archive failed");
5712 binmode STDOUT, ':raw';
5714 binmode STDOUT, ':utf8'; # as set at the beginning of gitweb.cgi
5718 sub git_log_generic {
5719 my ($fmt_name, $body_subr, $base, $parent, $file_name, $file_hash) = @_;
5721 my $head = git_get_head_hash($project);
5722 if (!defined $base) {
5725 if (!defined $page) {
5728 my $refs = git_get_references();
5730 my $commit_hash = $base;
5731 if (defined $parent) {
5732 $commit_hash = "$parent..$base";
5735 parse_commits($commit_hash, 101, (100 * $page),
5736 defined $file_name ? ($file_name, "--full-history") : ());
5739 if (!defined $file_hash && defined $file_name) {
5740 # some commits could have deleted file in question,
5741 # and not have it in tree, but one of them has to have it
5742 for (my $i = 0; $i < @commitlist; $i++) {
5743 $file_hash = git_get_hash_by_path($commitlist[$i]{'id'}, $file_name);
5744 last if defined $file_hash;
5747 if (defined $file_hash) {
5748 $ftype = git_get_type($file_hash);
5750 if (defined $file_name && !defined $ftype) {
5751 die_error(500, "Unknown type of object");
5754 if (defined $file_name) {
5755 %co = parse_commit($base)
5756 or die_error(404, "Unknown commit object");
5760 my $paging_nav = format_paging_nav($fmt_name, $page, $#commitlist >= 100);
5762 if ($#commitlist >= 100) {
5764 $cgi->a({-href => href(-replay=>1, page=>$page+1),
5765 -accesskey => "n", -title => "Alt-n"}, "next");
5767 my $patch_max = gitweb_get_feature('patches');
5768 if ($patch_max && !defined $file_name) {
5769 if ($patch_max < 0 || @commitlist <= $patch_max) {
5770 $paging_nav .= " ⋅ " .
5771 $cgi->a({-href => href(action=>"patches", -replay=>1)},
5777 git_print_page_nav($fmt_name,'', $hash,$hash,$hash, $paging_nav);
5778 if (defined $file_name) {
5779 git_print_header_div('commit', esc_html($co{'title'}), $base);
5781 git_print_header_div('summary', $project)
5783 git_print_page_path($file_name, $ftype, $hash_base)
5784 if (defined $file_name);
5786 $body_subr->(\@commitlist, 0, 99, $refs, $next_link,
5787 $file_name, $file_hash, $ftype);
5793 git_log_generic('log', \&git_log_body,
5794 $hash, $hash_parent);
5798 $hash ||= $hash_base || "HEAD";
5799 my %co = parse_commit($hash)
5800 or die_error(404, "Unknown commit object");
5802 my $parent = $co{'parent'};
5803 my $parents = $co{'parents'}; # listref
5805 # we need to prepare $formats_nav before any parameter munging
5807 if (!defined $parent) {
5809 $formats_nav .= '(initial)';
5810 } elsif (@$parents == 1) {
5811 # single parent commit
5814 $cgi->a({-href => href(action=>"commit",
5816 esc_html(substr($parent, 0, 7))) .
5823 $cgi->a({-href => href(action=>"commit",
5825 esc_html(substr($_, 0, 7)));
5829 if (gitweb_check_feature('patches') && @$parents <= 1) {
5830 $formats_nav .= " | " .
5831 $cgi->a({-href => href(action=>"patch", -replay=>1)},
5835 if (!defined $parent) {
5839 open my $fd, "-|", git_cmd(), "diff-tree", '-r', "--no-commit-id",
5841 (@$parents <= 1 ? $parent : '-c'),
5843 or die_error(500, "Open git-diff-tree failed");
5844 @difftree = map { chomp; $_ } <$fd>;
5845 close $fd or die_error(404, "Reading git-diff-tree failed");
5847 # non-textual hash id's can be cached
5849 if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
5852 my $refs = git_get_references();
5853 my $ref = format_ref_marker($refs, $co{'id'});
5855 git_header_html(undef, $expires);
5856 git_print_page_nav('commit', '',
5857 $hash, $co{'tree'}, $hash,
5860 if (defined $co{'parent'}) {
5861 git_print_header_div('commitdiff', esc_html($co{'title'}) . $ref, $hash);
5863 git_print_header_div('tree', esc_html($co{'title'}) . $ref, $co{'tree'}, $hash);
5865 print "<div class=\"title_text\">\n" .
5866 "<table class=\"object_header\">\n";
5867 git_print_authorship_rows(\%co);
5868 print "<tr><td>commit</td><td class=\"sha1\">$co{'id'}</td></tr>\n";
5871 "<td class=\"sha1\">" .
5872 $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$hash),
5873 class => "list"}, $co{'tree'}) .
5875 "<td class=\"link\">" .
5876 $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$hash)},
5878 my $snapshot_links = format_snapshot_links($hash);
5879 if (defined $snapshot_links) {
5880 print " | " . $snapshot_links;
5885 foreach my $par (@$parents) {
5888 "<td class=\"sha1\">" .
5889 $cgi->a({-href => href(action=>"commit", hash=>$par),
5890 class => "list"}, $par) .
5892 "<td class=\"link\">" .
5893 $cgi->a({-href => href(action=>"commit", hash=>$par)}, "commit") .
5895 $cgi->a({-href => href(action=>"commitdiff", hash=>$hash, hash_parent=>$par)}, "diff") .
5902 print "<div class=\"page_body\">\n";
5903 git_print_log($co{'comment'});
5906 git_difftree_body(\@difftree, $hash, @$parents);
5912 # object is defined by:
5913 # - hash or hash_base alone
5914 # - hash_base and file_name
5917 # - hash or hash_base alone
5918 if ($hash || ($hash_base && !defined $file_name)) {
5919 my $object_id = $hash || $hash_base;
5921 open my $fd, "-|", quote_command(
5922 git_cmd(), 'cat-file', '-t', $object_id) . ' 2> /dev/null'
5923 or die_error(404, "Object does not exist");
5927 or die_error(404, "Object does not exist");
5929 # - hash_base and file_name
5930 } elsif ($hash_base && defined $file_name) {
5931 $file_name =~ s,/+$,,;
5933 system(git_cmd(), "cat-file", '-e', $hash_base) == 0
5934 or die_error(404, "Base object does not exist");
5936 # here errors should not hapen
5937 open my $fd, "-|", git_cmd(), "ls-tree", $hash_base, "--", $file_name
5938 or die_error(500, "Open git-ls-tree failed");
5942 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'
5943 unless ($line && $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t/) {
5944 die_error(404, "File or directory for given base does not exist");
5949 die_error(400, "Not enough information to find object");
5952 print $cgi->redirect(-uri => href(action=>$type, -full=>1,
5953 hash=>$hash, hash_base=>$hash_base,
5954 file_name=>$file_name),
5955 -status => '302 Found');
5959 my $format = shift || 'html';
5966 # preparing $fd and %diffinfo for git_patchset_body
5968 if (defined $hash_base && defined $hash_parent_base) {
5969 if (defined $file_name) {
5971 open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
5972 $hash_parent_base, $hash_base,
5973 "--", (defined $file_parent ? $file_parent : ()), $file_name
5974 or die_error(500, "Open git-diff-tree failed");
5975 @difftree = map { chomp; $_ } <$fd>;
5977 or die_error(404, "Reading git-diff-tree failed");
5979 or die_error(404, "Blob diff not found");
5981 } elsif (defined $hash &&
5982 $hash =~ /[0-9a-fA-F]{40}/) {
5983 # try to find filename from $hash
5985 # read filtered raw output
5986 open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
5987 $hash_parent_base, $hash_base, "--"
5988 or die_error(500, "Open git-diff-tree failed");
5990 # ':100644 100644 03b21826... 3b93d5e7... M ls-files.c'
5992 grep { /^:[0-7]{6} [0-7]{6} [0-9a-fA-F]{40} $hash/ }
5993 map { chomp; $_ } <$fd>;
5995 or die_error(404, "Reading git-diff-tree failed");
5997 or die_error(404, "Blob diff not found");
6000 die_error(400, "Missing one of the blob diff parameters");
6003 if (@difftree > 1) {
6004 die_error(400, "Ambiguous blob diff specification");
6007 %diffinfo = parse_difftree_raw_line($difftree[0]);
6008 $file_parent ||= $diffinfo{'from_file'} || $file_name;
6009 $file_name ||= $diffinfo{'to_file'};
6011 $hash_parent ||= $diffinfo{'from_id'};
6012 $hash ||= $diffinfo{'to_id'};
6014 # non-textual hash id's can be cached
6015 if ($hash_base =~ m/^[0-9a-fA-F]{40}$/ &&
6016 $hash_parent_base =~ m/^[0-9a-fA-F]{40}$/) {
6021 open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
6022 '-p', ($format eq 'html' ? "--full-index" : ()),
6023 $hash_parent_base, $hash_base,
6024 "--", (defined $file_parent ? $file_parent : ()), $file_name
6025 or die_error(500, "Open git-diff-tree failed");
6028 # old/legacy style URI -- not generated anymore since 1.4.3.
6030 die_error('404 Not Found', "Missing one of the blob diff parameters")
6034 if ($format eq 'html') {
6036 $cgi->a({-href => href(action=>"blobdiff_plain", -replay=>1)},
6038 git_header_html(undef, $expires);
6039 if (defined $hash_base && (my %co = parse_commit($hash_base))) {
6040 git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
6041 git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
6043 print "<div class=\"page_nav\"><br/>$formats_nav<br/></div>\n";
6044 print "<div class=\"title\">$hash vs $hash_parent</div>\n";
6046 if (defined $file_name) {
6047 git_print_page_path($file_name, "blob", $hash_base);
6049 print "<div class=\"page_path\"></div>\n";
6052 } elsif ($format eq 'plain') {
6054 -type => 'text/plain',
6055 -charset => 'utf-8',
6056 -expires => $expires,
6057 -content_disposition => 'inline; filename="' . "$file_name" . '.patch"');
6059 print "X-Git-Url: " . $cgi->self_url() . "\n\n";
6062 die_error(400, "Unknown blobdiff format");
6066 if ($format eq 'html') {
6067 print "<div class=\"page_body\">\n";
6069 git_patchset_body($fd, [ \%diffinfo ], $hash_base, $hash_parent_base);
6072 print "</div>\n"; # class="page_body"
6076 while (my $line = <$fd>) {
6077 $line =~ s!a/($hash|$hash_parent)!'a/'.esc_path($diffinfo{'from_file'})!eg;
6078 $line =~ s!b/($hash|$hash_parent)!'b/'.esc_path($diffinfo{'to_file'})!eg;
6082 last if $line =~ m!^\+\+\+!;
6090 sub git_blobdiff_plain {
6091 git_blobdiff('plain');
6094 sub git_commitdiff {
6096 my $format = $params{-format} || 'html';
6098 my ($patch_max) = gitweb_get_feature('patches');
6099 if ($format eq 'patch') {
6100 die_error(403, "Patch view not allowed") unless $patch_max;
6103 $hash ||= $hash_base || "HEAD";
6104 my %co = parse_commit($hash)
6105 or die_error(404, "Unknown commit object");
6107 # choose format for commitdiff for merge
6108 if (! defined $hash_parent && @{$co{'parents'}} > 1) {
6109 $hash_parent = '--cc';
6111 # we need to prepare $formats_nav before almost any parameter munging
6113 if ($format eq 'html') {
6115 $cgi->a({-href => href(action=>"commitdiff_plain", -replay=>1)},
6117 if ($patch_max && @{$co{'parents'}} <= 1) {
6118 $formats_nav .= " | " .
6119 $cgi->a({-href => href(action=>"patch", -replay=>1)},
6123 if (defined $hash_parent &&
6124 $hash_parent ne '-c' && $hash_parent ne '--cc') {
6125 # commitdiff with two commits given
6126 my $hash_parent_short = $hash_parent;
6127 if ($hash_parent =~ m/^[0-9a-fA-F]{40}$/) {
6128 $hash_parent_short = substr($hash_parent, 0, 7);
6132 for (my $i = 0; $i < @{$co{'parents'}}; $i++) {
6133 if ($co{'parents'}[$i] eq $hash_parent) {
6134 $formats_nav .= ' parent ' . ($i+1);
6138 $formats_nav .= ': ' .
6139 $cgi->a({-href => href(action=>"commitdiff",
6140 hash=>$hash_parent)},
6141 esc_html($hash_parent_short)) .
6143 } elsif (!$co{'parent'}) {
6145 $formats_nav .= ' (initial)';
6146 } elsif (scalar @{$co{'parents'}} == 1) {
6147 # single parent commit
6150 $cgi->a({-href => href(action=>"commitdiff",
6151 hash=>$co{'parent'})},
6152 esc_html(substr($co{'parent'}, 0, 7))) .
6156 if ($hash_parent eq '--cc') {
6157 $formats_nav .= ' | ' .
6158 $cgi->a({-href => href(action=>"commitdiff",
6159 hash=>$hash, hash_parent=>'-c')},
6161 } else { # $hash_parent eq '-c'
6162 $formats_nav .= ' | ' .
6163 $cgi->a({-href => href(action=>"commitdiff",
6164 hash=>$hash, hash_parent=>'--cc')},
6170 $cgi->a({-href => href(action=>"commitdiff",
6172 esc_html(substr($_, 0, 7)));
6173 } @{$co{'parents'}} ) .
6178 my $hash_parent_param = $hash_parent;
6179 if (!defined $hash_parent_param) {
6180 # --cc for multiple parents, --root for parentless
6181 $hash_parent_param =
6182 @{$co{'parents'}} > 1 ? '--cc' : $co{'parent'} || '--root';
6188 if ($format eq 'html') {
6189 open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
6190 "--no-commit-id", "--patch-with-raw", "--full-index",
6191 $hash_parent_param, $hash, "--"
6192 or die_error(500, "Open git-diff-tree failed");
6194 while (my $line = <$fd>) {
6196 # empty line ends raw part of diff-tree output
6198 push @difftree, scalar parse_difftree_raw_line($line);
6201 } elsif ($format eq 'plain') {
6202 open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
6203 '-p', $hash_parent_param, $hash, "--"
6204 or die_error(500, "Open git-diff-tree failed");
6205 } elsif ($format eq 'patch') {
6206 # For commit ranges, we limit the output to the number of
6207 # patches specified in the 'patches' feature.
6208 # For single commits, we limit the output to a single patch,
6209 # diverging from the git-format-patch default.
6210 my @commit_spec = ();
6212 if ($patch_max > 0) {
6213 push @commit_spec, "-$patch_max";
6215 push @commit_spec, '-n', "$hash_parent..$hash";
6217 if ($params{-single}) {
6218 push @commit_spec, '-1';
6220 if ($patch_max > 0) {
6221 push @commit_spec, "-$patch_max";
6223 push @commit_spec, "-n";
6225 push @commit_spec, '--root', $hash;
6227 open $fd, "-|", git_cmd(), "format-patch", '--encoding=utf8',
6228 '--stdout', @commit_spec
6229 or die_error(500, "Open git-format-patch failed");
6231 die_error(400, "Unknown commitdiff format");
6234 # non-textual hash id's can be cached
6236 if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
6240 # write commit message
6241 if ($format eq 'html') {
6242 my $refs = git_get_references();
6243 my $ref = format_ref_marker($refs, $co{'id'});
6245 git_header_html(undef, $expires);
6246 git_print_page_nav('commitdiff','', $hash,$co{'tree'},$hash, $formats_nav);
6247 git_print_header_div('commit', esc_html($co{'title'}) . $ref, $hash);
6248 print "<div class=\"title_text\">\n" .
6249 "<table class=\"object_header\">\n";
6250 git_print_authorship_rows(\%co);
6253 print "<div class=\"page_body\">\n";
6254 if (@{$co{'comment'}} > 1) {
6255 print "<div class=\"log\">\n";
6256 git_print_log($co{'comment'}, -final_empty_line=> 1, -remove_title => 1);
6257 print "</div>\n"; # class="log"
6260 } elsif ($format eq 'plain') {
6261 my $refs = git_get_references("tags");
6262 my $tagname = git_get_rev_name_tags($hash);
6263 my $filename = basename($project) . "-$hash.patch";
6266 -type => 'text/plain',
6267 -charset => 'utf-8',
6268 -expires => $expires,
6269 -content_disposition => 'inline; filename="' . "$filename" . '"');
6270 my %ad = parse_date($co{'author_epoch'}, $co{'author_tz'});
6271 print "From: " . to_utf8($co{'author'}) . "\n";
6272 print "Date: $ad{'rfc2822'} ($ad{'tz_local'})\n";
6273 print "Subject: " . to_utf8($co{'title'}) . "\n";
6275 print "X-Git-Tag: $tagname\n" if $tagname;
6276 print "X-Git-Url: " . $cgi->self_url() . "\n\n";
6278 foreach my $line (@{$co{'comment'}}) {
6279 print to_utf8($line) . "\n";
6282 } elsif ($format eq 'patch') {
6283 my $filename = basename($project) . "-$hash.patch";
6286 -type => 'text/plain',
6287 -charset => 'utf-8',
6288 -expires => $expires,
6289 -content_disposition => 'inline; filename="' . "$filename" . '"');
6293 if ($format eq 'html') {
6294 my $use_parents = !defined $hash_parent ||
6295 $hash_parent eq '-c' || $hash_parent eq '--cc';
6296 git_difftree_body(\@difftree, $hash,
6297 $use_parents ? @{$co{'parents'}} : $hash_parent);
6300 git_patchset_body($fd, \@difftree, $hash,
6301 $use_parents ? @{$co{'parents'}} : $hash_parent);
6303 print "</div>\n"; # class="page_body"
6306 } elsif ($format eq 'plain') {
6310 or print "Reading git-diff-tree failed\n";
6311 } elsif ($format eq 'patch') {
6315 or print "Reading git-format-patch failed\n";
6319 sub git_commitdiff_plain {
6320 git_commitdiff(-format => 'plain');
6323 # format-patch-style patches
6325 git_commitdiff(-format => 'patch', -single => 1);
6329 git_commitdiff(-format => 'patch');
6333 git_log_generic('history', \&git_history_body,
6334 $hash_base, $hash_parent_base,
6339 gitweb_check_feature('search') or die_error(403, "Search is disabled");
6340 if (!defined $searchtext) {
6341 die_error(400, "Text field is empty");
6343 if (!defined $hash) {
6344 $hash = git_get_head_hash($project);
6346 my %co = parse_commit($hash);
6348 die_error(404, "Unknown commit object");
6350 if (!defined $page) {
6354 $searchtype ||= 'commit';
6355 if ($searchtype eq 'pickaxe') {
6356 # pickaxe may take all resources of your box and run for several minutes
6357 # with every query - so decide by yourself how public you make this feature
6358 gitweb_check_feature('pickaxe')
6359 or die_error(403, "Pickaxe is disabled");
6361 if ($searchtype eq 'grep') {
6362 gitweb_check_feature('grep')
6363 or die_error(403, "Grep is disabled");
6368 if ($searchtype eq 'commit' or $searchtype eq 'author' or $searchtype eq 'committer') {
6370 if ($searchtype eq 'commit') {
6371 $greptype = "--grep=";
6372 } elsif ($searchtype eq 'author') {
6373 $greptype = "--author=";
6374 } elsif ($searchtype eq 'committer') {
6375 $greptype = "--committer=";
6377 $greptype .= $searchtext;
6378 my @commitlist = parse_commits($hash, 101, (100 * $page), undef,
6379 $greptype, '--regexp-ignore-case',
6380 $search_use_regexp ? '--extended-regexp' : '--fixed-strings');
6382 my $paging_nav = '';
6385 $cgi->a({-href => href(action=>"search", hash=>$hash,
6386 searchtext=>$searchtext,
6387 searchtype=>$searchtype)},
6389 $paging_nav .= " ⋅ " .
6390 $cgi->a({-href => href(-replay=>1, page=>$page-1),
6391 -accesskey => "p", -title => "Alt-p"}, "prev");
6393 $paging_nav .= "first";
6394 $paging_nav .= " ⋅ prev";
6397 if ($#commitlist >= 100) {
6399 $cgi->a({-href => href(-replay=>1, page=>$page+1),
6400 -accesskey => "n", -title => "Alt-n"}, "next");
6401 $paging_nav .= " ⋅ $next_link";
6403 $paging_nav .= " ⋅ next";
6406 if ($#commitlist >= 100) {
6409 git_print_page_nav('','', $hash,$co{'tree'},$hash, $paging_nav);
6410 git_print_header_div('commit', esc_html($co{'title'}), $hash);
6411 git_search_grep_body(\@commitlist, 0, 99, $next_link);
6414 if ($searchtype eq 'pickaxe') {
6415 git_print_page_nav('','', $hash,$co{'tree'},$hash);
6416 git_print_header_div('commit', esc_html($co{'title'}), $hash);
6418 print "<table class=\"pickaxe search\">\n";
6421 open my $fd, '-|', git_cmd(), '--no-pager', 'log', @diff_opts,
6422 '--pretty=format:%H', '--no-abbrev', '--raw', "-S$searchtext",
6423 ($search_use_regexp ? '--pickaxe-regex' : ());
6426 while (my $line = <$fd>) {
6430 my %set = parse_difftree_raw_line($line);
6431 if (defined $set{'commit'}) {
6432 # finish previous commit
6435 "<td class=\"link\">" .
6436 $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") .
6438 $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree");
6444 print "<tr class=\"dark\">\n";
6446 print "<tr class=\"light\">\n";
6449 %co = parse_commit($set{'commit'});
6450 my $author = chop_and_escape_str($co{'author_name'}, 15, 5);
6451 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
6452 "<td><i>$author</i></td>\n" .
6454 $cgi->a({-href => href(action=>"commit", hash=>$co{'id'}),
6455 -class => "list subject"},
6456 chop_and_escape_str($co{'title'}, 50) . "<br/>");
6457 } elsif (defined $set{'to_id'}) {
6458 next if ($set{'to_id'} =~ m/^0{40}$/);
6460 print $cgi->a({-href => href(action=>"blob", hash_base=>$co{'id'},
6461 hash=>$set{'to_id'}, file_name=>$set{'to_file'}),
6463 "<span class=\"match\">" . esc_path($set{'file'}) . "</span>") .
6469 # finish last commit (warning: repetition!)
6472 "<td class=\"link\">" .
6473 $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") .
6475 $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree");
6483 if ($searchtype eq 'grep') {
6484 git_print_page_nav('','', $hash,$co{'tree'},$hash);
6485 git_print_header_div('commit', esc_html($co{'title'}), $hash);
6487 print "<table class=\"grep_search\">\n";
6491 open my $fd, "-|", git_cmd(), 'grep', '-n',
6492 $search_use_regexp ? ('-E', '-i') : '-F',
6493 $searchtext, $co{'tree'};
6495 while (my $line = <$fd>) {
6497 my ($file, $lno, $ltext, $binary);
6498 last if ($matches++ > 1000);
6499 if ($line =~ /^Binary file (.+) matches$/) {
6503 (undef, $file, $lno, $ltext) = split(/:/, $line, 4);
6505 if ($file ne $lastfile) {
6506 $lastfile and print "</td></tr>\n";
6508 print "<tr class=\"dark\">\n";
6510 print "<tr class=\"light\">\n";
6512 print "<td class=\"list\">".
6513 $cgi->a({-href => href(action=>"blob", hash=>$co{'hash'},
6514 file_name=>"$file"),
6515 -class => "list"}, esc_path($file));
6516 print "</td><td>\n";
6520 print "<div class=\"binary\">Binary file</div>\n";
6522 $ltext = untabify($ltext);
6523 if ($ltext =~ m/^(.*)($search_regexp)(.*)$/i) {
6524 $ltext = esc_html($1, -nbsp=>1);
6525 $ltext .= '<span class="match">';
6526 $ltext .= esc_html($2, -nbsp=>1);
6527 $ltext .= '</span>';
6528 $ltext .= esc_html($3, -nbsp=>1);
6530 $ltext = esc_html($ltext, -nbsp=>1);
6532 print "<div class=\"pre\">" .
6533 $cgi->a({-href => href(action=>"blob", hash=>$co{'hash'},
6534 file_name=>"$file").'#l'.$lno,
6535 -class => "linenr"}, sprintf('%4i', $lno))
6536 . ' ' . $ltext . "</div>\n";
6540 print "</td></tr>\n";
6541 if ($matches > 1000) {
6542 print "<div class=\"diff nodifferences\">Too many matches, listing trimmed</div>\n";
6545 print "<div class=\"diff nodifferences\">No matches found</div>\n";
6554 sub git_search_help {
6556 git_print_page_nav('','', $hash,$hash,$hash);
6558 <p><strong>Pattern</strong> is by default a normal string that is matched precisely (but without
6559 regard to case, except in the case of pickaxe). However, when you check the <em>re</em> checkbox,
6560 the pattern entered is recognized as the POSIX extended
6561 <a href="http://en.wikipedia.org/wiki/Regular_expression">regular expression</a> (also case
6564 <dt><b>commit</b></dt>
6565 <dd>The commit messages and authorship information will be scanned for the given pattern.</dd>
6567 my $have_grep = gitweb_check_feature('grep');
6570 <dt><b>grep</b></dt>
6571 <dd>All files in the currently selected tree (HEAD unless you are explicitly browsing
6572 a different one) are searched for the given pattern. On large trees, this search can take
6573 a while and put some strain on the server, so please use it with some consideration. Note that
6574 due to git-grep peculiarity, currently if regexp mode is turned off, the matches are
6575 case-sensitive.</dd>
6579 <dt><b>author</b></dt>
6580 <dd>Name and e-mail of the change author and date of birth of the patch will be scanned for the given pattern.</dd>
6581 <dt><b>committer</b></dt>
6582 <dd>Name and e-mail of the committer and date of commit will be scanned for the given pattern.</dd>
6584 my $have_pickaxe = gitweb_check_feature('pickaxe');
6585 if ($have_pickaxe) {
6587 <dt><b>pickaxe</b></dt>
6588 <dd>All commits that caused the string to appear or disappear from any file (changes that
6589 added, removed or "modified" the string) will be listed. This search can take a while and
6590 takes a lot of strain on the server, so please use it wisely. Note that since you may be
6591 interested even in changes just changing the case as well, this search is case sensitive.</dd>
6599 git_log_generic('shortlog', \&git_shortlog_body,
6600 $hash, $hash_parent);
6603 ## ......................................................................
6604 ## feeds (RSS, Atom; OPML)
6607 my $format = shift || 'atom';
6608 my $have_blame = gitweb_check_feature('blame');
6610 # Atom: http://www.atomenabled.org/developers/syndication/
6611 # RSS: http://www.notestips.com/80256B3A007F2692/1/NAMO5P9UPQ
6612 if ($format ne 'rss' && $format ne 'atom') {
6613 die_error(400, "Unknown web feed format");
6616 # log/feed of current (HEAD) branch, log of given branch, history of file/directory
6617 my $head = $hash || 'HEAD';
6618 my @commitlist = parse_commits($head, 150, 0, $file_name);
6622 my $content_type = "application/$format+xml";
6623 if (defined $cgi->http('HTTP_ACCEPT') &&
6624 $cgi->Accept('text/xml') > $cgi->Accept($content_type)) {
6625 # browser (feed reader) prefers text/xml
6626 $content_type = 'text/xml';
6628 if (defined($commitlist[0])) {
6629 %latest_commit = %{$commitlist[0]};
6630 my $latest_epoch = $latest_commit{'committer_epoch'};
6631 %latest_date = parse_date($latest_epoch);
6632 my $if_modified = $cgi->http('IF_MODIFIED_SINCE');
6633 if (defined $if_modified) {
6635 if (eval { require HTTP::Date; 1; }) {
6636 $since = HTTP::Date::str2time($if_modified);
6637 } elsif (eval { require Time::ParseDate; 1; }) {
6638 $since = Time::ParseDate::parsedate($if_modified, GMT => 1);
6640 if (defined $since && $latest_epoch <= $since) {
6642 -type => $content_type,
6643 -charset => 'utf-8',
6644 -last_modified => $latest_date{'rfc2822'},
6645 -status => '304 Not Modified');
6650 -type => $content_type,
6651 -charset => 'utf-8',
6652 -last_modified => $latest_date{'rfc2822'});
6655 -type => $content_type,
6656 -charset => 'utf-8');
6659 # Optimization: skip generating the body if client asks only
6660 # for Last-Modified date.
6661 return if ($cgi->request_method() eq 'HEAD');
6664 my $title = "$site_name - $project/$action";
6665 my $feed_type = 'log';
6666 if (defined $hash) {
6667 $title .= " - '$hash'";
6668 $feed_type = 'branch log';
6669 if (defined $file_name) {
6670 $title .= " :: $file_name";
6671 $feed_type = 'history';
6673 } elsif (defined $file_name) {
6674 $title .= " - $file_name";
6675 $feed_type = 'history';
6677 $title .= " $feed_type";
6678 my $descr = git_get_project_description($project);
6679 if (defined $descr) {
6680 $descr = esc_html($descr);
6682 $descr = "$project " .
6683 ($format eq 'rss' ? 'RSS' : 'Atom') .
6686 my $owner = git_get_project_owner($project);
6687 $owner = esc_html($owner);
6691 if (defined $file_name) {
6692 $alt_url = href(-full=>1, action=>"history", hash=>$hash, file_name=>$file_name);
6693 } elsif (defined $hash) {
6694 $alt_url = href(-full=>1, action=>"log", hash=>$hash);
6696 $alt_url = href(-full=>1, action=>"summary");
6698 print qq!<?xml version="1.0" encoding="utf-8"?>\n!;
6699 if ($format eq 'rss') {
6701 <rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
6704 print "<title>$title</title>\n" .
6705 "<link>$alt_url</link>\n" .
6706 "<description>$descr</description>\n" .
6707 "<language>en</language>\n" .
6708 # project owner is responsible for 'editorial' content
6709 "<managingEditor>$owner</managingEditor>\n";
6710 if (defined $logo || defined $favicon) {
6711 # prefer the logo to the favicon, since RSS
6712 # doesn't allow both
6713 my $img = esc_url($logo || $favicon);
6715 "<url>$img</url>\n" .
6716 "<title>$title</title>\n" .
6717 "<link>$alt_url</link>\n" .
6721 print "<pubDate>$latest_date{'rfc2822'}</pubDate>\n";
6722 print "<lastBuildDate>$latest_date{'rfc2822'}</lastBuildDate>\n";
6724 print "<generator>gitweb v.$version/$git_version</generator>\n";
6725 } elsif ($format eq 'atom') {
6727 <feed xmlns="http://www.w3.org/2005/Atom">
6729 print "<title>$title</title>\n" .
6730 "<subtitle>$descr</subtitle>\n" .
6731 '<link rel="alternate" type="text/html" href="' .
6732 $alt_url . '" />' . "\n" .
6733 '<link rel="self" type="' . $content_type . '" href="' .
6734 $cgi->self_url() . '" />' . "\n" .
6735 "<id>" . href(-full=>1) . "</id>\n" .
6736 # use project owner for feed author
6737 "<author><name>$owner</name></author>\n";
6738 if (defined $favicon) {
6739 print "<icon>" . esc_url($favicon) . "</icon>\n";
6741 if (defined $logo_url) {
6742 # not twice as wide as tall: 72 x 27 pixels
6743 print "<logo>" . esc_url($logo) . "</logo>\n";
6745 if (! %latest_date) {
6746 # dummy date to keep the feed valid until commits trickle in:
6747 print "<updated>1970-01-01T00:00:00Z</updated>\n";
6749 print "<updated>$latest_date{'iso-8601'}</updated>\n";
6751 print "<generator version='$version/$git_version'>gitweb</generator>\n";
6755 for (my $i = 0; $i <= $#commitlist; $i++) {
6756 my %co = %{$commitlist[$i]};
6757 my $commit = $co{'id'};
6758 # we read 150, we always show 30 and the ones more recent than 48 hours
6759 if (($i >= 20) && ((time - $co{'author_epoch'}) > 48*60*60)) {
6762 my %cd = parse_date($co{'author_epoch'});
6764 # get list of changed files
6765 open my $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
6766 $co{'parent'} || "--root",
6767 $co{'id'}, "--", (defined $file_name ? $file_name : ())
6769 my @difftree = map { chomp; $_ } <$fd>;
6773 # print element (entry, item)
6774 my $co_url = href(-full=>1, action=>"commitdiff", hash=>$commit);
6775 if ($format eq 'rss') {
6777 "<title>" . esc_html($co{'title'}) . "</title>\n" .
6778 "<author>" . esc_html($co{'author'}) . "</author>\n" .
6779 "<pubDate>$cd{'rfc2822'}</pubDate>\n" .
6780 "<guid isPermaLink=\"true\">$co_url</guid>\n" .
6781 "<link>$co_url</link>\n" .
6782 "<description>" . esc_html($co{'title'}) . "</description>\n" .
6783 "<content:encoded>" .
6785 } elsif ($format eq 'atom') {
6787 "<title type=\"html\">" . esc_html($co{'title'}) . "</title>\n" .
6788 "<updated>$cd{'iso-8601'}</updated>\n" .
6790 " <name>" . esc_html($co{'author_name'}) . "</name>\n";
6791 if ($co{'author_email'}) {
6792 print " <email>" . esc_html($co{'author_email'}) . "</email>\n";
6794 print "</author>\n" .
6795 # use committer for contributor
6797 " <name>" . esc_html($co{'committer_name'}) . "</name>\n";
6798 if ($co{'committer_email'}) {
6799 print " <email>" . esc_html($co{'committer_email'}) . "</email>\n";
6801 print "</contributor>\n" .
6802 "<published>$cd{'iso-8601'}</published>\n" .
6803 "<link rel=\"alternate\" type=\"text/html\" href=\"$co_url\" />\n" .
6804 "<id>$co_url</id>\n" .
6805 "<content type=\"xhtml\" xml:base=\"" . esc_url($my_url) . "\">\n" .
6806 "<div xmlns=\"http://www.w3.org/1999/xhtml\">\n";
6808 my $comment = $co{'comment'};
6810 foreach my $line (@$comment) {
6811 $line = esc_html($line);
6814 print "</pre><ul>\n";
6815 foreach my $difftree_line (@difftree) {
6816 my %difftree = parse_difftree_raw_line($difftree_line);
6817 next if !$difftree{'from_id'};
6819 my $file = $difftree{'file'} || $difftree{'to_file'};
6823 $cgi->a({-href => href(-full=>1, action=>"blobdiff",
6824 hash=>$difftree{'to_id'}, hash_parent=>$difftree{'from_id'},
6825 hash_base=>$co{'id'}, hash_parent_base=>$co{'parent'},
6826 file_name=>$file, file_parent=>$difftree{'from_file'}),
6827 -title => "diff"}, 'D');
6829 print $cgi->a({-href => href(-full=>1, action=>"blame",
6830 file_name=>$file, hash_base=>$commit),
6831 -title => "blame"}, 'B');
6833 # if this is not a feed of a file history
6834 if (!defined $file_name || $file_name ne $file) {
6835 print $cgi->a({-href => href(-full=>1, action=>"history",
6836 file_name=>$file, hash=>$commit),
6837 -title => "history"}, 'H');
6839 $file = esc_path($file);
6843 if ($format eq 'rss') {
6844 print "</ul>]]>\n" .
6845 "</content:encoded>\n" .
6847 } elsif ($format eq 'atom') {
6848 print "</ul>\n</div>\n" .
6855 if ($format eq 'rss') {
6856 print "</channel>\n</rss>\n";
6857 } elsif ($format eq 'atom') {
6871 my @list = git_get_projects_list();
6874 -type => 'text/xml',
6875 -charset => 'utf-8',
6876 -content_disposition => 'inline; filename="opml.xml"');
6879 <?xml version="1.0" encoding="utf-8"?>
6880 <opml version="1.0">
6882 <title>$site_name OPML Export</title>
6885 <outline text="git RSS feeds">
6888 foreach my $pr (@list) {
6890 my $head = git_get_head_hash($proj{'path'});
6891 if (!defined $head) {
6894 $git_dir = "$projectroot/$proj{'path'}";
6895 my %co = parse_commit($head);
6900 my $path = esc_html(chop_str($proj{'path'}, 25, 5));
6901 my $rss = href('project' => $proj{'path'}, 'action' => 'rss', -full => 1);
6902 my $html = href('project' => $proj{'path'}, 'action' => 'summary', -full => 1);
6903 print "<outline type=\"rss\" text=\"$path\" title=\"$path\" xmlUrl=\"$rss\" htmlUrl=\"$html\"/>\n";