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'};
32 our $version = "++GIT_VERSION++";
33 our $my_url = $cgi->url();
34 our $my_uri = $cgi->url(-absolute => 1);
36 # Base URL for relative URLs in gitweb ($logo, $favicon, ...),
37 # needed and used only for URLs with nonempty PATH_INFO
38 our $base_url = $my_url;
40 # When the script is used as DirectoryIndex, the URL does not contain the name
41 # of the script file itself, and $cgi->url() fails to strip PATH_INFO, so we
42 # have to do it ourselves. We make $path_info global because it's also used
45 # Another issue with the script being the DirectoryIndex is that the resulting
46 # $my_url data is not the full script URL: this is good, because we want
47 # generated links to keep implying the script name if it wasn't explicitly
48 # indicated in the URL we're handling, but it means that $my_url cannot be used
50 # Therefore, if we needed to strip PATH_INFO, then we know that we have
51 # to build the base URL ourselves:
52 our $path_info = $ENV{"PATH_INFO"};
54 if ($my_url =~ s,\Q$path_info\E$,, &&
55 $my_uri =~ s,\Q$path_info\E$,, &&
56 defined $ENV{'SCRIPT_NAME'}) {
57 $base_url = $cgi->url(-base => 1) . $ENV{'SCRIPT_NAME'};
61 # core git executable to use
62 # this can just be "git" if your webserver has a sensible PATH
63 our $GIT = "++GIT_BINDIR++/git";
65 # absolute fs-path which will be prepended to the project path
66 #our $projectroot = "/pub/scm";
67 our $projectroot = "++GITWEB_PROJECTROOT++";
69 # fs traversing limit for getting project list
70 # the number is relative to the projectroot
71 our $project_maxdepth = "++GITWEB_PROJECT_MAXDEPTH++";
73 # target of the home link on top of all pages
74 our $home_link = $my_uri || "/";
76 # string of the home link on top of all pages
77 our $home_link_str = "++GITWEB_HOME_LINK_STR++";
79 # name of your site or organization to appear in page titles
80 # replace this with something more descriptive for clearer bookmarks
81 our $site_name = "++GITWEB_SITENAME++"
82 || ($ENV{'SERVER_NAME'} || "Untitled") . " Git";
84 # filename of html text to include at top of each page
85 our $site_header = "++GITWEB_SITE_HEADER++";
86 # html text to include at home page
87 our $home_text = "++GITWEB_HOMETEXT++";
88 # filename of html text to include at bottom of each page
89 our $site_footer = "++GITWEB_SITE_FOOTER++";
92 our @stylesheets = ("++GITWEB_CSS++");
93 # URI of a single stylesheet, which can be overridden in GITWEB_CONFIG.
94 our $stylesheet = undef;
95 # URI of GIT logo (72x27 size)
96 our $logo = "++GITWEB_LOGO++";
97 # URI of GIT favicon, assumed to be image/png type
98 our $favicon = "++GITWEB_FAVICON++";
99 # URI of gitweb.js (JavaScript code for gitweb)
100 our $javascript = "++GITWEB_JS++";
102 # URI and label (title) of GIT logo link
103 #our $logo_url = "http://www.kernel.org/pub/software/scm/git/docs/";
104 #our $logo_label = "git documentation";
105 our $logo_url = "http://git-scm.com/";
106 our $logo_label = "git homepage";
108 # source of projects list
109 our $projects_list = "++GITWEB_LIST++";
111 # the width (in characters) of the projects list "Description" column
112 our $projects_list_description_width = 25;
114 # default order of projects list
115 # valid values are none, project, descr, owner, and age
116 our $default_projects_order = "project";
118 # show repository only if this file exists
119 # (only effective if this variable evaluates to true)
120 our $export_ok = "++GITWEB_EXPORT_OK++";
122 # show repository only if this subroutine returns true
123 # when given the path to the project, for example:
124 # sub { return -e "$_[0]/git-daemon-export-ok"; }
125 our $export_auth_hook = undef;
127 # only allow viewing of repositories also shown on the overview page
128 our $strict_export = "++GITWEB_STRICT_EXPORT++";
130 # list of git base URLs used for URL to where fetch project from,
131 # i.e. full URL is "$git_base_url/$project"
132 our @git_base_url_list = grep { $_ ne '' } ("++GITWEB_BASE_URL++");
134 # default blob_plain mimetype and default charset for text/plain blob
135 our $default_blob_plain_mimetype = 'text/plain';
136 our $default_text_plain_charset = undef;
138 # file to use for guessing MIME types before trying /etc/mime.types
139 # (relative to the current git repository)
140 our $mimetypes_file = undef;
142 # assume this charset if line contains non-UTF-8 characters;
143 # it should be valid encoding (see Encoding::Supported(3pm) for list),
144 # for which encoding all byte sequences are valid, for example
145 # 'iso-8859-1' aka 'latin1' (it is decoded without checking, so it
146 # could be even 'utf-8' for the old behavior)
147 our $fallback_encoding = 'latin1';
149 # rename detection options for git-diff and git-diff-tree
150 # - default is '-M', with the cost proportional to
151 # (number of removed files) * (number of new files).
152 # - more costly is '-C' (which implies '-M'), with the cost proportional to
153 # (number of changed files + number of removed files) * (number of new files)
154 # - even more costly is '-C', '--find-copies-harder' with cost
155 # (number of files in the original tree) * (number of new files)
156 # - one might want to include '-B' option, e.g. '-B', '-M'
157 our @diff_opts = ('-M'); # taken from git_commit
159 # Disables features that would allow repository owners to inject script into
161 our $prevent_xss = 0;
163 # information about snapshot formats that gitweb is capable of serving
164 our %known_snapshot_formats = (
166 # 'display' => display name,
167 # 'type' => mime type,
168 # 'suffix' => filename suffix,
169 # 'format' => --format for git-archive,
170 # 'compressor' => [compressor command and arguments]
171 # (array reference, optional)
172 # 'disabled' => boolean (optional)}
175 'display' => 'tar.gz',
176 'type' => 'application/x-gzip',
177 'suffix' => '.tar.gz',
179 'compressor' => ['gzip']},
182 'display' => 'tar.bz2',
183 'type' => 'application/x-bzip2',
184 'suffix' => '.tar.bz2',
186 'compressor' => ['bzip2']},
189 'display' => 'tar.xz',
190 'type' => 'application/x-xz',
191 'suffix' => '.tar.xz',
193 'compressor' => ['xz'],
198 'type' => 'application/x-zip',
203 # Aliases so we understand old gitweb.snapshot values in repository
205 our %known_snapshot_format_aliases = (
210 # backward compatibility: legacy gitweb config support
211 'x-gzip' => undef, 'gz' => undef,
212 'x-bzip2' => undef, 'bz2' => undef,
213 'x-zip' => undef, '' => undef,
216 # Pixel sizes for icons and avatars. If the default font sizes or lineheights
217 # are changed, it may be appropriate to change these values too via
224 # You define site-wide feature defaults here; override them with
225 # $GITWEB_CONFIG as necessary.
228 # 'sub' => feature-sub (subroutine),
229 # 'override' => allow-override (boolean),
230 # 'default' => [ default options...] (array reference)}
232 # if feature is overridable (it means that allow-override has true value),
233 # then feature-sub will be called with default options as parameters;
234 # return value of feature-sub indicates if to enable specified feature
236 # if there is no 'sub' key (no feature-sub), then feature cannot be
239 # use gitweb_get_feature(<feature>) to retrieve the <feature> value
240 # (an array) or gitweb_check_feature(<feature>) to check if <feature>
243 # Enable the 'blame' blob view, showing the last commit that modified
244 # each line in the file. This can be very CPU-intensive.
246 # To enable system wide have in $GITWEB_CONFIG
247 # $feature{'blame'}{'default'} = [1];
248 # To have project specific config enable override in $GITWEB_CONFIG
249 # $feature{'blame'}{'override'} = 1;
250 # and in project config gitweb.blame = 0|1;
252 'sub' => sub { feature_bool('blame', @_) },
256 # Enable the 'snapshot' link, providing a compressed archive of any
257 # tree. This can potentially generate high traffic if you have large
260 # Value is a list of formats defined in %known_snapshot_formats that
262 # To disable system wide have in $GITWEB_CONFIG
263 # $feature{'snapshot'}{'default'} = [];
264 # To have project specific config enable override in $GITWEB_CONFIG
265 # $feature{'snapshot'}{'override'} = 1;
266 # and in project config, a comma-separated list of formats or "none"
267 # to disable. Example: gitweb.snapshot = tbz2,zip;
269 'sub' => \&feature_snapshot,
271 'default' => ['tgz']},
273 # Enable text search, which will list the commits which match author,
274 # committer or commit text to a given string. Enabled by default.
275 # Project specific override is not supported.
280 # Enable grep search, which will list the files in currently selected
281 # tree containing the given string. Enabled by default. This can be
282 # potentially CPU-intensive, of course.
284 # To enable system wide have in $GITWEB_CONFIG
285 # $feature{'grep'}{'default'} = [1];
286 # To have project specific config enable override in $GITWEB_CONFIG
287 # $feature{'grep'}{'override'} = 1;
288 # and in project config gitweb.grep = 0|1;
290 'sub' => sub { feature_bool('grep', @_) },
294 # Enable the pickaxe search, which will list the commits that modified
295 # a given string in a file. This can be practical and quite faster
296 # alternative to 'blame', but still potentially CPU-intensive.
298 # To enable system wide have in $GITWEB_CONFIG
299 # $feature{'pickaxe'}{'default'} = [1];
300 # To have project specific config enable override in $GITWEB_CONFIG
301 # $feature{'pickaxe'}{'override'} = 1;
302 # and in project config gitweb.pickaxe = 0|1;
304 'sub' => sub { feature_bool('pickaxe', @_) },
308 # Enable showing size of blobs in a 'tree' view, in a separate
309 # column, similar to what 'ls -l' does. This cost a bit of IO.
311 # To disable system wide have in $GITWEB_CONFIG
312 # $feature{'show-sizes'}{'default'} = [0];
313 # To have project specific config enable override in $GITWEB_CONFIG
314 # $feature{'show-sizes'}{'override'} = 1;
315 # and in project config gitweb.showsizes = 0|1;
317 'sub' => sub { feature_bool('showsizes', @_) },
321 # Make gitweb use an alternative format of the URLs which can be
322 # more readable and natural-looking: project name is embedded
323 # directly in the path and the query string contains other
324 # auxiliary information. All gitweb installations recognize
325 # URL in either format; this configures in which formats gitweb
328 # To enable system wide have in $GITWEB_CONFIG
329 # $feature{'pathinfo'}{'default'} = [1];
330 # Project specific override is not supported.
332 # Note that you will need to change the default location of CSS,
333 # favicon, logo and possibly other files to an absolute URL. Also,
334 # if gitweb.cgi serves as your indexfile, you will need to force
335 # $my_uri to contain the script name in your $GITWEB_CONFIG.
340 # Make gitweb consider projects in project root subdirectories
341 # to be forks of existing projects. Given project $projname.git,
342 # projects matching $projname/*.git will not be shown in the main
343 # projects list, instead a '+' mark will be added to $projname
344 # there and a 'forks' view will be enabled for the project, listing
345 # all the forks. If project list is taken from a file, forks have
346 # to be listed after the main project.
348 # To enable system wide have in $GITWEB_CONFIG
349 # $feature{'forks'}{'default'} = [1];
350 # Project specific override is not supported.
355 # Insert custom links to the action bar of all project pages.
356 # This enables you mainly to link to third-party scripts integrating
357 # into gitweb; e.g. git-browser for graphical history representation
358 # or custom web-based repository administration interface.
360 # The 'default' value consists of a list of triplets in the form
361 # (label, link, position) where position is the label after which
362 # to insert the link and link is a format string where %n expands
363 # to the project name, %f to the project path within the filesystem,
364 # %h to the current hash (h gitweb parameter) and %b to the current
365 # hash base (hb gitweb parameter); %% expands to %.
367 # To enable system wide have in $GITWEB_CONFIG e.g.
368 # $feature{'actions'}{'default'} = [('graphiclog',
369 # '/git-browser/by-commit.html?r=%n', 'summary')];
370 # Project specific override is not supported.
375 # Allow gitweb scan project content tags described in ctags/
376 # of project repository, and display the popular Web 2.0-ish
377 # "tag cloud" near the project list. Note that this is something
378 # COMPLETELY different from the normal Git tags.
380 # gitweb by itself can show existing tags, but it does not handle
381 # tagging itself; you need an external application for that.
382 # For an example script, check Girocco's cgi/tagproj.cgi.
383 # You may want to install the HTML::TagCloud Perl module to get
384 # a pretty tag cloud instead of just a list of tags.
386 # To enable system wide have in $GITWEB_CONFIG
387 # $feature{'ctags'}{'default'} = ['path_to_tag_script'];
388 # Project specific override is not supported.
393 # The maximum number of patches in a patchset generated in patch
394 # view. Set this to 0 or undef to disable patch view, or to a
395 # negative number to remove any limit.
397 # To disable system wide have in $GITWEB_CONFIG
398 # $feature{'patches'}{'default'} = [0];
399 # To have project specific config enable override in $GITWEB_CONFIG
400 # $feature{'patches'}{'override'} = 1;
401 # and in project config gitweb.patches = 0|n;
402 # where n is the maximum number of patches allowed in a patchset.
404 'sub' => \&feature_patches,
408 # Avatar support. When this feature is enabled, views such as
409 # shortlog or commit will display an avatar associated with
410 # the email of the committer(s) and/or author(s).
412 # Currently available providers are gravatar and picon.
413 # If an unknown provider is specified, the feature is disabled.
415 # Gravatar depends on Digest::MD5.
416 # Picon currently relies on the indiana.edu database.
418 # To enable system wide have in $GITWEB_CONFIG
419 # $feature{'avatar'}{'default'} = ['<provider>'];
420 # where <provider> is either gravatar or picon.
421 # To have project specific config enable override in $GITWEB_CONFIG
422 # $feature{'avatar'}{'override'} = 1;
423 # and in project config gitweb.avatar = <provider>;
425 'sub' => \&feature_avatar,
429 # Enable displaying how much time and how many git commands
430 # it took to generate and display page. Disabled by default.
431 # Project specific override is not supported.
437 sub gitweb_get_feature {
439 return unless exists $feature{$name};
440 my ($sub, $override, @defaults) = (
441 $feature{$name}{'sub'},
442 $feature{$name}{'override'},
443 @{$feature{$name}{'default'}});
444 if (!$override) { return @defaults; }
446 warn "feature $name is not overridable";
449 return $sub->(@defaults);
452 # A wrapper to check if a given feature is enabled.
453 # With this, you can say
455 # my $bool_feat = gitweb_check_feature('bool_feat');
456 # gitweb_check_feature('bool_feat') or somecode;
460 # my ($bool_feat) = gitweb_get_feature('bool_feat');
461 # (gitweb_get_feature('bool_feat'))[0] or somecode;
463 sub gitweb_check_feature {
464 return (gitweb_get_feature(@_))[0];
470 my ($val) = git_get_project_config($key, '--bool');
474 } elsif ($val eq 'true') {
476 } elsif ($val eq 'false') {
481 sub feature_snapshot {
484 my ($val) = git_get_project_config('snapshot');
487 @fmts = ($val eq 'none' ? () : split /\s*[,\s]\s*/, $val);
493 sub feature_patches {
494 my @val = (git_get_project_config('patches', '--int'));
504 my @val = (git_get_project_config('avatar'));
506 return @val ? @val : @_;
509 # checking HEAD file with -e is fragile if the repository was
510 # initialized long time ago (i.e. symlink HEAD) and was pack-ref'ed
512 sub check_head_link {
514 my $headfile = "$dir/HEAD";
515 return ((-e $headfile) ||
516 (-l $headfile && readlink($headfile) =~ /^refs\/heads\//));
519 sub check_export_ok {
521 return (check_head_link($dir) &&
522 (!$export_ok || -e "$dir/$export_ok") &&
523 (!$export_auth_hook || $export_auth_hook->($dir)));
526 # process alternate names for backward compatibility
527 # filter out unsupported (unknown) snapshot formats
528 sub filter_snapshot_fmts {
532 exists $known_snapshot_format_aliases{$_} ?
533 $known_snapshot_format_aliases{$_} : $_} @fmts;
535 exists $known_snapshot_formats{$_} &&
536 !$known_snapshot_formats{$_}{'disabled'}} @fmts;
539 our $GITWEB_CONFIG = $ENV{'GITWEB_CONFIG'} || "++GITWEB_CONFIG++";
540 if (-e $GITWEB_CONFIG) {
543 our $GITWEB_CONFIG_SYSTEM = $ENV{'GITWEB_CONFIG_SYSTEM'} || "++GITWEB_CONFIG_SYSTEM++";
544 do $GITWEB_CONFIG_SYSTEM if -e $GITWEB_CONFIG_SYSTEM;
547 # version of the core git binary
548 our $git_version = qx("$GIT" --version) =~ m/git version (.*)$/ ? $1 : "unknown";
549 $number_of_git_cmds++;
551 $projects_list ||= $projectroot;
553 # ======================================================================
554 # input validation and dispatch
556 # input parameters can be collected from a variety of sources (presently, CGI
557 # and PATH_INFO), so we define an %input_params hash that collects them all
558 # together during validation: this allows subsequent uses (e.g. href()) to be
559 # agnostic of the parameter origin
561 our %input_params = ();
563 # input parameters are stored with the long parameter name as key. This will
564 # also be used in the href subroutine to convert parameters to their CGI
565 # equivalent, and since the href() usage is the most frequent one, we store
566 # the name -> CGI key mapping here, instead of the reverse.
568 # XXX: Warning: If you touch this, check the search form for updating,
571 our @cgi_param_mapping = (
579 hash_parent_base => "hpb",
584 snapshot_format => "sf",
585 extra_options => "opt",
586 search_use_regexp => "sr",
587 # this must be last entry (for manipulation from JavaScript)
590 our %cgi_param_mapping = @cgi_param_mapping;
592 # we will also need to know the possible actions, for validation
594 "blame" => \&git_blame,
595 "blame_incremental" => \&git_blame_incremental,
596 "blame_data" => \&git_blame_data,
597 "blobdiff" => \&git_blobdiff,
598 "blobdiff_plain" => \&git_blobdiff_plain,
599 "blob" => \&git_blob,
600 "blob_plain" => \&git_blob_plain,
601 "commitdiff" => \&git_commitdiff,
602 "commitdiff_plain" => \&git_commitdiff_plain,
603 "commit" => \&git_commit,
604 "forks" => \&git_forks,
605 "heads" => \&git_heads,
606 "history" => \&git_history,
608 "patch" => \&git_patch,
609 "patches" => \&git_patches,
611 "atom" => \&git_atom,
612 "search" => \&git_search,
613 "search_help" => \&git_search_help,
614 "shortlog" => \&git_shortlog,
615 "summary" => \&git_summary,
617 "tags" => \&git_tags,
618 "tree" => \&git_tree,
619 "snapshot" => \&git_snapshot,
620 "object" => \&git_object,
621 # those below don't need $project
622 "opml" => \&git_opml,
623 "project_list" => \&git_project_list,
624 "project_index" => \&git_project_index,
627 # finally, we have the hash of allowed extra_options for the commands that
629 our %allowed_options = (
630 "--no-merges" => [ qw(rss atom log shortlog history) ],
633 # fill %input_params with the CGI parameters. All values except for 'opt'
634 # should be single values, but opt can be an array. We should probably
635 # build an array of parameters that can be multi-valued, but since for the time
636 # being it's only this one, we just single it out
637 while (my ($name, $symbol) = each %cgi_param_mapping) {
638 if ($symbol eq 'opt') {
639 $input_params{$name} = [ $cgi->param($symbol) ];
641 $input_params{$name} = $cgi->param($symbol);
645 # now read PATH_INFO and update the parameter list for missing parameters
646 sub evaluate_path_info {
647 return if defined $input_params{'project'};
648 return if !$path_info;
649 $path_info =~ s,^/+,,;
650 return if !$path_info;
652 # find which part of PATH_INFO is project
653 my $project = $path_info;
655 while ($project && !check_head_link("$projectroot/$project")) {
656 $project =~ s,/*[^/]*$,,;
658 return unless $project;
659 $input_params{'project'} = $project;
661 # do not change any parameters if an action is given using the query string
662 return if $input_params{'action'};
663 $path_info =~ s,^\Q$project\E/*,,;
665 # next, check if we have an action
666 my $action = $path_info;
668 if (exists $actions{$action}) {
669 $path_info =~ s,^$action/*,,;
670 $input_params{'action'} = $action;
673 # list of actions that want hash_base instead of hash, but can have no
674 # pathname (f) parameter
681 # [$hash_parent_base[:$file_parent]..]$hash_parent[:$file_name]
682 my ($parentrefname, $parentpathname, $refname, $pathname) =
683 ($path_info =~ /^(?:(.+?)(?::(.+))?\.\.)?(.+?)(?::(.+))?$/);
685 # first, analyze the 'current' part
686 if (defined $pathname) {
687 # we got "branch:filename" or "branch:dir/"
688 # we could use git_get_type(branch:pathname), but:
689 # - it needs $git_dir
690 # - it does a git() call
691 # - the convention of terminating directories with a slash
692 # makes it superfluous
693 # - embedding the action in the PATH_INFO would make it even
695 $pathname =~ s,^/+,,;
696 if (!$pathname || substr($pathname, -1) eq "/") {
697 $input_params{'action'} ||= "tree";
700 # the default action depends on whether we had parent info
702 if ($parentrefname) {
703 $input_params{'action'} ||= "blobdiff_plain";
705 $input_params{'action'} ||= "blob_plain";
708 $input_params{'hash_base'} ||= $refname;
709 $input_params{'file_name'} ||= $pathname;
710 } elsif (defined $refname) {
711 # we got "branch". In this case we have to choose if we have to
712 # set hash or hash_base.
714 # Most of the actions without a pathname only want hash to be
715 # set, except for the ones specified in @wants_base that want
716 # hash_base instead. It should also be noted that hand-crafted
717 # links having 'history' as an action and no pathname or hash
718 # set will fail, but that happens regardless of PATH_INFO.
719 $input_params{'action'} ||= "shortlog";
720 if (grep { $_ eq $input_params{'action'} } @wants_base) {
721 $input_params{'hash_base'} ||= $refname;
723 $input_params{'hash'} ||= $refname;
727 # next, handle the 'parent' part, if present
728 if (defined $parentrefname) {
729 # a missing pathspec defaults to the 'current' filename, allowing e.g.
730 # someproject/blobdiff/oldrev..newrev:/filename
731 if ($parentpathname) {
732 $parentpathname =~ s,^/+,,;
733 $parentpathname =~ s,/$,,;
734 $input_params{'file_parent'} ||= $parentpathname;
736 $input_params{'file_parent'} ||= $input_params{'file_name'};
738 # we assume that hash_parent_base is wanted if a path was specified,
739 # or if the action wants hash_base instead of hash
740 if (defined $input_params{'file_parent'} ||
741 grep { $_ eq $input_params{'action'} } @wants_base) {
742 $input_params{'hash_parent_base'} ||= $parentrefname;
744 $input_params{'hash_parent'} ||= $parentrefname;
748 # for the snapshot action, we allow URLs in the form
749 # $project/snapshot/$hash.ext
750 # where .ext determines the snapshot and gets removed from the
751 # passed $refname to provide the $hash.
753 # To be able to tell that $refname includes the format extension, we
754 # require the following two conditions to be satisfied:
755 # - the hash input parameter MUST have been set from the $refname part
756 # of the URL (i.e. they must be equal)
757 # - the snapshot format MUST NOT have been defined already (e.g. from
759 # It's also useless to try any matching unless $refname has a dot,
760 # so we check for that too
761 if (defined $input_params{'action'} &&
762 $input_params{'action'} eq 'snapshot' &&
763 defined $refname && index($refname, '.') != -1 &&
764 $refname eq $input_params{'hash'} &&
765 !defined $input_params{'snapshot_format'}) {
766 # We loop over the known snapshot formats, checking for
767 # extensions. Allowed extensions are both the defined suffix
768 # (which includes the initial dot already) and the snapshot
769 # format key itself, with a prepended dot
770 while (my ($fmt, $opt) = each %known_snapshot_formats) {
772 unless ($hash =~ s/(\Q$opt->{'suffix'}\E|\Q.$fmt\E)$//) {
776 # a valid suffix was found, so set the snapshot format
777 # and reset the hash parameter
778 $input_params{'snapshot_format'} = $fmt;
779 $input_params{'hash'} = $hash;
780 # we also set the format suffix to the one requested
781 # in the URL: this way a request for e.g. .tgz returns
782 # a .tgz instead of a .tar.gz
783 $known_snapshot_formats{$fmt}{'suffix'} = $sfx;
788 evaluate_path_info();
790 our $action = $input_params{'action'};
791 if (defined $action) {
792 if (!validate_action($action)) {
793 die_error(400, "Invalid action parameter");
797 # parameters which are pathnames
798 our $project = $input_params{'project'};
799 if (defined $project) {
800 if (!validate_project($project)) {
802 die_error(404, "No such project");
806 our $file_name = $input_params{'file_name'};
807 if (defined $file_name) {
808 if (!validate_pathname($file_name)) {
809 die_error(400, "Invalid file parameter");
813 our $file_parent = $input_params{'file_parent'};
814 if (defined $file_parent) {
815 if (!validate_pathname($file_parent)) {
816 die_error(400, "Invalid file parent parameter");
820 # parameters which are refnames
821 our $hash = $input_params{'hash'};
823 if (!validate_refname($hash)) {
824 die_error(400, "Invalid hash parameter");
828 our $hash_parent = $input_params{'hash_parent'};
829 if (defined $hash_parent) {
830 if (!validate_refname($hash_parent)) {
831 die_error(400, "Invalid hash parent parameter");
835 our $hash_base = $input_params{'hash_base'};
836 if (defined $hash_base) {
837 if (!validate_refname($hash_base)) {
838 die_error(400, "Invalid hash base parameter");
842 our @extra_options = @{$input_params{'extra_options'}};
843 # @extra_options is always defined, since it can only be (currently) set from
844 # CGI, and $cgi->param() returns the empty array in array context if the param
846 foreach my $opt (@extra_options) {
847 if (not exists $allowed_options{$opt}) {
848 die_error(400, "Invalid option parameter");
850 if (not grep(/^$action$/, @{$allowed_options{$opt}})) {
851 die_error(400, "Invalid option parameter for this action");
855 our $hash_parent_base = $input_params{'hash_parent_base'};
856 if (defined $hash_parent_base) {
857 if (!validate_refname($hash_parent_base)) {
858 die_error(400, "Invalid hash parent base parameter");
863 our $page = $input_params{'page'};
865 if ($page =~ m/[^0-9]/) {
866 die_error(400, "Invalid page parameter");
870 our $searchtype = $input_params{'searchtype'};
871 if (defined $searchtype) {
872 if ($searchtype =~ m/[^a-z]/) {
873 die_error(400, "Invalid searchtype parameter");
877 our $search_use_regexp = $input_params{'search_use_regexp'};
879 our $searchtext = $input_params{'searchtext'};
881 if (defined $searchtext) {
882 if (length($searchtext) < 2) {
883 die_error(403, "At least two characters are required for search parameter");
885 $search_regexp = $search_use_regexp ? $searchtext : quotemeta $searchtext;
888 # path to the current git repository
890 $git_dir = "$projectroot/$project" if $project;
892 # list of supported snapshot formats
893 our @snapshot_fmts = gitweb_get_feature('snapshot');
894 @snapshot_fmts = filter_snapshot_fmts(@snapshot_fmts);
896 # check that the avatar feature is set to a known provider name,
897 # and for each provider check if the dependencies are satisfied.
898 # if the provider name is invalid or the dependencies are not met,
899 # reset $git_avatar to the empty string.
900 our ($git_avatar) = gitweb_get_feature('avatar');
901 if ($git_avatar eq 'gravatar') {
902 $git_avatar = '' unless (eval { require Digest::MD5; 1; });
903 } elsif ($git_avatar eq 'picon') {
910 if (!defined $action) {
912 $action = git_get_type($hash);
913 } elsif (defined $hash_base && defined $file_name) {
914 $action = git_get_type("$hash_base:$file_name");
915 } elsif (defined $project) {
918 $action = 'project_list';
921 if (!defined($actions{$action})) {
922 die_error(400, "Unknown action");
924 if ($action !~ m/^(?:opml|project_list|project_index)$/ &&
926 die_error(400, "Project needed");
928 $actions{$action}->();
931 ## ======================================================================
936 # default is to use -absolute url() i.e. $my_uri
937 my $href = $params{-full} ? $my_url : $my_uri;
939 $params{'project'} = $project unless exists $params{'project'};
941 if ($params{-replay}) {
942 while (my ($name, $symbol) = each %cgi_param_mapping) {
943 if (!exists $params{$name}) {
944 $params{$name} = $input_params{$name};
949 my $use_pathinfo = gitweb_check_feature('pathinfo');
950 if ($use_pathinfo and defined $params{'project'}) {
951 # try to put as many parameters as possible in PATH_INFO:
954 # - hash_parent or hash_parent_base:/file_parent
955 # - hash or hash_base:/filename
956 # - the snapshot_format as an appropriate suffix
958 # When the script is the root DirectoryIndex for the domain,
959 # $href here would be something like http://gitweb.example.com/
960 # Thus, we strip any trailing / from $href, to spare us double
961 # slashes in the final URL
964 # Then add the project name, if present
965 $href .= "/".esc_url($params{'project'});
966 delete $params{'project'};
968 # since we destructively absorb parameters, we keep this
969 # boolean that remembers if we're handling a snapshot
970 my $is_snapshot = $params{'action'} eq 'snapshot';
972 # Summary just uses the project path URL, any other action is
974 if (defined $params{'action'}) {
975 $href .= "/".esc_url($params{'action'}) unless $params{'action'} eq 'summary';
976 delete $params{'action'};
979 # Next, we put hash_parent_base:/file_parent..hash_base:/file_name,
980 # stripping nonexistent or useless pieces
981 $href .= "/" if ($params{'hash_base'} || $params{'hash_parent_base'}
982 || $params{'hash_parent'} || $params{'hash'});
983 if (defined $params{'hash_base'}) {
984 if (defined $params{'hash_parent_base'}) {
985 $href .= esc_url($params{'hash_parent_base'});
986 # skip the file_parent if it's the same as the file_name
987 if (defined $params{'file_parent'}) {
988 if (defined $params{'file_name'} && $params{'file_parent'} eq $params{'file_name'}) {
989 delete $params{'file_parent'};
990 } elsif ($params{'file_parent'} !~ /\.\./) {
991 $href .= ":/".esc_url($params{'file_parent'});
992 delete $params{'file_parent'};
996 delete $params{'hash_parent'};
997 delete $params{'hash_parent_base'};
998 } elsif (defined $params{'hash_parent'}) {
999 $href .= esc_url($params{'hash_parent'}). "..";
1000 delete $params{'hash_parent'};
1003 $href .= esc_url($params{'hash_base'});
1004 if (defined $params{'file_name'} && $params{'file_name'} !~ /\.\./) {
1005 $href .= ":/".esc_url($params{'file_name'});
1006 delete $params{'file_name'};
1008 delete $params{'hash'};
1009 delete $params{'hash_base'};
1010 } elsif (defined $params{'hash'}) {
1011 $href .= esc_url($params{'hash'});
1012 delete $params{'hash'};
1015 # If the action was a snapshot, we can absorb the
1016 # snapshot_format parameter too
1018 my $fmt = $params{'snapshot_format'};
1019 # snapshot_format should always be defined when href()
1020 # is called, but just in case some code forgets, we
1021 # fall back to the default
1022 $fmt ||= $snapshot_fmts[0];
1023 $href .= $known_snapshot_formats{$fmt}{'suffix'};
1024 delete $params{'snapshot_format'};
1028 # now encode the parameters explicitly
1030 for (my $i = 0; $i < @cgi_param_mapping; $i += 2) {
1031 my ($name, $symbol) = ($cgi_param_mapping[$i], $cgi_param_mapping[$i+1]);
1032 if (defined $params{$name}) {
1033 if (ref($params{$name}) eq "ARRAY") {
1034 foreach my $par (@{$params{$name}}) {
1035 push @result, $symbol . "=" . esc_param($par);
1038 push @result, $symbol . "=" . esc_param($params{$name});
1042 $href .= "?" . join(';', @result) if scalar @result;
1048 ## ======================================================================
1049 ## validation, quoting/unquoting and escaping
1051 sub validate_action {
1052 my $input = shift || return undef;
1053 return undef unless exists $actions{$input};
1057 sub validate_project {
1058 my $input = shift || return undef;
1059 if (!validate_pathname($input) ||
1060 !(-d "$projectroot/$input") ||
1061 !check_export_ok("$projectroot/$input") ||
1062 ($strict_export && !project_in_list($input))) {
1069 sub validate_pathname {
1070 my $input = shift || return undef;
1072 # no '.' or '..' as elements of path, i.e. no '.' nor '..'
1073 # at the beginning, at the end, and between slashes.
1074 # also this catches doubled slashes
1075 if ($input =~ m!(^|/)(|\.|\.\.)(/|$)!) {
1078 # no null characters
1079 if ($input =~ m!\0!) {
1085 sub validate_refname {
1086 my $input = shift || return undef;
1088 # textual hashes are O.K.
1089 if ($input =~ m/^[0-9a-fA-F]{40}$/) {
1092 # it must be correct pathname
1093 $input = validate_pathname($input)
1095 # restrictions on ref name according to git-check-ref-format
1096 if ($input =~ m!(/\.|\.\.|[\000-\040\177 ~^:?*\[]|/$)!) {
1102 # decode sequences of octets in utf8 into Perl's internal form,
1103 # which is utf-8 with utf8 flag set if needed. gitweb writes out
1104 # in utf-8 thanks to "binmode STDOUT, ':utf8'" at beginning
1107 if (utf8::valid($str)) {
1111 return decode($fallback_encoding, $str, Encode::FB_DEFAULT);
1115 # quote unsafe chars, but keep the slash, even when it's not
1116 # correct, but quoted slashes look too horrible in bookmarks
1119 $str =~ s/([^A-Za-z0-9\-_.~()\/:@])/sprintf("%%%02X", ord($1))/eg;
1125 # quote unsafe chars in whole URL, so some charactrs cannot be quoted
1128 $str =~ s/([^A-Za-z0-9\-_.~();\/;?:@&=])/sprintf("%%%02X", ord($1))/eg;
1134 # replace invalid utf8 character with SUBSTITUTION sequence
1139 $str = to_utf8($str);
1140 $str = $cgi->escapeHTML($str);
1141 if ($opts{'-nbsp'}) {
1142 $str =~ s/ / /g;
1144 $str =~ s|([[:cntrl:]])|(($1 ne "\t") ? quot_cec($1) : $1)|eg;
1148 # quote control characters and escape filename to HTML
1153 $str = to_utf8($str);
1154 $str = $cgi->escapeHTML($str);
1155 if ($opts{'-nbsp'}) {
1156 $str =~ s/ / /g;
1158 $str =~ s|([[:cntrl:]])|quot_cec($1)|eg;
1162 # Make control characters "printable", using character escape codes (CEC)
1166 my %es = ( # character escape codes, aka escape sequences
1167 "\t" => '\t', # tab (HT)
1168 "\n" => '\n', # line feed (LF)
1169 "\r" => '\r', # carrige return (CR)
1170 "\f" => '\f', # form feed (FF)
1171 "\b" => '\b', # backspace (BS)
1172 "\a" => '\a', # alarm (bell) (BEL)
1173 "\e" => '\e', # escape (ESC)
1174 "\013" => '\v', # vertical tab (VT)
1175 "\000" => '\0', # nul character (NUL)
1177 my $chr = ( (exists $es{$cntrl})
1179 : sprintf('\%2x', ord($cntrl)) );
1180 if ($opts{-nohtml}) {
1183 return "<span class=\"cntrl\">$chr</span>";
1187 # Alternatively use unicode control pictures codepoints,
1188 # Unicode "printable representation" (PR)
1193 my $chr = sprintf('&#%04d;', 0x2400+ord($cntrl));
1194 if ($opts{-nohtml}) {
1197 return "<span class=\"cntrl\">$chr</span>";
1201 # git may return quoted and escaped filenames
1207 my %es = ( # character escape codes, aka escape sequences
1208 't' => "\t", # tab (HT, TAB)
1209 'n' => "\n", # newline (NL)
1210 'r' => "\r", # return (CR)
1211 'f' => "\f", # form feed (FF)
1212 'b' => "\b", # backspace (BS)
1213 'a' => "\a", # alarm (bell) (BEL)
1214 'e' => "\e", # escape (ESC)
1215 'v' => "\013", # vertical tab (VT)
1218 if ($seq =~ m/^[0-7]{1,3}$/) {
1219 # octal char sequence
1220 return chr(oct($seq));
1221 } elsif (exists $es{$seq}) {
1222 # C escape sequence, aka character escape code
1225 # quoted ordinary character
1229 if ($str =~ m/^"(.*)"$/) {
1232 $str =~ s/\\([^0-7]|[0-7]{1,3})/unq($1)/eg;
1237 # escape tabs (convert tabs to spaces)
1241 while ((my $pos = index($line, "\t")) != -1) {
1242 if (my $count = (8 - ($pos % 8))) {
1243 my $spaces = ' ' x $count;
1244 $line =~ s/\t/$spaces/;
1251 sub project_in_list {
1252 my $project = shift;
1253 my @list = git_get_projects_list();
1254 return @list && scalar(grep { $_->{'path'} eq $project } @list);
1257 ## ----------------------------------------------------------------------
1258 ## HTML aware string manipulation
1260 # Try to chop given string on a word boundary between position
1261 # $len and $len+$add_len. If there is no word boundary there,
1262 # chop at $len+$add_len. Do not chop if chopped part plus ellipsis
1263 # (marking chopped part) would be longer than given string.
1267 my $add_len = shift || 10;
1268 my $where = shift || 'right'; # 'left' | 'center' | 'right'
1270 # Make sure perl knows it is utf8 encoded so we don't
1271 # cut in the middle of a utf8 multibyte char.
1272 $str = to_utf8($str);
1274 # allow only $len chars, but don't cut a word if it would fit in $add_len
1275 # if it doesn't fit, cut it if it's still longer than the dots we would add
1276 # remove chopped character entities entirely
1278 # when chopping in the middle, distribute $len into left and right part
1279 # return early if chopping wouldn't make string shorter
1280 if ($where eq 'center') {
1281 return $str if ($len + 5 >= length($str)); # filler is length 5
1284 return $str if ($len + 4 >= length($str)); # filler is length 4
1287 # regexps: ending and beginning with word part up to $add_len
1288 my $endre = qr/.{$len}\w{0,$add_len}/;
1289 my $begre = qr/\w{0,$add_len}.{$len}/;
1291 if ($where eq 'left') {
1292 $str =~ m/^(.*?)($begre)$/;
1293 my ($lead, $body) = ($1, $2);
1294 if (length($lead) > 4) {
1295 $body =~ s/^[^;]*;// if ($lead =~ m/&[^;]*$/);
1298 return "$lead$body";
1300 } elsif ($where eq 'center') {
1301 $str =~ m/^($endre)(.*)$/;
1302 my ($left, $str) = ($1, $2);
1303 $str =~ m/^(.*?)($begre)$/;
1304 my ($mid, $right) = ($1, $2);
1305 if (length($mid) > 5) {
1306 $left =~ s/&[^;]*$//;
1307 $right =~ s/^[^;]*;// if ($mid =~ m/&[^;]*$/);
1310 return "$left$mid$right";
1313 $str =~ m/^($endre)(.*)$/;
1316 if (length($tail) > 4) {
1317 $body =~ s/&[^;]*$//;
1320 return "$body$tail";
1324 # takes the same arguments as chop_str, but also wraps a <span> around the
1325 # result with a title attribute if it does get chopped. Additionally, the
1326 # string is HTML-escaped.
1327 sub chop_and_escape_str {
1330 my $chopped = chop_str(@_);
1331 if ($chopped eq $str) {
1332 return esc_html($chopped);
1334 $str =~ s/[[:cntrl:]]/?/g;
1335 return $cgi->span({-title=>$str}, esc_html($chopped));
1339 ## ----------------------------------------------------------------------
1340 ## functions returning short strings
1342 # CSS class for given age value (in seconds)
1346 if (!defined $age) {
1348 } elsif ($age < 60*60*2) {
1350 } elsif ($age < 60*60*24*2) {
1357 # convert age in seconds to "nn units ago" string
1362 if ($age > 60*60*24*365*2) {
1363 $age_str = (int $age/60/60/24/365);
1364 $age_str .= " years ago";
1365 } elsif ($age > 60*60*24*(365/12)*2) {
1366 $age_str = int $age/60/60/24/(365/12);
1367 $age_str .= " months ago";
1368 } elsif ($age > 60*60*24*7*2) {
1369 $age_str = int $age/60/60/24/7;
1370 $age_str .= " weeks ago";
1371 } elsif ($age > 60*60*24*2) {
1372 $age_str = int $age/60/60/24;
1373 $age_str .= " days ago";
1374 } elsif ($age > 60*60*2) {
1375 $age_str = int $age/60/60;
1376 $age_str .= " hours ago";
1377 } elsif ($age > 60*2) {
1378 $age_str = int $age/60;
1379 $age_str .= " min ago";
1380 } elsif ($age > 2) {
1381 $age_str = int $age;
1382 $age_str .= " sec ago";
1384 $age_str .= " right now";
1390 S_IFINVALID => 0030000,
1391 S_IFGITLINK => 0160000,
1394 # submodule/subproject, a commit object reference
1398 return (($mode & S_IFMT) == S_IFGITLINK)
1401 # convert file mode in octal to symbolic file mode string
1403 my $mode = oct shift;
1405 if (S_ISGITLINK($mode)) {
1406 return 'm---------';
1407 } elsif (S_ISDIR($mode & S_IFMT)) {
1408 return 'drwxr-xr-x';
1409 } elsif (S_ISLNK($mode)) {
1410 return 'lrwxrwxrwx';
1411 } elsif (S_ISREG($mode)) {
1412 # git cares only about the executable bit
1413 if ($mode & S_IXUSR) {
1414 return '-rwxr-xr-x';
1416 return '-rw-r--r--';
1419 return '----------';
1423 # convert file mode in octal to file type string
1427 if ($mode !~ m/^[0-7]+$/) {
1433 if (S_ISGITLINK($mode)) {
1435 } elsif (S_ISDIR($mode & S_IFMT)) {
1437 } elsif (S_ISLNK($mode)) {
1439 } elsif (S_ISREG($mode)) {
1446 # convert file mode in octal to file type description string
1447 sub file_type_long {
1450 if ($mode !~ m/^[0-7]+$/) {
1456 if (S_ISGITLINK($mode)) {
1458 } elsif (S_ISDIR($mode & S_IFMT)) {
1460 } elsif (S_ISLNK($mode)) {
1462 } elsif (S_ISREG($mode)) {
1463 if ($mode & S_IXUSR) {
1464 return "executable";
1474 ## ----------------------------------------------------------------------
1475 ## functions returning short HTML fragments, or transforming HTML fragments
1476 ## which don't belong to other sections
1478 # format line of commit message.
1479 sub format_log_line_html {
1482 $line = esc_html($line, -nbsp=>1);
1483 $line =~ s{\b([0-9a-fA-F]{8,40})\b}{
1484 $cgi->a({-href => href(action=>"object", hash=>$1),
1485 -class => "text"}, $1);
1491 # format marker of refs pointing to given object
1493 # the destination action is chosen based on object type and current context:
1494 # - for annotated tags, we choose the tag view unless it's the current view
1495 # already, in which case we go to shortlog view
1496 # - for other refs, we keep the current view if we're in history, shortlog or
1497 # log view, and select shortlog otherwise
1498 sub format_ref_marker {
1499 my ($refs, $id) = @_;
1502 if (defined $refs->{$id}) {
1503 foreach my $ref (@{$refs->{$id}}) {
1504 # this code exploits the fact that non-lightweight tags are the
1505 # only indirect objects, and that they are the only objects for which
1506 # we want to use tag instead of shortlog as action
1507 my ($type, $name) = qw();
1508 my $indirect = ($ref =~ s/\^\{\}$//);
1509 # e.g. tags/v2.6.11 or heads/next
1510 if ($ref =~ m!^(.*?)s?/(.*)$!) {
1519 $class .= " indirect" if $indirect;
1521 my $dest_action = "shortlog";
1524 $dest_action = "tag" unless $action eq "tag";
1525 } elsif ($action =~ /^(history|(short)?log)$/) {
1526 $dest_action = $action;
1530 $dest .= "refs/" unless $ref =~ m!^refs/!;
1533 my $link = $cgi->a({
1535 action=>$dest_action,
1539 $markers .= " <span class=\"$class\" title=\"$ref\">" .
1545 return ' <span class="refs">'. $markers . '</span>';
1551 # format, perhaps shortened and with markers, title line
1552 sub format_subject_html {
1553 my ($long, $short, $href, $extra) = @_;
1554 $extra = '' unless defined($extra);
1556 if (length($short) < length($long)) {
1557 $long =~ s/[[:cntrl:]]/?/g;
1558 return $cgi->a({-href => $href, -class => "list subject",
1559 -title => to_utf8($long)},
1560 esc_html($short)) . $extra;
1562 return $cgi->a({-href => $href, -class => "list subject"},
1563 esc_html($long)) . $extra;
1567 # Rather than recomputing the url for an email multiple times, we cache it
1568 # after the first hit. This gives a visible benefit in views where the avatar
1569 # for the same email is used repeatedly (e.g. shortlog).
1570 # The cache is shared by all avatar engines (currently gravatar only), which
1571 # are free to use it as preferred. Since only one avatar engine is used for any
1572 # given page, there's no risk for cache conflicts.
1573 our %avatar_cache = ();
1575 # Compute the picon url for a given email, by using the picon search service over at
1576 # http://www.cs.indiana.edu/picons/search.html
1578 my $email = lc shift;
1579 if (!$avatar_cache{$email}) {
1580 my ($user, $domain) = split('@', $email);
1581 $avatar_cache{$email} =
1582 "http://www.cs.indiana.edu/cgi-pub/kinzler/piconsearch.cgi/" .
1584 "users+domains+unknown/up/single";
1586 return $avatar_cache{$email};
1589 # Compute the gravatar url for a given email, if it's not in the cache already.
1590 # Gravatar stores only the part of the URL before the size, since that's the
1591 # one computationally more expensive. This also allows reuse of the cache for
1592 # different sizes (for this particular engine).
1594 my $email = lc shift;
1596 $avatar_cache{$email} ||=
1597 "http://www.gravatar.com/avatar/" .
1598 Digest::MD5::md5_hex($email) . "?s=";
1599 return $avatar_cache{$email} . $size;
1602 # Insert an avatar for the given $email at the given $size if the feature
1604 sub git_get_avatar {
1605 my ($email, %opts) = @_;
1606 my $pre_white = ($opts{-pad_before} ? " " : "");
1607 my $post_white = ($opts{-pad_after} ? " " : "");
1608 $opts{-size} ||= 'default';
1609 my $size = $avatar_size{$opts{-size}} || $avatar_size{'default'};
1611 if ($git_avatar eq 'gravatar') {
1612 $url = gravatar_url($email, $size);
1613 } elsif ($git_avatar eq 'picon') {
1614 $url = picon_url($email);
1616 # Other providers can be added by extending the if chain, defining $url
1617 # as needed. If no variant puts something in $url, we assume avatars
1618 # are completely disabled/unavailable.
1621 "<img width=\"$size\" " .
1622 "class=\"avatar\" " .
1631 # format the author name of the given commit with the given tag
1632 # the author name is chopped and escaped according to the other
1633 # optional parameters (see chop_str).
1634 sub format_author_html {
1637 my $author = chop_and_escape_str($co->{'author_name'}, @_);
1638 return "<$tag class=\"author\">" .
1639 git_get_avatar($co->{'author_email'}, -pad_after => 1) .
1640 $author . "</$tag>";
1643 # format git diff header line, i.e. "diff --(git|combined|cc) ..."
1644 sub format_git_diff_header_line {
1646 my $diffinfo = shift;
1647 my ($from, $to) = @_;
1649 if ($diffinfo->{'nparents'}) {
1651 $line =~ s!^(diff (.*?) )"?.*$!$1!;
1652 if ($to->{'href'}) {
1653 $line .= $cgi->a({-href => $to->{'href'}, -class => "path"},
1654 esc_path($to->{'file'}));
1655 } else { # file was deleted (no href)
1656 $line .= esc_path($to->{'file'});
1660 $line =~ s!^(diff (.*?) )"?a/.*$!$1!;
1661 if ($from->{'href'}) {
1662 $line .= $cgi->a({-href => $from->{'href'}, -class => "path"},
1663 'a/' . esc_path($from->{'file'}));
1664 } else { # file was added (no href)
1665 $line .= 'a/' . esc_path($from->{'file'});
1668 if ($to->{'href'}) {
1669 $line .= $cgi->a({-href => $to->{'href'}, -class => "path"},
1670 'b/' . esc_path($to->{'file'}));
1671 } else { # file was deleted
1672 $line .= 'b/' . esc_path($to->{'file'});
1676 return "<div class=\"diff header\">$line</div>\n";
1679 # format extended diff header line, before patch itself
1680 sub format_extended_diff_header_line {
1682 my $diffinfo = shift;
1683 my ($from, $to) = @_;
1686 if ($line =~ s!^((copy|rename) from ).*$!$1! && $from->{'href'}) {
1687 $line .= $cgi->a({-href=>$from->{'href'}, -class=>"path"},
1688 esc_path($from->{'file'}));
1690 if ($line =~ s!^((copy|rename) to ).*$!$1! && $to->{'href'}) {
1691 $line .= $cgi->a({-href=>$to->{'href'}, -class=>"path"},
1692 esc_path($to->{'file'}));
1694 # match single <mode>
1695 if ($line =~ m/\s(\d{6})$/) {
1696 $line .= '<span class="info"> (' .
1697 file_type_long($1) .
1701 if ($line =~ m/^index [0-9a-fA-F]{40},[0-9a-fA-F]{40}/) {
1702 # can match only for combined diff
1704 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
1705 if ($from->{'href'}[$i]) {
1706 $line .= $cgi->a({-href=>$from->{'href'}[$i],
1708 substr($diffinfo->{'from_id'}[$i],0,7));
1713 $line .= ',' if ($i < $diffinfo->{'nparents'} - 1);
1716 if ($to->{'href'}) {
1717 $line .= $cgi->a({-href=>$to->{'href'}, -class=>"hash"},
1718 substr($diffinfo->{'to_id'},0,7));
1723 } elsif ($line =~ m/^index [0-9a-fA-F]{40}..[0-9a-fA-F]{40}/) {
1724 # can match only for ordinary diff
1725 my ($from_link, $to_link);
1726 if ($from->{'href'}) {
1727 $from_link = $cgi->a({-href=>$from->{'href'}, -class=>"hash"},
1728 substr($diffinfo->{'from_id'},0,7));
1730 $from_link = '0' x 7;
1732 if ($to->{'href'}) {
1733 $to_link = $cgi->a({-href=>$to->{'href'}, -class=>"hash"},
1734 substr($diffinfo->{'to_id'},0,7));
1738 my ($from_id, $to_id) = ($diffinfo->{'from_id'}, $diffinfo->{'to_id'});
1739 $line =~ s!$from_id\.\.$to_id!$from_link..$to_link!;
1742 return $line . "<br/>\n";
1745 # format from-file/to-file diff header
1746 sub format_diff_from_to_header {
1747 my ($from_line, $to_line, $diffinfo, $from, $to, @parents) = @_;
1752 #assert($line =~ m/^---/) if DEBUG;
1753 # no extra formatting for "^--- /dev/null"
1754 if (! $diffinfo->{'nparents'}) {
1755 # ordinary (single parent) diff
1756 if ($line =~ m!^--- "?a/!) {
1757 if ($from->{'href'}) {
1759 $cgi->a({-href=>$from->{'href'}, -class=>"path"},
1760 esc_path($from->{'file'}));
1763 esc_path($from->{'file'});
1766 $result .= qq!<div class="diff from_file">$line</div>\n!;
1769 # combined diff (merge commit)
1770 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
1771 if ($from->{'href'}[$i]) {
1773 $cgi->a({-href=>href(action=>"blobdiff",
1774 hash_parent=>$diffinfo->{'from_id'}[$i],
1775 hash_parent_base=>$parents[$i],
1776 file_parent=>$from->{'file'}[$i],
1777 hash=>$diffinfo->{'to_id'},
1779 file_name=>$to->{'file'}),
1781 -title=>"diff" . ($i+1)},
1784 $cgi->a({-href=>$from->{'href'}[$i], -class=>"path"},
1785 esc_path($from->{'file'}[$i]));
1787 $line = '--- /dev/null';
1789 $result .= qq!<div class="diff from_file">$line</div>\n!;
1794 #assert($line =~ m/^\+\+\+/) if DEBUG;
1795 # no extra formatting for "^+++ /dev/null"
1796 if ($line =~ m!^\+\+\+ "?b/!) {
1797 if ($to->{'href'}) {
1799 $cgi->a({-href=>$to->{'href'}, -class=>"path"},
1800 esc_path($to->{'file'}));
1803 esc_path($to->{'file'});
1806 $result .= qq!<div class="diff to_file">$line</div>\n!;
1811 # create note for patch simplified by combined diff
1812 sub format_diff_cc_simplified {
1813 my ($diffinfo, @parents) = @_;
1816 $result .= "<div class=\"diff header\">" .
1818 if (!is_deleted($diffinfo)) {
1819 $result .= $cgi->a({-href => href(action=>"blob",
1821 hash=>$diffinfo->{'to_id'},
1822 file_name=>$diffinfo->{'to_file'}),
1824 esc_path($diffinfo->{'to_file'}));
1826 $result .= esc_path($diffinfo->{'to_file'});
1828 $result .= "</div>\n" . # class="diff header"
1829 "<div class=\"diff nodifferences\">" .
1831 "</div>\n"; # class="diff nodifferences"
1836 # format patch (diff) line (not to be used for diff headers)
1837 sub format_diff_line {
1839 my ($from, $to) = @_;
1840 my $diff_class = "";
1844 if ($from && $to && ref($from->{'href'}) eq "ARRAY") {
1846 my $prefix = substr($line, 0, scalar @{$from->{'href'}});
1847 if ($line =~ m/^\@{3}/) {
1848 $diff_class = " chunk_header";
1849 } elsif ($line =~ m/^\\/) {
1850 $diff_class = " incomplete";
1851 } elsif ($prefix =~ tr/+/+/) {
1852 $diff_class = " add";
1853 } elsif ($prefix =~ tr/-/-/) {
1854 $diff_class = " rem";
1857 # assume ordinary diff
1858 my $char = substr($line, 0, 1);
1860 $diff_class = " add";
1861 } elsif ($char eq '-') {
1862 $diff_class = " rem";
1863 } elsif ($char eq '@') {
1864 $diff_class = " chunk_header";
1865 } elsif ($char eq "\\") {
1866 $diff_class = " incomplete";
1869 $line = untabify($line);
1870 if ($from && $to && $line =~ m/^\@{2} /) {
1871 my ($from_text, $from_start, $from_lines, $to_text, $to_start, $to_lines, $section) =
1872 $line =~ m/^\@{2} (-(\d+)(?:,(\d+))?) (\+(\d+)(?:,(\d+))?) \@{2}(.*)$/;
1874 $from_lines = 0 unless defined $from_lines;
1875 $to_lines = 0 unless defined $to_lines;
1877 if ($from->{'href'}) {
1878 $from_text = $cgi->a({-href=>"$from->{'href'}#l$from_start",
1879 -class=>"list"}, $from_text);
1881 if ($to->{'href'}) {
1882 $to_text = $cgi->a({-href=>"$to->{'href'}#l$to_start",
1883 -class=>"list"}, $to_text);
1885 $line = "<span class=\"chunk_info\">@@ $from_text $to_text @@</span>" .
1886 "<span class=\"section\">" . esc_html($section, -nbsp=>1) . "</span>";
1887 return "<div class=\"diff$diff_class\">$line</div>\n";
1888 } elsif ($from && $to && $line =~ m/^\@{3}/) {
1889 my ($prefix, $ranges, $section) = $line =~ m/^(\@+) (.*?) \@+(.*)$/;
1890 my (@from_text, @from_start, @from_nlines, $to_text, $to_start, $to_nlines);
1892 @from_text = split(' ', $ranges);
1893 for (my $i = 0; $i < @from_text; ++$i) {
1894 ($from_start[$i], $from_nlines[$i]) =
1895 (split(',', substr($from_text[$i], 1)), 0);
1898 $to_text = pop @from_text;
1899 $to_start = pop @from_start;
1900 $to_nlines = pop @from_nlines;
1902 $line = "<span class=\"chunk_info\">$prefix ";
1903 for (my $i = 0; $i < @from_text; ++$i) {
1904 if ($from->{'href'}[$i]) {
1905 $line .= $cgi->a({-href=>"$from->{'href'}[$i]#l$from_start[$i]",
1906 -class=>"list"}, $from_text[$i]);
1908 $line .= $from_text[$i];
1912 if ($to->{'href'}) {
1913 $line .= $cgi->a({-href=>"$to->{'href'}#l$to_start",
1914 -class=>"list"}, $to_text);
1918 $line .= " $prefix</span>" .
1919 "<span class=\"section\">" . esc_html($section, -nbsp=>1) . "</span>";
1920 return "<div class=\"diff$diff_class\">$line</div>\n";
1922 return "<div class=\"diff$diff_class\">" . esc_html($line, -nbsp=>1) . "</div>\n";
1925 # Generates undef or something like "_snapshot_" or "snapshot (_tbz2_ _zip_)",
1926 # linked. Pass the hash of the tree/commit to snapshot.
1927 sub format_snapshot_links {
1929 my $num_fmts = @snapshot_fmts;
1930 if ($num_fmts > 1) {
1931 # A parenthesized list of links bearing format names.
1932 # e.g. "snapshot (_tar.gz_ _zip_)"
1933 return "snapshot (" . join(' ', map
1940 }, $known_snapshot_formats{$_}{'display'})
1941 , @snapshot_fmts) . ")";
1942 } elsif ($num_fmts == 1) {
1943 # A single "snapshot" link whose tooltip bears the format name.
1945 my ($fmt) = @snapshot_fmts;
1951 snapshot_format=>$fmt
1953 -title => "in format: $known_snapshot_formats{$fmt}{'display'}"
1955 } else { # $num_fmts == 0
1960 ## ......................................................................
1961 ## functions returning values to be passed, perhaps after some
1962 ## transformation, to other functions; e.g. returning arguments to href()
1964 # returns hash to be passed to href to generate gitweb URL
1965 # in -title key it returns description of link
1967 my $format = shift || 'Atom';
1968 my %res = (action => lc($format));
1970 # feed links are possible only for project views
1971 return unless (defined $project);
1972 # some views should link to OPML, or to generic project feed,
1973 # or don't have specific feed yet (so they should use generic)
1974 return if ($action =~ /^(?:tags|heads|forks|tag|search)$/x);
1977 # branches refs uses 'refs/heads/' prefix (fullname) to differentiate
1978 # from tag links; this also makes possible to detect branch links
1979 if ((defined $hash_base && $hash_base =~ m!^refs/heads/(.*)$!) ||
1980 (defined $hash && $hash =~ m!^refs/heads/(.*)$!)) {
1983 # find log type for feed description (title)
1985 if (defined $file_name) {
1986 $type = "history of $file_name";
1987 $type .= "/" if ($action eq 'tree');
1988 $type .= " on '$branch'" if (defined $branch);
1990 $type = "log of $branch" if (defined $branch);
1993 $res{-title} = $type;
1994 $res{'hash'} = (defined $branch ? "refs/heads/$branch" : undef);
1995 $res{'file_name'} = $file_name;
2000 ## ----------------------------------------------------------------------
2001 ## git utility subroutines, invoking git commands
2003 # returns path to the core git executable and the --git-dir parameter as list
2005 $number_of_git_cmds++;
2006 return $GIT, '--git-dir='.$git_dir;
2009 # quote the given arguments for passing them to the shell
2010 # quote_command("command", "arg 1", "arg with ' and ! characters")
2011 # => "'command' 'arg 1' 'arg with '\'' and '\!' characters'"
2012 # Try to avoid using this function wherever possible.
2015 map { my $a = $_; $a =~ s/(['!])/'\\$1'/g; "'$a'" } @_ );
2018 # get HEAD ref of given project as hash
2019 sub git_get_head_hash {
2020 return git_get_full_hash(shift, 'HEAD');
2023 sub git_get_full_hash {
2024 my $project = shift;
2026 my $o_git_dir = $git_dir;
2028 $git_dir = "$projectroot/$project";
2029 if (open my $fd, '-|', git_cmd(), 'rev-parse', '--verify', $hash) {
2032 if (defined $hash && $hash =~ /^([0-9a-fA-F]{40})$/) {
2036 if (defined $o_git_dir) {
2037 $git_dir = $o_git_dir;
2042 # try and get a shorter hash id
2043 sub git_get_short_hash {
2044 my $project = shift;
2046 my $o_git_dir = $git_dir;
2048 $git_dir = "$projectroot/$project";
2049 if (open my $fd, '-|', git_cmd(), 'rev-parse', '--short=7', $hash) {
2052 if (defined $hash && $hash =~ /^([0-9a-fA-F]{7,})$/) {
2056 if (defined $o_git_dir) {
2057 $git_dir = $o_git_dir;
2062 # get type of given object
2066 open my $fd, "-|", git_cmd(), "cat-file", '-t', $hash or return;
2068 close $fd or return;
2073 # repository configuration
2074 our $config_file = '';
2077 # store multiple values for single key as anonymous array reference
2078 # single values stored directly in the hash, not as [ <value> ]
2079 sub hash_set_multi {
2080 my ($hash, $key, $value) = @_;
2082 if (!exists $hash->{$key}) {
2083 $hash->{$key} = $value;
2084 } elsif (!ref $hash->{$key}) {
2085 $hash->{$key} = [ $hash->{$key}, $value ];
2087 push @{$hash->{$key}}, $value;
2091 # return hash of git project configuration
2092 # optionally limited to some section, e.g. 'gitweb'
2093 sub git_parse_project_config {
2094 my $section_regexp = shift;
2099 open my $fh, "-|", git_cmd(), "config", '-z', '-l',
2102 while (my $keyval = <$fh>) {
2104 my ($key, $value) = split(/\n/, $keyval, 2);
2106 hash_set_multi(\%config, $key, $value)
2107 if (!defined $section_regexp || $key =~ /^(?:$section_regexp)\./o);
2114 # convert config value to boolean: 'true' or 'false'
2115 # no value, number > 0, 'true' and 'yes' values are true
2116 # rest of values are treated as false (never as error)
2117 sub config_to_bool {
2120 return 1 if !defined $val; # section.key
2122 # strip leading and trailing whitespace
2126 return (($val =~ /^\d+$/ && $val) || # section.key = 1
2127 ($val =~ /^(?:true|yes)$/i)); # section.key = true
2130 # convert config value to simple decimal number
2131 # an optional value suffix of 'k', 'm', or 'g' will cause the value
2132 # to be multiplied by 1024, 1048576, or 1073741824
2136 # strip leading and trailing whitespace
2140 if (my ($num, $unit) = ($val =~ /^([0-9]*)([kmg])$/i)) {
2142 # unknown unit is treated as 1
2143 return $num * ($unit eq 'g' ? 1073741824 :
2144 $unit eq 'm' ? 1048576 :
2145 $unit eq 'k' ? 1024 : 1);
2150 # convert config value to array reference, if needed
2151 sub config_to_multi {
2154 return ref($val) ? $val : (defined($val) ? [ $val ] : []);
2157 sub git_get_project_config {
2158 my ($key, $type) = @_;
2161 return unless ($key);
2162 $key =~ s/^gitweb\.//;
2163 return if ($key =~ m/\W/);
2166 if (defined $type) {
2169 unless ($type eq 'bool' || $type eq 'int');
2173 if (!defined $config_file ||
2174 $config_file ne "$git_dir/config") {
2175 %config = git_parse_project_config('gitweb');
2176 $config_file = "$git_dir/config";
2179 # check if config variable (key) exists
2180 return unless exists $config{"gitweb.$key"};
2183 if (!defined $type) {
2184 return $config{"gitweb.$key"};
2185 } elsif ($type eq 'bool') {
2186 # backward compatibility: 'git config --bool' returns true/false
2187 return config_to_bool($config{"gitweb.$key"}) ? 'true' : 'false';
2188 } elsif ($type eq 'int') {
2189 return config_to_int($config{"gitweb.$key"});
2191 return $config{"gitweb.$key"};
2194 # get hash of given path at given ref
2195 sub git_get_hash_by_path {
2197 my $path = shift || return undef;
2202 open my $fd, "-|", git_cmd(), "ls-tree", $base, "--", $path
2203 or die_error(500, "Open git-ls-tree failed");
2205 close $fd or return undef;
2207 if (!defined $line) {
2208 # there is no tree or hash given by $path at $base
2212 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'
2213 $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t/;
2214 if (defined $type && $type ne $2) {
2215 # type doesn't match
2221 # get path of entry with given hash at given tree-ish (ref)
2222 # used to get 'from' filename for combined diff (merge commit) for renames
2223 sub git_get_path_by_hash {
2224 my $base = shift || return;
2225 my $hash = shift || return;
2229 open my $fd, "-|", git_cmd(), "ls-tree", '-r', '-t', '-z', $base
2231 while (my $line = <$fd>) {
2234 #'040000 tree 595596a6a9117ddba9fe379b6b012b558bac8423 gitweb'
2235 #'100644 blob e02e90f0429be0d2a69b76571101f20b8f75530f gitweb/README'
2236 if ($line =~ m/(?:[0-9]+) (?:.+) $hash\t(.+)$/) {
2245 ## ......................................................................
2246 ## git utility functions, directly accessing git repository
2248 sub git_get_project_description {
2251 $git_dir = "$projectroot/$path";
2252 open my $fd, '<', "$git_dir/description"
2253 or return git_get_project_config('description');
2256 if (defined $descr) {
2262 sub git_get_project_ctags {
2266 $git_dir = "$projectroot/$path";
2267 opendir my $dh, "$git_dir/ctags"
2269 foreach (grep { -f $_ } map { "$git_dir/ctags/$_" } readdir($dh)) {
2270 open my $ct, '<', $_ or next;
2274 my $ctag = $_; $ctag =~ s#.*/##;
2275 $ctags->{$ctag} = $val;
2281 sub git_populate_project_tagcloud {
2284 # First, merge different-cased tags; tags vote on casing
2286 foreach (keys %$ctags) {
2287 $ctags_lc{lc $_}->{count} += $ctags->{$_};
2288 if (not $ctags_lc{lc $_}->{topcount}
2289 or $ctags_lc{lc $_}->{topcount} < $ctags->{$_}) {
2290 $ctags_lc{lc $_}->{topcount} = $ctags->{$_};
2291 $ctags_lc{lc $_}->{topname} = $_;
2296 if (eval { require HTML::TagCloud; 1; }) {
2297 $cloud = HTML::TagCloud->new;
2298 foreach (sort keys %ctags_lc) {
2299 # Pad the title with spaces so that the cloud looks
2301 my $title = $ctags_lc{$_}->{topname};
2302 $title =~ s/ / /g;
2303 $title =~ s/^/ /g;
2304 $title =~ s/$/ /g;
2305 $cloud->add($title, $home_link."?by_tag=".$_, $ctags_lc{$_}->{count});
2308 $cloud = \%ctags_lc;
2313 sub git_show_project_tagcloud {
2314 my ($cloud, $count) = @_;
2315 print STDERR ref($cloud)."..\n";
2316 if (ref $cloud eq 'HTML::TagCloud') {
2317 return $cloud->html_and_css($count);
2319 my @tags = sort { $cloud->{$a}->{count} <=> $cloud->{$b}->{count} } keys %$cloud;
2320 return '<p align="center">' . join (', ', map {
2321 "<a href=\"$home_link?by_tag=$_\">$cloud->{$_}->{topname}</a>"
2322 } splice(@tags, 0, $count)) . '</p>';
2326 sub git_get_project_url_list {
2329 $git_dir = "$projectroot/$path";
2330 open my $fd, '<', "$git_dir/cloneurl"
2331 or return wantarray ?
2332 @{ config_to_multi(git_get_project_config('url')) } :
2333 config_to_multi(git_get_project_config('url'));
2334 my @git_project_url_list = map { chomp; $_ } <$fd>;
2337 return wantarray ? @git_project_url_list : \@git_project_url_list;
2340 sub git_get_projects_list {
2345 $filter =~ s/\.git$//;
2347 my $check_forks = gitweb_check_feature('forks');
2349 if (-d $projects_list) {
2350 # search in directory
2351 my $dir = $projects_list . ($filter ? "/$filter" : '');
2352 # remove the trailing "/"
2354 my $pfxlen = length("$dir");
2355 my $pfxdepth = ($dir =~ tr!/!!);
2358 follow_fast => 1, # follow symbolic links
2359 follow_skip => 2, # ignore duplicates
2360 dangling_symlinks => 0, # ignore dangling symlinks, silently
2362 # skip project-list toplevel, if we get it.
2363 return if (m!^[/.]$!);
2364 # only directories can be git repositories
2365 return unless (-d $_);
2366 # don't traverse too deep (Find is super slow on os x)
2367 if (($File::Find::name =~ tr!/!!) - $pfxdepth > $project_maxdepth) {
2368 $File::Find::prune = 1;
2372 my $subdir = substr($File::Find::name, $pfxlen + 1);
2373 # we check related file in $projectroot
2374 my $path = ($filter ? "$filter/" : '') . $subdir;
2375 if (check_export_ok("$projectroot/$path")) {
2376 push @list, { path => $path };
2377 $File::Find::prune = 1;
2382 } elsif (-f $projects_list) {
2383 # read from file(url-encoded):
2384 # 'git%2Fgit.git Linus+Torvalds'
2385 # 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin'
2386 # 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman'
2388 open my $fd, '<', $projects_list or return;
2390 while (my $line = <$fd>) {
2392 my ($path, $owner) = split ' ', $line;
2393 $path = unescape($path);
2394 $owner = unescape($owner);
2395 if (!defined $path) {
2398 if ($filter ne '') {
2399 # looking for forks;
2400 my $pfx = substr($path, 0, length($filter));
2401 if ($pfx ne $filter) {
2404 my $sfx = substr($path, length($filter));
2405 if ($sfx !~ /^\/.*\.git$/) {
2408 } elsif ($check_forks) {
2410 foreach my $filter (keys %paths) {
2411 # looking for forks;
2412 my $pfx = substr($path, 0, length($filter));
2413 if ($pfx ne $filter) {
2416 my $sfx = substr($path, length($filter));
2417 if ($sfx !~ /^\/.*\.git$/) {
2420 # is a fork, don't include it in
2425 if (check_export_ok("$projectroot/$path")) {
2428 owner => to_utf8($owner),
2431 (my $forks_path = $path) =~ s/\.git$//;
2432 $paths{$forks_path}++;
2440 our $gitweb_project_owner = undef;
2441 sub git_get_project_list_from_file {
2443 return if (defined $gitweb_project_owner);
2445 $gitweb_project_owner = {};
2446 # read from file (url-encoded):
2447 # 'git%2Fgit.git Linus+Torvalds'
2448 # 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin'
2449 # 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman'
2450 if (-f $projects_list) {
2451 open(my $fd, '<', $projects_list);
2452 while (my $line = <$fd>) {
2454 my ($pr, $ow) = split ' ', $line;
2455 $pr = unescape($pr);
2456 $ow = unescape($ow);
2457 $gitweb_project_owner->{$pr} = to_utf8($ow);
2463 sub git_get_project_owner {
2464 my $project = shift;
2467 return undef unless $project;
2468 $git_dir = "$projectroot/$project";
2470 if (!defined $gitweb_project_owner) {
2471 git_get_project_list_from_file();
2474 if (exists $gitweb_project_owner->{$project}) {
2475 $owner = $gitweb_project_owner->{$project};
2477 if (!defined $owner){
2478 $owner = git_get_project_config('owner');
2480 if (!defined $owner) {
2481 $owner = get_file_owner("$git_dir");
2487 sub git_get_last_activity {
2491 $git_dir = "$projectroot/$path";
2492 open($fd, "-|", git_cmd(), 'for-each-ref',
2493 '--format=%(committer)',
2494 '--sort=-committerdate',
2496 'refs/heads') or return;
2497 my $most_recent = <$fd>;
2498 close $fd or return;
2499 if (defined $most_recent &&
2500 $most_recent =~ / (\d+) [-+][01]\d\d\d$/) {
2502 my $age = time - $timestamp;
2503 return ($age, age_string($age));
2505 return (undef, undef);
2508 sub git_get_references {
2509 my $type = shift || "";
2511 # 5dc01c595e6c6ec9ccda4f6f69c131c0dd945f8c refs/tags/v2.6.11
2512 # c39ae07f393806ccf406ef966e9a15afc43cc36a refs/tags/v2.6.11^{}
2513 open my $fd, "-|", git_cmd(), "show-ref", "--dereference",
2514 ($type ? ("--", "refs/$type") : ()) # use -- <pattern> if $type
2517 while (my $line = <$fd>) {
2519 if ($line =~ m!^([0-9a-fA-F]{40})\srefs/($type.*)$!) {
2520 if (defined $refs{$1}) {
2521 push @{$refs{$1}}, $2;
2527 close $fd or return;
2531 sub git_get_rev_name_tags {
2532 my $hash = shift || return undef;
2534 open my $fd, "-|", git_cmd(), "name-rev", "--tags", $hash
2536 my $name_rev = <$fd>;
2539 if ($name_rev =~ m|^$hash tags/(.*)$|) {
2542 # catches also '$hash undefined' output
2547 ## ----------------------------------------------------------------------
2548 ## parse to hash functions
2552 my $tz = shift || "-0000";
2555 my @months = ("Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec");
2556 my @days = ("Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat");
2557 my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($epoch);
2558 $date{'hour'} = $hour;
2559 $date{'minute'} = $min;
2560 $date{'mday'} = $mday;
2561 $date{'day'} = $days[$wday];
2562 $date{'month'} = $months[$mon];
2563 $date{'rfc2822'} = sprintf "%s, %d %s %4d %02d:%02d:%02d +0000",
2564 $days[$wday], $mday, $months[$mon], 1900+$year, $hour ,$min, $sec;
2565 $date{'mday-time'} = sprintf "%d %s %02d:%02d",
2566 $mday, $months[$mon], $hour ,$min;
2567 $date{'iso-8601'} = sprintf "%04d-%02d-%02dT%02d:%02d:%02dZ",
2568 1900+$year, 1+$mon, $mday, $hour ,$min, $sec;
2570 $tz =~ m/^([+\-][0-9][0-9])([0-9][0-9])$/;
2571 my $local = $epoch + ((int $1 + ($2/60)) * 3600);
2572 ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($local);
2573 $date{'hour_local'} = $hour;
2574 $date{'minute_local'} = $min;
2575 $date{'tz_local'} = $tz;
2576 $date{'iso-tz'} = sprintf("%04d-%02d-%02d %02d:%02d:%02d %s",
2577 1900+$year, $mon+1, $mday,
2578 $hour, $min, $sec, $tz);
2587 open my $fd, "-|", git_cmd(), "cat-file", "tag", $tag_id or return;
2588 $tag{'id'} = $tag_id;
2589 while (my $line = <$fd>) {
2591 if ($line =~ m/^object ([0-9a-fA-F]{40})$/) {
2592 $tag{'object'} = $1;
2593 } elsif ($line =~ m/^type (.+)$/) {
2595 } elsif ($line =~ m/^tag (.+)$/) {
2597 } elsif ($line =~ m/^tagger (.*) ([0-9]+) (.*)$/) {
2598 $tag{'author'} = $1;
2599 $tag{'author_epoch'} = $2;
2600 $tag{'author_tz'} = $3;
2601 if ($tag{'author'} =~ m/^([^<]+) <([^>]*)>/) {
2602 $tag{'author_name'} = $1;
2603 $tag{'author_email'} = $2;
2605 $tag{'author_name'} = $tag{'author'};
2607 } elsif ($line =~ m/--BEGIN/) {
2608 push @comment, $line;
2610 } elsif ($line eq "") {
2614 push @comment, <$fd>;
2615 $tag{'comment'} = \@comment;
2616 close $fd or return;
2617 if (!defined $tag{'name'}) {
2623 sub parse_commit_text {
2624 my ($commit_text, $withparents) = @_;
2625 my @commit_lines = split '\n', $commit_text;
2628 pop @commit_lines; # Remove '\0'
2630 if (! @commit_lines) {
2634 my $header = shift @commit_lines;
2635 if ($header !~ m/^[0-9a-fA-F]{40}/) {
2638 ($co{'id'}, my @parents) = split ' ', $header;
2639 while (my $line = shift @commit_lines) {
2640 last if $line eq "\n";
2641 if ($line =~ m/^tree ([0-9a-fA-F]{40})$/) {
2643 } elsif ((!defined $withparents) && ($line =~ m/^parent ([0-9a-fA-F]{40})$/)) {
2645 } elsif ($line =~ m/^author (.*) ([0-9]+) (.*)$/) {
2646 $co{'author'} = to_utf8($1);
2647 $co{'author_epoch'} = $2;
2648 $co{'author_tz'} = $3;
2649 if ($co{'author'} =~ m/^([^<]+) <([^>]*)>/) {
2650 $co{'author_name'} = $1;
2651 $co{'author_email'} = $2;
2653 $co{'author_name'} = $co{'author'};
2655 } elsif ($line =~ m/^committer (.*) ([0-9]+) (.*)$/) {
2656 $co{'committer'} = to_utf8($1);
2657 $co{'committer_epoch'} = $2;
2658 $co{'committer_tz'} = $3;
2659 if ($co{'committer'} =~ m/^([^<]+) <([^>]*)>/) {
2660 $co{'committer_name'} = $1;
2661 $co{'committer_email'} = $2;
2663 $co{'committer_name'} = $co{'committer'};
2667 if (!defined $co{'tree'}) {
2670 $co{'parents'} = \@parents;
2671 $co{'parent'} = $parents[0];
2673 foreach my $title (@commit_lines) {
2676 $co{'title'} = chop_str($title, 80, 5);
2677 # remove leading stuff of merges to make the interesting part visible
2678 if (length($title) > 50) {
2679 $title =~ s/^Automatic //;
2680 $title =~ s/^merge (of|with) /Merge ... /i;
2681 if (length($title) > 50) {
2682 $title =~ s/(http|rsync):\/\///;
2684 if (length($title) > 50) {
2685 $title =~ s/(master|www|rsync)\.//;
2687 if (length($title) > 50) {
2688 $title =~ s/kernel.org:?//;
2690 if (length($title) > 50) {
2691 $title =~ s/\/pub\/scm//;
2694 $co{'title_short'} = chop_str($title, 50, 5);
2698 if (! defined $co{'title'} || $co{'title'} eq "") {
2699 $co{'title'} = $co{'title_short'} = '(no commit message)';
2701 # remove added spaces
2702 foreach my $line (@commit_lines) {
2705 $co{'comment'} = \@commit_lines;
2707 my $age = time - $co{'committer_epoch'};
2709 $co{'age_string'} = age_string($age);
2710 my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($co{'committer_epoch'});
2711 if ($age > 60*60*24*7*2) {
2712 $co{'age_string_date'} = sprintf "%4i-%02u-%02i", 1900 + $year, $mon+1, $mday;
2713 $co{'age_string_age'} = $co{'age_string'};
2715 $co{'age_string_date'} = $co{'age_string'};
2716 $co{'age_string_age'} = sprintf "%4i-%02u-%02i", 1900 + $year, $mon+1, $mday;
2722 my ($commit_id) = @_;
2727 open my $fd, "-|", git_cmd(), "rev-list",
2733 or die_error(500, "Open git-rev-list failed");
2734 %co = parse_commit_text(<$fd>, 1);
2741 my ($commit_id, $maxcount, $skip, $filename, @args) = @_;
2749 open my $fd, "-|", git_cmd(), "rev-list",
2752 ("--max-count=" . $maxcount),
2753 ("--skip=" . $skip),
2757 ($filename ? ($filename) : ())
2758 or die_error(500, "Open git-rev-list failed");
2759 while (my $line = <$fd>) {
2760 my %co = parse_commit_text($line);
2765 return wantarray ? @cos : \@cos;
2768 # parse line of git-diff-tree "raw" output
2769 sub parse_difftree_raw_line {
2773 # ':100644 100644 03b218260e99b78c6df0ed378e59ed9205ccc96d 3b93d5e7cc7f7dd4ebed13a5cc1a4ad976fc94d8 M ls-files.c'
2774 # ':100644 100644 7f9281985086971d3877aca27704f2aaf9c448ce bc190ebc71bbd923f2b728e505408f5e54bd073a M rev-tree.c'
2775 if ($line =~ m/^:([0-7]{6}) ([0-7]{6}) ([0-9a-fA-F]{40}) ([0-9a-fA-F]{40}) (.)([0-9]{0,3})\t(.*)$/) {
2776 $res{'from_mode'} = $1;
2777 $res{'to_mode'} = $2;
2778 $res{'from_id'} = $3;
2780 $res{'status'} = $5;
2781 $res{'similarity'} = $6;
2782 if ($res{'status'} eq 'R' || $res{'status'} eq 'C') { # renamed or copied
2783 ($res{'from_file'}, $res{'to_file'}) = map { unquote($_) } split("\t", $7);
2785 $res{'from_file'} = $res{'to_file'} = $res{'file'} = unquote($7);
2788 # '::100755 100755 100755 60e79ca1b01bc8b057abe17ddab484699a7f5fdb 94067cc5f73388f33722d52ae02f44692bc07490 94067cc5f73388f33722d52ae02f44692bc07490 MR git-gui/git-gui.sh'
2789 # combined diff (for merge commit)
2790 elsif ($line =~ s/^(::+)((?:[0-7]{6} )+)((?:[0-9a-fA-F]{40} )+)([a-zA-Z]+)\t(.*)$//) {
2791 $res{'nparents'} = length($1);
2792 $res{'from_mode'} = [ split(' ', $2) ];
2793 $res{'to_mode'} = pop @{$res{'from_mode'}};
2794 $res{'from_id'} = [ split(' ', $3) ];
2795 $res{'to_id'} = pop @{$res{'from_id'}};
2796 $res{'status'} = [ split('', $4) ];
2797 $res{'to_file'} = unquote($5);
2799 # 'c512b523472485aef4fff9e57b229d9d243c967f'
2800 elsif ($line =~ m/^([0-9a-fA-F]{40})$/) {
2801 $res{'commit'} = $1;
2804 return wantarray ? %res : \%res;
2807 # wrapper: return parsed line of git-diff-tree "raw" output
2808 # (the argument might be raw line, or parsed info)
2809 sub parsed_difftree_line {
2810 my $line_or_ref = shift;
2812 if (ref($line_or_ref) eq "HASH") {
2813 # pre-parsed (or generated by hand)
2814 return $line_or_ref;
2816 return parse_difftree_raw_line($line_or_ref);
2820 # parse line of git-ls-tree output
2821 sub parse_ls_tree_line {
2827 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa 16717 panic.c'
2828 $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40}) +(-|[0-9]+)\t(.+)$/s;
2837 $res{'name'} = unquote($5);
2840 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'
2841 $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t(.+)$/s;
2849 $res{'name'} = unquote($4);
2853 return wantarray ? %res : \%res;
2856 # generates _two_ hashes, references to which are passed as 2 and 3 argument
2857 sub parse_from_to_diffinfo {
2858 my ($diffinfo, $from, $to, @parents) = @_;
2860 if ($diffinfo->{'nparents'}) {
2862 $from->{'file'} = [];
2863 $from->{'href'} = [];
2864 fill_from_file_info($diffinfo, @parents)
2865 unless exists $diffinfo->{'from_file'};
2866 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
2867 $from->{'file'}[$i] =
2868 defined $diffinfo->{'from_file'}[$i] ?
2869 $diffinfo->{'from_file'}[$i] :
2870 $diffinfo->{'to_file'};
2871 if ($diffinfo->{'status'}[$i] ne "A") { # not new (added) file
2872 $from->{'href'}[$i] = href(action=>"blob",
2873 hash_base=>$parents[$i],
2874 hash=>$diffinfo->{'from_id'}[$i],
2875 file_name=>$from->{'file'}[$i]);
2877 $from->{'href'}[$i] = undef;
2881 # ordinary (not combined) diff
2882 $from->{'file'} = $diffinfo->{'from_file'};
2883 if ($diffinfo->{'status'} ne "A") { # not new (added) file
2884 $from->{'href'} = href(action=>"blob", hash_base=>$hash_parent,
2885 hash=>$diffinfo->{'from_id'},
2886 file_name=>$from->{'file'});
2888 delete $from->{'href'};
2892 $to->{'file'} = $diffinfo->{'to_file'};
2893 if (!is_deleted($diffinfo)) { # file exists in result
2894 $to->{'href'} = href(action=>"blob", hash_base=>$hash,
2895 hash=>$diffinfo->{'to_id'},
2896 file_name=>$to->{'file'});
2898 delete $to->{'href'};
2902 ## ......................................................................
2903 ## parse to array of hashes functions
2905 sub git_get_heads_list {
2909 open my $fd, '-|', git_cmd(), 'for-each-ref',
2910 ($limit ? '--count='.($limit+1) : ()), '--sort=-committerdate',
2911 '--format=%(objectname) %(refname) %(subject)%00%(committer)',
2914 while (my $line = <$fd>) {
2918 my ($refinfo, $committerinfo) = split(/\0/, $line);
2919 my ($hash, $name, $title) = split(' ', $refinfo, 3);
2920 my ($committer, $epoch, $tz) =
2921 ($committerinfo =~ /^(.*) ([0-9]+) (.*)$/);
2922 $ref_item{'fullname'} = $name;
2923 $name =~ s!^refs/heads/!!;
2925 $ref_item{'name'} = $name;
2926 $ref_item{'id'} = $hash;
2927 $ref_item{'title'} = $title || '(no commit message)';
2928 $ref_item{'epoch'} = $epoch;
2930 $ref_item{'age'} = age_string(time - $ref_item{'epoch'});
2932 $ref_item{'age'} = "unknown";
2935 push @headslist, \%ref_item;
2939 return wantarray ? @headslist : \@headslist;
2942 sub git_get_tags_list {
2946 open my $fd, '-|', git_cmd(), 'for-each-ref',
2947 ($limit ? '--count='.($limit+1) : ()), '--sort=-creatordate',
2948 '--format=%(objectname) %(objecttype) %(refname) '.
2949 '%(*objectname) %(*objecttype) %(subject)%00%(creator)',
2952 while (my $line = <$fd>) {
2956 my ($refinfo, $creatorinfo) = split(/\0/, $line);
2957 my ($id, $type, $name, $refid, $reftype, $title) = split(' ', $refinfo, 6);
2958 my ($creator, $epoch, $tz) =
2959 ($creatorinfo =~ /^(.*) ([0-9]+) (.*)$/);
2960 $ref_item{'fullname'} = $name;
2961 $name =~ s!^refs/tags/!!;
2963 $ref_item{'type'} = $type;
2964 $ref_item{'id'} = $id;
2965 $ref_item{'name'} = $name;
2966 if ($type eq "tag") {
2967 $ref_item{'subject'} = $title;
2968 $ref_item{'reftype'} = $reftype;
2969 $ref_item{'refid'} = $refid;
2971 $ref_item{'reftype'} = $type;
2972 $ref_item{'refid'} = $id;
2975 if ($type eq "tag" || $type eq "commit") {
2976 $ref_item{'epoch'} = $epoch;
2978 $ref_item{'age'} = age_string(time - $ref_item{'epoch'});
2980 $ref_item{'age'} = "unknown";
2984 push @tagslist, \%ref_item;
2988 return wantarray ? @tagslist : \@tagslist;
2991 ## ----------------------------------------------------------------------
2992 ## filesystem-related functions
2994 sub get_file_owner {
2997 my ($dev, $ino, $mode, $nlink, $st_uid, $st_gid, $rdev, $size) = stat($path);
2998 my ($name, $passwd, $uid, $gid, $quota, $comment, $gcos, $dir, $shell) = getpwuid($st_uid);
2999 if (!defined $gcos) {
3003 $owner =~ s/[,;].*$//;
3004 return to_utf8($owner);
3007 # assume that file exists
3009 my $filename = shift;
3011 open my $fd, '<', $filename;
3012 print map { to_utf8($_) } <$fd>;
3016 ## ......................................................................
3017 ## mimetype related functions
3019 sub mimetype_guess_file {
3020 my $filename = shift;
3021 my $mimemap = shift;
3022 -r $mimemap or return undef;
3025 open(my $mh, '<', $mimemap) or return undef;
3027 next if m/^#/; # skip comments
3028 my ($mimetype, $exts) = split(/\t+/);
3029 if (defined $exts) {
3030 my @exts = split(/\s+/, $exts);
3031 foreach my $ext (@exts) {
3032 $mimemap{$ext} = $mimetype;
3038 $filename =~ /\.([^.]*)$/;
3039 return $mimemap{$1};
3042 sub mimetype_guess {
3043 my $filename = shift;
3045 $filename =~ /\./ or return undef;
3047 if ($mimetypes_file) {
3048 my $file = $mimetypes_file;
3049 if ($file !~ m!^/!) { # if it is relative path
3050 # it is relative to project
3051 $file = "$projectroot/$project/$file";
3053 $mime = mimetype_guess_file($filename, $file);
3055 $mime ||= mimetype_guess_file($filename, '/etc/mime.types');
3061 my $filename = shift;
3064 my $mime = mimetype_guess($filename);
3065 $mime and return $mime;
3069 return $default_blob_plain_mimetype unless $fd;
3072 return 'text/plain';
3073 } elsif (! $filename) {
3074 return 'application/octet-stream';
3075 } elsif ($filename =~ m/\.png$/i) {
3077 } elsif ($filename =~ m/\.gif$/i) {
3079 } elsif ($filename =~ m/\.jpe?g$/i) {
3080 return 'image/jpeg';
3082 return 'application/octet-stream';
3086 sub blob_contenttype {
3087 my ($fd, $file_name, $type) = @_;
3089 $type ||= blob_mimetype($fd, $file_name);
3090 if ($type eq 'text/plain' && defined $default_text_plain_charset) {
3091 $type .= "; charset=$default_text_plain_charset";
3097 ## ======================================================================
3098 ## functions printing HTML: header, footer, error page
3100 sub git_header_html {
3101 my $status = shift || "200 OK";
3102 my $expires = shift;
3104 my $title = "$site_name";
3105 if (defined $project) {
3106 $title .= " - " . to_utf8($project);
3107 if (defined $action) {
3108 $title .= "/$action";
3109 if (defined $file_name) {
3110 $title .= " - " . esc_path($file_name);
3111 if ($action eq "tree" && $file_name !~ m|/$|) {
3118 # require explicit support from the UA if we are to send the page as
3119 # 'application/xhtml+xml', otherwise send it as plain old 'text/html'.
3120 # we have to do this because MSIE sometimes globs '*/*', pretending to
3121 # support xhtml+xml but choking when it gets what it asked for.
3122 if (defined $cgi->http('HTTP_ACCEPT') &&
3123 $cgi->http('HTTP_ACCEPT') =~ m/(,|;|\s|^)application\/xhtml\+xml(,|;|\s|$)/ &&
3124 $cgi->Accept('application/xhtml+xml') != 0) {
3125 $content_type = 'application/xhtml+xml';
3127 $content_type = 'text/html';
3129 print $cgi->header(-type=>$content_type, -charset => 'utf-8',
3130 -status=> $status, -expires => $expires);
3131 my $mod_perl_version = $ENV{'MOD_PERL'} ? " $ENV{'MOD_PERL'}" : '';
3133 <?xml version="1.0" encoding="utf-8"?>
3134 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
3135 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US" lang="en-US">
3136 <!-- git web interface version $version, (C) 2005-2006, Kay Sievers <kay.sievers\@vrfy.org>, Christian Gierke -->
3137 <!-- git core binaries version $git_version -->
3139 <meta http-equiv="content-type" content="$content_type; charset=utf-8"/>
3140 <meta name="generator" content="gitweb/$version git/$git_version$mod_perl_version"/>
3141 <meta name="robots" content="index, nofollow"/>
3142 <title>$title</title>
3144 # the stylesheet, favicon etc urls won't work correctly with path_info
3145 # unless we set the appropriate base URL
3146 if ($ENV{'PATH_INFO'}) {
3147 print "<base href=\"".esc_url($base_url)."\" />\n";
3149 # print out each stylesheet that exist, providing backwards capability
3150 # for those people who defined $stylesheet in a config file
3151 if (defined $stylesheet) {
3152 print '<link rel="stylesheet" type="text/css" href="'.$stylesheet.'"/>'."\n";
3154 foreach my $stylesheet (@stylesheets) {
3155 next unless $stylesheet;
3156 print '<link rel="stylesheet" type="text/css" href="'.$stylesheet.'"/>'."\n";
3159 if (defined $project) {
3160 my %href_params = get_feed_info();
3161 if (!exists $href_params{'-title'}) {
3162 $href_params{'-title'} = 'log';
3165 foreach my $format qw(RSS Atom) {
3166 my $type = lc($format);
3168 '-rel' => 'alternate',
3169 '-title' => "$project - $href_params{'-title'} - $format feed",
3170 '-type' => "application/$type+xml"
3173 $href_params{'action'} = $type;
3174 $link_attr{'-href'} = href(%href_params);
3176 "rel=\"$link_attr{'-rel'}\" ".
3177 "title=\"$link_attr{'-title'}\" ".
3178 "href=\"$link_attr{'-href'}\" ".
3179 "type=\"$link_attr{'-type'}\" ".
3182 $href_params{'extra_options'} = '--no-merges';
3183 $link_attr{'-href'} = href(%href_params);
3184 $link_attr{'-title'} .= ' (no merges)';
3186 "rel=\"$link_attr{'-rel'}\" ".
3187 "title=\"$link_attr{'-title'}\" ".
3188 "href=\"$link_attr{'-href'}\" ".
3189 "type=\"$link_attr{'-type'}\" ".
3194 printf('<link rel="alternate" title="%s projects list" '.
3195 'href="%s" type="text/plain; charset=utf-8" />'."\n",
3196 $site_name, href(project=>undef, action=>"project_index"));
3197 printf('<link rel="alternate" title="%s projects feeds" '.
3198 'href="%s" type="text/x-opml" />'."\n",
3199 $site_name, href(project=>undef, action=>"opml"));
3201 if (defined $favicon) {
3202 print qq(<link rel="shortcut icon" href="$favicon" type="image/png" />\n);
3208 if (-f $site_header) {
3209 insert_file($site_header);
3212 print "<div class=\"page_header\">\n" .
3213 $cgi->a({-href => esc_url($logo_url),
3214 -title => $logo_label},
3215 qq(<img src="$logo" width="72" height="27" alt="git" class="logo"/>));
3216 print $cgi->a({-href => esc_url($home_link)}, $home_link_str) . " / ";
3217 if (defined $project) {
3218 print $cgi->a({-href => href(action=>"summary")}, esc_html($project));
3219 if (defined $action) {
3226 my $have_search = gitweb_check_feature('search');
3227 if (defined $project && $have_search) {
3228 if (!defined $searchtext) {
3232 if (defined $hash_base) {
3233 $search_hash = $hash_base;
3234 } elsif (defined $hash) {
3235 $search_hash = $hash;
3237 $search_hash = "HEAD";
3239 my $action = $my_uri;
3240 my $use_pathinfo = gitweb_check_feature('pathinfo');
3241 if ($use_pathinfo) {
3242 $action .= "/".esc_url($project);
3244 print $cgi->startform(-method => "get", -action => $action) .
3245 "<div class=\"search\">\n" .
3247 $cgi->input({-name=>"p", -value=>$project, -type=>"hidden"}) . "\n") .
3248 $cgi->input({-name=>"a", -value=>"search", -type=>"hidden"}) . "\n" .
3249 $cgi->input({-name=>"h", -value=>$search_hash, -type=>"hidden"}) . "\n" .
3250 $cgi->popup_menu(-name => 'st', -default => 'commit',
3251 -values => ['commit', 'grep', 'author', 'committer', 'pickaxe']) .
3252 $cgi->sup($cgi->a({-href => href(action=>"search_help")}, "?")) .
3254 $cgi->textfield(-name => "s", -value => $searchtext) . "\n" .
3255 "<span title=\"Extended regular expression\">" .
3256 $cgi->checkbox(-name => 'sr', -value => 1, -label => 're',
3257 -checked => $search_use_regexp) .
3260 $cgi->end_form() . "\n";
3264 sub git_footer_html {
3265 my $feed_class = 'rss_logo';
3267 print "<div class=\"page_footer\">\n";
3268 if (defined $project) {
3269 my $descr = git_get_project_description($project);
3270 if (defined $descr) {
3271 print "<div class=\"page_footer_text\">" . esc_html($descr) . "</div>\n";
3274 my %href_params = get_feed_info();
3275 if (!%href_params) {
3276 $feed_class .= ' generic';
3278 $href_params{'-title'} ||= 'log';
3280 foreach my $format qw(RSS Atom) {
3281 $href_params{'action'} = lc($format);
3282 print $cgi->a({-href => href(%href_params),
3283 -title => "$href_params{'-title'} $format feed",
3284 -class => $feed_class}, $format)."\n";
3288 print $cgi->a({-href => href(project=>undef, action=>"opml"),
3289 -class => $feed_class}, "OPML") . " ";
3290 print $cgi->a({-href => href(project=>undef, action=>"project_index"),
3291 -class => $feed_class}, "TXT") . "\n";
3293 print "</div>\n"; # class="page_footer"
3295 if (defined $t0 && gitweb_check_feature('timed')) {
3296 print "<div id=\"generating_info\">\n";
3297 print 'This page took '.
3298 '<span id="generating_time" class="time_span">'.
3299 Time::HiRes::tv_interval($t0, [Time::HiRes::gettimeofday()]).
3302 '<span id="generating_cmd">'.
3303 $number_of_git_cmds.
3304 '</span> git commands '.
3306 print "</div>\n"; # class="page_footer"
3309 if (-f $site_footer) {
3310 insert_file($site_footer);
3313 print qq!<script type="text/javascript" src="$javascript"></script>\n!;
3314 if ($action eq 'blame_incremental') {
3315 print qq!<script type="text/javascript">\n!.
3316 qq!startBlame("!. href(action=>"blame_data", -replay=>1) .qq!",\n!.
3317 qq! "!. href() .qq!");\n!.
3320 print qq!<script type="text/javascript">\n!.
3321 qq!window.onload = fixLinks;\n!.
3329 # die_error(<http_status_code>, <error_message>)
3330 # Example: die_error(404, 'Hash not found')
3331 # By convention, use the following status codes (as defined in RFC 2616):
3332 # 400: Invalid or missing CGI parameters, or
3333 # requested object exists but has wrong type.
3334 # 403: Requested feature (like "pickaxe" or "snapshot") not enabled on
3335 # this server or project.
3336 # 404: Requested object/revision/project doesn't exist.
3337 # 500: The server isn't configured properly, or
3338 # an internal error occurred (e.g. failed assertions caused by bugs), or
3339 # an unknown error occurred (e.g. the git binary died unexpectedly).
3341 my $status = shift || 500;
3342 my $error = shift || "Internal server error";
3344 my %http_responses = (400 => '400 Bad Request',
3345 403 => '403 Forbidden',
3346 404 => '404 Not Found',
3347 500 => '500 Internal Server Error');
3348 git_header_html($http_responses{$status});
3350 <div class="page_body">
3360 ## ----------------------------------------------------------------------
3361 ## functions printing or outputting HTML: navigation
3363 sub git_print_page_nav {
3364 my ($current, $suppress, $head, $treehead, $treebase, $extra) = @_;
3365 $extra = '' if !defined $extra; # pager or formats
3367 my @navs = qw(summary shortlog log commit commitdiff tree);
3369 @navs = grep { $_ ne $suppress } @navs;
3372 my %arg = map { $_ => {action=>$_} } @navs;
3373 if (defined $head) {
3374 for (qw(commit commitdiff)) {
3375 $arg{$_}{'hash'} = $head;
3377 if ($current =~ m/^(tree | log | shortlog | commit | commitdiff | search)$/x) {
3378 for (qw(shortlog log)) {
3379 $arg{$_}{'hash'} = $head;
3384 $arg{'tree'}{'hash'} = $treehead if defined $treehead;
3385 $arg{'tree'}{'hash_base'} = $treebase if defined $treebase;
3387 my @actions = gitweb_get_feature('actions');
3390 'n' => $project, # project name
3391 'f' => $git_dir, # project path within filesystem
3392 'h' => $treehead || '', # current hash ('h' parameter)
3393 'b' => $treebase || '', # hash base ('hb' parameter)
3396 my ($label, $link, $pos) = splice(@actions,0,3);
3398 @navs = map { $_ eq $pos ? ($_, $label) : $_ } @navs;
3400 $link =~ s/%([%nfhb])/$repl{$1}/g;
3401 $arg{$label}{'_href'} = $link;
3404 print "<div class=\"page_nav\">\n" .
3406 map { $_ eq $current ?
3407 $_ : $cgi->a({-href => ($arg{$_}{_href} ? $arg{$_}{_href} : href(%{$arg{$_}}))}, "$_")
3409 print "<br/>\n$extra<br/>\n" .
3413 sub format_paging_nav {
3414 my ($action, $hash, $head, $page, $has_next_link) = @_;
3418 if ($hash ne $head || $page) {
3419 $paging_nav .= $cgi->a({-href => href(action=>$action)}, "HEAD");
3421 $paging_nav .= "HEAD";
3425 $paging_nav .= " ⋅ " .
3426 $cgi->a({-href => href(-replay=>1, page=>$page-1),
3427 -accesskey => "p", -title => "Alt-p"}, "prev");
3429 $paging_nav .= " ⋅ prev";
3432 if ($has_next_link) {
3433 $paging_nav .= " ⋅ " .
3434 $cgi->a({-href => href(-replay=>1, page=>$page+1),
3435 -accesskey => "n", -title => "Alt-n"}, "next");
3437 $paging_nav .= " ⋅ next";
3443 ## ......................................................................
3444 ## functions printing or outputting HTML: div
3446 sub git_print_header_div {
3447 my ($action, $title, $hash, $hash_base) = @_;
3450 $args{'action'} = $action;
3451 $args{'hash'} = $hash if $hash;
3452 $args{'hash_base'} = $hash_base if $hash_base;
3454 print "<div class=\"header\">\n" .
3455 $cgi->a({-href => href(%args), -class => "title"},
3456 $title ? $title : $action) .
3460 sub print_local_time {
3462 if ($date{'hour_local'} < 6) {
3463 printf(" (<span class=\"atnight\">%02d:%02d</span> %s)",
3464 $date{'hour_local'}, $date{'minute_local'}, $date{'tz_local'});
3466 printf(" (%02d:%02d %s)",
3467 $date{'hour_local'}, $date{'minute_local'}, $date{'tz_local'});
3471 # Outputs the author name and date in long form
3472 sub git_print_authorship {
3475 my $tag = $opts{-tag} || 'div';
3477 my %ad = parse_date($co->{'author_epoch'}, $co->{'author_tz'});
3478 print "<$tag class=\"author_date\">" .
3479 esc_html($co->{'author_name'}) .
3481 print_local_time(%ad) if ($opts{-localtime});
3482 print "]" . git_get_avatar($co->{'author_email'}, -pad_before => 1)
3486 # Outputs table rows containing the full author or committer information,
3487 # in the format expected for 'commit' view (& similia).
3488 # Parameters are a commit hash reference, followed by the list of people
3489 # to output information for. If the list is empty it defalts to both
3490 # author and committer.
3491 sub git_print_authorship_rows {
3493 # too bad we can't use @people = @_ || ('author', 'committer')
3495 @people = ('author', 'committer') unless @people;
3496 foreach my $who (@people) {
3497 my %wd = parse_date($co->{"${who}_epoch"}, $co->{"${who}_tz"});
3498 print "<tr><td>$who</td><td>" . esc_html($co->{$who}) . "</td>" .
3499 "<td rowspan=\"2\">" .
3500 git_get_avatar($co->{"${who}_email"}, -size => 'double') .
3503 "<td></td><td> $wd{'rfc2822'}";
3504 print_local_time(%wd);
3510 sub git_print_page_path {
3516 print "<div class=\"page_path\">";
3517 print $cgi->a({-href => href(action=>"tree", hash_base=>$hb),
3518 -title => 'tree root'}, to_utf8("[$project]"));
3520 if (defined $name) {
3521 my @dirname = split '/', $name;
3522 my $basename = pop @dirname;
3525 foreach my $dir (@dirname) {
3526 $fullname .= ($fullname ? '/' : '') . $dir;
3527 print $cgi->a({-href => href(action=>"tree", file_name=>$fullname,
3529 -title => $fullname}, esc_path($dir));
3532 if (defined $type && $type eq 'blob') {
3533 print $cgi->a({-href => href(action=>"blob_plain", file_name=>$file_name,
3535 -title => $name}, esc_path($basename));
3536 } elsif (defined $type && $type eq 'tree') {
3537 print $cgi->a({-href => href(action=>"tree", file_name=>$file_name,
3539 -title => $name}, esc_path($basename));
3542 print esc_path($basename);
3545 print "<br/></div>\n";
3552 if ($opts{'-remove_title'}) {
3553 # remove title, i.e. first line of log
3556 # remove leading empty lines
3557 while (defined $log->[0] && $log->[0] eq "") {
3564 foreach my $line (@$log) {
3565 if ($line =~ m/^ *(signed[ \-]off[ \-]by[ :]|acked[ \-]by[ :]|cc[ :])/i) {
3568 if (! $opts{'-remove_signoff'}) {
3569 print "<span class=\"signoff\">" . esc_html($line) . "</span><br/>\n";
3572 # remove signoff lines
3579 # print only one empty line
3580 # do not print empty line after signoff
3582 next if ($empty || $signoff);
3588 print format_log_line_html($line) . "<br/>\n";
3591 if ($opts{'-final_empty_line'}) {
3592 # end with single empty line
3593 print "<br/>\n" unless $empty;
3597 # return link target (what link points to)
3598 sub git_get_link_target {
3603 open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
3607 $link_target = <$fd>;
3612 return $link_target;
3615 # given link target, and the directory (basedir) the link is in,
3616 # return target of link relative to top directory (top tree);
3617 # return undef if it is not possible (including absolute links).
3618 sub normalize_link_target {
3619 my ($link_target, $basedir) = @_;
3621 # absolute symlinks (beginning with '/') cannot be normalized
3622 return if (substr($link_target, 0, 1) eq '/');
3624 # normalize link target to path from top (root) tree (dir)
3627 $path = $basedir . '/' . $link_target;
3629 # we are in top (root) tree (dir)
3630 $path = $link_target;
3633 # remove //, /./, and /../
3635 foreach my $part (split('/', $path)) {
3636 # discard '.' and ''
3637 next if (!$part || $part eq '.');
3639 if ($part eq '..') {
3643 # link leads outside repository (outside top dir)
3647 push @path_parts, $part;
3650 $path = join('/', @path_parts);
3655 # print tree entry (row of git_tree), but without encompassing <tr> element
3656 sub git_print_tree_entry {
3657 my ($t, $basedir, $hash_base, $have_blame) = @_;
3660 $base_key{'hash_base'} = $hash_base if defined $hash_base;
3662 # The format of a table row is: mode list link. Where mode is
3663 # the mode of the entry, list is the name of the entry, an href,
3664 # and link is the action links of the entry.
3666 print "<td class=\"mode\">" . mode_str($t->{'mode'}) . "</td>\n";
3667 if (exists $t->{'size'}) {
3668 print "<td class=\"size\">$t->{'size'}</td>\n";
3670 if ($t->{'type'} eq "blob") {
3671 print "<td class=\"list\">" .
3672 $cgi->a({-href => href(action=>"blob", hash=>$t->{'hash'},
3673 file_name=>"$basedir$t->{'name'}", %base_key),
3674 -class => "list"}, esc_path($t->{'name'}));
3675 if (S_ISLNK(oct $t->{'mode'})) {
3676 my $link_target = git_get_link_target($t->{'hash'});
3678 my $norm_target = normalize_link_target($link_target, $basedir);
3679 if (defined $norm_target) {
3681 $cgi->a({-href => href(action=>"object", hash_base=>$hash_base,
3682 file_name=>$norm_target),
3683 -title => $norm_target}, esc_path($link_target));
3685 print " -> " . esc_path($link_target);
3690 print "<td class=\"link\">";
3691 print $cgi->a({-href => href(action=>"blob", hash=>$t->{'hash'},
3692 file_name=>"$basedir$t->{'name'}", %base_key)},
3696 $cgi->a({-href => href(action=>"blame", hash=>$t->{'hash'},
3697 file_name=>"$basedir$t->{'name'}", %base_key)},
3700 if (defined $hash_base) {
3702 $cgi->a({-href => href(action=>"history", hash_base=>$hash_base,
3703 hash=>$t->{'hash'}, file_name=>"$basedir$t->{'name'}")},
3707 $cgi->a({-href => href(action=>"blob_plain", hash_base=>$hash_base,
3708 file_name=>"$basedir$t->{'name'}")},
3712 } elsif ($t->{'type'} eq "tree") {
3713 print "<td class=\"list\">";
3714 print $cgi->a({-href => href(action=>"tree", hash=>$t->{'hash'},
3715 file_name=>"$basedir$t->{'name'}",
3717 esc_path($t->{'name'}));
3719 print "<td class=\"link\">";
3720 print $cgi->a({-href => href(action=>"tree", hash=>$t->{'hash'},
3721 file_name=>"$basedir$t->{'name'}",
3724 if (defined $hash_base) {
3726 $cgi->a({-href => href(action=>"history", hash_base=>$hash_base,
3727 file_name=>"$basedir$t->{'name'}")},
3732 # unknown object: we can only present history for it
3733 # (this includes 'commit' object, i.e. submodule support)
3734 print "<td class=\"list\">" .
3735 esc_path($t->{'name'}) .
3737 print "<td class=\"link\">";
3738 if (defined $hash_base) {
3739 print $cgi->a({-href => href(action=>"history",
3740 hash_base=>$hash_base,
3741 file_name=>"$basedir$t->{'name'}")},
3748 ## ......................................................................
3749 ## functions printing large fragments of HTML
3751 # get pre-image filenames for merge (combined) diff
3752 sub fill_from_file_info {
3753 my ($diff, @parents) = @_;
3755 $diff->{'from_file'} = [ ];
3756 $diff->{'from_file'}[$diff->{'nparents'} - 1] = undef;
3757 for (my $i = 0; $i < $diff->{'nparents'}; $i++) {
3758 if ($diff->{'status'}[$i] eq 'R' ||
3759 $diff->{'status'}[$i] eq 'C') {
3760 $diff->{'from_file'}[$i] =
3761 git_get_path_by_hash($parents[$i], $diff->{'from_id'}[$i]);
3768 # is current raw difftree line of file deletion
3770 my $diffinfo = shift;
3772 return $diffinfo->{'to_id'} eq ('0' x 40);
3775 # does patch correspond to [previous] difftree raw line
3776 # $diffinfo - hashref of parsed raw diff format
3777 # $patchinfo - hashref of parsed patch diff format
3778 # (the same keys as in $diffinfo)
3779 sub is_patch_split {
3780 my ($diffinfo, $patchinfo) = @_;
3782 return defined $diffinfo && defined $patchinfo
3783 && $diffinfo->{'to_file'} eq $patchinfo->{'to_file'};
3787 sub git_difftree_body {
3788 my ($difftree, $hash, @parents) = @_;
3789 my ($parent) = $parents[0];
3790 my $have_blame = gitweb_check_feature('blame');
3791 print "<div class=\"list_head\">\n";
3792 if ($#{$difftree} > 10) {
3793 print(($#{$difftree} + 1) . " files changed:\n");
3797 print "<table class=\"" .
3798 (@parents > 1 ? "combined " : "") .
3801 # header only for combined diff in 'commitdiff' view
3802 my $has_header = @$difftree && @parents > 1 && $action eq 'commitdiff';
3805 print "<thead><tr>\n" .
3806 "<th></th><th></th>\n"; # filename, patchN link
3807 for (my $i = 0; $i < @parents; $i++) {
3808 my $par = $parents[$i];
3810 $cgi->a({-href => href(action=>"commitdiff",
3811 hash=>$hash, hash_parent=>$par),
3812 -title => 'commitdiff to parent number ' .
3813 ($i+1) . ': ' . substr($par,0,7)},
3817 print "</tr></thead>\n<tbody>\n";
3822 foreach my $line (@{$difftree}) {
3823 my $diff = parsed_difftree_line($line);
3826 print "<tr class=\"dark\">\n";
3828 print "<tr class=\"light\">\n";
3832 if (exists $diff->{'nparents'}) { # combined diff
3834 fill_from_file_info($diff, @parents)
3835 unless exists $diff->{'from_file'};
3837 if (!is_deleted($diff)) {
3838 # file exists in the result (child) commit
3840 $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3841 file_name=>$diff->{'to_file'},
3843 -class => "list"}, esc_path($diff->{'to_file'})) .
3847 esc_path($diff->{'to_file'}) .
3851 if ($action eq 'commitdiff') {
3854 print "<td class=\"link\">" .
3855 $cgi->a({-href => "#patch$patchno"}, "patch") .
3860 my $has_history = 0;
3861 my $not_deleted = 0;
3862 for (my $i = 0; $i < $diff->{'nparents'}; $i++) {
3863 my $hash_parent = $parents[$i];
3864 my $from_hash = $diff->{'from_id'}[$i];
3865 my $from_path = $diff->{'from_file'}[$i];
3866 my $status = $diff->{'status'}[$i];
3868 $has_history ||= ($status ne 'A');
3869 $not_deleted ||= ($status ne 'D');
3871 if ($status eq 'A') {
3872 print "<td class=\"link\" align=\"right\"> | </td>\n";
3873 } elsif ($status eq 'D') {
3874 print "<td class=\"link\">" .
3875 $cgi->a({-href => href(action=>"blob",
3878 file_name=>$from_path)},
3882 if ($diff->{'to_id'} eq $from_hash) {
3883 print "<td class=\"link nochange\">";
3885 print "<td class=\"link\">";
3887 print $cgi->a({-href => href(action=>"blobdiff",
3888 hash=>$diff->{'to_id'},
3889 hash_parent=>$from_hash,
3891 hash_parent_base=>$hash_parent,
3892 file_name=>$diff->{'to_file'},
3893 file_parent=>$from_path)},
3899 print "<td class=\"link\">";
3901 print $cgi->a({-href => href(action=>"blob",
3902 hash=>$diff->{'to_id'},
3903 file_name=>$diff->{'to_file'},
3906 print " | " if ($has_history);
3909 print $cgi->a({-href => href(action=>"history",
3910 file_name=>$diff->{'to_file'},
3917 next; # instead of 'else' clause, to avoid extra indent
3919 # else ordinary diff
3921 my ($to_mode_oct, $to_mode_str, $to_file_type);
3922 my ($from_mode_oct, $from_mode_str, $from_file_type);
3923 if ($diff->{'to_mode'} ne ('0' x 6)) {
3924 $to_mode_oct = oct $diff->{'to_mode'};
3925 if (S_ISREG($to_mode_oct)) { # only for regular file
3926 $to_mode_str = sprintf("%04o", $to_mode_oct & 0777); # permission bits
3928 $to_file_type = file_type($diff->{'to_mode'});
3930 if ($diff->{'from_mode'} ne ('0' x 6)) {
3931 $from_mode_oct = oct $diff->{'from_mode'};
3932 if (S_ISREG($to_mode_oct)) { # only for regular file
3933 $from_mode_str = sprintf("%04o", $from_mode_oct & 0777); # permission bits
3935 $from_file_type = file_type($diff->{'from_mode'});
3938 if ($diff->{'status'} eq "A") { # created
3939 my $mode_chng = "<span class=\"file_status new\">[new $to_file_type";
3940 $mode_chng .= " with mode: $to_mode_str" if $to_mode_str;
3941 $mode_chng .= "]</span>";
3943 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3944 hash_base=>$hash, file_name=>$diff->{'file'}),
3945 -class => "list"}, esc_path($diff->{'file'}));
3947 print "<td>$mode_chng</td>\n";
3948 print "<td class=\"link\">";
3949 if ($action eq 'commitdiff') {
3952 print $cgi->a({-href => "#patch$patchno"}, "patch");
3955 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3956 hash_base=>$hash, file_name=>$diff->{'file'})},
3960 } elsif ($diff->{'status'} eq "D") { # deleted
3961 my $mode_chng = "<span class=\"file_status deleted\">[deleted $from_file_type]</span>";
3963 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'from_id'},
3964 hash_base=>$parent, file_name=>$diff->{'file'}),
3965 -class => "list"}, esc_path($diff->{'file'}));
3967 print "<td>$mode_chng</td>\n";
3968 print "<td class=\"link\">";
3969 if ($action eq 'commitdiff') {
3972 print $cgi->a({-href => "#patch$patchno"}, "patch");
3975 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'from_id'},
3976 hash_base=>$parent, file_name=>$diff->{'file'})},
3979 print $cgi->a({-href => href(action=>"blame", hash_base=>$parent,
3980 file_name=>$diff->{'file'})},
3983 print $cgi->a({-href => href(action=>"history", hash_base=>$parent,
3984 file_name=>$diff->{'file'})},
3988 } elsif ($diff->{'status'} eq "M" || $diff->{'status'} eq "T") { # modified, or type changed
3989 my $mode_chnge = "";
3990 if ($diff->{'from_mode'} != $diff->{'to_mode'}) {
3991 $mode_chnge = "<span class=\"file_status mode_chnge\">[changed";
3992 if ($from_file_type ne $to_file_type) {
3993 $mode_chnge .= " from $from_file_type to $to_file_type";
3995 if (($from_mode_oct & 0777) != ($to_mode_oct & 0777)) {
3996 if ($from_mode_str && $to_mode_str) {
3997 $mode_chnge .= " mode: $from_mode_str->$to_mode_str";
3998 } elsif ($to_mode_str) {
3999 $mode_chnge .= " mode: $to_mode_str";
4002 $mode_chnge .= "]</span>\n";
4005 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
4006 hash_base=>$hash, file_name=>$diff->{'file'}),
4007 -class => "list"}, esc_path($diff->{'file'}));
4009 print "<td>$mode_chnge</td>\n";
4010 print "<td class=\"link\">";
4011 if ($action eq 'commitdiff') {
4014 print $cgi->a({-href => "#patch$patchno"}, "patch") .
4016 } elsif ($diff->{'to_id'} ne $diff->{'from_id'}) {
4017 # "commit" view and modified file (not onlu mode changed)
4018 print $cgi->a({-href => href(action=>"blobdiff",
4019 hash=>$diff->{'to_id'}, hash_parent=>$diff->{'from_id'},
4020 hash_base=>$hash, hash_parent_base=>$parent,
4021 file_name=>$diff->{'file'})},
4025 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
4026 hash_base=>$hash, file_name=>$diff->{'file'})},
4029 print $cgi->a({-href => href(action=>"blame", hash_base=>$hash,
4030 file_name=>$diff->{'file'})},
4033 print $cgi->a({-href => href(action=>"history", hash_base=>$hash,
4034 file_name=>$diff->{'file'})},
4038 } elsif ($diff->{'status'} eq "R" || $diff->{'status'} eq "C") { # renamed or copied
4039 my %status_name = ('R' => 'moved', 'C' => 'copied');
4040 my $nstatus = $status_name{$diff->{'status'}};
4042 if ($diff->{'from_mode'} != $diff->{'to_mode'}) {
4043 # mode also for directories, so we cannot use $to_mode_str
4044 $mode_chng = sprintf(", mode: %04o", $to_mode_oct & 0777);
4047 $cgi->a({-href => href(action=>"blob", hash_base=>$hash,
4048 hash=>$diff->{'to_id'}, file_name=>$diff->{'to_file'}),
4049 -class => "list"}, esc_path($diff->{'to_file'})) . "</td>\n" .
4050 "<td><span class=\"file_status $nstatus\">[$nstatus from " .
4051 $cgi->a({-href => href(action=>"blob", hash_base=>$parent,
4052 hash=>$diff->{'from_id'}, file_name=>$diff->{'from_file'}),
4053 -class => "list"}, esc_path($diff->{'from_file'})) .
4054 " with " . (int $diff->{'similarity'}) . "% similarity$mode_chng]</span></td>\n" .
4055 "<td class=\"link\">";
4056 if ($action eq 'commitdiff') {
4059 print $cgi->a({-href => "#patch$patchno"}, "patch") .
4061 } elsif ($diff->{'to_id'} ne $diff->{'from_id'}) {
4062 # "commit" view and modified file (not only pure rename or copy)
4063 print $cgi->a({-href => href(action=>"blobdiff",
4064 hash=>$diff->{'to_id'}, hash_parent=>$diff->{'from_id'},
4065 hash_base=>$hash, hash_parent_base=>$parent,
4066 file_name=>$diff->{'to_file'}, file_parent=>$diff->{'from_file'})},
4070 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
4071 hash_base=>$parent, file_name=>$diff->{'to_file'})},
4074 print $cgi->a({-href => href(action=>"blame", hash_base=>$hash,
4075 file_name=>$diff->{'to_file'})},
4078 print $cgi->a({-href => href(action=>"history", hash_base=>$hash,
4079 file_name=>$diff->{'to_file'})},
4083 } # we should not encounter Unmerged (U) or Unknown (X) status
4086 print "</tbody>" if $has_header;
4090 sub git_patchset_body {
4091 my ($fd, $difftree, $hash, @hash_parents) = @_;
4092 my ($hash_parent) = $hash_parents[0];
4094 my $is_combined = (@hash_parents > 1);
4096 my $patch_number = 0;
4102 print "<div class=\"patchset\">\n";
4104 # skip to first patch
4105 while ($patch_line = <$fd>) {
4108 last if ($patch_line =~ m/^diff /);
4112 while ($patch_line) {
4114 # parse "git diff" header line
4115 if ($patch_line =~ m/^diff --git (\"(?:[^\\\"]*(?:\\.[^\\\"]*)*)\"|[^ "]*) (.*)$/) {
4116 # $1 is from_name, which we do not use
4117 $to_name = unquote($2);
4118 $to_name =~ s!^b/!!;
4119 } elsif ($patch_line =~ m/^diff --(cc|combined) ("?.*"?)$/) {
4120 # $1 is 'cc' or 'combined', which we do not use
4121 $to_name = unquote($2);
4126 # check if current patch belong to current raw line
4127 # and parse raw git-diff line if needed
4128 if (is_patch_split($diffinfo, { 'to_file' => $to_name })) {
4129 # this is continuation of a split patch
4130 print "<div class=\"patch cont\">\n";
4132 # advance raw git-diff output if needed
4133 $patch_idx++ if defined $diffinfo;
4135 # read and prepare patch information
4136 $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
4138 # compact combined diff output can have some patches skipped
4139 # find which patch (using pathname of result) we are at now;
4141 while ($to_name ne $diffinfo->{'to_file'}) {
4142 print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n" .
4143 format_diff_cc_simplified($diffinfo, @hash_parents) .
4144 "</div>\n"; # class="patch"
4149 last if $patch_idx > $#$difftree;
4150 $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
4154 # modifies %from, %to hashes
4155 parse_from_to_diffinfo($diffinfo, \%from, \%to, @hash_parents);
4157 # this is first patch for raw difftree line with $patch_idx index
4158 # we index @$difftree array from 0, but number patches from 1
4159 print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n";
4163 #assert($patch_line =~ m/^diff /) if DEBUG;
4164 #assert($patch_line !~ m!$/$!) if DEBUG; # is chomp-ed
4166 # print "git diff" header
4167 print format_git_diff_header_line($patch_line, $diffinfo,
4170 # print extended diff header
4171 print "<div class=\"diff extended_header\">\n";
4173 while ($patch_line = <$fd>) {
4176 last EXTENDED_HEADER if ($patch_line =~ m/^--- |^diff /);
4178 print format_extended_diff_header_line($patch_line, $diffinfo,
4181 print "</div>\n"; # class="diff extended_header"
4183 # from-file/to-file diff header
4184 if (! $patch_line) {
4185 print "</div>\n"; # class="patch"
4188 next PATCH if ($patch_line =~ m/^diff /);
4189 #assert($patch_line =~ m/^---/) if DEBUG;
4191 my $last_patch_line = $patch_line;
4192 $patch_line = <$fd>;
4194 #assert($patch_line =~ m/^\+\+\+/) if DEBUG;
4196 print format_diff_from_to_header($last_patch_line, $patch_line,
4197 $diffinfo, \%from, \%to,
4202 while ($patch_line = <$fd>) {
4205 next PATCH if ($patch_line =~ m/^diff /);
4207 print format_diff_line($patch_line, \%from, \%to);
4211 print "</div>\n"; # class="patch"
4214 # for compact combined (--cc) format, with chunk and patch simpliciaction
4215 # patchset might be empty, but there might be unprocessed raw lines
4216 for (++$patch_idx if $patch_number > 0;
4217 $patch_idx < @$difftree;
4219 # read and prepare patch information
4220 $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
4222 # generate anchor for "patch" links in difftree / whatchanged part
4223 print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n" .
4224 format_diff_cc_simplified($diffinfo, @hash_parents) .
4225 "</div>\n"; # class="patch"
4230 if ($patch_number == 0) {
4231 if (@hash_parents > 1) {
4232 print "<div class=\"diff nodifferences\">Trivial merge</div>\n";
4234 print "<div class=\"diff nodifferences\">No differences found</div>\n";
4238 print "</div>\n"; # class="patchset"
4241 # . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
4243 # fills project list info (age, description, owner, forks) for each
4244 # project in the list, removing invalid projects from returned list
4245 # NOTE: modifies $projlist, but does not remove entries from it
4246 sub fill_project_list_info {
4247 my ($projlist, $check_forks) = @_;
4250 my $show_ctags = gitweb_check_feature('ctags');
4252 foreach my $pr (@$projlist) {
4253 my (@activity) = git_get_last_activity($pr->{'path'});
4254 unless (@activity) {
4257 ($pr->{'age'}, $pr->{'age_string'}) = @activity;
4258 if (!defined $pr->{'descr'}) {
4259 my $descr = git_get_project_description($pr->{'path'}) || "";
4260 $descr = to_utf8($descr);
4261 $pr->{'descr_long'} = $descr;
4262 $pr->{'descr'} = chop_str($descr, $projects_list_description_width, 5);
4264 if (!defined $pr->{'owner'}) {
4265 $pr->{'owner'} = git_get_project_owner("$pr->{'path'}") || "";
4268 my $pname = $pr->{'path'};
4269 if (($pname =~ s/\.git$//) &&
4270 ($pname !~ /\/$/) &&
4271 (-d "$projectroot/$pname")) {
4272 $pr->{'forks'} = "-d $projectroot/$pname";
4277 $show_ctags and $pr->{'ctags'} = git_get_project_ctags($pr->{'path'});
4278 push @projects, $pr;
4284 # print 'sort by' <th> element, generating 'sort by $name' replay link
4285 # if that order is not selected
4287 my ($name, $order, $header) = @_;
4288 $header ||= ucfirst($name);
4290 if ($order eq $name) {
4291 print "<th>$header</th>\n";
4294 $cgi->a({-href => href(-replay=>1, order=>$name),
4295 -class => "header"}, $header) .
4300 sub git_project_list_body {
4301 # actually uses global variable $project
4302 my ($projlist, $order, $from, $to, $extra, $no_header) = @_;
4304 my $check_forks = gitweb_check_feature('forks');
4305 my @projects = fill_project_list_info($projlist, $check_forks);
4307 $order ||= $default_projects_order;
4308 $from = 0 unless defined $from;
4309 $to = $#projects if (!defined $to || $#projects < $to);
4312 project => { key => 'path', type => 'str' },
4313 descr => { key => 'descr_long', type => 'str' },
4314 owner => { key => 'owner', type => 'str' },
4315 age => { key => 'age', type => 'num' }
4317 my $oi = $order_info{$order};
4318 if ($oi->{'type'} eq 'str') {
4319 @projects = sort {$a->{$oi->{'key'}} cmp $b->{$oi->{'key'}}} @projects;
4321 @projects = sort {$a->{$oi->{'key'}} <=> $b->{$oi->{'key'}}} @projects;
4324 my $show_ctags = gitweb_check_feature('ctags');
4327 foreach my $p (@projects) {
4328 foreach my $ct (keys %{$p->{'ctags'}}) {
4329 $ctags{$ct} += $p->{'ctags'}->{$ct};
4332 my $cloud = git_populate_project_tagcloud(\%ctags);
4333 print git_show_project_tagcloud($cloud, 64);
4336 print "<table class=\"project_list\">\n";
4337 unless ($no_header) {
4340 print "<th></th>\n";
4342 print_sort_th('project', $order, 'Project');
4343 print_sort_th('descr', $order, 'Description');
4344 print_sort_th('owner', $order, 'Owner');
4345 print_sort_th('age', $order, 'Last Change');
4346 print "<th></th>\n" . # for links
4350 my $tagfilter = $cgi->param('by_tag');
4351 for (my $i = $from; $i <= $to; $i++) {
4352 my $pr = $projects[$i];
4354 next if $tagfilter and $show_ctags and not grep { lc $_ eq lc $tagfilter } keys %{$pr->{'ctags'}};
4355 next if $searchtext and not $pr->{'path'} =~ /$searchtext/
4356 and not $pr->{'descr_long'} =~ /$searchtext/;
4357 # Weed out forks or non-matching entries of search
4359 my $forkbase = $project; $forkbase ||= ''; $forkbase =~ s#\.git$#/#;
4360 $forkbase="^$forkbase" if $forkbase;
4361 next if not $searchtext and not $tagfilter and $show_ctags
4362 and $pr->{'path'} =~ m#$forkbase.*/.*#; # regexp-safe
4366 print "<tr class=\"dark\">\n";
4368 print "<tr class=\"light\">\n";
4373 if ($pr->{'forks'}) {
4374 print "<!-- $pr->{'forks'} -->\n";
4375 print $cgi->a({-href => href(project=>$pr->{'path'}, action=>"forks")}, "+");
4379 print "<td>" . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary"),
4380 -class => "list"}, esc_html($pr->{'path'})) . "</td>\n" .
4381 "<td>" . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary"),
4382 -class => "list", -title => $pr->{'descr_long'}},
4383 esc_html($pr->{'descr'})) . "</td>\n" .
4384 "<td><i>" . chop_and_escape_str($pr->{'owner'}, 15) . "</i></td>\n";
4385 print "<td class=\"". age_class($pr->{'age'}) . "\">" .
4386 (defined $pr->{'age_string'} ? $pr->{'age_string'} : "No commits") . "</td>\n" .
4387 "<td class=\"link\">" .
4388 $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary")}, "summary") . " | " .
4389 $cgi->a({-href => href(project=>$pr->{'path'}, action=>"shortlog")}, "shortlog") . " | " .
4390 $cgi->a({-href => href(project=>$pr->{'path'}, action=>"log")}, "log") . " | " .
4391 $cgi->a({-href => href(project=>$pr->{'path'}, action=>"tree")}, "tree") .
4392 ($pr->{'forks'} ? " | " . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"forks")}, "forks") : '') .
4396 if (defined $extra) {
4399 print "<td></td>\n";
4401 print "<td colspan=\"5\">$extra</td>\n" .
4407 sub git_shortlog_body {
4408 # uses global variable $project
4409 my ($commitlist, $from, $to, $refs, $extra) = @_;
4411 $from = 0 unless defined $from;
4412 $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
4414 print "<table class=\"shortlog\">\n";
4416 for (my $i = $from; $i <= $to; $i++) {
4417 my %co = %{$commitlist->[$i]};
4418 my $commit = $co{'id'};
4419 my $ref = format_ref_marker($refs, $commit);
4421 print "<tr class=\"dark\">\n";
4423 print "<tr class=\"light\">\n";
4426 # git_summary() used print "<td><i>$co{'age_string'}</i></td>\n" .
4427 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
4428 format_author_html('td', \%co, 10) . "<td>";
4429 print format_subject_html($co{'title'}, $co{'title_short'},
4430 href(action=>"commit", hash=>$commit), $ref);
4432 "<td class=\"link\">" .
4433 $cgi->a({-href => href(action=>"commit", hash=>$commit)}, "commit") . " | " .
4434 $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff") . " | " .
4435 $cgi->a({-href => href(action=>"tree", hash=>$commit, hash_base=>$commit)}, "tree");
4436 my $snapshot_links = format_snapshot_links($commit);
4437 if (defined $snapshot_links) {
4438 print " | " . $snapshot_links;
4443 if (defined $extra) {
4445 "<td colspan=\"4\">$extra</td>\n" .
4451 sub git_history_body {
4452 # Warning: assumes constant type (blob or tree) during history
4453 my ($commitlist, $from, $to, $refs, $hash_base, $ftype, $extra) = @_;
4455 $from = 0 unless defined $from;
4456 $to = $#{$commitlist} unless (defined $to && $to <= $#{$commitlist});
4458 print "<table class=\"history\">\n";
4460 for (my $i = $from; $i <= $to; $i++) {
4461 my %co = %{$commitlist->[$i]};
4465 my $commit = $co{'id'};
4467 my $ref = format_ref_marker($refs, $commit);
4470 print "<tr class=\"dark\">\n";
4472 print "<tr class=\"light\">\n";
4475 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
4476 # shortlog: format_author_html('td', \%co, 10)
4477 format_author_html('td', \%co, 15, 3) . "<td>";
4478 # originally git_history used chop_str($co{'title'}, 50)
4479 print format_subject_html($co{'title'}, $co{'title_short'},
4480 href(action=>"commit", hash=>$commit), $ref);
4482 "<td class=\"link\">" .
4483 $cgi->a({-href => href(action=>$ftype, hash_base=>$commit, file_name=>$file_name)}, $ftype) . " | " .
4484 $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff");
4486 if ($ftype eq 'blob') {
4487 my $blob_current = git_get_hash_by_path($hash_base, $file_name);
4488 my $blob_parent = git_get_hash_by_path($commit, $file_name);
4489 if (defined $blob_current && defined $blob_parent &&
4490 $blob_current ne $blob_parent) {
4492 $cgi->a({-href => href(action=>"blobdiff",
4493 hash=>$blob_current, hash_parent=>$blob_parent,
4494 hash_base=>$hash_base, hash_parent_base=>$commit,
4495 file_name=>$file_name)},
4502 if (defined $extra) {
4504 "<td colspan=\"4\">$extra</td>\n" .
4511 # uses global variable $project
4512 my ($taglist, $from, $to, $extra) = @_;
4513 $from = 0 unless defined $from;
4514 $to = $#{$taglist} if (!defined $to || $#{$taglist} < $to);
4516 print "<table class=\"tags\">\n";
4518 for (my $i = $from; $i <= $to; $i++) {
4519 my $entry = $taglist->[$i];
4521 my $comment = $tag{'subject'};
4523 if (defined $comment) {
4524 $comment_short = chop_str($comment, 30, 5);
4527 print "<tr class=\"dark\">\n";
4529 print "<tr class=\"light\">\n";
4532 if (defined $tag{'age'}) {
4533 print "<td><i>$tag{'age'}</i></td>\n";
4535 print "<td></td>\n";
4538 $cgi->a({-href => href(action=>$tag{'reftype'}, hash=>$tag{'refid'}),
4539 -class => "list name"}, esc_html($tag{'name'})) .
4542 if (defined $comment) {
4543 print format_subject_html($comment, $comment_short,
4544 href(action=>"tag", hash=>$tag{'id'}));
4547 "<td class=\"selflink\">";
4548 if ($tag{'type'} eq "tag") {
4549 print $cgi->a({-href => href(action=>"tag", hash=>$tag{'id'})}, "tag");
4554 "<td class=\"link\">" . " | " .
4555 $cgi->a({-href => href(action=>$tag{'reftype'}, hash=>$tag{'refid'})}, $tag{'reftype'});
4556 if ($tag{'reftype'} eq "commit") {
4557 print " | " . $cgi->a({-href => href(action=>"shortlog", hash=>$tag{'fullname'})}, "shortlog") .
4558 " | " . $cgi->a({-href => href(action=>"log", hash=>$tag{'fullname'})}, "log");
4559 } elsif ($tag{'reftype'} eq "blob") {
4560 print " | " . $cgi->a({-href => href(action=>"blob_plain", hash=>$tag{'refid'})}, "raw");
4565 if (defined $extra) {
4567 "<td colspan=\"5\">$extra</td>\n" .
4573 sub git_heads_body {
4574 # uses global variable $project
4575 my ($headlist, $head, $from, $to, $extra) = @_;
4576 $from = 0 unless defined $from;
4577 $to = $#{$headlist} if (!defined $to || $#{$headlist} < $to);
4579 print "<table class=\"heads\">\n";
4581 for (my $i = $from; $i <= $to; $i++) {
4582 my $entry = $headlist->[$i];
4584 my $curr = $ref{'id'} eq $head;
4586 print "<tr class=\"dark\">\n";
4588 print "<tr class=\"light\">\n";
4591 print "<td><i>$ref{'age'}</i></td>\n" .
4592 ($curr ? "<td class=\"current_head\">" : "<td>") .
4593 $cgi->a({-href => href(action=>"shortlog", hash=>$ref{'fullname'}),
4594 -class => "list name"},esc_html($ref{'name'})) .
4596 "<td class=\"link\">" .
4597 $cgi->a({-href => href(action=>"shortlog", hash=>$ref{'fullname'})}, "shortlog") . " | " .
4598 $cgi->a({-href => href(action=>"log", hash=>$ref{'fullname'})}, "log") . " | " .
4599 $cgi->a({-href => href(action=>"tree", hash=>$ref{'fullname'}, hash_base=>$ref{'name'})}, "tree") .
4603 if (defined $extra) {
4605 "<td colspan=\"3\">$extra</td>\n" .
4611 sub git_search_grep_body {
4612 my ($commitlist, $from, $to, $extra) = @_;
4613 $from = 0 unless defined $from;
4614 $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
4616 print "<table class=\"commit_search\">\n";
4618 for (my $i = $from; $i <= $to; $i++) {
4619 my %co = %{$commitlist->[$i]};
4623 my $commit = $co{'id'};
4625 print "<tr class=\"dark\">\n";
4627 print "<tr class=\"light\">\n";
4630 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
4631 format_author_html('td', \%co, 15, 5) .
4633 $cgi->a({-href => href(action=>"commit", hash=>$co{'id'}),
4634 -class => "list subject"},
4635 chop_and_escape_str($co{'title'}, 50) . "<br/>");
4636 my $comment = $co{'comment'};
4637 foreach my $line (@$comment) {
4638 if ($line =~ m/^(.*?)($search_regexp)(.*)$/i) {
4639 my ($lead, $match, $trail) = ($1, $2, $3);
4640 $match = chop_str($match, 70, 5, 'center');
4641 my $contextlen = int((80 - length($match))/2);
4642 $contextlen = 30 if ($contextlen > 30);
4643 $lead = chop_str($lead, $contextlen, 10, 'left');
4644 $trail = chop_str($trail, $contextlen, 10, 'right');
4646 $lead = esc_html($lead);
4647 $match = esc_html($match);
4648 $trail = esc_html($trail);
4650 print "$lead<span class=\"match\">$match</span>$trail<br />";
4654 "<td class=\"link\">" .
4655 $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") .
4657 $cgi->a({-href => href(action=>"commitdiff", hash=>$co{'id'})}, "commitdiff") .
4659 $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree");
4663 if (defined $extra) {
4665 "<td colspan=\"3\">$extra</td>\n" .
4671 ## ======================================================================
4672 ## ======================================================================
4675 sub git_project_list {
4676 my $order = $input_params{'order'};
4677 if (defined $order && $order !~ m/none|project|descr|owner|age/) {
4678 die_error(400, "Unknown order parameter");
4681 my @list = git_get_projects_list();
4683 die_error(404, "No projects found");
4687 if (-f $home_text) {
4688 print "<div class=\"index_include\">\n";
4689 insert_file($home_text);
4692 print $cgi->startform(-method => "get") .
4693 "<p class=\"projsearch\">Search:\n" .
4694 $cgi->textfield(-name => "s", -value => $searchtext) . "\n" .
4696 $cgi->end_form() . "\n";
4697 git_project_list_body(\@list, $order);
4702 my $order = $input_params{'order'};
4703 if (defined $order && $order !~ m/none|project|descr|owner|age/) {
4704 die_error(400, "Unknown order parameter");
4707 my @list = git_get_projects_list($project);
4709 die_error(404, "No forks found");
4713 git_print_page_nav('','');
4714 git_print_header_div('summary', "$project forks");
4715 git_project_list_body(\@list, $order);
4719 sub git_project_index {
4720 my @projects = git_get_projects_list($project);
4723 -type => 'text/plain',
4724 -charset => 'utf-8',
4725 -content_disposition => 'inline; filename="index.aux"');
4727 foreach my $pr (@projects) {
4728 if (!exists $pr->{'owner'}) {
4729 $pr->{'owner'} = git_get_project_owner("$pr->{'path'}");
4732 my ($path, $owner) = ($pr->{'path'}, $pr->{'owner'});
4733 # quote as in CGI::Util::encode, but keep the slash, and use '+' for ' '
4734 $path =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf("%%%02X", ord($1))/eg;
4735 $owner =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf("%%%02X", ord($1))/eg;
4739 print "$path $owner\n";
4744 my $descr = git_get_project_description($project) || "none";
4745 my %co = parse_commit("HEAD");
4746 my %cd = %co ? parse_date($co{'committer_epoch'}, $co{'committer_tz'}) : ();
4747 my $head = $co{'id'};
4749 my $owner = git_get_project_owner($project);
4751 my $refs = git_get_references();
4752 # These get_*_list functions return one more to allow us to see if
4753 # there are more ...
4754 my @taglist = git_get_tags_list(16);
4755 my @headlist = git_get_heads_list(16);
4757 my $check_forks = gitweb_check_feature('forks');
4760 @forklist = git_get_projects_list($project);
4764 git_print_page_nav('summary','', $head);
4766 print "<div class=\"title\"> </div>\n";
4767 print "<table class=\"projects_list\">\n" .
4768 "<tr id=\"metadata_desc\"><td>description</td><td>" . esc_html($descr) . "</td></tr>\n" .
4769 "<tr id=\"metadata_owner\"><td>owner</td><td>" . esc_html($owner) . "</td></tr>\n";
4770 if (defined $cd{'rfc2822'}) {
4771 print "<tr id=\"metadata_lchange\"><td>last change</td><td>$cd{'rfc2822'}</td></tr>\n";
4774 # use per project git URL list in $projectroot/$project/cloneurl
4775 # or make project git URL from git base URL and project name
4776 my $url_tag = "URL";
4777 my @url_list = git_get_project_url_list($project);
4778 @url_list = map { "$_/$project" } @git_base_url_list unless @url_list;
4779 foreach my $git_url (@url_list) {
4780 next unless $git_url;
4781 print "<tr class=\"metadata_url\"><td>$url_tag</td><td>$git_url</td></tr>\n";
4786 my $show_ctags = gitweb_check_feature('ctags');
4788 my $ctags = git_get_project_ctags($project);
4789 my $cloud = git_populate_project_tagcloud($ctags);
4790 print "<tr id=\"metadata_ctags\"><td>Content tags:<br />";
4791 print "</td>\n<td>" unless %$ctags;
4792 print "<form action=\"$show_ctags\" method=\"post\"><input type=\"hidden\" name=\"p\" value=\"$project\" />Add: <input type=\"text\" name=\"t\" size=\"8\" /></form>";
4793 print "</td>\n<td>" if %$ctags;
4794 print git_show_project_tagcloud($cloud, 48);
4800 # If XSS prevention is on, we don't include README.html.
4801 # TODO: Allow a readme in some safe format.
4802 if (!$prevent_xss && -s "$projectroot/$project/README.html") {
4803 print "<div class=\"title\">readme</div>\n" .
4804 "<div class=\"readme\">\n";
4805 insert_file("$projectroot/$project/README.html");
4806 print "\n</div>\n"; # class="readme"
4809 # we need to request one more than 16 (0..15) to check if
4811 my @commitlist = $head ? parse_commits($head, 17) : ();
4813 git_print_header_div('shortlog');
4814 git_shortlog_body(\@commitlist, 0, 15, $refs,
4815 $#commitlist <= 15 ? undef :
4816 $cgi->a({-href => href(action=>"shortlog")}, "..."));
4820 git_print_header_div('tags');
4821 git_tags_body(\@taglist, 0, 15,
4822 $#taglist <= 15 ? undef :
4823 $cgi->a({-href => href(action=>"tags")}, "..."));
4827 git_print_header_div('heads');
4828 git_heads_body(\@headlist, $head, 0, 15,
4829 $#headlist <= 15 ? undef :
4830 $cgi->a({-href => href(action=>"heads")}, "..."));
4834 git_print_header_div('forks');
4835 git_project_list_body(\@forklist, 'age', 0, 15,
4836 $#forklist <= 15 ? undef :
4837 $cgi->a({-href => href(action=>"forks")}, "..."),
4845 my $head = git_get_head_hash($project);
4847 git_print_page_nav('','', $head,undef,$head);
4848 my %tag = parse_tag($hash);
4851 die_error(404, "Unknown tag object");
4854 git_print_header_div('commit', esc_html($tag{'name'}), $hash);
4855 print "<div class=\"title_text\">\n" .
4856 "<table class=\"object_header\">\n" .
4858 "<td>object</td>\n" .
4859 "<td>" . $cgi->a({-class => "list", -href => href(action=>$tag{'type'}, hash=>$tag{'object'})},
4860 $tag{'object'}) . "</td>\n" .
4861 "<td class=\"link\">" . $cgi->a({-href => href(action=>$tag{'type'}, hash=>$tag{'object'})},
4862 $tag{'type'}) . "</td>\n" .
4864 if (defined($tag{'author'})) {
4865 git_print_authorship_rows(\%tag, 'author');
4867 print "</table>\n\n" .
4869 print "<div class=\"page_body\">";
4870 my $comment = $tag{'comment'};
4871 foreach my $line (@$comment) {
4873 print esc_html($line, -nbsp=>1) . "<br/>\n";
4879 sub git_blame_common {
4880 my $format = shift || 'porcelain';
4881 if ($format eq 'porcelain' && $cgi->param('js')) {
4882 $format = 'incremental';
4883 $action = 'blame_incremental'; # for page title etc
4887 gitweb_check_feature('blame')
4888 or die_error(403, "Blame view not allowed");
4891 die_error(400, "No file name given") unless $file_name;
4892 $hash_base ||= git_get_head_hash($project);
4893 die_error(404, "Couldn't find base commit") unless $hash_base;
4894 my %co = parse_commit($hash_base)
4895 or die_error(404, "Commit not found");
4897 if (!defined $hash) {
4898 $hash = git_get_hash_by_path($hash_base, $file_name, "blob")
4899 or die_error(404, "Error looking up file");
4901 $ftype = git_get_type($hash);
4902 if ($ftype !~ "blob") {
4903 die_error(400, "Object is not a blob");
4908 if ($format eq 'incremental') {
4909 # get file contents (as base)
4910 open $fd, "-|", git_cmd(), 'cat-file', 'blob', $hash
4911 or die_error(500, "Open git-cat-file failed");
4912 } elsif ($format eq 'data') {
4913 # run git-blame --incremental
4914 open $fd, "-|", git_cmd(), "blame", "--incremental",
4915 $hash_base, "--", $file_name
4916 or die_error(500, "Open git-blame --incremental failed");
4918 # run git-blame --porcelain
4919 open $fd, "-|", git_cmd(), "blame", '-p',
4920 $hash_base, '--', $file_name
4921 or die_error(500, "Open git-blame --porcelain failed");
4924 # incremental blame data returns early
4925 if ($format eq 'data') {
4927 -type=>"text/plain", -charset => "utf-8",
4928 -status=> "200 OK");
4929 local $| = 1; # output autoflush
4932 or print "ERROR $!\n";
4935 if (defined $t0 && gitweb_check_feature('timed')) {
4937 Time::HiRes::tv_interval($t0, [Time::HiRes::gettimeofday()]).
4938 ' '.$number_of_git_cmds;
4948 $cgi->a({-href => href(action=>"blob", -replay=>1)},
4951 $cgi->a({-href => href(action=>"history", -replay=>1)},
4954 $cgi->a({-href => href(action=>$action, file_name=>$file_name)},
4956 git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
4957 git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
4958 git_print_page_path($file_name, $ftype, $hash_base);
4961 if ($format eq 'incremental') {
4962 print "<noscript>\n<div class=\"error\"><center><b>\n".
4963 "This page requires JavaScript to run.\n Use ".
4964 $cgi->a({-href => href(action=>'blame',javascript=>0,-replay=>1)},
4967 "</b></center></div>\n</noscript>\n";
4969 print qq!<div id="progress_bar" style="width: 100%; background-color: yellow"></div>\n!;
4972 print qq!<div class="page_body">\n!;
4973 print qq!<div id="progress_info">... / ...</div>\n!
4974 if ($format eq 'incremental');
4975 print qq!<table id="blame_table" class="blame" width="100%">\n!.
4976 #qq!<col width="5.5em" /><col width="2.5em" /><col width="*" />\n!.
4978 qq!<tr><th>Commit</th><th>Line</th><th>Data</th></tr>\n!.
4982 my @rev_color = qw(light dark);
4983 my $num_colors = scalar(@rev_color);
4984 my $current_color = 0;
4986 if ($format eq 'incremental') {
4987 my $color_class = $rev_color[$current_color];
4992 while (my $line = <$fd>) {
4996 print qq!<tr id="l$linenr" class="$color_class">!.
4997 qq!<td class="sha1"><a href=""> </a></td>!.
4998 qq!<td class="linenr">!.
4999 qq!<a class="linenr" href="">$linenr</a></td>!;
5000 print qq!<td class="pre">! . esc_html($line) . "</td>\n";
5004 } else { # porcelain, i.e. ordinary blame
5005 my %metainfo = (); # saves information about commits
5009 while (my $line = <$fd>) {
5011 # the header: <SHA-1> <src lineno> <dst lineno> [<lines in group>]
5012 # no <lines in group> for subsequent lines in group of lines
5013 my ($full_rev, $orig_lineno, $lineno, $group_size) =
5014 ($line =~ /^([0-9a-f]{40}) (\d+) (\d+)(?: (\d+))?$/);
5015 if (!exists $metainfo{$full_rev}) {
5016 $metainfo{$full_rev} = { 'nprevious' => 0 };
5018 my $meta = $metainfo{$full_rev};
5020 while ($data = <$fd>) {
5022 last if ($data =~ s/^\t//); # contents of line
5023 if ($data =~ /^(\S+)(?: (.*))?$/) {
5024 $meta->{$1} = $2 unless exists $meta->{$1};
5026 if ($data =~ /^previous /) {
5027 $meta->{'nprevious'}++;
5030 my $short_rev = substr($full_rev, 0, 8);
5031 my $author = $meta->{'author'};
5033 parse_date($meta->{'author-time'}, $meta->{'author-tz'});
5034 my $date = $date{'iso-tz'};
5036 $current_color = ($current_color + 1) % $num_colors;
5038 my $tr_class = $rev_color[$current_color];
5039 $tr_class .= ' boundary' if (exists $meta->{'boundary'});
5040 $tr_class .= ' no-previous' if ($meta->{'nprevious'} == 0);
5041 $tr_class .= ' multiple-previous' if ($meta->{'nprevious'} > 1);
5042 print "<tr id=\"l$lineno\" class=\"$tr_class\">\n";
5044 print "<td class=\"sha1\"";
5045 print " title=\"". esc_html($author) . ", $date\"";
5046 print " rowspan=\"$group_size\"" if ($group_size > 1);
5048 print $cgi->a({-href => href(action=>"commit",
5050 file_name=>$file_name)},
5051 esc_html($short_rev));
5052 if ($group_size >= 2) {
5053 my @author_initials = ($author =~ /\b([[:upper:]])\B/g);
5054 if (@author_initials) {
5056 esc_html(join('', @author_initials));
5062 # 'previous' <sha1 of parent commit> <filename at commit>
5063 if (exists $meta->{'previous'} &&
5064 $meta->{'previous'} =~ /^([a-fA-F0-9]{40}) (.*)$/) {
5065 $meta->{'parent'} = $1;
5066 $meta->{'file_parent'} = unquote($2);
5069 exists($meta->{'parent'}) ?
5070 $meta->{'parent'} : $full_rev;
5071 my $linenr_filename =
5072 exists($meta->{'file_parent'}) ?
5073 $meta->{'file_parent'} : unquote($meta->{'filename'});
5074 my $blamed = href(action => 'blame',
5075 file_name => $linenr_filename,
5076 hash_base => $linenr_commit);
5077 print "<td class=\"linenr\">";
5078 print $cgi->a({ -href => "$blamed#l$orig_lineno",
5079 -class => "linenr" },
5082 print "<td class=\"pre\">" . esc_html($data) . "</td>\n";
5090 "</table>\n"; # class="blame"
5091 print "</div>\n"; # class="blame_body"
5093 or print "Reading blob failed\n";
5102 sub git_blame_incremental {
5103 git_blame_common('incremental');
5106 sub git_blame_data {
5107 git_blame_common('data');
5111 my $head = git_get_head_hash($project);
5113 git_print_page_nav('','', $head,undef,$head);
5114 git_print_header_div('summary', $project);
5116 my @tagslist = git_get_tags_list();
5118 git_tags_body(\@tagslist);
5124 my $head = git_get_head_hash($project);
5126 git_print_page_nav('','', $head,undef,$head);
5127 git_print_header_div('summary', $project);
5129 my @headslist = git_get_heads_list();
5131 git_heads_body(\@headslist, $head);
5136 sub git_blob_plain {
5140 if (!defined $hash) {
5141 if (defined $file_name) {
5142 my $base = $hash_base || git_get_head_hash($project);
5143 $hash = git_get_hash_by_path($base, $file_name, "blob")
5144 or die_error(404, "Cannot find file");
5146 die_error(400, "No file name defined");
5148 } elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) {
5149 # blobs defined by non-textual hash id's can be cached
5153 open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
5154 or die_error(500, "Open git-cat-file blob '$hash' failed");
5156 # content-type (can include charset)
5157 $type = blob_contenttype($fd, $file_name, $type);
5159 # "save as" filename, even when no $file_name is given
5160 my $save_as = "$hash";
5161 if (defined $file_name) {
5162 $save_as = $file_name;
5163 } elsif ($type =~ m/^text\//) {
5167 # With XSS prevention on, blobs of all types except a few known safe
5168 # ones are served with "Content-Disposition: attachment" to make sure
5169 # they don't run in our security domain. For certain image types,
5170 # blob view writes an <img> tag referring to blob_plain view, and we
5171 # want to be sure not to break that by serving the image as an
5172 # attachment (though Firefox 3 doesn't seem to care).
5173 my $sandbox = $prevent_xss &&
5174 $type !~ m!^(?:text/plain|image/(?:gif|png|jpeg))$!;
5178 -expires => $expires,
5179 -content_disposition =>
5180 ($sandbox ? 'attachment' : 'inline')
5181 . '; filename="' . $save_as . '"');
5183 binmode STDOUT, ':raw';
5185 binmode STDOUT, ':utf8'; # as set at the beginning of gitweb.cgi
5192 if (!defined $hash) {
5193 if (defined $file_name) {
5194 my $base = $hash_base || git_get_head_hash($project);
5195 $hash = git_get_hash_by_path($base, $file_name, "blob")
5196 or die_error(404, "Cannot find file");
5198 die_error(400, "No file name defined");
5200 } elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) {
5201 # blobs defined by non-textual hash id's can be cached
5205 my $have_blame = gitweb_check_feature('blame');
5206 open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
5207 or die_error(500, "Couldn't cat $file_name, $hash");
5208 my $mimetype = blob_mimetype($fd, $file_name);
5209 if ($mimetype !~ m!^(?:text/|image/(?:gif|png|jpeg)$)! && -B $fd) {
5211 return git_blob_plain($mimetype);
5213 # we can have blame only for text/* mimetype
5214 $have_blame &&= ($mimetype =~ m!^text/!);
5216 git_header_html(undef, $expires);
5217 my $formats_nav = '';
5218 if (defined $hash_base && (my %co = parse_commit($hash_base))) {
5219 if (defined $file_name) {
5222 $cgi->a({-href => href(action=>"blame", -replay=>1)},
5227 $cgi->a({-href => href(action=>"history", -replay=>1)},
5230 $cgi->a({-href => href(action=>"blob_plain", -replay=>1)},
5233 $cgi->a({-href => href(action=>"blob",
5234 hash_base=>"HEAD", file_name=>$file_name)},
5238 $cgi->a({-href => href(action=>"blob_plain", -replay=>1)},
5241 git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
5242 git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
5244 print "<div class=\"page_nav\">\n" .
5245 "<br/><br/></div>\n" .
5246 "<div class=\"title\">$hash</div>\n";
5248 git_print_page_path($file_name, "blob", $hash_base);
5249 print "<div class=\"page_body\">\n";
5250 if ($mimetype =~ m!^image/!) {
5251 print qq!<img type="$mimetype"!;
5253 print qq! alt="$file_name" title="$file_name"!;
5256 href(action=>"blob_plain", hash=>$hash,
5257 hash_base=>$hash_base, file_name=>$file_name) .
5261 while (my $line = <$fd>) {
5264 $line = untabify($line);
5265 printf "<div class=\"pre\"><a id=\"l%i\" href=\"#l%i\" class=\"linenr\">%4i</a> %s</div>\n",
5266 $nr, $nr, $nr, esc_html($line, -nbsp=>1);
5270 or print "Reading blob failed.\n";
5276 if (!defined $hash_base) {
5277 $hash_base = "HEAD";
5279 if (!defined $hash) {
5280 if (defined $file_name) {
5281 $hash = git_get_hash_by_path($hash_base, $file_name, "tree");
5286 die_error(404, "No such tree") unless defined($hash);
5288 my $show_sizes = gitweb_check_feature('show-sizes');
5289 my $have_blame = gitweb_check_feature('blame');
5294 open my $fd, "-|", git_cmd(), "ls-tree", '-z',
5295 ($show_sizes ? '-l' : ()), @extra_options, $hash
5296 or die_error(500, "Open git-ls-tree failed");
5297 @entries = map { chomp; $_ } <$fd>;
5299 or die_error(404, "Reading tree failed");
5302 my $refs = git_get_references();
5303 my $ref = format_ref_marker($refs, $hash_base);
5306 if (defined $hash_base && (my %co = parse_commit($hash_base))) {
5308 if (defined $file_name) {
5310 $cgi->a({-href => href(action=>"history", -replay=>1)},
5312 $cgi->a({-href => href(action=>"tree",
5313 hash_base=>"HEAD", file_name=>$file_name)},
5316 my $snapshot_links = format_snapshot_links($hash);
5317 if (defined $snapshot_links) {
5318 # FIXME: Should be available when we have no hash base as well.
5319 push @views_nav, $snapshot_links;
5321 git_print_page_nav('tree','', $hash_base, undef, undef,
5322 join(' | ', @views_nav));
5323 git_print_header_div('commit', esc_html($co{'title'}) . $ref, $hash_base);
5326 print "<div class=\"page_nav\">\n";
5327 print "<br/><br/></div>\n";
5328 print "<div class=\"title\">$hash</div>\n";
5330 if (defined $file_name) {
5331 $basedir = $file_name;
5332 if ($basedir ne '' && substr($basedir, -1) ne '/') {
5335 git_print_page_path($file_name, 'tree', $hash_base);
5337 print "<div class=\"page_body\">\n";
5338 print "<table class=\"tree\">\n";
5340 # '..' (top directory) link if possible
5341 if (defined $hash_base &&
5342 defined $file_name && $file_name =~ m![^/]+$!) {
5344 print "<tr class=\"dark\">\n";
5346 print "<tr class=\"light\">\n";
5350 my $up = $file_name;
5351 $up =~ s!/?[^/]+$!!;
5352 undef $up unless $up;
5353 # based on git_print_tree_entry
5354 print '<td class="mode">' . mode_str('040000') . "</td>\n";
5355 print '<td class="size"> </td>'."\n" if $show_sizes;
5356 print '<td class="list">';
5357 print $cgi->a({-href => href(action=>"tree",
5358 hash_base=>$hash_base,
5362 print "<td class=\"link\"></td>\n";
5366 foreach my $line (@entries) {
5367 my %t = parse_ls_tree_line($line, -z => 1, -l => $show_sizes);
5370 print "<tr class=\"dark\">\n";
5372 print "<tr class=\"light\">\n";
5376 git_print_tree_entry(\%t, $basedir, $hash_base, $have_blame);
5380 print "</table>\n" .
5386 my $format = $input_params{'snapshot_format'};
5387 if (!@snapshot_fmts) {
5388 die_error(403, "Snapshots not allowed");
5390 # default to first supported snapshot format
5391 $format ||= $snapshot_fmts[0];
5392 if ($format !~ m/^[a-z0-9]+$/) {
5393 die_error(400, "Invalid snapshot format parameter");
5394 } elsif (!exists($known_snapshot_formats{$format})) {
5395 die_error(400, "Unknown snapshot format");
5396 } elsif ($known_snapshot_formats{$format}{'disabled'}) {
5397 die_error(403, "Snapshot format not allowed");
5398 } elsif (!grep($_ eq $format, @snapshot_fmts)) {
5399 die_error(403, "Unsupported snapshot format");
5402 my $type = git_get_type("$hash^{}");
5404 die_error(404, 'Object does not exist');
5405 } elsif ($type eq 'blob') {
5406 die_error(400, 'Object is not a tree-ish');
5410 my $full_hash = git_get_full_hash($project, $hash);
5411 if ($full_hash =~ /^$hash/) {
5412 $hash = git_get_short_hash($project, $hash);
5414 $hash .= '-' . git_get_short_hash($project, $hash);
5416 my $name = $project;
5417 $name =~ s,([^/])/*\.git$,$1,;
5418 $name = basename($name);
5419 my $filename = to_utf8($name);
5420 $name =~ s/\047/\047\\\047\047/g;
5422 $filename .= "-$hash$known_snapshot_formats{$format}{'suffix'}";
5423 $cmd = quote_command(
5424 git_cmd(), 'archive',
5425 "--format=$known_snapshot_formats{$format}{'format'}",
5426 "--prefix=$name/", $full_hash);
5427 if (exists $known_snapshot_formats{$format}{'compressor'}) {
5428 $cmd .= ' | ' . quote_command(@{$known_snapshot_formats{$format}{'compressor'}});
5432 -type => $known_snapshot_formats{$format}{'type'},
5433 -content_disposition => 'inline; filename="' . "$filename" . '"',
5434 -status => '200 OK');
5436 open my $fd, "-|", $cmd
5437 or die_error(500, "Execute git-archive failed");
5438 binmode STDOUT, ':raw';
5440 binmode STDOUT, ':utf8'; # as set at the beginning of gitweb.cgi
5445 my $head = git_get_head_hash($project);
5446 if (!defined $hash) {
5449 if (!defined $page) {
5452 my $refs = git_get_references();
5454 my @commitlist = parse_commits($hash, 101, (100 * $page));
5456 my $paging_nav = format_paging_nav('log', $hash, $head, $page, $#commitlist >= 100);
5458 my ($patch_max) = gitweb_get_feature('patches');
5460 if ($patch_max < 0 || @commitlist <= $patch_max) {
5461 $paging_nav .= " ⋅ " .
5462 $cgi->a({-href => href(action=>"patches", -replay=>1)},
5468 git_print_page_nav('log','', $hash,undef,undef, $paging_nav);
5471 my %co = parse_commit($hash);
5473 git_print_header_div('summary', $project);
5474 print "<div class=\"page_body\"> Last change $co{'age_string'}.<br/><br/></div>\n";
5476 my $to = ($#commitlist >= 99) ? (99) : ($#commitlist);
5477 for (my $i = 0; $i <= $to; $i++) {
5478 my %co = %{$commitlist[$i]};
5480 my $commit = $co{'id'};
5481 my $ref = format_ref_marker($refs, $commit);
5482 my %ad = parse_date($co{'author_epoch'});
5483 git_print_header_div('commit',
5484 "<span class=\"age\">$co{'age_string'}</span>" .
5485 esc_html($co{'title'}) . $ref,
5487 print "<div class=\"title_text\">\n" .
5488 "<div class=\"log_link\">\n" .
5489 $cgi->a({-href => href(action=>"commit", hash=>$commit)}, "commit") .
5491 $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff") .
5493 $cgi->a({-href => href(action=>"tree", hash=>$commit, hash_base=>$commit)}, "tree") .
5496 git_print_authorship(\%co, -tag => 'span');
5497 print "<br/>\n</div>\n";
5499 print "<div class=\"log_body\">\n";
5500 git_print_log($co{'comment'}, -final_empty_line=> 1);
5503 if ($#commitlist >= 100) {
5504 print "<div class=\"page_nav\">\n";
5505 print $cgi->a({-href => href(-replay=>1, page=>$page+1),
5506 -accesskey => "n", -title => "Alt-n"}, "next");
5513 $hash ||= $hash_base || "HEAD";
5514 my %co = parse_commit($hash)
5515 or die_error(404, "Unknown commit object");
5517 my $parent = $co{'parent'};
5518 my $parents = $co{'parents'}; # listref
5520 # we need to prepare $formats_nav before any parameter munging
5522 if (!defined $parent) {
5524 $formats_nav .= '(initial)';
5525 } elsif (@$parents == 1) {
5526 # single parent commit
5529 $cgi->a({-href => href(action=>"commit",
5531 esc_html(substr($parent, 0, 7))) .
5538 $cgi->a({-href => href(action=>"commit",
5540 esc_html(substr($_, 0, 7)));
5544 if (gitweb_check_feature('patches')) {
5545 $formats_nav .= " | " .
5546 $cgi->a({-href => href(action=>"patch", -replay=>1)},
5550 if (!defined $parent) {
5554 open my $fd, "-|", git_cmd(), "diff-tree", '-r', "--no-commit-id",
5556 (@$parents <= 1 ? $parent : '-c'),
5558 or die_error(500, "Open git-diff-tree failed");
5559 @difftree = map { chomp; $_ } <$fd>;
5560 close $fd or die_error(404, "Reading git-diff-tree failed");
5562 # non-textual hash id's can be cached
5564 if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
5567 my $refs = git_get_references();
5568 my $ref = format_ref_marker($refs, $co{'id'});
5570 git_header_html(undef, $expires);
5571 git_print_page_nav('commit', '',
5572 $hash, $co{'tree'}, $hash,
5575 if (defined $co{'parent'}) {
5576 git_print_header_div('commitdiff', esc_html($co{'title'}) . $ref, $hash);
5578 git_print_header_div('tree', esc_html($co{'title'}) . $ref, $co{'tree'}, $hash);
5580 print "<div class=\"title_text\">\n" .
5581 "<table class=\"object_header\">\n";
5582 git_print_authorship_rows(\%co);
5583 print "<tr><td>commit</td><td class=\"sha1\">$co{'id'}</td></tr>\n";
5586 "<td class=\"sha1\">" .
5587 $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$hash),
5588 class => "list"}, $co{'tree'}) .
5590 "<td class=\"link\">" .
5591 $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$hash)},
5593 my $snapshot_links = format_snapshot_links($hash);
5594 if (defined $snapshot_links) {
5595 print " | " . $snapshot_links;
5600 foreach my $par (@$parents) {
5603 "<td class=\"sha1\">" .
5604 $cgi->a({-href => href(action=>"commit", hash=>$par),
5605 class => "list"}, $par) .
5607 "<td class=\"link\">" .
5608 $cgi->a({-href => href(action=>"commit", hash=>$par)}, "commit") .
5610 $cgi->a({-href => href(action=>"commitdiff", hash=>$hash, hash_parent=>$par)}, "diff") .
5617 print "<div class=\"page_body\">\n";
5618 git_print_log($co{'comment'});
5621 git_difftree_body(\@difftree, $hash, @$parents);
5627 # object is defined by:
5628 # - hash or hash_base alone
5629 # - hash_base and file_name
5632 # - hash or hash_base alone
5633 if ($hash || ($hash_base && !defined $file_name)) {
5634 my $object_id = $hash || $hash_base;
5636 open my $fd, "-|", quote_command(
5637 git_cmd(), 'cat-file', '-t', $object_id) . ' 2> /dev/null'
5638 or die_error(404, "Object does not exist");
5642 or die_error(404, "Object does not exist");
5644 # - hash_base and file_name
5645 } elsif ($hash_base && defined $file_name) {
5646 $file_name =~ s,/+$,,;
5648 system(git_cmd(), "cat-file", '-e', $hash_base) == 0
5649 or die_error(404, "Base object does not exist");
5651 # here errors should not hapen
5652 open my $fd, "-|", git_cmd(), "ls-tree", $hash_base, "--", $file_name
5653 or die_error(500, "Open git-ls-tree failed");
5657 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'
5658 unless ($line && $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t/) {
5659 die_error(404, "File or directory for given base does not exist");
5664 die_error(400, "Not enough information to find object");
5667 print $cgi->redirect(-uri => href(action=>$type, -full=>1,
5668 hash=>$hash, hash_base=>$hash_base,
5669 file_name=>$file_name),
5670 -status => '302 Found');
5674 my $format = shift || 'html';
5681 # preparing $fd and %diffinfo for git_patchset_body
5683 if (defined $hash_base && defined $hash_parent_base) {
5684 if (defined $file_name) {
5686 open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
5687 $hash_parent_base, $hash_base,
5688 "--", (defined $file_parent ? $file_parent : ()), $file_name
5689 or die_error(500, "Open git-diff-tree failed");
5690 @difftree = map { chomp; $_ } <$fd>;
5692 or die_error(404, "Reading git-diff-tree failed");
5694 or die_error(404, "Blob diff not found");
5696 } elsif (defined $hash &&
5697 $hash =~ /[0-9a-fA-F]{40}/) {
5698 # try to find filename from $hash
5700 # read filtered raw output
5701 open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
5702 $hash_parent_base, $hash_base, "--"
5703 or die_error(500, "Open git-diff-tree failed");
5705 # ':100644 100644 03b21826... 3b93d5e7... M ls-files.c'
5707 grep { /^:[0-7]{6} [0-7]{6} [0-9a-fA-F]{40} $hash/ }
5708 map { chomp; $_ } <$fd>;
5710 or die_error(404, "Reading git-diff-tree failed");
5712 or die_error(404, "Blob diff not found");
5715 die_error(400, "Missing one of the blob diff parameters");
5718 if (@difftree > 1) {
5719 die_error(400, "Ambiguous blob diff specification");
5722 %diffinfo = parse_difftree_raw_line($difftree[0]);
5723 $file_parent ||= $diffinfo{'from_file'} || $file_name;
5724 $file_name ||= $diffinfo{'to_file'};
5726 $hash_parent ||= $diffinfo{'from_id'};
5727 $hash ||= $diffinfo{'to_id'};
5729 # non-textual hash id's can be cached
5730 if ($hash_base =~ m/^[0-9a-fA-F]{40}$/ &&
5731 $hash_parent_base =~ m/^[0-9a-fA-F]{40}$/) {
5736 open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
5737 '-p', ($format eq 'html' ? "--full-index" : ()),
5738 $hash_parent_base, $hash_base,
5739 "--", (defined $file_parent ? $file_parent : ()), $file_name
5740 or die_error(500, "Open git-diff-tree failed");
5743 # old/legacy style URI -- not generated anymore since 1.4.3.
5745 die_error('404 Not Found', "Missing one of the blob diff parameters")
5749 if ($format eq 'html') {
5751 $cgi->a({-href => href(action=>"blobdiff_plain", -replay=>1)},
5753 git_header_html(undef, $expires);
5754 if (defined $hash_base && (my %co = parse_commit($hash_base))) {
5755 git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
5756 git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
5758 print "<div class=\"page_nav\"><br/>$formats_nav<br/></div>\n";
5759 print "<div class=\"title\">$hash vs $hash_parent</div>\n";
5761 if (defined $file_name) {
5762 git_print_page_path($file_name, "blob", $hash_base);
5764 print "<div class=\"page_path\"></div>\n";
5767 } elsif ($format eq 'plain') {
5769 -type => 'text/plain',
5770 -charset => 'utf-8',
5771 -expires => $expires,
5772 -content_disposition => 'inline; filename="' . "$file_name" . '.patch"');
5774 print "X-Git-Url: " . $cgi->self_url() . "\n\n";
5777 die_error(400, "Unknown blobdiff format");
5781 if ($format eq 'html') {
5782 print "<div class=\"page_body\">\n";
5784 git_patchset_body($fd, [ \%diffinfo ], $hash_base, $hash_parent_base);
5787 print "</div>\n"; # class="page_body"
5791 while (my $line = <$fd>) {
5792 $line =~ s!a/($hash|$hash_parent)!'a/'.esc_path($diffinfo{'from_file'})!eg;
5793 $line =~ s!b/($hash|$hash_parent)!'b/'.esc_path($diffinfo{'to_file'})!eg;
5797 last if $line =~ m!^\+\+\+!;
5805 sub git_blobdiff_plain {
5806 git_blobdiff('plain');
5809 sub git_commitdiff {
5811 my $format = $params{-format} || 'html';
5813 my ($patch_max) = gitweb_get_feature('patches');
5814 if ($format eq 'patch') {
5815 die_error(403, "Patch view not allowed") unless $patch_max;
5818 $hash ||= $hash_base || "HEAD";
5819 my %co = parse_commit($hash)
5820 or die_error(404, "Unknown commit object");
5822 # choose format for commitdiff for merge
5823 if (! defined $hash_parent && @{$co{'parents'}} > 1) {
5824 $hash_parent = '--cc';
5826 # we need to prepare $formats_nav before almost any parameter munging
5828 if ($format eq 'html') {
5830 $cgi->a({-href => href(action=>"commitdiff_plain", -replay=>1)},
5833 $formats_nav .= " | " .
5834 $cgi->a({-href => href(action=>"patch", -replay=>1)},
5838 if (defined $hash_parent &&
5839 $hash_parent ne '-c' && $hash_parent ne '--cc') {
5840 # commitdiff with two commits given
5841 my $hash_parent_short = $hash_parent;
5842 if ($hash_parent =~ m/^[0-9a-fA-F]{40}$/) {
5843 $hash_parent_short = substr($hash_parent, 0, 7);
5847 for (my $i = 0; $i < @{$co{'parents'}}; $i++) {
5848 if ($co{'parents'}[$i] eq $hash_parent) {
5849 $formats_nav .= ' parent ' . ($i+1);
5853 $formats_nav .= ': ' .
5854 $cgi->a({-href => href(action=>"commitdiff",
5855 hash=>$hash_parent)},
5856 esc_html($hash_parent_short)) .
5858 } elsif (!$co{'parent'}) {
5860 $formats_nav .= ' (initial)';
5861 } elsif (scalar @{$co{'parents'}} == 1) {
5862 # single parent commit
5865 $cgi->a({-href => href(action=>"commitdiff",
5866 hash=>$co{'parent'})},
5867 esc_html(substr($co{'parent'}, 0, 7))) .
5871 if ($hash_parent eq '--cc') {
5872 $formats_nav .= ' | ' .
5873 $cgi->a({-href => href(action=>"commitdiff",
5874 hash=>$hash, hash_parent=>'-c')},
5876 } else { # $hash_parent eq '-c'
5877 $formats_nav .= ' | ' .
5878 $cgi->a({-href => href(action=>"commitdiff",
5879 hash=>$hash, hash_parent=>'--cc')},
5885 $cgi->a({-href => href(action=>"commitdiff",
5887 esc_html(substr($_, 0, 7)));
5888 } @{$co{'parents'}} ) .
5893 my $hash_parent_param = $hash_parent;
5894 if (!defined $hash_parent_param) {
5895 # --cc for multiple parents, --root for parentless
5896 $hash_parent_param =
5897 @{$co{'parents'}} > 1 ? '--cc' : $co{'parent'} || '--root';
5903 if ($format eq 'html') {
5904 open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
5905 "--no-commit-id", "--patch-with-raw", "--full-index",
5906 $hash_parent_param, $hash, "--"
5907 or die_error(500, "Open git-diff-tree failed");
5909 while (my $line = <$fd>) {
5911 # empty line ends raw part of diff-tree output
5913 push @difftree, scalar parse_difftree_raw_line($line);
5916 } elsif ($format eq 'plain') {
5917 open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
5918 '-p', $hash_parent_param, $hash, "--"
5919 or die_error(500, "Open git-diff-tree failed");
5920 } elsif ($format eq 'patch') {
5921 # For commit ranges, we limit the output to the number of
5922 # patches specified in the 'patches' feature.
5923 # For single commits, we limit the output to a single patch,
5924 # diverging from the git-format-patch default.
5925 my @commit_spec = ();
5927 if ($patch_max > 0) {
5928 push @commit_spec, "-$patch_max";
5930 push @commit_spec, '-n', "$hash_parent..$hash";
5932 if ($params{-single}) {
5933 push @commit_spec, '-1';
5935 if ($patch_max > 0) {
5936 push @commit_spec, "-$patch_max";
5938 push @commit_spec, "-n";
5940 push @commit_spec, '--root', $hash;
5942 open $fd, "-|", git_cmd(), "format-patch", '--encoding=utf8',
5943 '--stdout', @commit_spec
5944 or die_error(500, "Open git-format-patch failed");
5946 die_error(400, "Unknown commitdiff format");
5949 # non-textual hash id's can be cached
5951 if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
5955 # write commit message
5956 if ($format eq 'html') {
5957 my $refs = git_get_references();
5958 my $ref = format_ref_marker($refs, $co{'id'});
5960 git_header_html(undef, $expires);
5961 git_print_page_nav('commitdiff','', $hash,$co{'tree'},$hash, $formats_nav);
5962 git_print_header_div('commit', esc_html($co{'title'}) . $ref, $hash);
5963 print "<div class=\"title_text\">\n" .
5964 "<table class=\"object_header\">\n";
5965 git_print_authorship_rows(\%co);
5968 print "<div class=\"page_body\">\n";
5969 if (@{$co{'comment'}} > 1) {
5970 print "<div class=\"log\">\n";
5971 git_print_log($co{'comment'}, -final_empty_line=> 1, -remove_title => 1);
5972 print "</div>\n"; # class="log"
5975 } elsif ($format eq 'plain') {
5976 my $refs = git_get_references("tags");
5977 my $tagname = git_get_rev_name_tags($hash);
5978 my $filename = basename($project) . "-$hash.patch";
5981 -type => 'text/plain',
5982 -charset => 'utf-8',
5983 -expires => $expires,
5984 -content_disposition => 'inline; filename="' . "$filename" . '"');
5985 my %ad = parse_date($co{'author_epoch'}, $co{'author_tz'});
5986 print "From: " . to_utf8($co{'author'}) . "\n";
5987 print "Date: $ad{'rfc2822'} ($ad{'tz_local'})\n";
5988 print "Subject: " . to_utf8($co{'title'}) . "\n";
5990 print "X-Git-Tag: $tagname\n" if $tagname;
5991 print "X-Git-Url: " . $cgi->self_url() . "\n\n";
5993 foreach my $line (@{$co{'comment'}}) {
5994 print to_utf8($line) . "\n";
5997 } elsif ($format eq 'patch') {
5998 my $filename = basename($project) . "-$hash.patch";
6001 -type => 'text/plain',
6002 -charset => 'utf-8',
6003 -expires => $expires,
6004 -content_disposition => 'inline; filename="' . "$filename" . '"');
6008 if ($format eq 'html') {
6009 my $use_parents = !defined $hash_parent ||
6010 $hash_parent eq '-c' || $hash_parent eq '--cc';
6011 git_difftree_body(\@difftree, $hash,
6012 $use_parents ? @{$co{'parents'}} : $hash_parent);
6015 git_patchset_body($fd, \@difftree, $hash,
6016 $use_parents ? @{$co{'parents'}} : $hash_parent);
6018 print "</div>\n"; # class="page_body"
6021 } elsif ($format eq 'plain') {
6025 or print "Reading git-diff-tree failed\n";
6026 } elsif ($format eq 'patch') {
6030 or print "Reading git-format-patch failed\n";
6034 sub git_commitdiff_plain {
6035 git_commitdiff(-format => 'plain');
6038 # format-patch-style patches
6040 git_commitdiff(-format => 'patch', -single=> 1);
6044 git_commitdiff(-format => 'patch');
6048 if (!defined $hash_base) {
6049 $hash_base = git_get_head_hash($project);
6051 if (!defined $page) {
6055 my %co = parse_commit($hash_base)
6056 or die_error(404, "Unknown commit object");
6058 my $refs = git_get_references();
6059 my $limit = sprintf("--max-count=%i", (100 * ($page+1)));
6061 my @commitlist = parse_commits($hash_base, 101, (100 * $page),
6062 $file_name, "--full-history")
6063 or die_error(404, "No such file or directory on given branch");
6065 if (!defined $hash && defined $file_name) {
6066 # some commits could have deleted file in question,
6067 # and not have it in tree, but one of them has to have it
6068 for (my $i = 0; $i <= @commitlist; $i++) {
6069 $hash = git_get_hash_by_path($commitlist[$i]{'id'}, $file_name);
6070 last if defined $hash;
6073 if (defined $hash) {
6074 $ftype = git_get_type($hash);
6076 if (!defined $ftype) {
6077 die_error(500, "Unknown type of object");
6080 my $paging_nav = '';
6083 $cgi->a({-href => href(action=>"history", hash=>$hash, hash_base=>$hash_base,
6084 file_name=>$file_name)},
6086 $paging_nav .= " ⋅ " .
6087 $cgi->a({-href => href(-replay=>1, page=>$page-1),
6088 -accesskey => "p", -title => "Alt-p"}, "prev");
6090 $paging_nav .= "first";
6091 $paging_nav .= " ⋅ prev";
6094 if ($#commitlist >= 100) {
6096 $cgi->a({-href => href(-replay=>1, page=>$page+1),
6097 -accesskey => "n", -title => "Alt-n"}, "next");
6098 $paging_nav .= " ⋅ $next_link";
6100 $paging_nav .= " ⋅ next";
6104 git_print_page_nav('history','', $hash_base,$co{'tree'},$hash_base, $paging_nav);
6105 git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
6106 git_print_page_path($file_name, $ftype, $hash_base);
6108 git_history_body(\@commitlist, 0, 99,
6109 $refs, $hash_base, $ftype, $next_link);
6115 gitweb_check_feature('search') or die_error(403, "Search is disabled");
6116 if (!defined $searchtext) {
6117 die_error(400, "Text field is empty");
6119 if (!defined $hash) {
6120 $hash = git_get_head_hash($project);
6122 my %co = parse_commit($hash);
6124 die_error(404, "Unknown commit object");
6126 if (!defined $page) {
6130 $searchtype ||= 'commit';
6131 if ($searchtype eq 'pickaxe') {
6132 # pickaxe may take all resources of your box and run for several minutes
6133 # with every query - so decide by yourself how public you make this feature
6134 gitweb_check_feature('pickaxe')
6135 or die_error(403, "Pickaxe is disabled");
6137 if ($searchtype eq 'grep') {
6138 gitweb_check_feature('grep')
6139 or die_error(403, "Grep is disabled");
6144 if ($searchtype eq 'commit' or $searchtype eq 'author' or $searchtype eq 'committer') {
6146 if ($searchtype eq 'commit') {
6147 $greptype = "--grep=";
6148 } elsif ($searchtype eq 'author') {
6149 $greptype = "--author=";
6150 } elsif ($searchtype eq 'committer') {
6151 $greptype = "--committer=";
6153 $greptype .= $searchtext;
6154 my @commitlist = parse_commits($hash, 101, (100 * $page), undef,
6155 $greptype, '--regexp-ignore-case',
6156 $search_use_regexp ? '--extended-regexp' : '--fixed-strings');
6158 my $paging_nav = '';
6161 $cgi->a({-href => href(action=>"search", hash=>$hash,
6162 searchtext=>$searchtext,
6163 searchtype=>$searchtype)},
6165 $paging_nav .= " ⋅ " .
6166 $cgi->a({-href => href(-replay=>1, page=>$page-1),
6167 -accesskey => "p", -title => "Alt-p"}, "prev");
6169 $paging_nav .= "first";
6170 $paging_nav .= " ⋅ prev";
6173 if ($#commitlist >= 100) {
6175 $cgi->a({-href => href(-replay=>1, page=>$page+1),
6176 -accesskey => "n", -title => "Alt-n"}, "next");
6177 $paging_nav .= " ⋅ $next_link";
6179 $paging_nav .= " ⋅ next";
6182 if ($#commitlist >= 100) {
6185 git_print_page_nav('','', $hash,$co{'tree'},$hash, $paging_nav);
6186 git_print_header_div('commit', esc_html($co{'title'}), $hash);
6187 git_search_grep_body(\@commitlist, 0, 99, $next_link);
6190 if ($searchtype eq 'pickaxe') {
6191 git_print_page_nav('','', $hash,$co{'tree'},$hash);
6192 git_print_header_div('commit', esc_html($co{'title'}), $hash);
6194 print "<table class=\"pickaxe search\">\n";
6197 open my $fd, '-|', git_cmd(), '--no-pager', 'log', @diff_opts,
6198 '--pretty=format:%H', '--no-abbrev', '--raw', "-S$searchtext",
6199 ($search_use_regexp ? '--pickaxe-regex' : ());
6202 while (my $line = <$fd>) {
6206 my %set = parse_difftree_raw_line($line);
6207 if (defined $set{'commit'}) {
6208 # finish previous commit
6211 "<td class=\"link\">" .
6212 $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") .
6214 $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree");
6220 print "<tr class=\"dark\">\n";
6222 print "<tr class=\"light\">\n";
6225 %co = parse_commit($set{'commit'});
6226 my $author = chop_and_escape_str($co{'author_name'}, 15, 5);
6227 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
6228 "<td><i>$author</i></td>\n" .
6230 $cgi->a({-href => href(action=>"commit", hash=>$co{'id'}),
6231 -class => "list subject"},
6232 chop_and_escape_str($co{'title'}, 50) . "<br/>");
6233 } elsif (defined $set{'to_id'}) {
6234 next if ($set{'to_id'} =~ m/^0{40}$/);
6236 print $cgi->a({-href => href(action=>"blob", hash_base=>$co{'id'},
6237 hash=>$set{'to_id'}, file_name=>$set{'to_file'}),
6239 "<span class=\"match\">" . esc_path($set{'file'}) . "</span>") .
6245 # finish last commit (warning: repetition!)
6248 "<td class=\"link\">" .
6249 $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") .
6251 $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree");
6259 if ($searchtype eq 'grep') {
6260 git_print_page_nav('','', $hash,$co{'tree'},$hash);
6261 git_print_header_div('commit', esc_html($co{'title'}), $hash);
6263 print "<table class=\"grep_search\">\n";
6267 open my $fd, "-|", git_cmd(), 'grep', '-n',
6268 $search_use_regexp ? ('-E', '-i') : '-F',
6269 $searchtext, $co{'tree'};
6271 while (my $line = <$fd>) {
6273 my ($file, $lno, $ltext, $binary);
6274 last if ($matches++ > 1000);
6275 if ($line =~ /^Binary file (.+) matches$/) {
6279 (undef, $file, $lno, $ltext) = split(/:/, $line, 4);
6281 if ($file ne $lastfile) {
6282 $lastfile and print "</td></tr>\n";
6284 print "<tr class=\"dark\">\n";
6286 print "<tr class=\"light\">\n";
6288 print "<td class=\"list\">".
6289 $cgi->a({-href => href(action=>"blob", hash=>$co{'hash'},
6290 file_name=>"$file"),
6291 -class => "list"}, esc_path($file));
6292 print "</td><td>\n";
6296 print "<div class=\"binary\">Binary file</div>\n";
6298 $ltext = untabify($ltext);
6299 if ($ltext =~ m/^(.*)($search_regexp)(.*)$/i) {
6300 $ltext = esc_html($1, -nbsp=>1);
6301 $ltext .= '<span class="match">';
6302 $ltext .= esc_html($2, -nbsp=>1);
6303 $ltext .= '</span>';
6304 $ltext .= esc_html($3, -nbsp=>1);
6306 $ltext = esc_html($ltext, -nbsp=>1);
6308 print "<div class=\"pre\">" .
6309 $cgi->a({-href => href(action=>"blob", hash=>$co{'hash'},
6310 file_name=>"$file").'#l'.$lno,
6311 -class => "linenr"}, sprintf('%4i', $lno))
6312 . ' ' . $ltext . "</div>\n";
6316 print "</td></tr>\n";
6317 if ($matches > 1000) {
6318 print "<div class=\"diff nodifferences\">Too many matches, listing trimmed</div>\n";
6321 print "<div class=\"diff nodifferences\">No matches found</div>\n";
6330 sub git_search_help {
6332 git_print_page_nav('','', $hash,$hash,$hash);
6334 <p><strong>Pattern</strong> is by default a normal string that is matched precisely (but without
6335 regard to case, except in the case of pickaxe). However, when you check the <em>re</em> checkbox,
6336 the pattern entered is recognized as the POSIX extended
6337 <a href="http://en.wikipedia.org/wiki/Regular_expression">regular expression</a> (also case
6340 <dt><b>commit</b></dt>
6341 <dd>The commit messages and authorship information will be scanned for the given pattern.</dd>
6343 my $have_grep = gitweb_check_feature('grep');
6346 <dt><b>grep</b></dt>
6347 <dd>All files in the currently selected tree (HEAD unless you are explicitly browsing
6348 a different one) are searched for the given pattern. On large trees, this search can take
6349 a while and put some strain on the server, so please use it with some consideration. Note that
6350 due to git-grep peculiarity, currently if regexp mode is turned off, the matches are
6351 case-sensitive.</dd>
6355 <dt><b>author</b></dt>
6356 <dd>Name and e-mail of the change author and date of birth of the patch will be scanned for the given pattern.</dd>
6357 <dt><b>committer</b></dt>
6358 <dd>Name and e-mail of the committer and date of commit will be scanned for the given pattern.</dd>
6360 my $have_pickaxe = gitweb_check_feature('pickaxe');
6361 if ($have_pickaxe) {
6363 <dt><b>pickaxe</b></dt>
6364 <dd>All commits that caused the string to appear or disappear from any file (changes that
6365 added, removed or "modified" the string) will be listed. This search can take a while and
6366 takes a lot of strain on the server, so please use it wisely. Note that since you may be
6367 interested even in changes just changing the case as well, this search is case sensitive.</dd>
6375 my $head = git_get_head_hash($project);
6376 if (!defined $hash) {
6379 if (!defined $page) {
6382 my $refs = git_get_references();
6384 my $commit_hash = $hash;
6385 if (defined $hash_parent) {
6386 $commit_hash = "$hash_parent..$hash";
6388 my @commitlist = parse_commits($commit_hash, 101, (100 * $page));
6390 my $paging_nav = format_paging_nav('shortlog', $hash, $head, $page, $#commitlist >= 100);
6392 if ($#commitlist >= 100) {
6394 $cgi->a({-href => href(-replay=>1, page=>$page+1),
6395 -accesskey => "n", -title => "Alt-n"}, "next");
6397 my $patch_max = gitweb_check_feature('patches');
6399 if ($patch_max < 0 || @commitlist <= $patch_max) {
6400 $paging_nav .= " ⋅ " .
6401 $cgi->a({-href => href(action=>"patches", -replay=>1)},
6407 git_print_page_nav('shortlog','', $hash,$hash,$hash, $paging_nav);
6408 git_print_header_div('summary', $project);
6410 git_shortlog_body(\@commitlist, 0, 99, $refs, $next_link);
6415 ## ......................................................................
6416 ## feeds (RSS, Atom; OPML)
6419 my $format = shift || 'atom';
6420 my $have_blame = gitweb_check_feature('blame');
6422 # Atom: http://www.atomenabled.org/developers/syndication/
6423 # RSS: http://www.notestips.com/80256B3A007F2692/1/NAMO5P9UPQ
6424 if ($format ne 'rss' && $format ne 'atom') {
6425 die_error(400, "Unknown web feed format");
6428 # log/feed of current (HEAD) branch, log of given branch, history of file/directory
6429 my $head = $hash || 'HEAD';
6430 my @commitlist = parse_commits($head, 150, 0, $file_name);
6434 my $content_type = "application/$format+xml";
6435 if (defined $cgi->http('HTTP_ACCEPT') &&
6436 $cgi->Accept('text/xml') > $cgi->Accept($content_type)) {
6437 # browser (feed reader) prefers text/xml
6438 $content_type = 'text/xml';
6440 if (defined($commitlist[0])) {
6441 %latest_commit = %{$commitlist[0]};
6442 my $latest_epoch = $latest_commit{'committer_epoch'};
6443 %latest_date = parse_date($latest_epoch);
6444 my $if_modified = $cgi->http('IF_MODIFIED_SINCE');
6445 if (defined $if_modified) {
6447 if (eval { require HTTP::Date; 1; }) {
6448 $since = HTTP::Date::str2time($if_modified);
6449 } elsif (eval { require Time::ParseDate; 1; }) {
6450 $since = Time::ParseDate::parsedate($if_modified, GMT => 1);
6452 if (defined $since && $latest_epoch <= $since) {
6454 -type => $content_type,
6455 -charset => 'utf-8',
6456 -last_modified => $latest_date{'rfc2822'},
6457 -status => '304 Not Modified');
6462 -type => $content_type,
6463 -charset => 'utf-8',
6464 -last_modified => $latest_date{'rfc2822'});
6467 -type => $content_type,
6468 -charset => 'utf-8');
6471 # Optimization: skip generating the body if client asks only
6472 # for Last-Modified date.
6473 return if ($cgi->request_method() eq 'HEAD');
6476 my $title = "$site_name - $project/$action";
6477 my $feed_type = 'log';
6478 if (defined $hash) {
6479 $title .= " - '$hash'";
6480 $feed_type = 'branch log';
6481 if (defined $file_name) {
6482 $title .= " :: $file_name";
6483 $feed_type = 'history';
6485 } elsif (defined $file_name) {
6486 $title .= " - $file_name";
6487 $feed_type = 'history';
6489 $title .= " $feed_type";
6490 my $descr = git_get_project_description($project);
6491 if (defined $descr) {
6492 $descr = esc_html($descr);
6494 $descr = "$project " .
6495 ($format eq 'rss' ? 'RSS' : 'Atom') .
6498 my $owner = git_get_project_owner($project);
6499 $owner = esc_html($owner);
6503 if (defined $file_name) {
6504 $alt_url = href(-full=>1, action=>"history", hash=>$hash, file_name=>$file_name);
6505 } elsif (defined $hash) {
6506 $alt_url = href(-full=>1, action=>"log", hash=>$hash);
6508 $alt_url = href(-full=>1, action=>"summary");
6510 print qq!<?xml version="1.0" encoding="utf-8"?>\n!;
6511 if ($format eq 'rss') {
6513 <rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
6516 print "<title>$title</title>\n" .
6517 "<link>$alt_url</link>\n" .
6518 "<description>$descr</description>\n" .
6519 "<language>en</language>\n" .
6520 # project owner is responsible for 'editorial' content
6521 "<managingEditor>$owner</managingEditor>\n";
6522 if (defined $logo || defined $favicon) {
6523 # prefer the logo to the favicon, since RSS
6524 # doesn't allow both
6525 my $img = esc_url($logo || $favicon);
6527 "<url>$img</url>\n" .
6528 "<title>$title</title>\n" .
6529 "<link>$alt_url</link>\n" .
6533 print "<pubDate>$latest_date{'rfc2822'}</pubDate>\n";
6534 print "<lastBuildDate>$latest_date{'rfc2822'}</lastBuildDate>\n";
6536 print "<generator>gitweb v.$version/$git_version</generator>\n";
6537 } elsif ($format eq 'atom') {
6539 <feed xmlns="http://www.w3.org/2005/Atom">
6541 print "<title>$title</title>\n" .
6542 "<subtitle>$descr</subtitle>\n" .
6543 '<link rel="alternate" type="text/html" href="' .
6544 $alt_url . '" />' . "\n" .
6545 '<link rel="self" type="' . $content_type . '" href="' .
6546 $cgi->self_url() . '" />' . "\n" .
6547 "<id>" . href(-full=>1) . "</id>\n" .
6548 # use project owner for feed author
6549 "<author><name>$owner</name></author>\n";
6550 if (defined $favicon) {
6551 print "<icon>" . esc_url($favicon) . "</icon>\n";
6553 if (defined $logo_url) {
6554 # not twice as wide as tall: 72 x 27 pixels
6555 print "<logo>" . esc_url($logo) . "</logo>\n";
6557 if (! %latest_date) {
6558 # dummy date to keep the feed valid until commits trickle in:
6559 print "<updated>1970-01-01T00:00:00Z</updated>\n";
6561 print "<updated>$latest_date{'iso-8601'}</updated>\n";
6563 print "<generator version='$version/$git_version'>gitweb</generator>\n";
6567 for (my $i = 0; $i <= $#commitlist; $i++) {
6568 my %co = %{$commitlist[$i]};
6569 my $commit = $co{'id'};
6570 # we read 150, we always show 30 and the ones more recent than 48 hours
6571 if (($i >= 20) && ((time - $co{'author_epoch'}) > 48*60*60)) {
6574 my %cd = parse_date($co{'author_epoch'});
6576 # get list of changed files
6577 open my $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
6578 $co{'parent'} || "--root",
6579 $co{'id'}, "--", (defined $file_name ? $file_name : ())
6581 my @difftree = map { chomp; $_ } <$fd>;
6585 # print element (entry, item)
6586 my $co_url = href(-full=>1, action=>"commitdiff", hash=>$commit);
6587 if ($format eq 'rss') {
6589 "<title>" . esc_html($co{'title'}) . "</title>\n" .
6590 "<author>" . esc_html($co{'author'}) . "</author>\n" .
6591 "<pubDate>$cd{'rfc2822'}</pubDate>\n" .
6592 "<guid isPermaLink=\"true\">$co_url</guid>\n" .
6593 "<link>$co_url</link>\n" .
6594 "<description>" . esc_html($co{'title'}) . "</description>\n" .
6595 "<content:encoded>" .
6597 } elsif ($format eq 'atom') {
6599 "<title type=\"html\">" . esc_html($co{'title'}) . "</title>\n" .
6600 "<updated>$cd{'iso-8601'}</updated>\n" .
6602 " <name>" . esc_html($co{'author_name'}) . "</name>\n";
6603 if ($co{'author_email'}) {
6604 print " <email>" . esc_html($co{'author_email'}) . "</email>\n";
6606 print "</author>\n" .
6607 # use committer for contributor
6609 " <name>" . esc_html($co{'committer_name'}) . "</name>\n";
6610 if ($co{'committer_email'}) {
6611 print " <email>" . esc_html($co{'committer_email'}) . "</email>\n";
6613 print "</contributor>\n" .
6614 "<published>$cd{'iso-8601'}</published>\n" .
6615 "<link rel=\"alternate\" type=\"text/html\" href=\"$co_url\" />\n" .
6616 "<id>$co_url</id>\n" .
6617 "<content type=\"xhtml\" xml:base=\"" . esc_url($my_url) . "\">\n" .
6618 "<div xmlns=\"http://www.w3.org/1999/xhtml\">\n";
6620 my $comment = $co{'comment'};
6622 foreach my $line (@$comment) {
6623 $line = esc_html($line);
6626 print "</pre><ul>\n";
6627 foreach my $difftree_line (@difftree) {
6628 my %difftree = parse_difftree_raw_line($difftree_line);
6629 next if !$difftree{'from_id'};
6631 my $file = $difftree{'file'} || $difftree{'to_file'};
6635 $cgi->a({-href => href(-full=>1, action=>"blobdiff",
6636 hash=>$difftree{'to_id'}, hash_parent=>$difftree{'from_id'},
6637 hash_base=>$co{'id'}, hash_parent_base=>$co{'parent'},
6638 file_name=>$file, file_parent=>$difftree{'from_file'}),
6639 -title => "diff"}, 'D');
6641 print $cgi->a({-href => href(-full=>1, action=>"blame",
6642 file_name=>$file, hash_base=>$commit),
6643 -title => "blame"}, 'B');
6645 # if this is not a feed of a file history
6646 if (!defined $file_name || $file_name ne $file) {
6647 print $cgi->a({-href => href(-full=>1, action=>"history",
6648 file_name=>$file, hash=>$commit),
6649 -title => "history"}, 'H');
6651 $file = esc_path($file);
6655 if ($format eq 'rss') {
6656 print "</ul>]]>\n" .
6657 "</content:encoded>\n" .
6659 } elsif ($format eq 'atom') {
6660 print "</ul>\n</div>\n" .
6667 if ($format eq 'rss') {
6668 print "</channel>\n</rss>\n";
6669 } elsif ($format eq 'atom') {
6683 my @list = git_get_projects_list();
6686 -type => 'text/xml',
6687 -charset => 'utf-8',
6688 -content_disposition => 'inline; filename="opml.xml"');
6691 <?xml version="1.0" encoding="utf-8"?>
6692 <opml version="1.0">
6694 <title>$site_name OPML Export</title>
6697 <outline text="git RSS feeds">
6700 foreach my $pr (@list) {
6702 my $head = git_get_head_hash($proj{'path'});
6703 if (!defined $head) {
6706 $git_dir = "$projectroot/$proj{'path'}";
6707 my %co = parse_commit($head);
6712 my $path = esc_html(chop_str($proj{'path'}, 25, 5));
6713 my $rss = href('project' => $proj{'path'}, 'action' => 'rss', -full => 1);
6714 my $html = href('project' => $proj{'path'}, 'action' => 'summary', -full => 1);
6715 print "<outline type=\"rss\" text=\"$path\" title=\"$path\" xmlUrl=\"$rss\" htmlUrl=\"$html\"/>\n";