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 CGI->compile() if $ENV{'MOD_PERL'};
26 our $version = "++GIT_VERSION++";
27 our $my_url = $cgi->url();
28 our $my_uri = $cgi->url(-absolute => 1);
30 # Base URL for relative URLs in gitweb ($logo, $favicon, ...),
31 # needed and used only for URLs with nonempty PATH_INFO
32 our $base_url = $my_url;
34 # When the script is used as DirectoryIndex, the URL does not contain the name
35 # of the script file itself, and $cgi->url() fails to strip PATH_INFO, so we
36 # have to do it ourselves. We make $path_info global because it's also used
39 # Another issue with the script being the DirectoryIndex is that the resulting
40 # $my_url data is not the full script URL: this is good, because we want
41 # generated links to keep implying the script name if it wasn't explicitly
42 # indicated in the URL we're handling, but it means that $my_url cannot be used
44 # Therefore, if we needed to strip PATH_INFO, then we know that we have
45 # to build the base URL ourselves:
46 our $path_info = $ENV{"PATH_INFO"};
48 if ($my_url =~ s,\Q$path_info\E$,, &&
49 $my_uri =~ s,\Q$path_info\E$,, &&
50 defined $ENV{'SCRIPT_NAME'}) {
51 $base_url = $cgi->url(-base => 1) . $ENV{'SCRIPT_NAME'};
55 # core git executable to use
56 # this can just be "git" if your webserver has a sensible PATH
57 our $GIT = "++GIT_BINDIR++/git";
59 # absolute fs-path which will be prepended to the project path
60 #our $projectroot = "/pub/scm";
61 our $projectroot = "++GITWEB_PROJECTROOT++";
63 # fs traversing limit for getting project list
64 # the number is relative to the projectroot
65 our $project_maxdepth = "++GITWEB_PROJECT_MAXDEPTH++";
67 # target of the home link on top of all pages
68 our $home_link = $my_uri || "/";
70 # string of the home link on top of all pages
71 our $home_link_str = "++GITWEB_HOME_LINK_STR++";
73 # name of your site or organization to appear in page titles
74 # replace this with something more descriptive for clearer bookmarks
75 our $site_name = "++GITWEB_SITENAME++"
76 || ($ENV{'SERVER_NAME'} || "Untitled") . " Git";
78 # filename of html text to include at top of each page
79 our $site_header = "++GITWEB_SITE_HEADER++";
80 # html text to include at home page
81 our $home_text = "++GITWEB_HOMETEXT++";
82 # filename of html text to include at bottom of each page
83 our $site_footer = "++GITWEB_SITE_FOOTER++";
86 our @stylesheets = ("++GITWEB_CSS++");
87 # URI of a single stylesheet, which can be overridden in GITWEB_CONFIG.
88 our $stylesheet = undef;
89 # URI of GIT logo (72x27 size)
90 our $logo = "++GITWEB_LOGO++";
91 # URI of GIT favicon, assumed to be image/png type
92 our $favicon = "++GITWEB_FAVICON++";
94 # URI and label (title) of GIT logo link
95 #our $logo_url = "http://www.kernel.org/pub/software/scm/git/docs/";
96 #our $logo_label = "git documentation";
97 our $logo_url = "http://git-scm.com/";
98 our $logo_label = "git homepage";
100 # source of projects list
101 our $projects_list = "++GITWEB_LIST++";
103 # the width (in characters) of the projects list "Description" column
104 our $projects_list_description_width = 25;
106 # default order of projects list
107 # valid values are none, project, descr, owner, and age
108 our $default_projects_order = "project";
110 # show repository only if this file exists
111 # (only effective if this variable evaluates to true)
112 our $export_ok = "++GITWEB_EXPORT_OK++";
114 # show repository only if this subroutine returns true
115 # when given the path to the project, for example:
116 # sub { return -e "$_[0]/git-daemon-export-ok"; }
117 our $export_auth_hook = undef;
119 # only allow viewing of repositories also shown on the overview page
120 our $strict_export = "++GITWEB_STRICT_EXPORT++";
122 # list of git base URLs used for URL to where fetch project from,
123 # i.e. full URL is "$git_base_url/$project"
124 our @git_base_url_list = grep { $_ ne '' } ("++GITWEB_BASE_URL++");
126 # default blob_plain mimetype and default charset for text/plain blob
127 our $default_blob_plain_mimetype = 'text/plain';
128 our $default_text_plain_charset = undef;
130 # file to use for guessing MIME types before trying /etc/mime.types
131 # (relative to the current git repository)
132 our $mimetypes_file = undef;
134 # assume this charset if line contains non-UTF-8 characters;
135 # it should be valid encoding (see Encoding::Supported(3pm) for list),
136 # for which encoding all byte sequences are valid, for example
137 # 'iso-8859-1' aka 'latin1' (it is decoded without checking, so it
138 # could be even 'utf-8' for the old behavior)
139 our $fallback_encoding = 'latin1';
141 # rename detection options for git-diff and git-diff-tree
142 # - default is '-M', with the cost proportional to
143 # (number of removed files) * (number of new files).
144 # - more costly is '-C' (which implies '-M'), with the cost proportional to
145 # (number of changed files + number of removed files) * (number of new files)
146 # - even more costly is '-C', '--find-copies-harder' with cost
147 # (number of files in the original tree) * (number of new files)
148 # - one might want to include '-B' option, e.g. '-B', '-M'
149 our @diff_opts = ('-M'); # taken from git_commit
151 # Disables features that would allow repository owners to inject script into
153 our $prevent_xss = 0;
155 # information about snapshot formats that gitweb is capable of serving
156 our %known_snapshot_formats = (
158 # 'display' => display name,
159 # 'type' => mime type,
160 # 'suffix' => filename suffix,
161 # 'format' => --format for git-archive,
162 # 'compressor' => [compressor command and arguments]
163 # (array reference, optional)
164 # 'disabled' => boolean (optional)}
167 'display' => 'tar.gz',
168 'type' => 'application/x-gzip',
169 'suffix' => '.tar.gz',
171 'compressor' => ['gzip']},
174 'display' => 'tar.bz2',
175 'type' => 'application/x-bzip2',
176 'suffix' => '.tar.bz2',
178 'compressor' => ['bzip2']},
181 'display' => 'tar.xz',
182 'type' => 'application/x-xz',
183 'suffix' => '.tar.xz',
185 'compressor' => ['xz'],
190 'type' => 'application/x-zip',
195 # Aliases so we understand old gitweb.snapshot values in repository
197 our %known_snapshot_format_aliases = (
202 # backward compatibility: legacy gitweb config support
203 'x-gzip' => undef, 'gz' => undef,
204 'x-bzip2' => undef, 'bz2' => undef,
205 'x-zip' => undef, '' => undef,
208 # Pixel sizes for icons and avatars. If the default font sizes or lineheights
209 # are changed, it may be appropriate to change these values too via
216 # You define site-wide feature defaults here; override them with
217 # $GITWEB_CONFIG as necessary.
220 # 'sub' => feature-sub (subroutine),
221 # 'override' => allow-override (boolean),
222 # 'default' => [ default options...] (array reference)}
224 # if feature is overridable (it means that allow-override has true value),
225 # then feature-sub will be called with default options as parameters;
226 # return value of feature-sub indicates if to enable specified feature
228 # if there is no 'sub' key (no feature-sub), then feature cannot be
231 # use gitweb_get_feature(<feature>) to retrieve the <feature> value
232 # (an array) or gitweb_check_feature(<feature>) to check if <feature>
235 # Enable the 'blame' blob view, showing the last commit that modified
236 # each line in the file. This can be very CPU-intensive.
238 # To enable system wide have in $GITWEB_CONFIG
239 # $feature{'blame'}{'default'} = [1];
240 # To have project specific config enable override in $GITWEB_CONFIG
241 # $feature{'blame'}{'override'} = 1;
242 # and in project config gitweb.blame = 0|1;
244 'sub' => sub { feature_bool('blame', @_) },
248 # Enable the 'snapshot' link, providing a compressed archive of any
249 # tree. This can potentially generate high traffic if you have large
252 # Value is a list of formats defined in %known_snapshot_formats that
254 # To disable system wide have in $GITWEB_CONFIG
255 # $feature{'snapshot'}{'default'} = [];
256 # To have project specific config enable override in $GITWEB_CONFIG
257 # $feature{'snapshot'}{'override'} = 1;
258 # and in project config, a comma-separated list of formats or "none"
259 # to disable. Example: gitweb.snapshot = tbz2,zip;
261 'sub' => \&feature_snapshot,
263 'default' => ['tgz']},
265 # Enable text search, which will list the commits which match author,
266 # committer or commit text to a given string. Enabled by default.
267 # Project specific override is not supported.
272 # Enable grep search, which will list the files in currently selected
273 # tree containing the given string. Enabled by default. This can be
274 # potentially CPU-intensive, of course.
276 # To enable system wide have in $GITWEB_CONFIG
277 # $feature{'grep'}{'default'} = [1];
278 # To have project specific config enable override in $GITWEB_CONFIG
279 # $feature{'grep'}{'override'} = 1;
280 # and in project config gitweb.grep = 0|1;
282 'sub' => sub { feature_bool('grep', @_) },
286 # Enable the pickaxe search, which will list the commits that modified
287 # a given string in a file. This can be practical and quite faster
288 # alternative to 'blame', but still potentially CPU-intensive.
290 # To enable system wide have in $GITWEB_CONFIG
291 # $feature{'pickaxe'}{'default'} = [1];
292 # To have project specific config enable override in $GITWEB_CONFIG
293 # $feature{'pickaxe'}{'override'} = 1;
294 # and in project config gitweb.pickaxe = 0|1;
296 'sub' => sub { feature_bool('pickaxe', @_) },
300 # Make gitweb use an alternative format of the URLs which can be
301 # more readable and natural-looking: project name is embedded
302 # directly in the path and the query string contains other
303 # auxiliary information. All gitweb installations recognize
304 # URL in either format; this configures in which formats gitweb
307 # To enable system wide have in $GITWEB_CONFIG
308 # $feature{'pathinfo'}{'default'} = [1];
309 # Project specific override is not supported.
311 # Note that you will need to change the default location of CSS,
312 # favicon, logo and possibly other files to an absolute URL. Also,
313 # if gitweb.cgi serves as your indexfile, you will need to force
314 # $my_uri to contain the script name in your $GITWEB_CONFIG.
319 # Make gitweb consider projects in project root subdirectories
320 # to be forks of existing projects. Given project $projname.git,
321 # projects matching $projname/*.git will not be shown in the main
322 # projects list, instead a '+' mark will be added to $projname
323 # there and a 'forks' view will be enabled for the project, listing
324 # all the forks. If project list is taken from a file, forks have
325 # to be listed after the main project.
327 # To enable system wide have in $GITWEB_CONFIG
328 # $feature{'forks'}{'default'} = [1];
329 # Project specific override is not supported.
334 # Insert custom links to the action bar of all project pages.
335 # This enables you mainly to link to third-party scripts integrating
336 # into gitweb; e.g. git-browser for graphical history representation
337 # or custom web-based repository administration interface.
339 # The 'default' value consists of a list of triplets in the form
340 # (label, link, position) where position is the label after which
341 # to insert the link and link is a format string where %n expands
342 # to the project name, %f to the project path within the filesystem,
343 # %h to the current hash (h gitweb parameter) and %b to the current
344 # hash base (hb gitweb parameter); %% expands to %.
346 # To enable system wide have in $GITWEB_CONFIG e.g.
347 # $feature{'actions'}{'default'} = [('graphiclog',
348 # '/git-browser/by-commit.html?r=%n', 'summary')];
349 # Project specific override is not supported.
354 # Allow gitweb scan project content tags described in ctags/
355 # of project repository, and display the popular Web 2.0-ish
356 # "tag cloud" near the project list. Note that this is something
357 # COMPLETELY different from the normal Git tags.
359 # gitweb by itself can show existing tags, but it does not handle
360 # tagging itself; you need an external application for that.
361 # For an example script, check Girocco's cgi/tagproj.cgi.
362 # You may want to install the HTML::TagCloud Perl module to get
363 # a pretty tag cloud instead of just a list of tags.
365 # To enable system wide have in $GITWEB_CONFIG
366 # $feature{'ctags'}{'default'} = ['path_to_tag_script'];
367 # Project specific override is not supported.
372 # The maximum number of patches in a patchset generated in patch
373 # view. Set this to 0 or undef to disable patch view, or to a
374 # negative number to remove any limit.
376 # To disable system wide have in $GITWEB_CONFIG
377 # $feature{'patches'}{'default'} = [0];
378 # To have project specific config enable override in $GITWEB_CONFIG
379 # $feature{'patches'}{'override'} = 1;
380 # and in project config gitweb.patches = 0|n;
381 # where n is the maximum number of patches allowed in a patchset.
383 'sub' => \&feature_patches,
387 # Avatar support. When this feature is enabled, views such as
388 # shortlog or commit will display an avatar associated with
389 # the email of the committer(s) and/or author(s).
391 # Currently available providers are gravatar and picon.
392 # If an unknown provider is specified, the feature is disabled.
394 # Gravatar depends on Digest::MD5.
395 # Picon currently relies on the indiana.edu database.
397 # To enable system wide have in $GITWEB_CONFIG
398 # $feature{'avatar'}{'default'} = ['<provider>'];
399 # where <provider> is either gravatar or picon.
400 # To have project specific config enable override in $GITWEB_CONFIG
401 # $feature{'avatar'}{'override'} = 1;
402 # and in project config gitweb.avatar = <provider>;
404 'sub' => \&feature_avatar,
409 sub gitweb_get_feature {
411 return unless exists $feature{$name};
412 my ($sub, $override, @defaults) = (
413 $feature{$name}{'sub'},
414 $feature{$name}{'override'},
415 @{$feature{$name}{'default'}});
416 if (!$override) { return @defaults; }
418 warn "feature $name is not overridable";
421 return $sub->(@defaults);
424 # A wrapper to check if a given feature is enabled.
425 # With this, you can say
427 # my $bool_feat = gitweb_check_feature('bool_feat');
428 # gitweb_check_feature('bool_feat') or somecode;
432 # my ($bool_feat) = gitweb_get_feature('bool_feat');
433 # (gitweb_get_feature('bool_feat'))[0] or somecode;
435 sub gitweb_check_feature {
436 return (gitweb_get_feature(@_))[0];
442 my ($val) = git_get_project_config($key, '--bool');
446 } elsif ($val eq 'true') {
448 } elsif ($val eq 'false') {
453 sub feature_snapshot {
456 my ($val) = git_get_project_config('snapshot');
459 @fmts = ($val eq 'none' ? () : split /\s*[,\s]\s*/, $val);
465 sub feature_patches {
466 my @val = (git_get_project_config('patches', '--int'));
476 my @val = (git_get_project_config('avatar'));
478 return @val ? @val : @_;
481 # checking HEAD file with -e is fragile if the repository was
482 # initialized long time ago (i.e. symlink HEAD) and was pack-ref'ed
484 sub check_head_link {
486 my $headfile = "$dir/HEAD";
487 return ((-e $headfile) ||
488 (-l $headfile && readlink($headfile) =~ /^refs\/heads\//));
491 sub check_export_ok {
493 return (check_head_link($dir) &&
494 (!$export_ok || -e "$dir/$export_ok") &&
495 (!$export_auth_hook || $export_auth_hook->($dir)));
498 # process alternate names for backward compatibility
499 # filter out unsupported (unknown) snapshot formats
500 sub filter_snapshot_fmts {
504 exists $known_snapshot_format_aliases{$_} ?
505 $known_snapshot_format_aliases{$_} : $_} @fmts;
507 exists $known_snapshot_formats{$_} &&
508 !$known_snapshot_formats{$_}{'disabled'}} @fmts;
511 our $GITWEB_CONFIG = $ENV{'GITWEB_CONFIG'} || "++GITWEB_CONFIG++";
512 if (-e $GITWEB_CONFIG) {
515 our $GITWEB_CONFIG_SYSTEM = $ENV{'GITWEB_CONFIG_SYSTEM'} || "++GITWEB_CONFIG_SYSTEM++";
516 do $GITWEB_CONFIG_SYSTEM if -e $GITWEB_CONFIG_SYSTEM;
519 # version of the core git binary
520 our $git_version = qx("$GIT" --version) =~ m/git version (.*)$/ ? $1 : "unknown";
522 $projects_list ||= $projectroot;
524 # ======================================================================
525 # input validation and dispatch
527 # input parameters can be collected from a variety of sources (presently, CGI
528 # and PATH_INFO), so we define an %input_params hash that collects them all
529 # together during validation: this allows subsequent uses (e.g. href()) to be
530 # agnostic of the parameter origin
532 our %input_params = ();
534 # input parameters are stored with the long parameter name as key. This will
535 # also be used in the href subroutine to convert parameters to their CGI
536 # equivalent, and since the href() usage is the most frequent one, we store
537 # the name -> CGI key mapping here, instead of the reverse.
539 # XXX: Warning: If you touch this, check the search form for updating,
542 our @cgi_param_mapping = (
550 hash_parent_base => "hpb",
555 snapshot_format => "sf",
556 extra_options => "opt",
557 search_use_regexp => "sr",
559 our %cgi_param_mapping = @cgi_param_mapping;
561 # we will also need to know the possible actions, for validation
563 "blame" => \&git_blame,
564 "blobdiff" => \&git_blobdiff,
565 "blobdiff_plain" => \&git_blobdiff_plain,
566 "blob" => \&git_blob,
567 "blob_plain" => \&git_blob_plain,
568 "commitdiff" => \&git_commitdiff,
569 "commitdiff_plain" => \&git_commitdiff_plain,
570 "commit" => \&git_commit,
571 "forks" => \&git_forks,
572 "heads" => \&git_heads,
573 "history" => \&git_history,
575 "patch" => \&git_patch,
576 "patches" => \&git_patches,
578 "atom" => \&git_atom,
579 "search" => \&git_search,
580 "search_help" => \&git_search_help,
581 "shortlog" => \&git_shortlog,
582 "summary" => \&git_summary,
584 "tags" => \&git_tags,
585 "tree" => \&git_tree,
586 "snapshot" => \&git_snapshot,
587 "object" => \&git_object,
588 # those below don't need $project
589 "opml" => \&git_opml,
590 "project_list" => \&git_project_list,
591 "project_index" => \&git_project_index,
594 # finally, we have the hash of allowed extra_options for the commands that
596 our %allowed_options = (
597 "--no-merges" => [ qw(rss atom log shortlog history) ],
600 # fill %input_params with the CGI parameters. All values except for 'opt'
601 # should be single values, but opt can be an array. We should probably
602 # build an array of parameters that can be multi-valued, but since for the time
603 # being it's only this one, we just single it out
604 while (my ($name, $symbol) = each %cgi_param_mapping) {
605 if ($symbol eq 'opt') {
606 $input_params{$name} = [ $cgi->param($symbol) ];
608 $input_params{$name} = $cgi->param($symbol);
612 # now read PATH_INFO and update the parameter list for missing parameters
613 sub evaluate_path_info {
614 return if defined $input_params{'project'};
615 return if !$path_info;
616 $path_info =~ s,^/+,,;
617 return if !$path_info;
619 # find which part of PATH_INFO is project
620 my $project = $path_info;
622 while ($project && !check_head_link("$projectroot/$project")) {
623 $project =~ s,/*[^/]*$,,;
625 return unless $project;
626 $input_params{'project'} = $project;
628 # do not change any parameters if an action is given using the query string
629 return if $input_params{'action'};
630 $path_info =~ s,^\Q$project\E/*,,;
632 # next, check if we have an action
633 my $action = $path_info;
635 if (exists $actions{$action}) {
636 $path_info =~ s,^$action/*,,;
637 $input_params{'action'} = $action;
640 # list of actions that want hash_base instead of hash, but can have no
641 # pathname (f) parameter
648 # [$hash_parent_base[:$file_parent]..]$hash_parent[:$file_name]
649 my ($parentrefname, $parentpathname, $refname, $pathname) =
650 ($path_info =~ /^(?:(.+?)(?::(.+))?\.\.)?(.+?)(?::(.+))?$/);
652 # first, analyze the 'current' part
653 if (defined $pathname) {
654 # we got "branch:filename" or "branch:dir/"
655 # we could use git_get_type(branch:pathname), but:
656 # - it needs $git_dir
657 # - it does a git() call
658 # - the convention of terminating directories with a slash
659 # makes it superfluous
660 # - embedding the action in the PATH_INFO would make it even
662 $pathname =~ s,^/+,,;
663 if (!$pathname || substr($pathname, -1) eq "/") {
664 $input_params{'action'} ||= "tree";
667 # the default action depends on whether we had parent info
669 if ($parentrefname) {
670 $input_params{'action'} ||= "blobdiff_plain";
672 $input_params{'action'} ||= "blob_plain";
675 $input_params{'hash_base'} ||= $refname;
676 $input_params{'file_name'} ||= $pathname;
677 } elsif (defined $refname) {
678 # we got "branch". In this case we have to choose if we have to
679 # set hash or hash_base.
681 # Most of the actions without a pathname only want hash to be
682 # set, except for the ones specified in @wants_base that want
683 # hash_base instead. It should also be noted that hand-crafted
684 # links having 'history' as an action and no pathname or hash
685 # set will fail, but that happens regardless of PATH_INFO.
686 $input_params{'action'} ||= "shortlog";
687 if (grep { $_ eq $input_params{'action'} } @wants_base) {
688 $input_params{'hash_base'} ||= $refname;
690 $input_params{'hash'} ||= $refname;
694 # next, handle the 'parent' part, if present
695 if (defined $parentrefname) {
696 # a missing pathspec defaults to the 'current' filename, allowing e.g.
697 # someproject/blobdiff/oldrev..newrev:/filename
698 if ($parentpathname) {
699 $parentpathname =~ s,^/+,,;
700 $parentpathname =~ s,/$,,;
701 $input_params{'file_parent'} ||= $parentpathname;
703 $input_params{'file_parent'} ||= $input_params{'file_name'};
705 # we assume that hash_parent_base is wanted if a path was specified,
706 # or if the action wants hash_base instead of hash
707 if (defined $input_params{'file_parent'} ||
708 grep { $_ eq $input_params{'action'} } @wants_base) {
709 $input_params{'hash_parent_base'} ||= $parentrefname;
711 $input_params{'hash_parent'} ||= $parentrefname;
715 # for the snapshot action, we allow URLs in the form
716 # $project/snapshot/$hash.ext
717 # where .ext determines the snapshot and gets removed from the
718 # passed $refname to provide the $hash.
720 # To be able to tell that $refname includes the format extension, we
721 # require the following two conditions to be satisfied:
722 # - the hash input parameter MUST have been set from the $refname part
723 # of the URL (i.e. they must be equal)
724 # - the snapshot format MUST NOT have been defined already (e.g. from
726 # It's also useless to try any matching unless $refname has a dot,
727 # so we check for that too
728 if (defined $input_params{'action'} &&
729 $input_params{'action'} eq 'snapshot' &&
730 defined $refname && index($refname, '.') != -1 &&
731 $refname eq $input_params{'hash'} &&
732 !defined $input_params{'snapshot_format'}) {
733 # We loop over the known snapshot formats, checking for
734 # extensions. Allowed extensions are both the defined suffix
735 # (which includes the initial dot already) and the snapshot
736 # format key itself, with a prepended dot
737 while (my ($fmt, $opt) = each %known_snapshot_formats) {
739 unless ($hash =~ s/(\Q$opt->{'suffix'}\E|\Q.$fmt\E)$//) {
743 # a valid suffix was found, so set the snapshot format
744 # and reset the hash parameter
745 $input_params{'snapshot_format'} = $fmt;
746 $input_params{'hash'} = $hash;
747 # we also set the format suffix to the one requested
748 # in the URL: this way a request for e.g. .tgz returns
749 # a .tgz instead of a .tar.gz
750 $known_snapshot_formats{$fmt}{'suffix'} = $sfx;
755 evaluate_path_info();
757 our $action = $input_params{'action'};
758 if (defined $action) {
759 if (!validate_action($action)) {
760 die_error(400, "Invalid action parameter");
764 # parameters which are pathnames
765 our $project = $input_params{'project'};
766 if (defined $project) {
767 if (!validate_project($project)) {
769 die_error(404, "No such project");
773 our $file_name = $input_params{'file_name'};
774 if (defined $file_name) {
775 if (!validate_pathname($file_name)) {
776 die_error(400, "Invalid file parameter");
780 our $file_parent = $input_params{'file_parent'};
781 if (defined $file_parent) {
782 if (!validate_pathname($file_parent)) {
783 die_error(400, "Invalid file parent parameter");
787 # parameters which are refnames
788 our $hash = $input_params{'hash'};
790 if (!validate_refname($hash)) {
791 die_error(400, "Invalid hash parameter");
795 our $hash_parent = $input_params{'hash_parent'};
796 if (defined $hash_parent) {
797 if (!validate_refname($hash_parent)) {
798 die_error(400, "Invalid hash parent parameter");
802 our $hash_base = $input_params{'hash_base'};
803 if (defined $hash_base) {
804 if (!validate_refname($hash_base)) {
805 die_error(400, "Invalid hash base parameter");
809 our @extra_options = @{$input_params{'extra_options'}};
810 # @extra_options is always defined, since it can only be (currently) set from
811 # CGI, and $cgi->param() returns the empty array in array context if the param
813 foreach my $opt (@extra_options) {
814 if (not exists $allowed_options{$opt}) {
815 die_error(400, "Invalid option parameter");
817 if (not grep(/^$action$/, @{$allowed_options{$opt}})) {
818 die_error(400, "Invalid option parameter for this action");
822 our $hash_parent_base = $input_params{'hash_parent_base'};
823 if (defined $hash_parent_base) {
824 if (!validate_refname($hash_parent_base)) {
825 die_error(400, "Invalid hash parent base parameter");
830 our $page = $input_params{'page'};
832 if ($page =~ m/[^0-9]/) {
833 die_error(400, "Invalid page parameter");
837 our $searchtype = $input_params{'searchtype'};
838 if (defined $searchtype) {
839 if ($searchtype =~ m/[^a-z]/) {
840 die_error(400, "Invalid searchtype parameter");
844 our $search_use_regexp = $input_params{'search_use_regexp'};
846 our $searchtext = $input_params{'searchtext'};
848 if (defined $searchtext) {
849 if (length($searchtext) < 2) {
850 die_error(403, "At least two characters are required for search parameter");
852 $search_regexp = $search_use_regexp ? $searchtext : quotemeta $searchtext;
855 # path to the current git repository
857 $git_dir = "$projectroot/$project" if $project;
859 # list of supported snapshot formats
860 our @snapshot_fmts = gitweb_get_feature('snapshot');
861 @snapshot_fmts = filter_snapshot_fmts(@snapshot_fmts);
863 # check that the avatar feature is set to a known provider name,
864 # and for each provider check if the dependencies are satisfied.
865 # if the provider name is invalid or the dependencies are not met,
866 # reset $git_avatar to the empty string.
867 our ($git_avatar) = gitweb_get_feature('avatar');
868 if ($git_avatar eq 'gravatar') {
869 $git_avatar = '' unless (eval { require Digest::MD5; 1; });
870 } elsif ($git_avatar eq 'picon') {
877 if (!defined $action) {
879 $action = git_get_type($hash);
880 } elsif (defined $hash_base && defined $file_name) {
881 $action = git_get_type("$hash_base:$file_name");
882 } elsif (defined $project) {
885 $action = 'project_list';
888 if (!defined($actions{$action})) {
889 die_error(400, "Unknown action");
891 if ($action !~ m/^(?:opml|project_list|project_index)$/ &&
893 die_error(400, "Project needed");
895 $actions{$action}->();
898 ## ======================================================================
903 # default is to use -absolute url() i.e. $my_uri
904 my $href = $params{-full} ? $my_url : $my_uri;
906 $params{'project'} = $project unless exists $params{'project'};
908 if ($params{-replay}) {
909 while (my ($name, $symbol) = each %cgi_param_mapping) {
910 if (!exists $params{$name}) {
911 $params{$name} = $input_params{$name};
916 my $use_pathinfo = gitweb_check_feature('pathinfo');
917 if ($use_pathinfo and defined $params{'project'}) {
918 # try to put as many parameters as possible in PATH_INFO:
921 # - hash_parent or hash_parent_base:/file_parent
922 # - hash or hash_base:/filename
923 # - the snapshot_format as an appropriate suffix
925 # When the script is the root DirectoryIndex for the domain,
926 # $href here would be something like http://gitweb.example.com/
927 # Thus, we strip any trailing / from $href, to spare us double
928 # slashes in the final URL
931 # Then add the project name, if present
932 $href .= "/".esc_url($params{'project'});
933 delete $params{'project'};
935 # since we destructively absorb parameters, we keep this
936 # boolean that remembers if we're handling a snapshot
937 my $is_snapshot = $params{'action'} eq 'snapshot';
939 # Summary just uses the project path URL, any other action is
941 if (defined $params{'action'}) {
942 $href .= "/".esc_url($params{'action'}) unless $params{'action'} eq 'summary';
943 delete $params{'action'};
946 # Next, we put hash_parent_base:/file_parent..hash_base:/file_name,
947 # stripping nonexistent or useless pieces
948 $href .= "/" if ($params{'hash_base'} || $params{'hash_parent_base'}
949 || $params{'hash_parent'} || $params{'hash'});
950 if (defined $params{'hash_base'}) {
951 if (defined $params{'hash_parent_base'}) {
952 $href .= esc_url($params{'hash_parent_base'});
953 # skip the file_parent if it's the same as the file_name
954 if (defined $params{'file_parent'}) {
955 if (defined $params{'file_name'} && $params{'file_parent'} eq $params{'file_name'}) {
956 delete $params{'file_parent'};
957 } elsif ($params{'file_parent'} !~ /\.\./) {
958 $href .= ":/".esc_url($params{'file_parent'});
959 delete $params{'file_parent'};
963 delete $params{'hash_parent'};
964 delete $params{'hash_parent_base'};
965 } elsif (defined $params{'hash_parent'}) {
966 $href .= esc_url($params{'hash_parent'}). "..";
967 delete $params{'hash_parent'};
970 $href .= esc_url($params{'hash_base'});
971 if (defined $params{'file_name'} && $params{'file_name'} !~ /\.\./) {
972 $href .= ":/".esc_url($params{'file_name'});
973 delete $params{'file_name'};
975 delete $params{'hash'};
976 delete $params{'hash_base'};
977 } elsif (defined $params{'hash'}) {
978 $href .= esc_url($params{'hash'});
979 delete $params{'hash'};
982 # If the action was a snapshot, we can absorb the
983 # snapshot_format parameter too
985 my $fmt = $params{'snapshot_format'};
986 # snapshot_format should always be defined when href()
987 # is called, but just in case some code forgets, we
988 # fall back to the default
989 $fmt ||= $snapshot_fmts[0];
990 $href .= $known_snapshot_formats{$fmt}{'suffix'};
991 delete $params{'snapshot_format'};
995 # now encode the parameters explicitly
997 for (my $i = 0; $i < @cgi_param_mapping; $i += 2) {
998 my ($name, $symbol) = ($cgi_param_mapping[$i], $cgi_param_mapping[$i+1]);
999 if (defined $params{$name}) {
1000 if (ref($params{$name}) eq "ARRAY") {
1001 foreach my $par (@{$params{$name}}) {
1002 push @result, $symbol . "=" . esc_param($par);
1005 push @result, $symbol . "=" . esc_param($params{$name});
1009 $href .= "?" . join(';', @result) if scalar @result;
1015 ## ======================================================================
1016 ## validation, quoting/unquoting and escaping
1018 sub validate_action {
1019 my $input = shift || return undef;
1020 return undef unless exists $actions{$input};
1024 sub validate_project {
1025 my $input = shift || return undef;
1026 if (!validate_pathname($input) ||
1027 !(-d "$projectroot/$input") ||
1028 !check_export_ok("$projectroot/$input") ||
1029 ($strict_export && !project_in_list($input))) {
1036 sub validate_pathname {
1037 my $input = shift || return undef;
1039 # no '.' or '..' as elements of path, i.e. no '.' nor '..'
1040 # at the beginning, at the end, and between slashes.
1041 # also this catches doubled slashes
1042 if ($input =~ m!(^|/)(|\.|\.\.)(/|$)!) {
1045 # no null characters
1046 if ($input =~ m!\0!) {
1052 sub validate_refname {
1053 my $input = shift || return undef;
1055 # textual hashes are O.K.
1056 if ($input =~ m/^[0-9a-fA-F]{40}$/) {
1059 # it must be correct pathname
1060 $input = validate_pathname($input)
1062 # restrictions on ref name according to git-check-ref-format
1063 if ($input =~ m!(/\.|\.\.|[\000-\040\177 ~^:?*\[]|/$)!) {
1069 # decode sequences of octets in utf8 into Perl's internal form,
1070 # which is utf-8 with utf8 flag set if needed. gitweb writes out
1071 # in utf-8 thanks to "binmode STDOUT, ':utf8'" at beginning
1074 if (utf8::valid($str)) {
1078 return decode($fallback_encoding, $str, Encode::FB_DEFAULT);
1082 # quote unsafe chars, but keep the slash, even when it's not
1083 # correct, but quoted slashes look too horrible in bookmarks
1086 $str =~ s/([^A-Za-z0-9\-_.~()\/:@ ]+)/CGI::escape($1)/eg;
1091 # quote unsafe chars in whole URL, so some charactrs cannot be quoted
1094 $str =~ s/([^A-Za-z0-9\-_.~();\/;?:@&=])/sprintf("%%%02X", ord($1))/eg;
1100 # replace invalid utf8 character with SUBSTITUTION sequence
1105 $str = to_utf8($str);
1106 $str = $cgi->escapeHTML($str);
1107 if ($opts{'-nbsp'}) {
1108 $str =~ s/ / /g;
1110 $str =~ s|([[:cntrl:]])|(($1 ne "\t") ? quot_cec($1) : $1)|eg;
1114 # quote control characters and escape filename to HTML
1119 $str = to_utf8($str);
1120 $str = $cgi->escapeHTML($str);
1121 if ($opts{'-nbsp'}) {
1122 $str =~ s/ / /g;
1124 $str =~ s|([[:cntrl:]])|quot_cec($1)|eg;
1128 # Make control characters "printable", using character escape codes (CEC)
1132 my %es = ( # character escape codes, aka escape sequences
1133 "\t" => '\t', # tab (HT)
1134 "\n" => '\n', # line feed (LF)
1135 "\r" => '\r', # carrige return (CR)
1136 "\f" => '\f', # form feed (FF)
1137 "\b" => '\b', # backspace (BS)
1138 "\a" => '\a', # alarm (bell) (BEL)
1139 "\e" => '\e', # escape (ESC)
1140 "\013" => '\v', # vertical tab (VT)
1141 "\000" => '\0', # nul character (NUL)
1143 my $chr = ( (exists $es{$cntrl})
1145 : sprintf('\%2x', ord($cntrl)) );
1146 if ($opts{-nohtml}) {
1149 return "<span class=\"cntrl\">$chr</span>";
1153 # Alternatively use unicode control pictures codepoints,
1154 # Unicode "printable representation" (PR)
1159 my $chr = sprintf('&#%04d;', 0x2400+ord($cntrl));
1160 if ($opts{-nohtml}) {
1163 return "<span class=\"cntrl\">$chr</span>";
1167 # git may return quoted and escaped filenames
1173 my %es = ( # character escape codes, aka escape sequences
1174 't' => "\t", # tab (HT, TAB)
1175 'n' => "\n", # newline (NL)
1176 'r' => "\r", # return (CR)
1177 'f' => "\f", # form feed (FF)
1178 'b' => "\b", # backspace (BS)
1179 'a' => "\a", # alarm (bell) (BEL)
1180 'e' => "\e", # escape (ESC)
1181 'v' => "\013", # vertical tab (VT)
1184 if ($seq =~ m/^[0-7]{1,3}$/) {
1185 # octal char sequence
1186 return chr(oct($seq));
1187 } elsif (exists $es{$seq}) {
1188 # C escape sequence, aka character escape code
1191 # quoted ordinary character
1195 if ($str =~ m/^"(.*)"$/) {
1198 $str =~ s/\\([^0-7]|[0-7]{1,3})/unq($1)/eg;
1203 # escape tabs (convert tabs to spaces)
1207 while ((my $pos = index($line, "\t")) != -1) {
1208 if (my $count = (8 - ($pos % 8))) {
1209 my $spaces = ' ' x $count;
1210 $line =~ s/\t/$spaces/;
1217 sub project_in_list {
1218 my $project = shift;
1219 my @list = git_get_projects_list();
1220 return @list && scalar(grep { $_->{'path'} eq $project } @list);
1223 ## ----------------------------------------------------------------------
1224 ## HTML aware string manipulation
1226 # Try to chop given string on a word boundary between position
1227 # $len and $len+$add_len. If there is no word boundary there,
1228 # chop at $len+$add_len. Do not chop if chopped part plus ellipsis
1229 # (marking chopped part) would be longer than given string.
1233 my $add_len = shift || 10;
1234 my $where = shift || 'right'; # 'left' | 'center' | 'right'
1236 # Make sure perl knows it is utf8 encoded so we don't
1237 # cut in the middle of a utf8 multibyte char.
1238 $str = to_utf8($str);
1240 # allow only $len chars, but don't cut a word if it would fit in $add_len
1241 # if it doesn't fit, cut it if it's still longer than the dots we would add
1242 # remove chopped character entities entirely
1244 # when chopping in the middle, distribute $len into left and right part
1245 # return early if chopping wouldn't make string shorter
1246 if ($where eq 'center') {
1247 return $str if ($len + 5 >= length($str)); # filler is length 5
1250 return $str if ($len + 4 >= length($str)); # filler is length 4
1253 # regexps: ending and beginning with word part up to $add_len
1254 my $endre = qr/.{$len}\w{0,$add_len}/;
1255 my $begre = qr/\w{0,$add_len}.{$len}/;
1257 if ($where eq 'left') {
1258 $str =~ m/^(.*?)($begre)$/;
1259 my ($lead, $body) = ($1, $2);
1260 if (length($lead) > 4) {
1261 $body =~ s/^[^;]*;// if ($lead =~ m/&[^;]*$/);
1264 return "$lead$body";
1266 } elsif ($where eq 'center') {
1267 $str =~ m/^($endre)(.*)$/;
1268 my ($left, $str) = ($1, $2);
1269 $str =~ m/^(.*?)($begre)$/;
1270 my ($mid, $right) = ($1, $2);
1271 if (length($mid) > 5) {
1272 $left =~ s/&[^;]*$//;
1273 $right =~ s/^[^;]*;// if ($mid =~ m/&[^;]*$/);
1276 return "$left$mid$right";
1279 $str =~ m/^($endre)(.*)$/;
1282 if (length($tail) > 4) {
1283 $body =~ s/&[^;]*$//;
1286 return "$body$tail";
1290 # takes the same arguments as chop_str, but also wraps a <span> around the
1291 # result with a title attribute if it does get chopped. Additionally, the
1292 # string is HTML-escaped.
1293 sub chop_and_escape_str {
1296 my $chopped = chop_str(@_);
1297 if ($chopped eq $str) {
1298 return esc_html($chopped);
1300 $str =~ s/[[:cntrl:]]/?/g;
1301 return $cgi->span({-title=>$str}, esc_html($chopped));
1305 ## ----------------------------------------------------------------------
1306 ## functions returning short strings
1308 # CSS class for given age value (in seconds)
1312 if (!defined $age) {
1314 } elsif ($age < 60*60*2) {
1316 } elsif ($age < 60*60*24*2) {
1323 # convert age in seconds to "nn units ago" string
1328 if ($age > 60*60*24*365*2) {
1329 $age_str = (int $age/60/60/24/365);
1330 $age_str .= " years ago";
1331 } elsif ($age > 60*60*24*(365/12)*2) {
1332 $age_str = int $age/60/60/24/(365/12);
1333 $age_str .= " months ago";
1334 } elsif ($age > 60*60*24*7*2) {
1335 $age_str = int $age/60/60/24/7;
1336 $age_str .= " weeks ago";
1337 } elsif ($age > 60*60*24*2) {
1338 $age_str = int $age/60/60/24;
1339 $age_str .= " days ago";
1340 } elsif ($age > 60*60*2) {
1341 $age_str = int $age/60/60;
1342 $age_str .= " hours ago";
1343 } elsif ($age > 60*2) {
1344 $age_str = int $age/60;
1345 $age_str .= " min ago";
1346 } elsif ($age > 2) {
1347 $age_str = int $age;
1348 $age_str .= " sec ago";
1350 $age_str .= " right now";
1356 S_IFINVALID => 0030000,
1357 S_IFGITLINK => 0160000,
1360 # submodule/subproject, a commit object reference
1364 return (($mode & S_IFMT) == S_IFGITLINK)
1367 # convert file mode in octal to symbolic file mode string
1369 my $mode = oct shift;
1371 if (S_ISGITLINK($mode)) {
1372 return 'm---------';
1373 } elsif (S_ISDIR($mode & S_IFMT)) {
1374 return 'drwxr-xr-x';
1375 } elsif (S_ISLNK($mode)) {
1376 return 'lrwxrwxrwx';
1377 } elsif (S_ISREG($mode)) {
1378 # git cares only about the executable bit
1379 if ($mode & S_IXUSR) {
1380 return '-rwxr-xr-x';
1382 return '-rw-r--r--';
1385 return '----------';
1389 # convert file mode in octal to file type string
1393 if ($mode !~ m/^[0-7]+$/) {
1399 if (S_ISGITLINK($mode)) {
1401 } elsif (S_ISDIR($mode & S_IFMT)) {
1403 } elsif (S_ISLNK($mode)) {
1405 } elsif (S_ISREG($mode)) {
1412 # convert file mode in octal to file type description string
1413 sub file_type_long {
1416 if ($mode !~ m/^[0-7]+$/) {
1422 if (S_ISGITLINK($mode)) {
1424 } elsif (S_ISDIR($mode & S_IFMT)) {
1426 } elsif (S_ISLNK($mode)) {
1428 } elsif (S_ISREG($mode)) {
1429 if ($mode & S_IXUSR) {
1430 return "executable";
1440 ## ----------------------------------------------------------------------
1441 ## functions returning short HTML fragments, or transforming HTML fragments
1442 ## which don't belong to other sections
1444 # format line of commit message.
1445 sub format_log_line_html {
1448 $line = esc_html($line, -nbsp=>1);
1449 $line =~ s{\b([0-9a-fA-F]{8,40})\b}{
1450 $cgi->a({-href => href(action=>"object", hash=>$1),
1451 -class => "text"}, $1);
1457 # format marker of refs pointing to given object
1459 # the destination action is chosen based on object type and current context:
1460 # - for annotated tags, we choose the tag view unless it's the current view
1461 # already, in which case we go to shortlog view
1462 # - for other refs, we keep the current view if we're in history, shortlog or
1463 # log view, and select shortlog otherwise
1464 sub format_ref_marker {
1465 my ($refs, $id) = @_;
1468 if (defined $refs->{$id}) {
1469 foreach my $ref (@{$refs->{$id}}) {
1470 # this code exploits the fact that non-lightweight tags are the
1471 # only indirect objects, and that they are the only objects for which
1472 # we want to use tag instead of shortlog as action
1473 my ($type, $name) = qw();
1474 my $indirect = ($ref =~ s/\^\{\}$//);
1475 # e.g. tags/v2.6.11 or heads/next
1476 if ($ref =~ m!^(.*?)s?/(.*)$!) {
1485 $class .= " indirect" if $indirect;
1487 my $dest_action = "shortlog";
1490 $dest_action = "tag" unless $action eq "tag";
1491 } elsif ($action =~ /^(history|(short)?log)$/) {
1492 $dest_action = $action;
1496 $dest .= "refs/" unless $ref =~ m!^refs/!;
1499 my $link = $cgi->a({
1501 action=>$dest_action,
1505 $markers .= " <span class=\"$class\" title=\"$ref\">" .
1511 return ' <span class="refs">'. $markers . '</span>';
1517 # format, perhaps shortened and with markers, title line
1518 sub format_subject_html {
1519 my ($long, $short, $href, $extra) = @_;
1520 $extra = '' unless defined($extra);
1522 if (length($short) < length($long)) {
1523 $long =~ s/[[:cntrl:]]/?/g;
1524 return $cgi->a({-href => $href, -class => "list subject",
1525 -title => to_utf8($long)},
1526 esc_html($short)) . $extra;
1528 return $cgi->a({-href => $href, -class => "list subject"},
1529 esc_html($long)) . $extra;
1533 # Rather than recomputing the url for an email multiple times, we cache it
1534 # after the first hit. This gives a visible benefit in views where the avatar
1535 # for the same email is used repeatedly (e.g. shortlog).
1536 # The cache is shared by all avatar engines (currently gravatar only), which
1537 # are free to use it as preferred. Since only one avatar engine is used for any
1538 # given page, there's no risk for cache conflicts.
1539 our %avatar_cache = ();
1541 # Compute the picon url for a given email, by using the picon search service over at
1542 # http://www.cs.indiana.edu/picons/search.html
1544 my $email = lc shift;
1545 if (!$avatar_cache{$email}) {
1546 my ($user, $domain) = split('@', $email);
1547 $avatar_cache{$email} =
1548 "http://www.cs.indiana.edu/cgi-pub/kinzler/piconsearch.cgi/" .
1550 "users+domains+unknown/up/single";
1552 return $avatar_cache{$email};
1555 # Compute the gravatar url for a given email, if it's not in the cache already.
1556 # Gravatar stores only the part of the URL before the size, since that's the
1557 # one computationally more expensive. This also allows reuse of the cache for
1558 # different sizes (for this particular engine).
1560 my $email = lc shift;
1562 $avatar_cache{$email} ||=
1563 "http://www.gravatar.com/avatar/" .
1564 Digest::MD5::md5_hex($email) . "?s=";
1565 return $avatar_cache{$email} . $size;
1568 # Insert an avatar for the given $email at the given $size if the feature
1570 sub git_get_avatar {
1571 my ($email, %opts) = @_;
1572 my $pre_white = ($opts{-pad_before} ? " " : "");
1573 my $post_white = ($opts{-pad_after} ? " " : "");
1574 $opts{-size} ||= 'default';
1575 my $size = $avatar_size{$opts{-size}} || $avatar_size{'default'};
1577 if ($git_avatar eq 'gravatar') {
1578 $url = gravatar_url($email, $size);
1579 } elsif ($git_avatar eq 'picon') {
1580 $url = picon_url($email);
1582 # Other providers can be added by extending the if chain, defining $url
1583 # as needed. If no variant puts something in $url, we assume avatars
1584 # are completely disabled/unavailable.
1587 "<img width=\"$size\" " .
1588 "class=\"avatar\" " .
1597 # format the author name of the given commit with the given tag
1598 # the author name is chopped and escaped according to the other
1599 # optional parameters (see chop_str).
1600 sub format_author_html {
1603 my $author = chop_and_escape_str($co->{'author_name'}, @_);
1604 return "<$tag class=\"author\">" .
1605 git_get_avatar($co->{'author_email'}, -pad_after => 1) .
1606 $author . "</$tag>";
1609 # format git diff header line, i.e. "diff --(git|combined|cc) ..."
1610 sub format_git_diff_header_line {
1612 my $diffinfo = shift;
1613 my ($from, $to) = @_;
1615 if ($diffinfo->{'nparents'}) {
1617 $line =~ s!^(diff (.*?) )"?.*$!$1!;
1618 if ($to->{'href'}) {
1619 $line .= $cgi->a({-href => $to->{'href'}, -class => "path"},
1620 esc_path($to->{'file'}));
1621 } else { # file was deleted (no href)
1622 $line .= esc_path($to->{'file'});
1626 $line =~ s!^(diff (.*?) )"?a/.*$!$1!;
1627 if ($from->{'href'}) {
1628 $line .= $cgi->a({-href => $from->{'href'}, -class => "path"},
1629 'a/' . esc_path($from->{'file'}));
1630 } else { # file was added (no href)
1631 $line .= 'a/' . esc_path($from->{'file'});
1634 if ($to->{'href'}) {
1635 $line .= $cgi->a({-href => $to->{'href'}, -class => "path"},
1636 'b/' . esc_path($to->{'file'}));
1637 } else { # file was deleted
1638 $line .= 'b/' . esc_path($to->{'file'});
1642 return "<div class=\"diff header\">$line</div>\n";
1645 # format extended diff header line, before patch itself
1646 sub format_extended_diff_header_line {
1648 my $diffinfo = shift;
1649 my ($from, $to) = @_;
1652 if ($line =~ s!^((copy|rename) from ).*$!$1! && $from->{'href'}) {
1653 $line .= $cgi->a({-href=>$from->{'href'}, -class=>"path"},
1654 esc_path($from->{'file'}));
1656 if ($line =~ s!^((copy|rename) to ).*$!$1! && $to->{'href'}) {
1657 $line .= $cgi->a({-href=>$to->{'href'}, -class=>"path"},
1658 esc_path($to->{'file'}));
1660 # match single <mode>
1661 if ($line =~ m/\s(\d{6})$/) {
1662 $line .= '<span class="info"> (' .
1663 file_type_long($1) .
1667 if ($line =~ m/^index [0-9a-fA-F]{40},[0-9a-fA-F]{40}/) {
1668 # can match only for combined diff
1670 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
1671 if ($from->{'href'}[$i]) {
1672 $line .= $cgi->a({-href=>$from->{'href'}[$i],
1674 substr($diffinfo->{'from_id'}[$i],0,7));
1679 $line .= ',' if ($i < $diffinfo->{'nparents'} - 1);
1682 if ($to->{'href'}) {
1683 $line .= $cgi->a({-href=>$to->{'href'}, -class=>"hash"},
1684 substr($diffinfo->{'to_id'},0,7));
1689 } elsif ($line =~ m/^index [0-9a-fA-F]{40}..[0-9a-fA-F]{40}/) {
1690 # can match only for ordinary diff
1691 my ($from_link, $to_link);
1692 if ($from->{'href'}) {
1693 $from_link = $cgi->a({-href=>$from->{'href'}, -class=>"hash"},
1694 substr($diffinfo->{'from_id'},0,7));
1696 $from_link = '0' x 7;
1698 if ($to->{'href'}) {
1699 $to_link = $cgi->a({-href=>$to->{'href'}, -class=>"hash"},
1700 substr($diffinfo->{'to_id'},0,7));
1704 my ($from_id, $to_id) = ($diffinfo->{'from_id'}, $diffinfo->{'to_id'});
1705 $line =~ s!$from_id\.\.$to_id!$from_link..$to_link!;
1708 return $line . "<br/>\n";
1711 # format from-file/to-file diff header
1712 sub format_diff_from_to_header {
1713 my ($from_line, $to_line, $diffinfo, $from, $to, @parents) = @_;
1718 #assert($line =~ m/^---/) if DEBUG;
1719 # no extra formatting for "^--- /dev/null"
1720 if (! $diffinfo->{'nparents'}) {
1721 # ordinary (single parent) diff
1722 if ($line =~ m!^--- "?a/!) {
1723 if ($from->{'href'}) {
1725 $cgi->a({-href=>$from->{'href'}, -class=>"path"},
1726 esc_path($from->{'file'}));
1729 esc_path($from->{'file'});
1732 $result .= qq!<div class="diff from_file">$line</div>\n!;
1735 # combined diff (merge commit)
1736 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
1737 if ($from->{'href'}[$i]) {
1739 $cgi->a({-href=>href(action=>"blobdiff",
1740 hash_parent=>$diffinfo->{'from_id'}[$i],
1741 hash_parent_base=>$parents[$i],
1742 file_parent=>$from->{'file'}[$i],
1743 hash=>$diffinfo->{'to_id'},
1745 file_name=>$to->{'file'}),
1747 -title=>"diff" . ($i+1)},
1750 $cgi->a({-href=>$from->{'href'}[$i], -class=>"path"},
1751 esc_path($from->{'file'}[$i]));
1753 $line = '--- /dev/null';
1755 $result .= qq!<div class="diff from_file">$line</div>\n!;
1760 #assert($line =~ m/^\+\+\+/) if DEBUG;
1761 # no extra formatting for "^+++ /dev/null"
1762 if ($line =~ m!^\+\+\+ "?b/!) {
1763 if ($to->{'href'}) {
1765 $cgi->a({-href=>$to->{'href'}, -class=>"path"},
1766 esc_path($to->{'file'}));
1769 esc_path($to->{'file'});
1772 $result .= qq!<div class="diff to_file">$line</div>\n!;
1777 # create note for patch simplified by combined diff
1778 sub format_diff_cc_simplified {
1779 my ($diffinfo, @parents) = @_;
1782 $result .= "<div class=\"diff header\">" .
1784 if (!is_deleted($diffinfo)) {
1785 $result .= $cgi->a({-href => href(action=>"blob",
1787 hash=>$diffinfo->{'to_id'},
1788 file_name=>$diffinfo->{'to_file'}),
1790 esc_path($diffinfo->{'to_file'}));
1792 $result .= esc_path($diffinfo->{'to_file'});
1794 $result .= "</div>\n" . # class="diff header"
1795 "<div class=\"diff nodifferences\">" .
1797 "</div>\n"; # class="diff nodifferences"
1802 # format patch (diff) line (not to be used for diff headers)
1803 sub format_diff_line {
1805 my ($from, $to) = @_;
1806 my $diff_class = "";
1810 if ($from && $to && ref($from->{'href'}) eq "ARRAY") {
1812 my $prefix = substr($line, 0, scalar @{$from->{'href'}});
1813 if ($line =~ m/^\@{3}/) {
1814 $diff_class = " chunk_header";
1815 } elsif ($line =~ m/^\\/) {
1816 $diff_class = " incomplete";
1817 } elsif ($prefix =~ tr/+/+/) {
1818 $diff_class = " add";
1819 } elsif ($prefix =~ tr/-/-/) {
1820 $diff_class = " rem";
1823 # assume ordinary diff
1824 my $char = substr($line, 0, 1);
1826 $diff_class = " add";
1827 } elsif ($char eq '-') {
1828 $diff_class = " rem";
1829 } elsif ($char eq '@') {
1830 $diff_class = " chunk_header";
1831 } elsif ($char eq "\\") {
1832 $diff_class = " incomplete";
1835 $line = untabify($line);
1836 if ($from && $to && $line =~ m/^\@{2} /) {
1837 my ($from_text, $from_start, $from_lines, $to_text, $to_start, $to_lines, $section) =
1838 $line =~ m/^\@{2} (-(\d+)(?:,(\d+))?) (\+(\d+)(?:,(\d+))?) \@{2}(.*)$/;
1840 $from_lines = 0 unless defined $from_lines;
1841 $to_lines = 0 unless defined $to_lines;
1843 if ($from->{'href'}) {
1844 $from_text = $cgi->a({-href=>"$from->{'href'}#l$from_start",
1845 -class=>"list"}, $from_text);
1847 if ($to->{'href'}) {
1848 $to_text = $cgi->a({-href=>"$to->{'href'}#l$to_start",
1849 -class=>"list"}, $to_text);
1851 $line = "<span class=\"chunk_info\">@@ $from_text $to_text @@</span>" .
1852 "<span class=\"section\">" . esc_html($section, -nbsp=>1) . "</span>";
1853 return "<div class=\"diff$diff_class\">$line</div>\n";
1854 } elsif ($from && $to && $line =~ m/^\@{3}/) {
1855 my ($prefix, $ranges, $section) = $line =~ m/^(\@+) (.*?) \@+(.*)$/;
1856 my (@from_text, @from_start, @from_nlines, $to_text, $to_start, $to_nlines);
1858 @from_text = split(' ', $ranges);
1859 for (my $i = 0; $i < @from_text; ++$i) {
1860 ($from_start[$i], $from_nlines[$i]) =
1861 (split(',', substr($from_text[$i], 1)), 0);
1864 $to_text = pop @from_text;
1865 $to_start = pop @from_start;
1866 $to_nlines = pop @from_nlines;
1868 $line = "<span class=\"chunk_info\">$prefix ";
1869 for (my $i = 0; $i < @from_text; ++$i) {
1870 if ($from->{'href'}[$i]) {
1871 $line .= $cgi->a({-href=>"$from->{'href'}[$i]#l$from_start[$i]",
1872 -class=>"list"}, $from_text[$i]);
1874 $line .= $from_text[$i];
1878 if ($to->{'href'}) {
1879 $line .= $cgi->a({-href=>"$to->{'href'}#l$to_start",
1880 -class=>"list"}, $to_text);
1884 $line .= " $prefix</span>" .
1885 "<span class=\"section\">" . esc_html($section, -nbsp=>1) . "</span>";
1886 return "<div class=\"diff$diff_class\">$line</div>\n";
1888 return "<div class=\"diff$diff_class\">" . esc_html($line, -nbsp=>1) . "</div>\n";
1891 # Generates undef or something like "_snapshot_" or "snapshot (_tbz2_ _zip_)",
1892 # linked. Pass the hash of the tree/commit to snapshot.
1893 sub format_snapshot_links {
1895 my $num_fmts = @snapshot_fmts;
1896 if ($num_fmts > 1) {
1897 # A parenthesized list of links bearing format names.
1898 # e.g. "snapshot (_tar.gz_ _zip_)"
1899 return "snapshot (" . join(' ', map
1906 }, $known_snapshot_formats{$_}{'display'})
1907 , @snapshot_fmts) . ")";
1908 } elsif ($num_fmts == 1) {
1909 # A single "snapshot" link whose tooltip bears the format name.
1911 my ($fmt) = @snapshot_fmts;
1917 snapshot_format=>$fmt
1919 -title => "in format: $known_snapshot_formats{$fmt}{'display'}"
1921 } else { # $num_fmts == 0
1926 ## ......................................................................
1927 ## functions returning values to be passed, perhaps after some
1928 ## transformation, to other functions; e.g. returning arguments to href()
1930 # returns hash to be passed to href to generate gitweb URL
1931 # in -title key it returns description of link
1933 my $format = shift || 'Atom';
1934 my %res = (action => lc($format));
1936 # feed links are possible only for project views
1937 return unless (defined $project);
1938 # some views should link to OPML, or to generic project feed,
1939 # or don't have specific feed yet (so they should use generic)
1940 return if ($action =~ /^(?:tags|heads|forks|tag|search)$/x);
1943 # branches refs uses 'refs/heads/' prefix (fullname) to differentiate
1944 # from tag links; this also makes possible to detect branch links
1945 if ((defined $hash_base && $hash_base =~ m!^refs/heads/(.*)$!) ||
1946 (defined $hash && $hash =~ m!^refs/heads/(.*)$!)) {
1949 # find log type for feed description (title)
1951 if (defined $file_name) {
1952 $type = "history of $file_name";
1953 $type .= "/" if ($action eq 'tree');
1954 $type .= " on '$branch'" if (defined $branch);
1956 $type = "log of $branch" if (defined $branch);
1959 $res{-title} = $type;
1960 $res{'hash'} = (defined $branch ? "refs/heads/$branch" : undef);
1961 $res{'file_name'} = $file_name;
1966 ## ----------------------------------------------------------------------
1967 ## git utility subroutines, invoking git commands
1969 # returns path to the core git executable and the --git-dir parameter as list
1971 return $GIT, '--git-dir='.$git_dir;
1974 # quote the given arguments for passing them to the shell
1975 # quote_command("command", "arg 1", "arg with ' and ! characters")
1976 # => "'command' 'arg 1' 'arg with '\'' and '\!' characters'"
1977 # Try to avoid using this function wherever possible.
1980 map { my $a = $_; $a =~ s/(['!])/'\\$1'/g; "'$a'" } @_ );
1983 # get HEAD ref of given project as hash
1984 sub git_get_head_hash {
1985 my $project = shift;
1986 my $o_git_dir = $git_dir;
1988 $git_dir = "$projectroot/$project";
1989 if (open my $fd, "-|", git_cmd(), "rev-parse", "--verify", "HEAD") {
1992 if (defined $head && $head =~ /^([0-9a-fA-F]{40})$/) {
1996 if (defined $o_git_dir) {
1997 $git_dir = $o_git_dir;
2002 # get type of given object
2006 open my $fd, "-|", git_cmd(), "cat-file", '-t', $hash or return;
2008 close $fd or return;
2013 # repository configuration
2014 our $config_file = '';
2017 # store multiple values for single key as anonymous array reference
2018 # single values stored directly in the hash, not as [ <value> ]
2019 sub hash_set_multi {
2020 my ($hash, $key, $value) = @_;
2022 if (!exists $hash->{$key}) {
2023 $hash->{$key} = $value;
2024 } elsif (!ref $hash->{$key}) {
2025 $hash->{$key} = [ $hash->{$key}, $value ];
2027 push @{$hash->{$key}}, $value;
2031 # return hash of git project configuration
2032 # optionally limited to some section, e.g. 'gitweb'
2033 sub git_parse_project_config {
2034 my $section_regexp = shift;
2039 open my $fh, "-|", git_cmd(), "config", '-z', '-l',
2042 while (my $keyval = <$fh>) {
2044 my ($key, $value) = split(/\n/, $keyval, 2);
2046 hash_set_multi(\%config, $key, $value)
2047 if (!defined $section_regexp || $key =~ /^(?:$section_regexp)\./o);
2054 # convert config value to boolean: 'true' or 'false'
2055 # no value, number > 0, 'true' and 'yes' values are true
2056 # rest of values are treated as false (never as error)
2057 sub config_to_bool {
2060 return 1 if !defined $val; # section.key
2062 # strip leading and trailing whitespace
2066 return (($val =~ /^\d+$/ && $val) || # section.key = 1
2067 ($val =~ /^(?:true|yes)$/i)); # section.key = true
2070 # convert config value to simple decimal number
2071 # an optional value suffix of 'k', 'm', or 'g' will cause the value
2072 # to be multiplied by 1024, 1048576, or 1073741824
2076 # strip leading and trailing whitespace
2080 if (my ($num, $unit) = ($val =~ /^([0-9]*)([kmg])$/i)) {
2082 # unknown unit is treated as 1
2083 return $num * ($unit eq 'g' ? 1073741824 :
2084 $unit eq 'm' ? 1048576 :
2085 $unit eq 'k' ? 1024 : 1);
2090 # convert config value to array reference, if needed
2091 sub config_to_multi {
2094 return ref($val) ? $val : (defined($val) ? [ $val ] : []);
2097 sub git_get_project_config {
2098 my ($key, $type) = @_;
2101 return unless ($key);
2102 $key =~ s/^gitweb\.//;
2103 return if ($key =~ m/\W/);
2106 if (defined $type) {
2109 unless ($type eq 'bool' || $type eq 'int');
2113 if (!defined $config_file ||
2114 $config_file ne "$git_dir/config") {
2115 %config = git_parse_project_config('gitweb');
2116 $config_file = "$git_dir/config";
2119 # check if config variable (key) exists
2120 return unless exists $config{"gitweb.$key"};
2123 if (!defined $type) {
2124 return $config{"gitweb.$key"};
2125 } elsif ($type eq 'bool') {
2126 # backward compatibility: 'git config --bool' returns true/false
2127 return config_to_bool($config{"gitweb.$key"}) ? 'true' : 'false';
2128 } elsif ($type eq 'int') {
2129 return config_to_int($config{"gitweb.$key"});
2131 return $config{"gitweb.$key"};
2134 # get hash of given path at given ref
2135 sub git_get_hash_by_path {
2137 my $path = shift || return undef;
2142 open my $fd, "-|", git_cmd(), "ls-tree", $base, "--", $path
2143 or die_error(500, "Open git-ls-tree failed");
2145 close $fd or return undef;
2147 if (!defined $line) {
2148 # there is no tree or hash given by $path at $base
2152 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'
2153 $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t/;
2154 if (defined $type && $type ne $2) {
2155 # type doesn't match
2161 # get path of entry with given hash at given tree-ish (ref)
2162 # used to get 'from' filename for combined diff (merge commit) for renames
2163 sub git_get_path_by_hash {
2164 my $base = shift || return;
2165 my $hash = shift || return;
2169 open my $fd, "-|", git_cmd(), "ls-tree", '-r', '-t', '-z', $base
2171 while (my $line = <$fd>) {
2174 #'040000 tree 595596a6a9117ddba9fe379b6b012b558bac8423 gitweb'
2175 #'100644 blob e02e90f0429be0d2a69b76571101f20b8f75530f gitweb/README'
2176 if ($line =~ m/(?:[0-9]+) (?:.+) $hash\t(.+)$/) {
2185 ## ......................................................................
2186 ## git utility functions, directly accessing git repository
2188 sub git_get_project_description {
2191 $git_dir = "$projectroot/$path";
2192 open my $fd, '<', "$git_dir/description"
2193 or return git_get_project_config('description');
2196 if (defined $descr) {
2202 sub git_get_project_ctags {
2206 $git_dir = "$projectroot/$path";
2207 opendir my $dh, "$git_dir/ctags"
2209 foreach (grep { -f $_ } map { "$git_dir/ctags/$_" } readdir($dh)) {
2210 open my $ct, '<', $_ or next;
2214 my $ctag = $_; $ctag =~ s#.*/##;
2215 $ctags->{$ctag} = $val;
2221 sub git_populate_project_tagcloud {
2224 # First, merge different-cased tags; tags vote on casing
2226 foreach (keys %$ctags) {
2227 $ctags_lc{lc $_}->{count} += $ctags->{$_};
2228 if (not $ctags_lc{lc $_}->{topcount}
2229 or $ctags_lc{lc $_}->{topcount} < $ctags->{$_}) {
2230 $ctags_lc{lc $_}->{topcount} = $ctags->{$_};
2231 $ctags_lc{lc $_}->{topname} = $_;
2236 if (eval { require HTML::TagCloud; 1; }) {
2237 $cloud = HTML::TagCloud->new;
2238 foreach (sort keys %ctags_lc) {
2239 # Pad the title with spaces so that the cloud looks
2241 my $title = $ctags_lc{$_}->{topname};
2242 $title =~ s/ / /g;
2243 $title =~ s/^/ /g;
2244 $title =~ s/$/ /g;
2245 $cloud->add($title, $home_link."?by_tag=".$_, $ctags_lc{$_}->{count});
2248 $cloud = \%ctags_lc;
2253 sub git_show_project_tagcloud {
2254 my ($cloud, $count) = @_;
2255 print STDERR ref($cloud)."..\n";
2256 if (ref $cloud eq 'HTML::TagCloud') {
2257 return $cloud->html_and_css($count);
2259 my @tags = sort { $cloud->{$a}->{count} <=> $cloud->{$b}->{count} } keys %$cloud;
2260 return '<p align="center">' . join (', ', map {
2261 "<a href=\"$home_link?by_tag=$_\">$cloud->{$_}->{topname}</a>"
2262 } splice(@tags, 0, $count)) . '</p>';
2266 sub git_get_project_url_list {
2269 $git_dir = "$projectroot/$path";
2270 open my $fd, '<', "$git_dir/cloneurl"
2271 or return wantarray ?
2272 @{ config_to_multi(git_get_project_config('url')) } :
2273 config_to_multi(git_get_project_config('url'));
2274 my @git_project_url_list = map { chomp; $_ } <$fd>;
2277 return wantarray ? @git_project_url_list : \@git_project_url_list;
2280 sub git_get_projects_list {
2285 $filter =~ s/\.git$//;
2287 my $check_forks = gitweb_check_feature('forks');
2289 if (-d $projects_list) {
2290 # search in directory
2291 my $dir = $projects_list . ($filter ? "/$filter" : '');
2292 # remove the trailing "/"
2294 my $pfxlen = length("$dir");
2295 my $pfxdepth = ($dir =~ tr!/!!);
2298 follow_fast => 1, # follow symbolic links
2299 follow_skip => 2, # ignore duplicates
2300 dangling_symlinks => 0, # ignore dangling symlinks, silently
2302 # skip project-list toplevel, if we get it.
2303 return if (m!^[/.]$!);
2304 # only directories can be git repositories
2305 return unless (-d $_);
2306 # don't traverse too deep (Find is super slow on os x)
2307 if (($File::Find::name =~ tr!/!!) - $pfxdepth > $project_maxdepth) {
2308 $File::Find::prune = 1;
2312 my $subdir = substr($File::Find::name, $pfxlen + 1);
2313 # we check related file in $projectroot
2314 my $path = ($filter ? "$filter/" : '') . $subdir;
2315 if (check_export_ok("$projectroot/$path")) {
2316 push @list, { path => $path };
2317 $File::Find::prune = 1;
2322 } elsif (-f $projects_list) {
2323 # read from file(url-encoded):
2324 # 'git%2Fgit.git Linus+Torvalds'
2325 # 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin'
2326 # 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman'
2328 open my $fd, '<', $projects_list or return;
2330 while (my $line = <$fd>) {
2332 my ($path, $owner) = split ' ', $line;
2333 $path = unescape($path);
2334 $owner = unescape($owner);
2335 if (!defined $path) {
2338 if ($filter ne '') {
2339 # looking for forks;
2340 my $pfx = substr($path, 0, length($filter));
2341 if ($pfx ne $filter) {
2344 my $sfx = substr($path, length($filter));
2345 if ($sfx !~ /^\/.*\.git$/) {
2348 } elsif ($check_forks) {
2350 foreach my $filter (keys %paths) {
2351 # looking for forks;
2352 my $pfx = substr($path, 0, length($filter));
2353 if ($pfx ne $filter) {
2356 my $sfx = substr($path, length($filter));
2357 if ($sfx !~ /^\/.*\.git$/) {
2360 # is a fork, don't include it in
2365 if (check_export_ok("$projectroot/$path")) {
2368 owner => to_utf8($owner),
2371 (my $forks_path = $path) =~ s/\.git$//;
2372 $paths{$forks_path}++;
2380 our $gitweb_project_owner = undef;
2381 sub git_get_project_list_from_file {
2383 return if (defined $gitweb_project_owner);
2385 $gitweb_project_owner = {};
2386 # read from file (url-encoded):
2387 # 'git%2Fgit.git Linus+Torvalds'
2388 # 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin'
2389 # 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman'
2390 if (-f $projects_list) {
2391 open(my $fd, '<', $projects_list);
2392 while (my $line = <$fd>) {
2394 my ($pr, $ow) = split ' ', $line;
2395 $pr = unescape($pr);
2396 $ow = unescape($ow);
2397 $gitweb_project_owner->{$pr} = to_utf8($ow);
2403 sub git_get_project_owner {
2404 my $project = shift;
2407 return undef unless $project;
2408 $git_dir = "$projectroot/$project";
2410 if (!defined $gitweb_project_owner) {
2411 git_get_project_list_from_file();
2414 if (exists $gitweb_project_owner->{$project}) {
2415 $owner = $gitweb_project_owner->{$project};
2417 if (!defined $owner){
2418 $owner = git_get_project_config('owner');
2420 if (!defined $owner) {
2421 $owner = get_file_owner("$git_dir");
2427 sub git_get_last_activity {
2431 $git_dir = "$projectroot/$path";
2432 open($fd, "-|", git_cmd(), 'for-each-ref',
2433 '--format=%(committer)',
2434 '--sort=-committerdate',
2436 'refs/heads') or return;
2437 my $most_recent = <$fd>;
2438 close $fd or return;
2439 if (defined $most_recent &&
2440 $most_recent =~ / (\d+) [-+][01]\d\d\d$/) {
2442 my $age = time - $timestamp;
2443 return ($age, age_string($age));
2445 return (undef, undef);
2448 sub git_get_references {
2449 my $type = shift || "";
2451 # 5dc01c595e6c6ec9ccda4f6f69c131c0dd945f8c refs/tags/v2.6.11
2452 # c39ae07f393806ccf406ef966e9a15afc43cc36a refs/tags/v2.6.11^{}
2453 open my $fd, "-|", git_cmd(), "show-ref", "--dereference",
2454 ($type ? ("--", "refs/$type") : ()) # use -- <pattern> if $type
2457 while (my $line = <$fd>) {
2459 if ($line =~ m!^([0-9a-fA-F]{40})\srefs/($type.*)$!) {
2460 if (defined $refs{$1}) {
2461 push @{$refs{$1}}, $2;
2467 close $fd or return;
2471 sub git_get_rev_name_tags {
2472 my $hash = shift || return undef;
2474 open my $fd, "-|", git_cmd(), "name-rev", "--tags", $hash
2476 my $name_rev = <$fd>;
2479 if ($name_rev =~ m|^$hash tags/(.*)$|) {
2482 # catches also '$hash undefined' output
2487 ## ----------------------------------------------------------------------
2488 ## parse to hash functions
2492 my $tz = shift || "-0000";
2495 my @months = ("Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec");
2496 my @days = ("Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat");
2497 my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($epoch);
2498 $date{'hour'} = $hour;
2499 $date{'minute'} = $min;
2500 $date{'mday'} = $mday;
2501 $date{'day'} = $days[$wday];
2502 $date{'month'} = $months[$mon];
2503 $date{'rfc2822'} = sprintf "%s, %d %s %4d %02d:%02d:%02d +0000",
2504 $days[$wday], $mday, $months[$mon], 1900+$year, $hour ,$min, $sec;
2505 $date{'mday-time'} = sprintf "%d %s %02d:%02d",
2506 $mday, $months[$mon], $hour ,$min;
2507 $date{'iso-8601'} = sprintf "%04d-%02d-%02dT%02d:%02d:%02dZ",
2508 1900+$year, 1+$mon, $mday, $hour ,$min, $sec;
2510 $tz =~ m/^([+\-][0-9][0-9])([0-9][0-9])$/;
2511 my $local = $epoch + ((int $1 + ($2/60)) * 3600);
2512 ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($local);
2513 $date{'hour_local'} = $hour;
2514 $date{'minute_local'} = $min;
2515 $date{'tz_local'} = $tz;
2516 $date{'iso-tz'} = sprintf("%04d-%02d-%02d %02d:%02d:%02d %s",
2517 1900+$year, $mon+1, $mday,
2518 $hour, $min, $sec, $tz);
2527 open my $fd, "-|", git_cmd(), "cat-file", "tag", $tag_id or return;
2528 $tag{'id'} = $tag_id;
2529 while (my $line = <$fd>) {
2531 if ($line =~ m/^object ([0-9a-fA-F]{40})$/) {
2532 $tag{'object'} = $1;
2533 } elsif ($line =~ m/^type (.+)$/) {
2535 } elsif ($line =~ m/^tag (.+)$/) {
2537 } elsif ($line =~ m/^tagger (.*) ([0-9]+) (.*)$/) {
2538 $tag{'author'} = $1;
2539 $tag{'author_epoch'} = $2;
2540 $tag{'author_tz'} = $3;
2541 if ($tag{'author'} =~ m/^([^<]+) <([^>]*)>/) {
2542 $tag{'author_name'} = $1;
2543 $tag{'author_email'} = $2;
2545 $tag{'author_name'} = $tag{'author'};
2547 } elsif ($line =~ m/--BEGIN/) {
2548 push @comment, $line;
2550 } elsif ($line eq "") {
2554 push @comment, <$fd>;
2555 $tag{'comment'} = \@comment;
2556 close $fd or return;
2557 if (!defined $tag{'name'}) {
2563 sub parse_commit_text {
2564 my ($commit_text, $withparents) = @_;
2565 my @commit_lines = split '\n', $commit_text;
2568 pop @commit_lines; # Remove '\0'
2570 if (! @commit_lines) {
2574 my $header = shift @commit_lines;
2575 if ($header !~ m/^[0-9a-fA-F]{40}/) {
2578 ($co{'id'}, my @parents) = split ' ', $header;
2579 while (my $line = shift @commit_lines) {
2580 last if $line eq "\n";
2581 if ($line =~ m/^tree ([0-9a-fA-F]{40})$/) {
2583 } elsif ((!defined $withparents) && ($line =~ m/^parent ([0-9a-fA-F]{40})$/)) {
2585 } elsif ($line =~ m/^author (.*) ([0-9]+) (.*)$/) {
2586 $co{'author'} = to_utf8($1);
2587 $co{'author_epoch'} = $2;
2588 $co{'author_tz'} = $3;
2589 if ($co{'author'} =~ m/^([^<]+) <([^>]*)>/) {
2590 $co{'author_name'} = $1;
2591 $co{'author_email'} = $2;
2593 $co{'author_name'} = $co{'author'};
2595 } elsif ($line =~ m/^committer (.*) ([0-9]+) (.*)$/) {
2596 $co{'committer'} = to_utf8($1);
2597 $co{'committer_epoch'} = $2;
2598 $co{'committer_tz'} = $3;
2599 if ($co{'committer'} =~ m/^([^<]+) <([^>]*)>/) {
2600 $co{'committer_name'} = $1;
2601 $co{'committer_email'} = $2;
2603 $co{'committer_name'} = $co{'committer'};
2607 if (!defined $co{'tree'}) {
2610 $co{'parents'} = \@parents;
2611 $co{'parent'} = $parents[0];
2613 foreach my $title (@commit_lines) {
2616 $co{'title'} = chop_str($title, 80, 5);
2617 # remove leading stuff of merges to make the interesting part visible
2618 if (length($title) > 50) {
2619 $title =~ s/^Automatic //;
2620 $title =~ s/^merge (of|with) /Merge ... /i;
2621 if (length($title) > 50) {
2622 $title =~ s/(http|rsync):\/\///;
2624 if (length($title) > 50) {
2625 $title =~ s/(master|www|rsync)\.//;
2627 if (length($title) > 50) {
2628 $title =~ s/kernel.org:?//;
2630 if (length($title) > 50) {
2631 $title =~ s/\/pub\/scm//;
2634 $co{'title_short'} = chop_str($title, 50, 5);
2638 if (! defined $co{'title'} || $co{'title'} eq "") {
2639 $co{'title'} = $co{'title_short'} = '(no commit message)';
2641 # remove added spaces
2642 foreach my $line (@commit_lines) {
2645 $co{'comment'} = \@commit_lines;
2647 my $age = time - $co{'committer_epoch'};
2649 $co{'age_string'} = age_string($age);
2650 my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($co{'committer_epoch'});
2651 if ($age > 60*60*24*7*2) {
2652 $co{'age_string_date'} = sprintf "%4i-%02u-%02i", 1900 + $year, $mon+1, $mday;
2653 $co{'age_string_age'} = $co{'age_string'};
2655 $co{'age_string_date'} = $co{'age_string'};
2656 $co{'age_string_age'} = sprintf "%4i-%02u-%02i", 1900 + $year, $mon+1, $mday;
2662 my ($commit_id) = @_;
2667 open my $fd, "-|", git_cmd(), "rev-list",
2673 or die_error(500, "Open git-rev-list failed");
2674 %co = parse_commit_text(<$fd>, 1);
2681 my ($commit_id, $maxcount, $skip, $filename, @args) = @_;
2689 open my $fd, "-|", git_cmd(), "rev-list",
2692 ("--max-count=" . $maxcount),
2693 ("--skip=" . $skip),
2697 ($filename ? ($filename) : ())
2698 or die_error(500, "Open git-rev-list failed");
2699 while (my $line = <$fd>) {
2700 my %co = parse_commit_text($line);
2705 return wantarray ? @cos : \@cos;
2708 # parse line of git-diff-tree "raw" output
2709 sub parse_difftree_raw_line {
2713 # ':100644 100644 03b218260e99b78c6df0ed378e59ed9205ccc96d 3b93d5e7cc7f7dd4ebed13a5cc1a4ad976fc94d8 M ls-files.c'
2714 # ':100644 100644 7f9281985086971d3877aca27704f2aaf9c448ce bc190ebc71bbd923f2b728e505408f5e54bd073a M rev-tree.c'
2715 if ($line =~ m/^:([0-7]{6}) ([0-7]{6}) ([0-9a-fA-F]{40}) ([0-9a-fA-F]{40}) (.)([0-9]{0,3})\t(.*)$/) {
2716 $res{'from_mode'} = $1;
2717 $res{'to_mode'} = $2;
2718 $res{'from_id'} = $3;
2720 $res{'status'} = $5;
2721 $res{'similarity'} = $6;
2722 if ($res{'status'} eq 'R' || $res{'status'} eq 'C') { # renamed or copied
2723 ($res{'from_file'}, $res{'to_file'}) = map { unquote($_) } split("\t", $7);
2725 $res{'from_file'} = $res{'to_file'} = $res{'file'} = unquote($7);
2728 # '::100755 100755 100755 60e79ca1b01bc8b057abe17ddab484699a7f5fdb 94067cc5f73388f33722d52ae02f44692bc07490 94067cc5f73388f33722d52ae02f44692bc07490 MR git-gui/git-gui.sh'
2729 # combined diff (for merge commit)
2730 elsif ($line =~ s/^(::+)((?:[0-7]{6} )+)((?:[0-9a-fA-F]{40} )+)([a-zA-Z]+)\t(.*)$//) {
2731 $res{'nparents'} = length($1);
2732 $res{'from_mode'} = [ split(' ', $2) ];
2733 $res{'to_mode'} = pop @{$res{'from_mode'}};
2734 $res{'from_id'} = [ split(' ', $3) ];
2735 $res{'to_id'} = pop @{$res{'from_id'}};
2736 $res{'status'} = [ split('', $4) ];
2737 $res{'to_file'} = unquote($5);
2739 # 'c512b523472485aef4fff9e57b229d9d243c967f'
2740 elsif ($line =~ m/^([0-9a-fA-F]{40})$/) {
2741 $res{'commit'} = $1;
2744 return wantarray ? %res : \%res;
2747 # wrapper: return parsed line of git-diff-tree "raw" output
2748 # (the argument might be raw line, or parsed info)
2749 sub parsed_difftree_line {
2750 my $line_or_ref = shift;
2752 if (ref($line_or_ref) eq "HASH") {
2753 # pre-parsed (or generated by hand)
2754 return $line_or_ref;
2756 return parse_difftree_raw_line($line_or_ref);
2760 # parse line of git-ls-tree output
2761 sub parse_ls_tree_line {
2766 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'
2767 $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t(.+)$/s;
2775 $res{'name'} = unquote($4);
2778 return wantarray ? %res : \%res;
2781 # generates _two_ hashes, references to which are passed as 2 and 3 argument
2782 sub parse_from_to_diffinfo {
2783 my ($diffinfo, $from, $to, @parents) = @_;
2785 if ($diffinfo->{'nparents'}) {
2787 $from->{'file'} = [];
2788 $from->{'href'} = [];
2789 fill_from_file_info($diffinfo, @parents)
2790 unless exists $diffinfo->{'from_file'};
2791 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
2792 $from->{'file'}[$i] =
2793 defined $diffinfo->{'from_file'}[$i] ?
2794 $diffinfo->{'from_file'}[$i] :
2795 $diffinfo->{'to_file'};
2796 if ($diffinfo->{'status'}[$i] ne "A") { # not new (added) file
2797 $from->{'href'}[$i] = href(action=>"blob",
2798 hash_base=>$parents[$i],
2799 hash=>$diffinfo->{'from_id'}[$i],
2800 file_name=>$from->{'file'}[$i]);
2802 $from->{'href'}[$i] = undef;
2806 # ordinary (not combined) diff
2807 $from->{'file'} = $diffinfo->{'from_file'};
2808 if ($diffinfo->{'status'} ne "A") { # not new (added) file
2809 $from->{'href'} = href(action=>"blob", hash_base=>$hash_parent,
2810 hash=>$diffinfo->{'from_id'},
2811 file_name=>$from->{'file'});
2813 delete $from->{'href'};
2817 $to->{'file'} = $diffinfo->{'to_file'};
2818 if (!is_deleted($diffinfo)) { # file exists in result
2819 $to->{'href'} = href(action=>"blob", hash_base=>$hash,
2820 hash=>$diffinfo->{'to_id'},
2821 file_name=>$to->{'file'});
2823 delete $to->{'href'};
2827 ## ......................................................................
2828 ## parse to array of hashes functions
2830 sub git_get_heads_list {
2834 open my $fd, '-|', git_cmd(), 'for-each-ref',
2835 ($limit ? '--count='.($limit+1) : ()), '--sort=-committerdate',
2836 '--format=%(objectname) %(refname) %(subject)%00%(committer)',
2839 while (my $line = <$fd>) {
2843 my ($refinfo, $committerinfo) = split(/\0/, $line);
2844 my ($hash, $name, $title) = split(' ', $refinfo, 3);
2845 my ($committer, $epoch, $tz) =
2846 ($committerinfo =~ /^(.*) ([0-9]+) (.*)$/);
2847 $ref_item{'fullname'} = $name;
2848 $name =~ s!^refs/heads/!!;
2850 $ref_item{'name'} = $name;
2851 $ref_item{'id'} = $hash;
2852 $ref_item{'title'} = $title || '(no commit message)';
2853 $ref_item{'epoch'} = $epoch;
2855 $ref_item{'age'} = age_string(time - $ref_item{'epoch'});
2857 $ref_item{'age'} = "unknown";
2860 push @headslist, \%ref_item;
2864 return wantarray ? @headslist : \@headslist;
2867 sub git_get_tags_list {
2871 open my $fd, '-|', git_cmd(), 'for-each-ref',
2872 ($limit ? '--count='.($limit+1) : ()), '--sort=-creatordate',
2873 '--format=%(objectname) %(objecttype) %(refname) '.
2874 '%(*objectname) %(*objecttype) %(subject)%00%(creator)',
2877 while (my $line = <$fd>) {
2881 my ($refinfo, $creatorinfo) = split(/\0/, $line);
2882 my ($id, $type, $name, $refid, $reftype, $title) = split(' ', $refinfo, 6);
2883 my ($creator, $epoch, $tz) =
2884 ($creatorinfo =~ /^(.*) ([0-9]+) (.*)$/);
2885 $ref_item{'fullname'} = $name;
2886 $name =~ s!^refs/tags/!!;
2888 $ref_item{'type'} = $type;
2889 $ref_item{'id'} = $id;
2890 $ref_item{'name'} = $name;
2891 if ($type eq "tag") {
2892 $ref_item{'subject'} = $title;
2893 $ref_item{'reftype'} = $reftype;
2894 $ref_item{'refid'} = $refid;
2896 $ref_item{'reftype'} = $type;
2897 $ref_item{'refid'} = $id;
2900 if ($type eq "tag" || $type eq "commit") {
2901 $ref_item{'epoch'} = $epoch;
2903 $ref_item{'age'} = age_string(time - $ref_item{'epoch'});
2905 $ref_item{'age'} = "unknown";
2909 push @tagslist, \%ref_item;
2913 return wantarray ? @tagslist : \@tagslist;
2916 ## ----------------------------------------------------------------------
2917 ## filesystem-related functions
2919 sub get_file_owner {
2922 my ($dev, $ino, $mode, $nlink, $st_uid, $st_gid, $rdev, $size) = stat($path);
2923 my ($name, $passwd, $uid, $gid, $quota, $comment, $gcos, $dir, $shell) = getpwuid($st_uid);
2924 if (!defined $gcos) {
2928 $owner =~ s/[,;].*$//;
2929 return to_utf8($owner);
2932 # assume that file exists
2934 my $filename = shift;
2936 open my $fd, '<', $filename;
2937 print map { to_utf8($_) } <$fd>;
2941 ## ......................................................................
2942 ## mimetype related functions
2944 sub mimetype_guess_file {
2945 my $filename = shift;
2946 my $mimemap = shift;
2947 -r $mimemap or return undef;
2950 open(my $mh, '<', $mimemap) or return undef;
2952 next if m/^#/; # skip comments
2953 my ($mimetype, $exts) = split(/\t+/);
2954 if (defined $exts) {
2955 my @exts = split(/\s+/, $exts);
2956 foreach my $ext (@exts) {
2957 $mimemap{$ext} = $mimetype;
2963 $filename =~ /\.([^.]*)$/;
2964 return $mimemap{$1};
2967 sub mimetype_guess {
2968 my $filename = shift;
2970 $filename =~ /\./ or return undef;
2972 if ($mimetypes_file) {
2973 my $file = $mimetypes_file;
2974 if ($file !~ m!^/!) { # if it is relative path
2975 # it is relative to project
2976 $file = "$projectroot/$project/$file";
2978 $mime = mimetype_guess_file($filename, $file);
2980 $mime ||= mimetype_guess_file($filename, '/etc/mime.types');
2986 my $filename = shift;
2989 my $mime = mimetype_guess($filename);
2990 $mime and return $mime;
2994 return $default_blob_plain_mimetype unless $fd;
2997 return 'text/plain';
2998 } elsif (! $filename) {
2999 return 'application/octet-stream';
3000 } elsif ($filename =~ m/\.png$/i) {
3002 } elsif ($filename =~ m/\.gif$/i) {
3004 } elsif ($filename =~ m/\.jpe?g$/i) {
3005 return 'image/jpeg';
3007 return 'application/octet-stream';
3011 sub blob_contenttype {
3012 my ($fd, $file_name, $type) = @_;
3014 $type ||= blob_mimetype($fd, $file_name);
3015 if ($type eq 'text/plain' && defined $default_text_plain_charset) {
3016 $type .= "; charset=$default_text_plain_charset";
3022 ## ======================================================================
3023 ## functions printing HTML: header, footer, error page
3025 sub git_header_html {
3026 my $status = shift || "200 OK";
3027 my $expires = shift;
3029 my $title = "$site_name";
3030 if (defined $project) {
3031 $title .= " - " . to_utf8($project);
3032 if (defined $action) {
3033 $title .= "/$action";
3034 if (defined $file_name) {
3035 $title .= " - " . esc_path($file_name);
3036 if ($action eq "tree" && $file_name !~ m|/$|) {
3043 # require explicit support from the UA if we are to send the page as
3044 # 'application/xhtml+xml', otherwise send it as plain old 'text/html'.
3045 # we have to do this because MSIE sometimes globs '*/*', pretending to
3046 # support xhtml+xml but choking when it gets what it asked for.
3047 if (defined $cgi->http('HTTP_ACCEPT') &&
3048 $cgi->http('HTTP_ACCEPT') =~ m/(,|;|\s|^)application\/xhtml\+xml(,|;|\s|$)/ &&
3049 $cgi->Accept('application/xhtml+xml') != 0) {
3050 $content_type = 'application/xhtml+xml';
3052 $content_type = 'text/html';
3054 print $cgi->header(-type=>$content_type, -charset => 'utf-8',
3055 -status=> $status, -expires => $expires);
3056 my $mod_perl_version = $ENV{'MOD_PERL'} ? " $ENV{'MOD_PERL'}" : '';
3058 <?xml version="1.0" encoding="utf-8"?>
3059 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
3060 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US" lang="en-US">
3061 <!-- git web interface version $version, (C) 2005-2006, Kay Sievers <kay.sievers\@vrfy.org>, Christian Gierke -->
3062 <!-- git core binaries version $git_version -->
3064 <meta http-equiv="content-type" content="$content_type; charset=utf-8"/>
3065 <meta name="generator" content="gitweb/$version git/$git_version$mod_perl_version"/>
3066 <meta name="robots" content="index, nofollow"/>
3067 <title>$title</title>
3069 # the stylesheet, favicon etc urls won't work correctly with path_info
3070 # unless we set the appropriate base URL
3071 if ($ENV{'PATH_INFO'}) {
3072 print "<base href=\"".esc_url($base_url)."\" />\n";
3074 # print out each stylesheet that exist, providing backwards capability
3075 # for those people who defined $stylesheet in a config file
3076 if (defined $stylesheet) {
3077 print '<link rel="stylesheet" type="text/css" href="'.$stylesheet.'"/>'."\n";
3079 foreach my $stylesheet (@stylesheets) {
3080 next unless $stylesheet;
3081 print '<link rel="stylesheet" type="text/css" href="'.$stylesheet.'"/>'."\n";
3084 if (defined $project) {
3085 my %href_params = get_feed_info();
3086 if (!exists $href_params{'-title'}) {
3087 $href_params{'-title'} = 'log';
3090 foreach my $format qw(RSS Atom) {
3091 my $type = lc($format);
3093 '-rel' => 'alternate',
3094 '-title' => "$project - $href_params{'-title'} - $format feed",
3095 '-type' => "application/$type+xml"
3098 $href_params{'action'} = $type;
3099 $link_attr{'-href'} = href(%href_params);
3101 "rel=\"$link_attr{'-rel'}\" ".
3102 "title=\"$link_attr{'-title'}\" ".
3103 "href=\"$link_attr{'-href'}\" ".
3104 "type=\"$link_attr{'-type'}\" ".
3107 $href_params{'extra_options'} = '--no-merges';
3108 $link_attr{'-href'} = href(%href_params);
3109 $link_attr{'-title'} .= ' (no merges)';
3111 "rel=\"$link_attr{'-rel'}\" ".
3112 "title=\"$link_attr{'-title'}\" ".
3113 "href=\"$link_attr{'-href'}\" ".
3114 "type=\"$link_attr{'-type'}\" ".
3119 printf('<link rel="alternate" title="%s projects list" '.
3120 'href="%s" type="text/plain; charset=utf-8" />'."\n",
3121 $site_name, href(project=>undef, action=>"project_index"));
3122 printf('<link rel="alternate" title="%s projects feeds" '.
3123 'href="%s" type="text/x-opml" />'."\n",
3124 $site_name, href(project=>undef, action=>"opml"));
3126 if (defined $favicon) {
3127 print qq(<link rel="shortcut icon" href="$favicon" type="image/png" />\n);
3133 if (-f $site_header) {
3134 insert_file($site_header);
3137 print "<div class=\"page_header\">\n" .
3138 $cgi->a({-href => esc_url($logo_url),
3139 -title => $logo_label},
3140 qq(<img src="$logo" width="72" height="27" alt="git" class="logo"/>));
3141 print $cgi->a({-href => esc_url($home_link)}, $home_link_str) . " / ";
3142 if (defined $project) {
3143 print $cgi->a({-href => href(action=>"summary")}, esc_html($project));
3144 if (defined $action) {
3151 my $have_search = gitweb_check_feature('search');
3152 if (defined $project && $have_search) {
3153 if (!defined $searchtext) {
3157 if (defined $hash_base) {
3158 $search_hash = $hash_base;
3159 } elsif (defined $hash) {
3160 $search_hash = $hash;
3162 $search_hash = "HEAD";
3164 my $action = $my_uri;
3165 my $use_pathinfo = gitweb_check_feature('pathinfo');
3166 if ($use_pathinfo) {
3167 $action .= "/".esc_url($project);
3169 print $cgi->startform(-method => "get", -action => $action) .
3170 "<div class=\"search\">\n" .
3172 $cgi->input({-name=>"p", -value=>$project, -type=>"hidden"}) . "\n") .
3173 $cgi->input({-name=>"a", -value=>"search", -type=>"hidden"}) . "\n" .
3174 $cgi->input({-name=>"h", -value=>$search_hash, -type=>"hidden"}) . "\n" .
3175 $cgi->popup_menu(-name => 'st', -default => 'commit',
3176 -values => ['commit', 'grep', 'author', 'committer', 'pickaxe']) .
3177 $cgi->sup($cgi->a({-href => href(action=>"search_help")}, "?")) .
3179 $cgi->textfield(-name => "s", -value => $searchtext) . "\n" .
3180 "<span title=\"Extended regular expression\">" .
3181 $cgi->checkbox(-name => 'sr', -value => 1, -label => 're',
3182 -checked => $search_use_regexp) .
3185 $cgi->end_form() . "\n";
3189 sub git_footer_html {
3190 my $feed_class = 'rss_logo';
3192 print "<div class=\"page_footer\">\n";
3193 if (defined $project) {
3194 my $descr = git_get_project_description($project);
3195 if (defined $descr) {
3196 print "<div class=\"page_footer_text\">" . esc_html($descr) . "</div>\n";
3199 my %href_params = get_feed_info();
3200 if (!%href_params) {
3201 $feed_class .= ' generic';
3203 $href_params{'-title'} ||= 'log';
3205 foreach my $format qw(RSS Atom) {
3206 $href_params{'action'} = lc($format);
3207 print $cgi->a({-href => href(%href_params),
3208 -title => "$href_params{'-title'} $format feed",
3209 -class => $feed_class}, $format)."\n";
3213 print $cgi->a({-href => href(project=>undef, action=>"opml"),
3214 -class => $feed_class}, "OPML") . " ";
3215 print $cgi->a({-href => href(project=>undef, action=>"project_index"),
3216 -class => $feed_class}, "TXT") . "\n";
3218 print "</div>\n"; # class="page_footer"
3220 if (-f $site_footer) {
3221 insert_file($site_footer);
3228 # die_error(<http_status_code>, <error_message>)
3229 # Example: die_error(404, 'Hash not found')
3230 # By convention, use the following status codes (as defined in RFC 2616):
3231 # 400: Invalid or missing CGI parameters, or
3232 # requested object exists but has wrong type.
3233 # 403: Requested feature (like "pickaxe" or "snapshot") not enabled on
3234 # this server or project.
3235 # 404: Requested object/revision/project doesn't exist.
3236 # 500: The server isn't configured properly, or
3237 # an internal error occurred (e.g. failed assertions caused by bugs), or
3238 # an unknown error occurred (e.g. the git binary died unexpectedly).
3240 my $status = shift || 500;
3241 my $error = shift || "Internal server error";
3243 my %http_responses = (400 => '400 Bad Request',
3244 403 => '403 Forbidden',
3245 404 => '404 Not Found',
3246 500 => '500 Internal Server Error');
3247 git_header_html($http_responses{$status});
3249 <div class="page_body">
3259 ## ----------------------------------------------------------------------
3260 ## functions printing or outputting HTML: navigation
3262 sub git_print_page_nav {
3263 my ($current, $suppress, $head, $treehead, $treebase, $extra) = @_;
3264 $extra = '' if !defined $extra; # pager or formats
3266 my @navs = qw(summary shortlog log commit commitdiff tree);
3268 @navs = grep { $_ ne $suppress } @navs;
3271 my %arg = map { $_ => {action=>$_} } @navs;
3272 if (defined $head) {
3273 for (qw(commit commitdiff)) {
3274 $arg{$_}{'hash'} = $head;
3276 if ($current =~ m/^(tree | log | shortlog | commit | commitdiff | search)$/x) {
3277 for (qw(shortlog log)) {
3278 $arg{$_}{'hash'} = $head;
3283 $arg{'tree'}{'hash'} = $treehead if defined $treehead;
3284 $arg{'tree'}{'hash_base'} = $treebase if defined $treebase;
3286 my @actions = gitweb_get_feature('actions');
3289 'n' => $project, # project name
3290 'f' => $git_dir, # project path within filesystem
3291 'h' => $treehead || '', # current hash ('h' parameter)
3292 'b' => $treebase || '', # hash base ('hb' parameter)
3295 my ($label, $link, $pos) = splice(@actions,0,3);
3297 @navs = map { $_ eq $pos ? ($_, $label) : $_ } @navs;
3299 $link =~ s/%([%nfhb])/$repl{$1}/g;
3300 $arg{$label}{'_href'} = $link;
3303 print "<div class=\"page_nav\">\n" .
3305 map { $_ eq $current ?
3306 $_ : $cgi->a({-href => ($arg{$_}{_href} ? $arg{$_}{_href} : href(%{$arg{$_}}))}, "$_")
3308 print "<br/>\n$extra<br/>\n" .
3312 sub format_paging_nav {
3313 my ($action, $hash, $head, $page, $has_next_link) = @_;
3317 if ($hash ne $head || $page) {
3318 $paging_nav .= $cgi->a({-href => href(action=>$action)}, "HEAD");
3320 $paging_nav .= "HEAD";
3324 $paging_nav .= " ⋅ " .
3325 $cgi->a({-href => href(-replay=>1, page=>$page-1),
3326 -accesskey => "p", -title => "Alt-p"}, "prev");
3328 $paging_nav .= " ⋅ prev";
3331 if ($has_next_link) {
3332 $paging_nav .= " ⋅ " .
3333 $cgi->a({-href => href(-replay=>1, page=>$page+1),
3334 -accesskey => "n", -title => "Alt-n"}, "next");
3336 $paging_nav .= " ⋅ next";
3342 ## ......................................................................
3343 ## functions printing or outputting HTML: div
3345 sub git_print_header_div {
3346 my ($action, $title, $hash, $hash_base) = @_;
3349 $args{'action'} = $action;
3350 $args{'hash'} = $hash if $hash;
3351 $args{'hash_base'} = $hash_base if $hash_base;
3353 print "<div class=\"header\">\n" .
3354 $cgi->a({-href => href(%args), -class => "title"},
3355 $title ? $title : $action) .
3359 sub print_local_time {
3361 if ($date{'hour_local'} < 6) {
3362 printf(" (<span class=\"atnight\">%02d:%02d</span> %s)",
3363 $date{'hour_local'}, $date{'minute_local'}, $date{'tz_local'});
3365 printf(" (%02d:%02d %s)",
3366 $date{'hour_local'}, $date{'minute_local'}, $date{'tz_local'});
3370 # Outputs the author name and date in long form
3371 sub git_print_authorship {
3374 my $tag = $opts{-tag} || 'div';
3376 my %ad = parse_date($co->{'author_epoch'}, $co->{'author_tz'});
3377 print "<$tag class=\"author_date\">" .
3378 esc_html($co->{'author_name'}) .
3380 print_local_time(%ad) if ($opts{-localtime});
3381 print "]" . git_get_avatar($co->{'author_email'}, -pad_before => 1)
3385 # Outputs table rows containing the full author or committer information,
3386 # in the format expected for 'commit' view (& similia).
3387 # Parameters are a commit hash reference, followed by the list of people
3388 # to output information for. If the list is empty it defalts to both
3389 # author and committer.
3390 sub git_print_authorship_rows {
3392 # too bad we can't use @people = @_ || ('author', 'committer')
3394 @people = ('author', 'committer') unless @people;
3395 foreach my $who (@people) {
3396 my %wd = parse_date($co->{"${who}_epoch"}, $co->{"${who}_tz"});
3397 print "<tr><td>$who</td><td>" . esc_html($co->{$who}) . "</td>" .
3398 "<td rowspan=\"2\">" .
3399 git_get_avatar($co->{"${who}_email"}, -size => 'double') .
3402 "<td></td><td> $wd{'rfc2822'}";
3403 print_local_time(%wd);
3409 sub git_print_page_path {
3415 print "<div class=\"page_path\">";
3416 print $cgi->a({-href => href(action=>"tree", hash_base=>$hb),
3417 -title => 'tree root'}, to_utf8("[$project]"));
3419 if (defined $name) {
3420 my @dirname = split '/', $name;
3421 my $basename = pop @dirname;
3424 foreach my $dir (@dirname) {
3425 $fullname .= ($fullname ? '/' : '') . $dir;
3426 print $cgi->a({-href => href(action=>"tree", file_name=>$fullname,
3428 -title => $fullname}, esc_path($dir));
3431 if (defined $type && $type eq 'blob') {
3432 print $cgi->a({-href => href(action=>"blob_plain", file_name=>$file_name,
3434 -title => $name}, esc_path($basename));
3435 } elsif (defined $type && $type eq 'tree') {
3436 print $cgi->a({-href => href(action=>"tree", file_name=>$file_name,
3438 -title => $name}, esc_path($basename));
3441 print esc_path($basename);
3444 print "<br/></div>\n";
3451 if ($opts{'-remove_title'}) {
3452 # remove title, i.e. first line of log
3455 # remove leading empty lines
3456 while (defined $log->[0] && $log->[0] eq "") {
3463 foreach my $line (@$log) {
3464 if ($line =~ m/^ *(signed[ \-]off[ \-]by[ :]|acked[ \-]by[ :]|cc[ :])/i) {
3467 if (! $opts{'-remove_signoff'}) {
3468 print "<span class=\"signoff\">" . esc_html($line) . "</span><br/>\n";
3471 # remove signoff lines
3478 # print only one empty line
3479 # do not print empty line after signoff
3481 next if ($empty || $signoff);
3487 print format_log_line_html($line) . "<br/>\n";
3490 if ($opts{'-final_empty_line'}) {
3491 # end with single empty line
3492 print "<br/>\n" unless $empty;
3496 # return link target (what link points to)
3497 sub git_get_link_target {
3502 open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
3506 $link_target = <$fd>;
3511 return $link_target;
3514 # given link target, and the directory (basedir) the link is in,
3515 # return target of link relative to top directory (top tree);
3516 # return undef if it is not possible (including absolute links).
3517 sub normalize_link_target {
3518 my ($link_target, $basedir) = @_;
3520 # absolute symlinks (beginning with '/') cannot be normalized
3521 return if (substr($link_target, 0, 1) eq '/');
3523 # normalize link target to path from top (root) tree (dir)
3526 $path = $basedir . '/' . $link_target;
3528 # we are in top (root) tree (dir)
3529 $path = $link_target;
3532 # remove //, /./, and /../
3534 foreach my $part (split('/', $path)) {
3535 # discard '.' and ''
3536 next if (!$part || $part eq '.');
3538 if ($part eq '..') {
3542 # link leads outside repository (outside top dir)
3546 push @path_parts, $part;
3549 $path = join('/', @path_parts);
3554 # print tree entry (row of git_tree), but without encompassing <tr> element
3555 sub git_print_tree_entry {
3556 my ($t, $basedir, $hash_base, $have_blame) = @_;
3559 $base_key{'hash_base'} = $hash_base if defined $hash_base;
3561 # The format of a table row is: mode list link. Where mode is
3562 # the mode of the entry, list is the name of the entry, an href,
3563 # and link is the action links of the entry.
3565 print "<td class=\"mode\">" . mode_str($t->{'mode'}) . "</td>\n";
3566 if ($t->{'type'} eq "blob") {
3567 print "<td class=\"list\">" .
3568 $cgi->a({-href => href(action=>"blob", hash=>$t->{'hash'},
3569 file_name=>"$basedir$t->{'name'}", %base_key),
3570 -class => "list"}, esc_path($t->{'name'}));
3571 if (S_ISLNK(oct $t->{'mode'})) {
3572 my $link_target = git_get_link_target($t->{'hash'});
3574 my $norm_target = normalize_link_target($link_target, $basedir);
3575 if (defined $norm_target) {
3577 $cgi->a({-href => href(action=>"object", hash_base=>$hash_base,
3578 file_name=>$norm_target),
3579 -title => $norm_target}, esc_path($link_target));
3581 print " -> " . esc_path($link_target);
3586 print "<td class=\"link\">";
3587 print $cgi->a({-href => href(action=>"blob", hash=>$t->{'hash'},
3588 file_name=>"$basedir$t->{'name'}", %base_key)},
3592 $cgi->a({-href => href(action=>"blame", hash=>$t->{'hash'},
3593 file_name=>"$basedir$t->{'name'}", %base_key)},
3596 if (defined $hash_base) {
3598 $cgi->a({-href => href(action=>"history", hash_base=>$hash_base,
3599 hash=>$t->{'hash'}, file_name=>"$basedir$t->{'name'}")},
3603 $cgi->a({-href => href(action=>"blob_plain", hash_base=>$hash_base,
3604 file_name=>"$basedir$t->{'name'}")},
3608 } elsif ($t->{'type'} eq "tree") {
3609 print "<td class=\"list\">";
3610 print $cgi->a({-href => href(action=>"tree", hash=>$t->{'hash'},
3611 file_name=>"$basedir$t->{'name'}", %base_key)},
3612 esc_path($t->{'name'}));
3614 print "<td class=\"link\">";
3615 print $cgi->a({-href => href(action=>"tree", hash=>$t->{'hash'},
3616 file_name=>"$basedir$t->{'name'}", %base_key)},
3618 if (defined $hash_base) {
3620 $cgi->a({-href => href(action=>"history", hash_base=>$hash_base,
3621 file_name=>"$basedir$t->{'name'}")},
3626 # unknown object: we can only present history for it
3627 # (this includes 'commit' object, i.e. submodule support)
3628 print "<td class=\"list\">" .
3629 esc_path($t->{'name'}) .
3631 print "<td class=\"link\">";
3632 if (defined $hash_base) {
3633 print $cgi->a({-href => href(action=>"history",
3634 hash_base=>$hash_base,
3635 file_name=>"$basedir$t->{'name'}")},
3642 ## ......................................................................
3643 ## functions printing large fragments of HTML
3645 # get pre-image filenames for merge (combined) diff
3646 sub fill_from_file_info {
3647 my ($diff, @parents) = @_;
3649 $diff->{'from_file'} = [ ];
3650 $diff->{'from_file'}[$diff->{'nparents'} - 1] = undef;
3651 for (my $i = 0; $i < $diff->{'nparents'}; $i++) {
3652 if ($diff->{'status'}[$i] eq 'R' ||
3653 $diff->{'status'}[$i] eq 'C') {
3654 $diff->{'from_file'}[$i] =
3655 git_get_path_by_hash($parents[$i], $diff->{'from_id'}[$i]);
3662 # is current raw difftree line of file deletion
3664 my $diffinfo = shift;
3666 return $diffinfo->{'to_id'} eq ('0' x 40);
3669 # does patch correspond to [previous] difftree raw line
3670 # $diffinfo - hashref of parsed raw diff format
3671 # $patchinfo - hashref of parsed patch diff format
3672 # (the same keys as in $diffinfo)
3673 sub is_patch_split {
3674 my ($diffinfo, $patchinfo) = @_;
3676 return defined $diffinfo && defined $patchinfo
3677 && $diffinfo->{'to_file'} eq $patchinfo->{'to_file'};
3681 sub git_difftree_body {
3682 my ($difftree, $hash, @parents) = @_;
3683 my ($parent) = $parents[0];
3684 my $have_blame = gitweb_check_feature('blame');
3685 print "<div class=\"list_head\">\n";
3686 if ($#{$difftree} > 10) {
3687 print(($#{$difftree} + 1) . " files changed:\n");
3691 print "<table class=\"" .
3692 (@parents > 1 ? "combined " : "") .
3695 # header only for combined diff in 'commitdiff' view
3696 my $has_header = @$difftree && @parents > 1 && $action eq 'commitdiff';
3699 print "<thead><tr>\n" .
3700 "<th></th><th></th>\n"; # filename, patchN link
3701 for (my $i = 0; $i < @parents; $i++) {
3702 my $par = $parents[$i];
3704 $cgi->a({-href => href(action=>"commitdiff",
3705 hash=>$hash, hash_parent=>$par),
3706 -title => 'commitdiff to parent number ' .
3707 ($i+1) . ': ' . substr($par,0,7)},
3711 print "</tr></thead>\n<tbody>\n";
3716 foreach my $line (@{$difftree}) {
3717 my $diff = parsed_difftree_line($line);
3720 print "<tr class=\"dark\">\n";
3722 print "<tr class=\"light\">\n";
3726 if (exists $diff->{'nparents'}) { # combined diff
3728 fill_from_file_info($diff, @parents)
3729 unless exists $diff->{'from_file'};
3731 if (!is_deleted($diff)) {
3732 # file exists in the result (child) commit
3734 $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3735 file_name=>$diff->{'to_file'},
3737 -class => "list"}, esc_path($diff->{'to_file'})) .
3741 esc_path($diff->{'to_file'}) .
3745 if ($action eq 'commitdiff') {
3748 print "<td class=\"link\">" .
3749 $cgi->a({-href => "#patch$patchno"}, "patch") .
3754 my $has_history = 0;
3755 my $not_deleted = 0;
3756 for (my $i = 0; $i < $diff->{'nparents'}; $i++) {
3757 my $hash_parent = $parents[$i];
3758 my $from_hash = $diff->{'from_id'}[$i];
3759 my $from_path = $diff->{'from_file'}[$i];
3760 my $status = $diff->{'status'}[$i];
3762 $has_history ||= ($status ne 'A');
3763 $not_deleted ||= ($status ne 'D');
3765 if ($status eq 'A') {
3766 print "<td class=\"link\" align=\"right\"> | </td>\n";
3767 } elsif ($status eq 'D') {
3768 print "<td class=\"link\">" .
3769 $cgi->a({-href => href(action=>"blob",
3772 file_name=>$from_path)},
3776 if ($diff->{'to_id'} eq $from_hash) {
3777 print "<td class=\"link nochange\">";
3779 print "<td class=\"link\">";
3781 print $cgi->a({-href => href(action=>"blobdiff",
3782 hash=>$diff->{'to_id'},
3783 hash_parent=>$from_hash,
3785 hash_parent_base=>$hash_parent,
3786 file_name=>$diff->{'to_file'},
3787 file_parent=>$from_path)},
3793 print "<td class=\"link\">";
3795 print $cgi->a({-href => href(action=>"blob",
3796 hash=>$diff->{'to_id'},
3797 file_name=>$diff->{'to_file'},
3800 print " | " if ($has_history);
3803 print $cgi->a({-href => href(action=>"history",
3804 file_name=>$diff->{'to_file'},
3811 next; # instead of 'else' clause, to avoid extra indent
3813 # else ordinary diff
3815 my ($to_mode_oct, $to_mode_str, $to_file_type);
3816 my ($from_mode_oct, $from_mode_str, $from_file_type);
3817 if ($diff->{'to_mode'} ne ('0' x 6)) {
3818 $to_mode_oct = oct $diff->{'to_mode'};
3819 if (S_ISREG($to_mode_oct)) { # only for regular file
3820 $to_mode_str = sprintf("%04o", $to_mode_oct & 0777); # permission bits
3822 $to_file_type = file_type($diff->{'to_mode'});
3824 if ($diff->{'from_mode'} ne ('0' x 6)) {
3825 $from_mode_oct = oct $diff->{'from_mode'};
3826 if (S_ISREG($to_mode_oct)) { # only for regular file
3827 $from_mode_str = sprintf("%04o", $from_mode_oct & 0777); # permission bits
3829 $from_file_type = file_type($diff->{'from_mode'});
3832 if ($diff->{'status'} eq "A") { # created
3833 my $mode_chng = "<span class=\"file_status new\">[new $to_file_type";
3834 $mode_chng .= " with mode: $to_mode_str" if $to_mode_str;
3835 $mode_chng .= "]</span>";
3837 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3838 hash_base=>$hash, file_name=>$diff->{'file'}),
3839 -class => "list"}, esc_path($diff->{'file'}));
3841 print "<td>$mode_chng</td>\n";
3842 print "<td class=\"link\">";
3843 if ($action eq 'commitdiff') {
3846 print $cgi->a({-href => "#patch$patchno"}, "patch");
3849 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3850 hash_base=>$hash, file_name=>$diff->{'file'})},
3854 } elsif ($diff->{'status'} eq "D") { # deleted
3855 my $mode_chng = "<span class=\"file_status deleted\">[deleted $from_file_type]</span>";
3857 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'from_id'},
3858 hash_base=>$parent, file_name=>$diff->{'file'}),
3859 -class => "list"}, esc_path($diff->{'file'}));
3861 print "<td>$mode_chng</td>\n";
3862 print "<td class=\"link\">";
3863 if ($action eq 'commitdiff') {
3866 print $cgi->a({-href => "#patch$patchno"}, "patch");
3869 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'from_id'},
3870 hash_base=>$parent, file_name=>$diff->{'file'})},
3873 print $cgi->a({-href => href(action=>"blame", hash_base=>$parent,
3874 file_name=>$diff->{'file'})},
3877 print $cgi->a({-href => href(action=>"history", hash_base=>$parent,
3878 file_name=>$diff->{'file'})},
3882 } elsif ($diff->{'status'} eq "M" || $diff->{'status'} eq "T") { # modified, or type changed
3883 my $mode_chnge = "";
3884 if ($diff->{'from_mode'} != $diff->{'to_mode'}) {
3885 $mode_chnge = "<span class=\"file_status mode_chnge\">[changed";
3886 if ($from_file_type ne $to_file_type) {
3887 $mode_chnge .= " from $from_file_type to $to_file_type";
3889 if (($from_mode_oct & 0777) != ($to_mode_oct & 0777)) {
3890 if ($from_mode_str && $to_mode_str) {
3891 $mode_chnge .= " mode: $from_mode_str->$to_mode_str";
3892 } elsif ($to_mode_str) {
3893 $mode_chnge .= " mode: $to_mode_str";
3896 $mode_chnge .= "]</span>\n";
3899 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3900 hash_base=>$hash, file_name=>$diff->{'file'}),
3901 -class => "list"}, esc_path($diff->{'file'}));
3903 print "<td>$mode_chnge</td>\n";
3904 print "<td class=\"link\">";
3905 if ($action eq 'commitdiff') {
3908 print $cgi->a({-href => "#patch$patchno"}, "patch") .
3910 } elsif ($diff->{'to_id'} ne $diff->{'from_id'}) {
3911 # "commit" view and modified file (not onlu mode changed)
3912 print $cgi->a({-href => href(action=>"blobdiff",
3913 hash=>$diff->{'to_id'}, hash_parent=>$diff->{'from_id'},
3914 hash_base=>$hash, hash_parent_base=>$parent,
3915 file_name=>$diff->{'file'})},
3919 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3920 hash_base=>$hash, file_name=>$diff->{'file'})},
3923 print $cgi->a({-href => href(action=>"blame", hash_base=>$hash,
3924 file_name=>$diff->{'file'})},
3927 print $cgi->a({-href => href(action=>"history", hash_base=>$hash,
3928 file_name=>$diff->{'file'})},
3932 } elsif ($diff->{'status'} eq "R" || $diff->{'status'} eq "C") { # renamed or copied
3933 my %status_name = ('R' => 'moved', 'C' => 'copied');
3934 my $nstatus = $status_name{$diff->{'status'}};
3936 if ($diff->{'from_mode'} != $diff->{'to_mode'}) {
3937 # mode also for directories, so we cannot use $to_mode_str
3938 $mode_chng = sprintf(", mode: %04o", $to_mode_oct & 0777);
3941 $cgi->a({-href => href(action=>"blob", hash_base=>$hash,
3942 hash=>$diff->{'to_id'}, file_name=>$diff->{'to_file'}),
3943 -class => "list"}, esc_path($diff->{'to_file'})) . "</td>\n" .
3944 "<td><span class=\"file_status $nstatus\">[$nstatus from " .
3945 $cgi->a({-href => href(action=>"blob", hash_base=>$parent,
3946 hash=>$diff->{'from_id'}, file_name=>$diff->{'from_file'}),
3947 -class => "list"}, esc_path($diff->{'from_file'})) .
3948 " with " . (int $diff->{'similarity'}) . "% similarity$mode_chng]</span></td>\n" .
3949 "<td class=\"link\">";
3950 if ($action eq 'commitdiff') {
3953 print $cgi->a({-href => "#patch$patchno"}, "patch") .
3955 } elsif ($diff->{'to_id'} ne $diff->{'from_id'}) {
3956 # "commit" view and modified file (not only pure rename or copy)
3957 print $cgi->a({-href => href(action=>"blobdiff",
3958 hash=>$diff->{'to_id'}, hash_parent=>$diff->{'from_id'},
3959 hash_base=>$hash, hash_parent_base=>$parent,
3960 file_name=>$diff->{'to_file'}, file_parent=>$diff->{'from_file'})},
3964 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3965 hash_base=>$parent, file_name=>$diff->{'to_file'})},
3968 print $cgi->a({-href => href(action=>"blame", hash_base=>$hash,
3969 file_name=>$diff->{'to_file'})},
3972 print $cgi->a({-href => href(action=>"history", hash_base=>$hash,
3973 file_name=>$diff->{'to_file'})},
3977 } # we should not encounter Unmerged (U) or Unknown (X) status
3980 print "</tbody>" if $has_header;
3984 sub git_patchset_body {
3985 my ($fd, $difftree, $hash, @hash_parents) = @_;
3986 my ($hash_parent) = $hash_parents[0];
3988 my $is_combined = (@hash_parents > 1);
3990 my $patch_number = 0;
3996 print "<div class=\"patchset\">\n";
3998 # skip to first patch
3999 while ($patch_line = <$fd>) {
4002 last if ($patch_line =~ m/^diff /);
4006 while ($patch_line) {
4008 # parse "git diff" header line
4009 if ($patch_line =~ m/^diff --git (\"(?:[^\\\"]*(?:\\.[^\\\"]*)*)\"|[^ "]*) (.*)$/) {
4010 # $1 is from_name, which we do not use
4011 $to_name = unquote($2);
4012 $to_name =~ s!^b/!!;
4013 } elsif ($patch_line =~ m/^diff --(cc|combined) ("?.*"?)$/) {
4014 # $1 is 'cc' or 'combined', which we do not use
4015 $to_name = unquote($2);
4020 # check if current patch belong to current raw line
4021 # and parse raw git-diff line if needed
4022 if (is_patch_split($diffinfo, { 'to_file' => $to_name })) {
4023 # this is continuation of a split patch
4024 print "<div class=\"patch cont\">\n";
4026 # advance raw git-diff output if needed
4027 $patch_idx++ if defined $diffinfo;
4029 # read and prepare patch information
4030 $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
4032 # compact combined diff output can have some patches skipped
4033 # find which patch (using pathname of result) we are at now;
4035 while ($to_name ne $diffinfo->{'to_file'}) {
4036 print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n" .
4037 format_diff_cc_simplified($diffinfo, @hash_parents) .
4038 "</div>\n"; # class="patch"
4043 last if $patch_idx > $#$difftree;
4044 $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
4048 # modifies %from, %to hashes
4049 parse_from_to_diffinfo($diffinfo, \%from, \%to, @hash_parents);
4051 # this is first patch for raw difftree line with $patch_idx index
4052 # we index @$difftree array from 0, but number patches from 1
4053 print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n";
4057 #assert($patch_line =~ m/^diff /) if DEBUG;
4058 #assert($patch_line !~ m!$/$!) if DEBUG; # is chomp-ed
4060 # print "git diff" header
4061 print format_git_diff_header_line($patch_line, $diffinfo,
4064 # print extended diff header
4065 print "<div class=\"diff extended_header\">\n";
4067 while ($patch_line = <$fd>) {
4070 last EXTENDED_HEADER if ($patch_line =~ m/^--- |^diff /);
4072 print format_extended_diff_header_line($patch_line, $diffinfo,
4075 print "</div>\n"; # class="diff extended_header"
4077 # from-file/to-file diff header
4078 if (! $patch_line) {
4079 print "</div>\n"; # class="patch"
4082 next PATCH if ($patch_line =~ m/^diff /);
4083 #assert($patch_line =~ m/^---/) if DEBUG;
4085 my $last_patch_line = $patch_line;
4086 $patch_line = <$fd>;
4088 #assert($patch_line =~ m/^\+\+\+/) if DEBUG;
4090 print format_diff_from_to_header($last_patch_line, $patch_line,
4091 $diffinfo, \%from, \%to,
4096 while ($patch_line = <$fd>) {
4099 next PATCH if ($patch_line =~ m/^diff /);
4101 print format_diff_line($patch_line, \%from, \%to);
4105 print "</div>\n"; # class="patch"
4108 # for compact combined (--cc) format, with chunk and patch simpliciaction
4109 # patchset might be empty, but there might be unprocessed raw lines
4110 for (++$patch_idx if $patch_number > 0;
4111 $patch_idx < @$difftree;
4113 # read and prepare patch information
4114 $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
4116 # generate anchor for "patch" links in difftree / whatchanged part
4117 print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n" .
4118 format_diff_cc_simplified($diffinfo, @hash_parents) .
4119 "</div>\n"; # class="patch"
4124 if ($patch_number == 0) {
4125 if (@hash_parents > 1) {
4126 print "<div class=\"diff nodifferences\">Trivial merge</div>\n";
4128 print "<div class=\"diff nodifferences\">No differences found</div>\n";
4132 print "</div>\n"; # class="patchset"
4135 # . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
4137 # fills project list info (age, description, owner, forks) for each
4138 # project in the list, removing invalid projects from returned list
4139 # NOTE: modifies $projlist, but does not remove entries from it
4140 sub fill_project_list_info {
4141 my ($projlist, $check_forks) = @_;
4144 my $show_ctags = gitweb_check_feature('ctags');
4146 foreach my $pr (@$projlist) {
4147 my (@activity) = git_get_last_activity($pr->{'path'});
4148 unless (@activity) {
4151 ($pr->{'age'}, $pr->{'age_string'}) = @activity;
4152 if (!defined $pr->{'descr'}) {
4153 my $descr = git_get_project_description($pr->{'path'}) || "";
4154 $descr = to_utf8($descr);
4155 $pr->{'descr_long'} = $descr;
4156 $pr->{'descr'} = chop_str($descr, $projects_list_description_width, 5);
4158 if (!defined $pr->{'owner'}) {
4159 $pr->{'owner'} = git_get_project_owner("$pr->{'path'}") || "";
4162 my $pname = $pr->{'path'};
4163 if (($pname =~ s/\.git$//) &&
4164 ($pname !~ /\/$/) &&
4165 (-d "$projectroot/$pname")) {
4166 $pr->{'forks'} = "-d $projectroot/$pname";
4171 $show_ctags and $pr->{'ctags'} = git_get_project_ctags($pr->{'path'});
4172 push @projects, $pr;
4178 # print 'sort by' <th> element, generating 'sort by $name' replay link
4179 # if that order is not selected
4181 my ($name, $order, $header) = @_;
4182 $header ||= ucfirst($name);
4184 if ($order eq $name) {
4185 print "<th>$header</th>\n";
4188 $cgi->a({-href => href(-replay=>1, order=>$name),
4189 -class => "header"}, $header) .
4194 sub git_project_list_body {
4195 # actually uses global variable $project
4196 my ($projlist, $order, $from, $to, $extra, $no_header) = @_;
4198 my $check_forks = gitweb_check_feature('forks');
4199 my @projects = fill_project_list_info($projlist, $check_forks);
4201 $order ||= $default_projects_order;
4202 $from = 0 unless defined $from;
4203 $to = $#projects if (!defined $to || $#projects < $to);
4206 project => { key => 'path', type => 'str' },
4207 descr => { key => 'descr_long', type => 'str' },
4208 owner => { key => 'owner', type => 'str' },
4209 age => { key => 'age', type => 'num' }
4211 my $oi = $order_info{$order};
4212 if ($oi->{'type'} eq 'str') {
4213 @projects = sort {$a->{$oi->{'key'}} cmp $b->{$oi->{'key'}}} @projects;
4215 @projects = sort {$a->{$oi->{'key'}} <=> $b->{$oi->{'key'}}} @projects;
4218 my $show_ctags = gitweb_check_feature('ctags');
4221 foreach my $p (@projects) {
4222 foreach my $ct (keys %{$p->{'ctags'}}) {
4223 $ctags{$ct} += $p->{'ctags'}->{$ct};
4226 my $cloud = git_populate_project_tagcloud(\%ctags);
4227 print git_show_project_tagcloud($cloud, 64);
4230 print "<table class=\"project_list\">\n";
4231 unless ($no_header) {
4234 print "<th></th>\n";
4236 print_sort_th('project', $order, 'Project');
4237 print_sort_th('descr', $order, 'Description');
4238 print_sort_th('owner', $order, 'Owner');
4239 print_sort_th('age', $order, 'Last Change');
4240 print "<th></th>\n" . # for links
4244 my $tagfilter = $cgi->param('by_tag');
4245 for (my $i = $from; $i <= $to; $i++) {
4246 my $pr = $projects[$i];
4248 next if $tagfilter and $show_ctags and not grep { lc $_ eq lc $tagfilter } keys %{$pr->{'ctags'}};
4249 next if $searchtext and not $pr->{'path'} =~ /$searchtext/
4250 and not $pr->{'descr_long'} =~ /$searchtext/;
4251 # Weed out forks or non-matching entries of search
4253 my $forkbase = $project; $forkbase ||= ''; $forkbase =~ s#\.git$#/#;
4254 $forkbase="^$forkbase" if $forkbase;
4255 next if not $searchtext and not $tagfilter and $show_ctags
4256 and $pr->{'path'} =~ m#$forkbase.*/.*#; # regexp-safe
4260 print "<tr class=\"dark\">\n";
4262 print "<tr class=\"light\">\n";
4267 if ($pr->{'forks'}) {
4268 print "<!-- $pr->{'forks'} -->\n";
4269 print $cgi->a({-href => href(project=>$pr->{'path'}, action=>"forks")}, "+");
4273 print "<td>" . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary"),
4274 -class => "list"}, esc_html($pr->{'path'})) . "</td>\n" .
4275 "<td>" . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary"),
4276 -class => "list", -title => $pr->{'descr_long'}},
4277 esc_html($pr->{'descr'})) . "</td>\n" .
4278 "<td><i>" . chop_and_escape_str($pr->{'owner'}, 15) . "</i></td>\n";
4279 print "<td class=\"". age_class($pr->{'age'}) . "\">" .
4280 (defined $pr->{'age_string'} ? $pr->{'age_string'} : "No commits") . "</td>\n" .
4281 "<td class=\"link\">" .
4282 $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary")}, "summary") . " | " .
4283 $cgi->a({-href => href(project=>$pr->{'path'}, action=>"shortlog")}, "shortlog") . " | " .
4284 $cgi->a({-href => href(project=>$pr->{'path'}, action=>"log")}, "log") . " | " .
4285 $cgi->a({-href => href(project=>$pr->{'path'}, action=>"tree")}, "tree") .
4286 ($pr->{'forks'} ? " | " . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"forks")}, "forks") : '') .
4290 if (defined $extra) {
4293 print "<td></td>\n";
4295 print "<td colspan=\"5\">$extra</td>\n" .
4301 sub git_shortlog_body {
4302 # uses global variable $project
4303 my ($commitlist, $from, $to, $refs, $extra) = @_;
4305 $from = 0 unless defined $from;
4306 $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
4308 print "<table class=\"shortlog\">\n";
4310 for (my $i = $from; $i <= $to; $i++) {
4311 my %co = %{$commitlist->[$i]};
4312 my $commit = $co{'id'};
4313 my $ref = format_ref_marker($refs, $commit);
4315 print "<tr class=\"dark\">\n";
4317 print "<tr class=\"light\">\n";
4320 # git_summary() used print "<td><i>$co{'age_string'}</i></td>\n" .
4321 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
4322 format_author_html('td', \%co, 10) . "<td>";
4323 print format_subject_html($co{'title'}, $co{'title_short'},
4324 href(action=>"commit", hash=>$commit), $ref);
4326 "<td class=\"link\">" .
4327 $cgi->a({-href => href(action=>"commit", hash=>$commit)}, "commit") . " | " .
4328 $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff") . " | " .
4329 $cgi->a({-href => href(action=>"tree", hash=>$commit, hash_base=>$commit)}, "tree");
4330 my $snapshot_links = format_snapshot_links($commit);
4331 if (defined $snapshot_links) {
4332 print " | " . $snapshot_links;
4337 if (defined $extra) {
4339 "<td colspan=\"4\">$extra</td>\n" .
4345 sub git_history_body {
4346 # Warning: assumes constant type (blob or tree) during history
4347 my ($commitlist, $from, $to, $refs, $hash_base, $ftype, $extra) = @_;
4349 $from = 0 unless defined $from;
4350 $to = $#{$commitlist} unless (defined $to && $to <= $#{$commitlist});
4352 print "<table class=\"history\">\n";
4354 for (my $i = $from; $i <= $to; $i++) {
4355 my %co = %{$commitlist->[$i]};
4359 my $commit = $co{'id'};
4361 my $ref = format_ref_marker($refs, $commit);
4364 print "<tr class=\"dark\">\n";
4366 print "<tr class=\"light\">\n";
4369 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
4370 # shortlog: format_author_html('td', \%co, 10)
4371 format_author_html('td', \%co, 15, 3) . "<td>";
4372 # originally git_history used chop_str($co{'title'}, 50)
4373 print format_subject_html($co{'title'}, $co{'title_short'},
4374 href(action=>"commit", hash=>$commit), $ref);
4376 "<td class=\"link\">" .
4377 $cgi->a({-href => href(action=>$ftype, hash_base=>$commit, file_name=>$file_name)}, $ftype) . " | " .
4378 $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff");
4380 if ($ftype eq 'blob') {
4381 my $blob_current = git_get_hash_by_path($hash_base, $file_name);
4382 my $blob_parent = git_get_hash_by_path($commit, $file_name);
4383 if (defined $blob_current && defined $blob_parent &&
4384 $blob_current ne $blob_parent) {
4386 $cgi->a({-href => href(action=>"blobdiff",
4387 hash=>$blob_current, hash_parent=>$blob_parent,
4388 hash_base=>$hash_base, hash_parent_base=>$commit,
4389 file_name=>$file_name)},
4396 if (defined $extra) {
4398 "<td colspan=\"4\">$extra</td>\n" .
4405 # uses global variable $project
4406 my ($taglist, $from, $to, $extra) = @_;
4407 $from = 0 unless defined $from;
4408 $to = $#{$taglist} if (!defined $to || $#{$taglist} < $to);
4410 print "<table class=\"tags\">\n";
4412 for (my $i = $from; $i <= $to; $i++) {
4413 my $entry = $taglist->[$i];
4415 my $comment = $tag{'subject'};
4417 if (defined $comment) {
4418 $comment_short = chop_str($comment, 30, 5);
4421 print "<tr class=\"dark\">\n";
4423 print "<tr class=\"light\">\n";
4426 if (defined $tag{'age'}) {
4427 print "<td><i>$tag{'age'}</i></td>\n";
4429 print "<td></td>\n";
4432 $cgi->a({-href => href(action=>$tag{'reftype'}, hash=>$tag{'refid'}),
4433 -class => "list name"}, esc_html($tag{'name'})) .
4436 if (defined $comment) {
4437 print format_subject_html($comment, $comment_short,
4438 href(action=>"tag", hash=>$tag{'id'}));
4441 "<td class=\"selflink\">";
4442 if ($tag{'type'} eq "tag") {
4443 print $cgi->a({-href => href(action=>"tag", hash=>$tag{'id'})}, "tag");
4448 "<td class=\"link\">" . " | " .
4449 $cgi->a({-href => href(action=>$tag{'reftype'}, hash=>$tag{'refid'})}, $tag{'reftype'});
4450 if ($tag{'reftype'} eq "commit") {
4451 print " | " . $cgi->a({-href => href(action=>"shortlog", hash=>$tag{'fullname'})}, "shortlog") .
4452 " | " . $cgi->a({-href => href(action=>"log", hash=>$tag{'fullname'})}, "log");
4453 } elsif ($tag{'reftype'} eq "blob") {
4454 print " | " . $cgi->a({-href => href(action=>"blob_plain", hash=>$tag{'refid'})}, "raw");
4459 if (defined $extra) {
4461 "<td colspan=\"5\">$extra</td>\n" .
4467 sub git_heads_body {
4468 # uses global variable $project
4469 my ($headlist, $head, $from, $to, $extra) = @_;
4470 $from = 0 unless defined $from;
4471 $to = $#{$headlist} if (!defined $to || $#{$headlist} < $to);
4473 print "<table class=\"heads\">\n";
4475 for (my $i = $from; $i <= $to; $i++) {
4476 my $entry = $headlist->[$i];
4478 my $curr = $ref{'id'} eq $head;
4480 print "<tr class=\"dark\">\n";
4482 print "<tr class=\"light\">\n";
4485 print "<td><i>$ref{'age'}</i></td>\n" .
4486 ($curr ? "<td class=\"current_head\">" : "<td>") .
4487 $cgi->a({-href => href(action=>"shortlog", hash=>$ref{'fullname'}),
4488 -class => "list name"},esc_html($ref{'name'})) .
4490 "<td class=\"link\">" .
4491 $cgi->a({-href => href(action=>"shortlog", hash=>$ref{'fullname'})}, "shortlog") . " | " .
4492 $cgi->a({-href => href(action=>"log", hash=>$ref{'fullname'})}, "log") . " | " .
4493 $cgi->a({-href => href(action=>"tree", hash=>$ref{'fullname'}, hash_base=>$ref{'name'})}, "tree") .
4497 if (defined $extra) {
4499 "<td colspan=\"3\">$extra</td>\n" .
4505 sub git_search_grep_body {
4506 my ($commitlist, $from, $to, $extra) = @_;
4507 $from = 0 unless defined $from;
4508 $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
4510 print "<table class=\"commit_search\">\n";
4512 for (my $i = $from; $i <= $to; $i++) {
4513 my %co = %{$commitlist->[$i]};
4517 my $commit = $co{'id'};
4519 print "<tr class=\"dark\">\n";
4521 print "<tr class=\"light\">\n";
4524 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
4525 format_author_html('td', \%co, 15, 5) .
4527 $cgi->a({-href => href(action=>"commit", hash=>$co{'id'}),
4528 -class => "list subject"},
4529 chop_and_escape_str($co{'title'}, 50) . "<br/>");
4530 my $comment = $co{'comment'};
4531 foreach my $line (@$comment) {
4532 if ($line =~ m/^(.*?)($search_regexp)(.*)$/i) {
4533 my ($lead, $match, $trail) = ($1, $2, $3);
4534 $match = chop_str($match, 70, 5, 'center');
4535 my $contextlen = int((80 - length($match))/2);
4536 $contextlen = 30 if ($contextlen > 30);
4537 $lead = chop_str($lead, $contextlen, 10, 'left');
4538 $trail = chop_str($trail, $contextlen, 10, 'right');
4540 $lead = esc_html($lead);
4541 $match = esc_html($match);
4542 $trail = esc_html($trail);
4544 print "$lead<span class=\"match\">$match</span>$trail<br />";
4548 "<td class=\"link\">" .
4549 $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") .
4551 $cgi->a({-href => href(action=>"commitdiff", hash=>$co{'id'})}, "commitdiff") .
4553 $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree");
4557 if (defined $extra) {
4559 "<td colspan=\"3\">$extra</td>\n" .
4565 ## ======================================================================
4566 ## ======================================================================
4569 sub git_project_list {
4570 my $order = $input_params{'order'};
4571 if (defined $order && $order !~ m/none|project|descr|owner|age/) {
4572 die_error(400, "Unknown order parameter");
4575 my @list = git_get_projects_list();
4577 die_error(404, "No projects found");
4581 if (-f $home_text) {
4582 print "<div class=\"index_include\">\n";
4583 insert_file($home_text);
4586 print $cgi->startform(-method => "get") .
4587 "<p class=\"projsearch\">Search:\n" .
4588 $cgi->textfield(-name => "s", -value => $searchtext) . "\n" .
4590 $cgi->end_form() . "\n";
4591 git_project_list_body(\@list, $order);
4596 my $order = $input_params{'order'};
4597 if (defined $order && $order !~ m/none|project|descr|owner|age/) {
4598 die_error(400, "Unknown order parameter");
4601 my @list = git_get_projects_list($project);
4603 die_error(404, "No forks found");
4607 git_print_page_nav('','');
4608 git_print_header_div('summary', "$project forks");
4609 git_project_list_body(\@list, $order);
4613 sub git_project_index {
4614 my @projects = git_get_projects_list($project);
4617 -type => 'text/plain',
4618 -charset => 'utf-8',
4619 -content_disposition => 'inline; filename="index.aux"');
4621 foreach my $pr (@projects) {
4622 if (!exists $pr->{'owner'}) {
4623 $pr->{'owner'} = git_get_project_owner("$pr->{'path'}");
4626 my ($path, $owner) = ($pr->{'path'}, $pr->{'owner'});
4627 # quote as in CGI::Util::encode, but keep the slash, and use '+' for ' '
4628 $path =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf("%%%02X", ord($1))/eg;
4629 $owner =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf("%%%02X", ord($1))/eg;
4633 print "$path $owner\n";
4638 my $descr = git_get_project_description($project) || "none";
4639 my %co = parse_commit("HEAD");
4640 my %cd = %co ? parse_date($co{'committer_epoch'}, $co{'committer_tz'}) : ();
4641 my $head = $co{'id'};
4643 my $owner = git_get_project_owner($project);
4645 my $refs = git_get_references();
4646 # These get_*_list functions return one more to allow us to see if
4647 # there are more ...
4648 my @taglist = git_get_tags_list(16);
4649 my @headlist = git_get_heads_list(16);
4651 my $check_forks = gitweb_check_feature('forks');
4654 @forklist = git_get_projects_list($project);
4658 git_print_page_nav('summary','', $head);
4660 print "<div class=\"title\"> </div>\n";
4661 print "<table class=\"projects_list\">\n" .
4662 "<tr id=\"metadata_desc\"><td>description</td><td>" . esc_html($descr) . "</td></tr>\n" .
4663 "<tr id=\"metadata_owner\"><td>owner</td><td>" . esc_html($owner) . "</td></tr>\n";
4664 if (defined $cd{'rfc2822'}) {
4665 print "<tr id=\"metadata_lchange\"><td>last change</td><td>$cd{'rfc2822'}</td></tr>\n";
4668 # use per project git URL list in $projectroot/$project/cloneurl
4669 # or make project git URL from git base URL and project name
4670 my $url_tag = "URL";
4671 my @url_list = git_get_project_url_list($project);
4672 @url_list = map { "$_/$project" } @git_base_url_list unless @url_list;
4673 foreach my $git_url (@url_list) {
4674 next unless $git_url;
4675 print "<tr class=\"metadata_url\"><td>$url_tag</td><td>$git_url</td></tr>\n";
4680 my $show_ctags = gitweb_check_feature('ctags');
4682 my $ctags = git_get_project_ctags($project);
4683 my $cloud = git_populate_project_tagcloud($ctags);
4684 print "<tr id=\"metadata_ctags\"><td>Content tags:<br />";
4685 print "</td>\n<td>" unless %$ctags;
4686 print "<form action=\"$show_ctags\" method=\"post\"><input type=\"hidden\" name=\"p\" value=\"$project\" />Add: <input type=\"text\" name=\"t\" size=\"8\" /></form>";
4687 print "</td>\n<td>" if %$ctags;
4688 print git_show_project_tagcloud($cloud, 48);
4694 # If XSS prevention is on, we don't include README.html.
4695 # TODO: Allow a readme in some safe format.
4696 if (!$prevent_xss && -s "$projectroot/$project/README.html") {
4697 print "<div class=\"title\">readme</div>\n" .
4698 "<div class=\"readme\">\n";
4699 insert_file("$projectroot/$project/README.html");
4700 print "\n</div>\n"; # class="readme"
4703 # we need to request one more than 16 (0..15) to check if
4705 my @commitlist = $head ? parse_commits($head, 17) : ();
4707 git_print_header_div('shortlog');
4708 git_shortlog_body(\@commitlist, 0, 15, $refs,
4709 $#commitlist <= 15 ? undef :
4710 $cgi->a({-href => href(action=>"shortlog")}, "..."));
4714 git_print_header_div('tags');
4715 git_tags_body(\@taglist, 0, 15,
4716 $#taglist <= 15 ? undef :
4717 $cgi->a({-href => href(action=>"tags")}, "..."));
4721 git_print_header_div('heads');
4722 git_heads_body(\@headlist, $head, 0, 15,
4723 $#headlist <= 15 ? undef :
4724 $cgi->a({-href => href(action=>"heads")}, "..."));
4728 git_print_header_div('forks');
4729 git_project_list_body(\@forklist, 'age', 0, 15,
4730 $#forklist <= 15 ? undef :
4731 $cgi->a({-href => href(action=>"forks")}, "..."),
4739 my $head = git_get_head_hash($project);
4741 git_print_page_nav('','', $head,undef,$head);
4742 my %tag = parse_tag($hash);
4745 die_error(404, "Unknown tag object");
4748 git_print_header_div('commit', esc_html($tag{'name'}), $hash);
4749 print "<div class=\"title_text\">\n" .
4750 "<table class=\"object_header\">\n" .
4752 "<td>object</td>\n" .
4753 "<td>" . $cgi->a({-class => "list", -href => href(action=>$tag{'type'}, hash=>$tag{'object'})},
4754 $tag{'object'}) . "</td>\n" .
4755 "<td class=\"link\">" . $cgi->a({-href => href(action=>$tag{'type'}, hash=>$tag{'object'})},
4756 $tag{'type'}) . "</td>\n" .
4758 if (defined($tag{'author'})) {
4759 git_print_authorship_rows(\%tag, 'author');
4761 print "</table>\n\n" .
4763 print "<div class=\"page_body\">";
4764 my $comment = $tag{'comment'};
4765 foreach my $line (@$comment) {
4767 print esc_html($line, -nbsp=>1) . "<br/>\n";
4775 gitweb_check_feature('blame')
4776 or die_error(403, "Blame view not allowed");
4779 die_error(400, "No file name given") unless $file_name;
4780 $hash_base ||= git_get_head_hash($project);
4781 die_error(404, "Couldn't find base commit") unless $hash_base;
4782 my %co = parse_commit($hash_base)
4783 or die_error(404, "Commit not found");
4785 if (!defined $hash) {
4786 $hash = git_get_hash_by_path($hash_base, $file_name, "blob")
4787 or die_error(404, "Error looking up file");
4789 $ftype = git_get_type($hash);
4790 if ($ftype !~ "blob") {
4791 die_error(400, "Object is not a blob");
4795 # run git-blame --porcelain
4796 open my $fd, "-|", git_cmd(), "blame", '-p',
4797 $hash_base, '--', $file_name
4798 or die_error(500, "Open git-blame failed");
4803 $cgi->a({-href => href(action=>"blob", -replay=>1)},
4806 $cgi->a({-href => href(action=>"history", -replay=>1)},
4809 $cgi->a({-href => href(action=>"blame", file_name=>$file_name)},
4811 git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
4812 git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
4813 git_print_page_path($file_name, $ftype, $hash_base);
4816 my @rev_color = qw(light dark);
4817 my $num_colors = scalar(@rev_color);
4818 my $current_color = 0;
4822 <div class="page_body">
4823 <table class="blame">
4824 <tr><th>Commit</th><th>Line</th><th>Data</th></tr>
4827 while (my $line = <$fd>) {
4829 # the header: <SHA-1> <src lineno> <dst lineno> [<lines in group>]
4830 # no <lines in group> for subsequent lines in group of lines
4831 my ($full_rev, $orig_lineno, $lineno, $group_size) =
4832 ($line =~ /^([0-9a-f]{40}) (\d+) (\d+)(?: (\d+))?$/);
4833 if (!exists $metainfo{$full_rev}) {
4834 $metainfo{$full_rev} = { 'nprevious' => 0 };
4836 my $meta = $metainfo{$full_rev};
4838 while ($data = <$fd>) {
4840 last if ($data =~ s/^\t//); # contents of line
4841 if ($data =~ /^(\S+)(?: (.*))?$/) {
4842 $meta->{$1} = $2 unless exists $meta->{$1};
4844 if ($data =~ /^previous /) {
4845 $meta->{'nprevious'}++;
4848 my $short_rev = substr($full_rev, 0, 8);
4849 my $author = $meta->{'author'};
4851 parse_date($meta->{'author-time'}, $meta->{'author-tz'});
4852 my $date = $date{'iso-tz'};
4854 $current_color = ($current_color + 1) % $num_colors;
4856 my $tr_class = $rev_color[$current_color];
4857 $tr_class .= ' boundary' if (exists $meta->{'boundary'});
4858 $tr_class .= ' no-previous' if ($meta->{'nprevious'} == 0);
4859 $tr_class .= ' multiple-previous' if ($meta->{'nprevious'} > 1);
4860 print "<tr id=\"l$lineno\" class=\"$tr_class\">\n";
4862 print "<td class=\"sha1\"";
4863 print " title=\"". esc_html($author) . ", $date\"";
4864 print " rowspan=\"$group_size\"" if ($group_size > 1);
4866 print $cgi->a({-href => href(action=>"commit",
4868 file_name=>$file_name)},
4869 esc_html($short_rev));
4870 if ($group_size >= 2) {
4871 my @author_initials = ($author =~ /\b([[:upper:]])\B/g);
4872 if (@author_initials) {
4874 esc_html(join('', @author_initials));
4880 # 'previous' <sha1 of parent commit> <filename at commit>
4881 if (exists $meta->{'previous'} &&
4882 $meta->{'previous'} =~ /^([a-fA-F0-9]{40}) (.*)$/) {
4883 $meta->{'parent'} = $1;
4884 $meta->{'file_parent'} = unquote($2);
4887 exists($meta->{'parent'}) ?
4888 $meta->{'parent'} : $full_rev;
4889 my $linenr_filename =
4890 exists($meta->{'file_parent'}) ?
4891 $meta->{'file_parent'} : unquote($meta->{'filename'});
4892 my $blamed = href(action => 'blame',
4893 file_name => $linenr_filename,
4894 hash_base => $linenr_commit);
4895 print "<td class=\"linenr\">";
4896 print $cgi->a({ -href => "$blamed#l$orig_lineno",
4897 -class => "linenr" },
4900 print "<td class=\"pre\">" . esc_html($data) . "</td>\n";
4906 or print "Reading blob failed\n";
4913 my $head = git_get_head_hash($project);
4915 git_print_page_nav('','', $head,undef,$head);
4916 git_print_header_div('summary', $project);
4918 my @tagslist = git_get_tags_list();
4920 git_tags_body(\@tagslist);
4926 my $head = git_get_head_hash($project);
4928 git_print_page_nav('','', $head,undef,$head);
4929 git_print_header_div('summary', $project);
4931 my @headslist = git_get_heads_list();
4933 git_heads_body(\@headslist, $head);
4938 sub git_blob_plain {
4942 if (!defined $hash) {
4943 if (defined $file_name) {
4944 my $base = $hash_base || git_get_head_hash($project);
4945 $hash = git_get_hash_by_path($base, $file_name, "blob")
4946 or die_error(404, "Cannot find file");
4948 die_error(400, "No file name defined");
4950 } elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) {
4951 # blobs defined by non-textual hash id's can be cached
4955 open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
4956 or die_error(500, "Open git-cat-file blob '$hash' failed");
4958 # content-type (can include charset)
4959 $type = blob_contenttype($fd, $file_name, $type);
4961 # "save as" filename, even when no $file_name is given
4962 my $save_as = "$hash";
4963 if (defined $file_name) {
4964 $save_as = $file_name;
4965 } elsif ($type =~ m/^text\//) {
4969 # With XSS prevention on, blobs of all types except a few known safe
4970 # ones are served with "Content-Disposition: attachment" to make sure
4971 # they don't run in our security domain. For certain image types,
4972 # blob view writes an <img> tag referring to blob_plain view, and we
4973 # want to be sure not to break that by serving the image as an
4974 # attachment (though Firefox 3 doesn't seem to care).
4975 my $sandbox = $prevent_xss &&
4976 $type !~ m!^(?:text/plain|image/(?:gif|png|jpeg))$!;
4980 -expires => $expires,
4981 -content_disposition =>
4982 ($sandbox ? 'attachment' : 'inline')
4983 . '; filename="' . $save_as . '"');
4985 binmode STDOUT, ':raw';
4987 binmode STDOUT, ':utf8'; # as set at the beginning of gitweb.cgi
4994 if (!defined $hash) {
4995 if (defined $file_name) {
4996 my $base = $hash_base || git_get_head_hash($project);
4997 $hash = git_get_hash_by_path($base, $file_name, "blob")
4998 or die_error(404, "Cannot find file");
5000 die_error(400, "No file name defined");
5002 } elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) {
5003 # blobs defined by non-textual hash id's can be cached
5007 my $have_blame = gitweb_check_feature('blame');
5008 open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
5009 or die_error(500, "Couldn't cat $file_name, $hash");
5010 my $mimetype = blob_mimetype($fd, $file_name);
5011 if ($mimetype !~ m!^(?:text/|image/(?:gif|png|jpeg)$)! && -B $fd) {
5013 return git_blob_plain($mimetype);
5015 # we can have blame only for text/* mimetype
5016 $have_blame &&= ($mimetype =~ m!^text/!);
5018 git_header_html(undef, $expires);
5019 my $formats_nav = '';
5020 if (defined $hash_base && (my %co = parse_commit($hash_base))) {
5021 if (defined $file_name) {
5024 $cgi->a({-href => href(action=>"blame", -replay=>1)},
5029 $cgi->a({-href => href(action=>"history", -replay=>1)},
5032 $cgi->a({-href => href(action=>"blob_plain", -replay=>1)},
5035 $cgi->a({-href => href(action=>"blob",
5036 hash_base=>"HEAD", file_name=>$file_name)},
5040 $cgi->a({-href => href(action=>"blob_plain", -replay=>1)},
5043 git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
5044 git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
5046 print "<div class=\"page_nav\">\n" .
5047 "<br/><br/></div>\n" .
5048 "<div class=\"title\">$hash</div>\n";
5050 git_print_page_path($file_name, "blob", $hash_base);
5051 print "<div class=\"page_body\">\n";
5052 if ($mimetype =~ m!^image/!) {
5053 print qq!<img type="$mimetype"!;
5055 print qq! alt="$file_name" title="$file_name"!;
5058 href(action=>"blob_plain", hash=>$hash,
5059 hash_base=>$hash_base, file_name=>$file_name) .
5063 while (my $line = <$fd>) {
5066 $line = untabify($line);
5067 printf "<div class=\"pre\"><a id=\"l%i\" href=\"" . href(-replay => 1)
5068 . "#l%i\" class=\"linenr\">%4i</a> %s</div>\n",
5069 $nr, $nr, $nr, esc_html($line, -nbsp=>1);
5073 or print "Reading blob failed.\n";
5079 if (!defined $hash_base) {
5080 $hash_base = "HEAD";
5082 if (!defined $hash) {
5083 if (defined $file_name) {
5084 $hash = git_get_hash_by_path($hash_base, $file_name, "tree");
5089 die_error(404, "No such tree") unless defined($hash);
5094 open my $fd, "-|", git_cmd(), "ls-tree", '-z', $hash
5095 or die_error(500, "Open git-ls-tree failed");
5096 @entries = map { chomp; $_ } <$fd>;
5098 or die_error(404, "Reading tree failed");
5101 my $refs = git_get_references();
5102 my $ref = format_ref_marker($refs, $hash_base);
5105 my $have_blame = gitweb_check_feature('blame');
5106 if (defined $hash_base && (my %co = parse_commit($hash_base))) {
5108 if (defined $file_name) {
5110 $cgi->a({-href => href(action=>"history", -replay=>1)},
5112 $cgi->a({-href => href(action=>"tree",
5113 hash_base=>"HEAD", file_name=>$file_name)},
5116 my $snapshot_links = format_snapshot_links($hash);
5117 if (defined $snapshot_links) {
5118 # FIXME: Should be available when we have no hash base as well.
5119 push @views_nav, $snapshot_links;
5121 git_print_page_nav('tree','', $hash_base, undef, undef, join(' | ', @views_nav));
5122 git_print_header_div('commit', esc_html($co{'title'}) . $ref, $hash_base);
5125 print "<div class=\"page_nav\">\n";
5126 print "<br/><br/></div>\n";
5127 print "<div class=\"title\">$hash</div>\n";
5129 if (defined $file_name) {
5130 $basedir = $file_name;
5131 if ($basedir ne '' && substr($basedir, -1) ne '/') {
5134 git_print_page_path($file_name, 'tree', $hash_base);
5136 print "<div class=\"page_body\">\n";
5137 print "<table class=\"tree\">\n";
5139 # '..' (top directory) link if possible
5140 if (defined $hash_base &&
5141 defined $file_name && $file_name =~ m![^/]+$!) {
5143 print "<tr class=\"dark\">\n";
5145 print "<tr class=\"light\">\n";
5149 my $up = $file_name;
5150 $up =~ s!/?[^/]+$!!;
5151 undef $up unless $up;
5152 # based on git_print_tree_entry
5153 print '<td class="mode">' . mode_str('040000') . "</td>\n";
5154 print '<td class="list">';
5155 print $cgi->a({-href => href(action=>"tree", hash_base=>$hash_base,
5159 print "<td class=\"link\"></td>\n";
5163 foreach my $line (@entries) {
5164 my %t = parse_ls_tree_line($line, -z => 1);
5167 print "<tr class=\"dark\">\n";
5169 print "<tr class=\"light\">\n";
5173 git_print_tree_entry(\%t, $basedir, $hash_base, $have_blame);
5177 print "</table>\n" .
5183 my $format = $input_params{'snapshot_format'};
5184 if (!@snapshot_fmts) {
5185 die_error(403, "Snapshots not allowed");
5187 # default to first supported snapshot format
5188 $format ||= $snapshot_fmts[0];
5189 if ($format !~ m/^[a-z0-9]+$/) {
5190 die_error(400, "Invalid snapshot format parameter");
5191 } elsif (!exists($known_snapshot_formats{$format})) {
5192 die_error(400, "Unknown snapshot format");
5193 } elsif ($known_snapshot_formats{$format}{'disabled'}) {
5194 die_error(403, "Snapshot format not allowed");
5195 } elsif (!grep($_ eq $format, @snapshot_fmts)) {
5196 die_error(403, "Unsupported snapshot format");
5199 if (!defined $hash) {
5200 $hash = git_get_head_hash($project);
5203 my $name = $project;
5204 $name =~ s,([^/])/*\.git$,$1,;
5205 $name = basename($name);
5206 my $filename = to_utf8($name);
5207 $name =~ s/\047/\047\\\047\047/g;
5209 $filename .= "-$hash$known_snapshot_formats{$format}{'suffix'}";
5210 $cmd = quote_command(
5211 git_cmd(), 'archive',
5212 "--format=$known_snapshot_formats{$format}{'format'}",
5213 "--prefix=$name/", $hash);
5214 if (exists $known_snapshot_formats{$format}{'compressor'}) {
5215 $cmd .= ' | ' . quote_command(@{$known_snapshot_formats{$format}{'compressor'}});
5219 -type => $known_snapshot_formats{$format}{'type'},
5220 -content_disposition => 'inline; filename="' . "$filename" . '"',
5221 -status => '200 OK');
5223 open my $fd, "-|", $cmd
5224 or die_error(500, "Execute git-archive failed");
5225 binmode STDOUT, ':raw';
5227 binmode STDOUT, ':utf8'; # as set at the beginning of gitweb.cgi
5232 my $head = git_get_head_hash($project);
5233 if (!defined $hash) {
5236 if (!defined $page) {
5239 my $refs = git_get_references();
5241 my @commitlist = parse_commits($hash, 101, (100 * $page));
5243 my $paging_nav = format_paging_nav('log', $hash, $head, $page, $#commitlist >= 100);
5245 my ($patch_max) = gitweb_get_feature('patches');
5247 if ($patch_max < 0 || @commitlist <= $patch_max) {
5248 $paging_nav .= " ⋅ " .
5249 $cgi->a({-href => href(action=>"patches", -replay=>1)},
5255 git_print_page_nav('log','', $hash,undef,undef, $paging_nav);
5258 my %co = parse_commit($hash);
5260 git_print_header_div('summary', $project);
5261 print "<div class=\"page_body\"> Last change $co{'age_string'}.<br/><br/></div>\n";
5263 my $to = ($#commitlist >= 99) ? (99) : ($#commitlist);
5264 for (my $i = 0; $i <= $to; $i++) {
5265 my %co = %{$commitlist[$i]};
5267 my $commit = $co{'id'};
5268 my $ref = format_ref_marker($refs, $commit);
5269 my %ad = parse_date($co{'author_epoch'});
5270 git_print_header_div('commit',
5271 "<span class=\"age\">$co{'age_string'}</span>" .
5272 esc_html($co{'title'}) . $ref,
5274 print "<div class=\"title_text\">\n" .
5275 "<div class=\"log_link\">\n" .
5276 $cgi->a({-href => href(action=>"commit", hash=>$commit)}, "commit") .
5278 $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff") .
5280 $cgi->a({-href => href(action=>"tree", hash=>$commit, hash_base=>$commit)}, "tree") .
5283 git_print_authorship(\%co, -tag => 'span');
5284 print "<br/>\n</div>\n";
5286 print "<div class=\"log_body\">\n";
5287 git_print_log($co{'comment'}, -final_empty_line=> 1);
5290 if ($#commitlist >= 100) {
5291 print "<div class=\"page_nav\">\n";
5292 print $cgi->a({-href => href(-replay=>1, page=>$page+1),
5293 -accesskey => "n", -title => "Alt-n"}, "next");
5300 $hash ||= $hash_base || "HEAD";
5301 my %co = parse_commit($hash)
5302 or die_error(404, "Unknown commit object");
5304 my $parent = $co{'parent'};
5305 my $parents = $co{'parents'}; # listref
5307 # we need to prepare $formats_nav before any parameter munging
5309 if (!defined $parent) {
5311 $formats_nav .= '(initial)';
5312 } elsif (@$parents == 1) {
5313 # single parent commit
5316 $cgi->a({-href => href(action=>"commit",
5318 esc_html(substr($parent, 0, 7))) .
5325 $cgi->a({-href => href(action=>"commit",
5327 esc_html(substr($_, 0, 7)));
5331 if (gitweb_check_feature('patches') && @$parents <= 1) {
5332 $formats_nav .= " | " .
5333 $cgi->a({-href => href(action=>"patch", -replay=>1)},
5337 if (!defined $parent) {
5341 open my $fd, "-|", git_cmd(), "diff-tree", '-r', "--no-commit-id",
5343 (@$parents <= 1 ? $parent : '-c'),
5345 or die_error(500, "Open git-diff-tree failed");
5346 @difftree = map { chomp; $_ } <$fd>;
5347 close $fd or die_error(404, "Reading git-diff-tree failed");
5349 # non-textual hash id's can be cached
5351 if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
5354 my $refs = git_get_references();
5355 my $ref = format_ref_marker($refs, $co{'id'});
5357 git_header_html(undef, $expires);
5358 git_print_page_nav('commit', '',
5359 $hash, $co{'tree'}, $hash,
5362 if (defined $co{'parent'}) {
5363 git_print_header_div('commitdiff', esc_html($co{'title'}) . $ref, $hash);
5365 git_print_header_div('tree', esc_html($co{'title'}) . $ref, $co{'tree'}, $hash);
5367 print "<div class=\"title_text\">\n" .
5368 "<table class=\"object_header\">\n";
5369 git_print_authorship_rows(\%co);
5370 print "<tr><td>commit</td><td class=\"sha1\">$co{'id'}</td></tr>\n";
5373 "<td class=\"sha1\">" .
5374 $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$hash),
5375 class => "list"}, $co{'tree'}) .
5377 "<td class=\"link\">" .
5378 $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$hash)},
5380 my $snapshot_links = format_snapshot_links($hash);
5381 if (defined $snapshot_links) {
5382 print " | " . $snapshot_links;
5387 foreach my $par (@$parents) {
5390 "<td class=\"sha1\">" .
5391 $cgi->a({-href => href(action=>"commit", hash=>$par),
5392 class => "list"}, $par) .
5394 "<td class=\"link\">" .
5395 $cgi->a({-href => href(action=>"commit", hash=>$par)}, "commit") .
5397 $cgi->a({-href => href(action=>"commitdiff", hash=>$hash, hash_parent=>$par)}, "diff") .
5404 print "<div class=\"page_body\">\n";
5405 git_print_log($co{'comment'});
5408 git_difftree_body(\@difftree, $hash, @$parents);
5414 # object is defined by:
5415 # - hash or hash_base alone
5416 # - hash_base and file_name
5419 # - hash or hash_base alone
5420 if ($hash || ($hash_base && !defined $file_name)) {
5421 my $object_id = $hash || $hash_base;
5423 open my $fd, "-|", quote_command(
5424 git_cmd(), 'cat-file', '-t', $object_id) . ' 2> /dev/null'
5425 or die_error(404, "Object does not exist");
5429 or die_error(404, "Object does not exist");
5431 # - hash_base and file_name
5432 } elsif ($hash_base && defined $file_name) {
5433 $file_name =~ s,/+$,,;
5435 system(git_cmd(), "cat-file", '-e', $hash_base) == 0
5436 or die_error(404, "Base object does not exist");
5438 # here errors should not hapen
5439 open my $fd, "-|", git_cmd(), "ls-tree", $hash_base, "--", $file_name
5440 or die_error(500, "Open git-ls-tree failed");
5444 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'
5445 unless ($line && $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t/) {
5446 die_error(404, "File or directory for given base does not exist");
5451 die_error(400, "Not enough information to find object");
5454 print $cgi->redirect(-uri => href(action=>$type, -full=>1,
5455 hash=>$hash, hash_base=>$hash_base,
5456 file_name=>$file_name),
5457 -status => '302 Found');
5461 my $format = shift || 'html';
5468 # preparing $fd and %diffinfo for git_patchset_body
5470 if (defined $hash_base && defined $hash_parent_base) {
5471 if (defined $file_name) {
5473 open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
5474 $hash_parent_base, $hash_base,
5475 "--", (defined $file_parent ? $file_parent : ()), $file_name
5476 or die_error(500, "Open git-diff-tree failed");
5477 @difftree = map { chomp; $_ } <$fd>;
5479 or die_error(404, "Reading git-diff-tree failed");
5481 or die_error(404, "Blob diff not found");
5483 } elsif (defined $hash &&
5484 $hash =~ /[0-9a-fA-F]{40}/) {
5485 # try to find filename from $hash
5487 # read filtered raw output
5488 open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
5489 $hash_parent_base, $hash_base, "--"
5490 or die_error(500, "Open git-diff-tree failed");
5492 # ':100644 100644 03b21826... 3b93d5e7... M ls-files.c'
5494 grep { /^:[0-7]{6} [0-7]{6} [0-9a-fA-F]{40} $hash/ }
5495 map { chomp; $_ } <$fd>;
5497 or die_error(404, "Reading git-diff-tree failed");
5499 or die_error(404, "Blob diff not found");
5502 die_error(400, "Missing one of the blob diff parameters");
5505 if (@difftree > 1) {
5506 die_error(400, "Ambiguous blob diff specification");
5509 %diffinfo = parse_difftree_raw_line($difftree[0]);
5510 $file_parent ||= $diffinfo{'from_file'} || $file_name;
5511 $file_name ||= $diffinfo{'to_file'};
5513 $hash_parent ||= $diffinfo{'from_id'};
5514 $hash ||= $diffinfo{'to_id'};
5516 # non-textual hash id's can be cached
5517 if ($hash_base =~ m/^[0-9a-fA-F]{40}$/ &&
5518 $hash_parent_base =~ m/^[0-9a-fA-F]{40}$/) {
5523 open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
5524 '-p', ($format eq 'html' ? "--full-index" : ()),
5525 $hash_parent_base, $hash_base,
5526 "--", (defined $file_parent ? $file_parent : ()), $file_name
5527 or die_error(500, "Open git-diff-tree failed");
5530 # old/legacy style URI -- not generated anymore since 1.4.3.
5532 die_error('404 Not Found', "Missing one of the blob diff parameters")
5536 if ($format eq 'html') {
5538 $cgi->a({-href => href(action=>"blobdiff_plain", -replay=>1)},
5540 git_header_html(undef, $expires);
5541 if (defined $hash_base && (my %co = parse_commit($hash_base))) {
5542 git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
5543 git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
5545 print "<div class=\"page_nav\"><br/>$formats_nav<br/></div>\n";
5546 print "<div class=\"title\">$hash vs $hash_parent</div>\n";
5548 if (defined $file_name) {
5549 git_print_page_path($file_name, "blob", $hash_base);
5551 print "<div class=\"page_path\"></div>\n";
5554 } elsif ($format eq 'plain') {
5556 -type => 'text/plain',
5557 -charset => 'utf-8',
5558 -expires => $expires,
5559 -content_disposition => 'inline; filename="' . "$file_name" . '.patch"');
5561 print "X-Git-Url: " . $cgi->self_url() . "\n\n";
5564 die_error(400, "Unknown blobdiff format");
5568 if ($format eq 'html') {
5569 print "<div class=\"page_body\">\n";
5571 git_patchset_body($fd, [ \%diffinfo ], $hash_base, $hash_parent_base);
5574 print "</div>\n"; # class="page_body"
5578 while (my $line = <$fd>) {
5579 $line =~ s!a/($hash|$hash_parent)!'a/'.esc_path($diffinfo{'from_file'})!eg;
5580 $line =~ s!b/($hash|$hash_parent)!'b/'.esc_path($diffinfo{'to_file'})!eg;
5584 last if $line =~ m!^\+\+\+!;
5592 sub git_blobdiff_plain {
5593 git_blobdiff('plain');
5596 sub git_commitdiff {
5598 my $format = $params{-format} || 'html';
5600 my ($patch_max) = gitweb_get_feature('patches');
5601 if ($format eq 'patch') {
5602 die_error(403, "Patch view not allowed") unless $patch_max;
5605 $hash ||= $hash_base || "HEAD";
5606 my %co = parse_commit($hash)
5607 or die_error(404, "Unknown commit object");
5609 # choose format for commitdiff for merge
5610 if (! defined $hash_parent && @{$co{'parents'}} > 1) {
5611 $hash_parent = '--cc';
5613 # we need to prepare $formats_nav before almost any parameter munging
5615 if ($format eq 'html') {
5617 $cgi->a({-href => href(action=>"commitdiff_plain", -replay=>1)},
5619 if ($patch_max && @{$co{'parents'}} <= 1) {
5620 $formats_nav .= " | " .
5621 $cgi->a({-href => href(action=>"patch", -replay=>1)},
5625 if (defined $hash_parent &&
5626 $hash_parent ne '-c' && $hash_parent ne '--cc') {
5627 # commitdiff with two commits given
5628 my $hash_parent_short = $hash_parent;
5629 if ($hash_parent =~ m/^[0-9a-fA-F]{40}$/) {
5630 $hash_parent_short = substr($hash_parent, 0, 7);
5634 for (my $i = 0; $i < @{$co{'parents'}}; $i++) {
5635 if ($co{'parents'}[$i] eq $hash_parent) {
5636 $formats_nav .= ' parent ' . ($i+1);
5640 $formats_nav .= ': ' .
5641 $cgi->a({-href => href(action=>"commitdiff",
5642 hash=>$hash_parent)},
5643 esc_html($hash_parent_short)) .
5645 } elsif (!$co{'parent'}) {
5647 $formats_nav .= ' (initial)';
5648 } elsif (scalar @{$co{'parents'}} == 1) {
5649 # single parent commit
5652 $cgi->a({-href => href(action=>"commitdiff",
5653 hash=>$co{'parent'})},
5654 esc_html(substr($co{'parent'}, 0, 7))) .
5658 if ($hash_parent eq '--cc') {
5659 $formats_nav .= ' | ' .
5660 $cgi->a({-href => href(action=>"commitdiff",
5661 hash=>$hash, hash_parent=>'-c')},
5663 } else { # $hash_parent eq '-c'
5664 $formats_nav .= ' | ' .
5665 $cgi->a({-href => href(action=>"commitdiff",
5666 hash=>$hash, hash_parent=>'--cc')},
5672 $cgi->a({-href => href(action=>"commitdiff",
5674 esc_html(substr($_, 0, 7)));
5675 } @{$co{'parents'}} ) .
5680 my $hash_parent_param = $hash_parent;
5681 if (!defined $hash_parent_param) {
5682 # --cc for multiple parents, --root for parentless
5683 $hash_parent_param =
5684 @{$co{'parents'}} > 1 ? '--cc' : $co{'parent'} || '--root';
5690 if ($format eq 'html') {
5691 open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
5692 "--no-commit-id", "--patch-with-raw", "--full-index",
5693 $hash_parent_param, $hash, "--"
5694 or die_error(500, "Open git-diff-tree failed");
5696 while (my $line = <$fd>) {
5698 # empty line ends raw part of diff-tree output
5700 push @difftree, scalar parse_difftree_raw_line($line);
5703 } elsif ($format eq 'plain') {
5704 open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
5705 '-p', $hash_parent_param, $hash, "--"
5706 or die_error(500, "Open git-diff-tree failed");
5707 } elsif ($format eq 'patch') {
5708 # For commit ranges, we limit the output to the number of
5709 # patches specified in the 'patches' feature.
5710 # For single commits, we limit the output to a single patch,
5711 # diverging from the git-format-patch default.
5712 my @commit_spec = ();
5714 if ($patch_max > 0) {
5715 push @commit_spec, "-$patch_max";
5717 push @commit_spec, '-n', "$hash_parent..$hash";
5719 if ($params{-single}) {
5720 push @commit_spec, '-1';
5722 if ($patch_max > 0) {
5723 push @commit_spec, "-$patch_max";
5725 push @commit_spec, "-n";
5727 push @commit_spec, '--root', $hash;
5729 open $fd, "-|", git_cmd(), "format-patch", '--encoding=utf8',
5730 '--stdout', @commit_spec
5731 or die_error(500, "Open git-format-patch failed");
5733 die_error(400, "Unknown commitdiff format");
5736 # non-textual hash id's can be cached
5738 if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
5742 # write commit message
5743 if ($format eq 'html') {
5744 my $refs = git_get_references();
5745 my $ref = format_ref_marker($refs, $co{'id'});
5747 git_header_html(undef, $expires);
5748 git_print_page_nav('commitdiff','', $hash,$co{'tree'},$hash, $formats_nav);
5749 git_print_header_div('commit', esc_html($co{'title'}) . $ref, $hash);
5750 print "<div class=\"title_text\">\n" .
5751 "<table class=\"object_header\">\n";
5752 git_print_authorship_rows(\%co);
5755 print "<div class=\"page_body\">\n";
5756 if (@{$co{'comment'}} > 1) {
5757 print "<div class=\"log\">\n";
5758 git_print_log($co{'comment'}, -final_empty_line=> 1, -remove_title => 1);
5759 print "</div>\n"; # class="log"
5762 } elsif ($format eq 'plain') {
5763 my $refs = git_get_references("tags");
5764 my $tagname = git_get_rev_name_tags($hash);
5765 my $filename = basename($project) . "-$hash.patch";
5768 -type => 'text/plain',
5769 -charset => 'utf-8',
5770 -expires => $expires,
5771 -content_disposition => 'inline; filename="' . "$filename" . '"');
5772 my %ad = parse_date($co{'author_epoch'}, $co{'author_tz'});
5773 print "From: " . to_utf8($co{'author'}) . "\n";
5774 print "Date: $ad{'rfc2822'} ($ad{'tz_local'})\n";
5775 print "Subject: " . to_utf8($co{'title'}) . "\n";
5777 print "X-Git-Tag: $tagname\n" if $tagname;
5778 print "X-Git-Url: " . $cgi->self_url() . "\n\n";
5780 foreach my $line (@{$co{'comment'}}) {
5781 print to_utf8($line) . "\n";
5784 } elsif ($format eq 'patch') {
5785 my $filename = basename($project) . "-$hash.patch";
5788 -type => 'text/plain',
5789 -charset => 'utf-8',
5790 -expires => $expires,
5791 -content_disposition => 'inline; filename="' . "$filename" . '"');
5795 if ($format eq 'html') {
5796 my $use_parents = !defined $hash_parent ||
5797 $hash_parent eq '-c' || $hash_parent eq '--cc';
5798 git_difftree_body(\@difftree, $hash,
5799 $use_parents ? @{$co{'parents'}} : $hash_parent);
5802 git_patchset_body($fd, \@difftree, $hash,
5803 $use_parents ? @{$co{'parents'}} : $hash_parent);
5805 print "</div>\n"; # class="page_body"
5808 } elsif ($format eq 'plain') {
5812 or print "Reading git-diff-tree failed\n";
5813 } elsif ($format eq 'patch') {
5817 or print "Reading git-format-patch failed\n";
5821 sub git_commitdiff_plain {
5822 git_commitdiff(-format => 'plain');
5825 # format-patch-style patches
5827 git_commitdiff(-format => 'patch', -single => 1);
5831 git_commitdiff(-format => 'patch');
5835 if (!defined $hash_base) {
5836 $hash_base = git_get_head_hash($project);
5838 if (!defined $page) {
5842 my %co = parse_commit($hash_base)
5843 or die_error(404, "Unknown commit object");
5845 my $refs = git_get_references();
5846 my $limit = sprintf("--max-count=%i", (100 * ($page+1)));
5848 my @commitlist = parse_commits($hash_base, 101, (100 * $page),
5849 $file_name, "--full-history")
5850 or die_error(404, "No such file or directory on given branch");
5852 if (!defined $hash && defined $file_name) {
5853 # some commits could have deleted file in question,
5854 # and not have it in tree, but one of them has to have it
5855 for (my $i = 0; $i <= @commitlist; $i++) {
5856 $hash = git_get_hash_by_path($commitlist[$i]{'id'}, $file_name);
5857 last if defined $hash;
5860 if (defined $hash) {
5861 $ftype = git_get_type($hash);
5863 if (!defined $ftype) {
5864 die_error(500, "Unknown type of object");
5867 my $paging_nav = '';
5870 $cgi->a({-href => href(action=>"history", hash=>$hash, hash_base=>$hash_base,
5871 file_name=>$file_name)},
5873 $paging_nav .= " ⋅ " .
5874 $cgi->a({-href => href(-replay=>1, page=>$page-1),
5875 -accesskey => "p", -title => "Alt-p"}, "prev");
5877 $paging_nav .= "first";
5878 $paging_nav .= " ⋅ prev";
5881 if ($#commitlist >= 100) {
5883 $cgi->a({-href => href(-replay=>1, page=>$page+1),
5884 -accesskey => "n", -title => "Alt-n"}, "next");
5885 $paging_nav .= " ⋅ $next_link";
5887 $paging_nav .= " ⋅ next";
5891 git_print_page_nav('history','', $hash_base,$co{'tree'},$hash_base, $paging_nav);
5892 git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
5893 git_print_page_path($file_name, $ftype, $hash_base);
5895 git_history_body(\@commitlist, 0, 99,
5896 $refs, $hash_base, $ftype, $next_link);
5902 gitweb_check_feature('search') or die_error(403, "Search is disabled");
5903 if (!defined $searchtext) {
5904 die_error(400, "Text field is empty");
5906 if (!defined $hash) {
5907 $hash = git_get_head_hash($project);
5909 my %co = parse_commit($hash);
5911 die_error(404, "Unknown commit object");
5913 if (!defined $page) {
5917 $searchtype ||= 'commit';
5918 if ($searchtype eq 'pickaxe') {
5919 # pickaxe may take all resources of your box and run for several minutes
5920 # with every query - so decide by yourself how public you make this feature
5921 gitweb_check_feature('pickaxe')
5922 or die_error(403, "Pickaxe is disabled");
5924 if ($searchtype eq 'grep') {
5925 gitweb_check_feature('grep')
5926 or die_error(403, "Grep is disabled");
5931 if ($searchtype eq 'commit' or $searchtype eq 'author' or $searchtype eq 'committer') {
5933 if ($searchtype eq 'commit') {
5934 $greptype = "--grep=";
5935 } elsif ($searchtype eq 'author') {
5936 $greptype = "--author=";
5937 } elsif ($searchtype eq 'committer') {
5938 $greptype = "--committer=";
5940 $greptype .= $searchtext;
5941 my @commitlist = parse_commits($hash, 101, (100 * $page), undef,
5942 $greptype, '--regexp-ignore-case',
5943 $search_use_regexp ? '--extended-regexp' : '--fixed-strings');
5945 my $paging_nav = '';
5948 $cgi->a({-href => href(action=>"search", hash=>$hash,
5949 searchtext=>$searchtext,
5950 searchtype=>$searchtype)},
5952 $paging_nav .= " ⋅ " .
5953 $cgi->a({-href => href(-replay=>1, page=>$page-1),
5954 -accesskey => "p", -title => "Alt-p"}, "prev");
5956 $paging_nav .= "first";
5957 $paging_nav .= " ⋅ prev";
5960 if ($#commitlist >= 100) {
5962 $cgi->a({-href => href(-replay=>1, page=>$page+1),
5963 -accesskey => "n", -title => "Alt-n"}, "next");
5964 $paging_nav .= " ⋅ $next_link";
5966 $paging_nav .= " ⋅ next";
5969 if ($#commitlist >= 100) {
5972 git_print_page_nav('','', $hash,$co{'tree'},$hash, $paging_nav);
5973 git_print_header_div('commit', esc_html($co{'title'}), $hash);
5974 git_search_grep_body(\@commitlist, 0, 99, $next_link);
5977 if ($searchtype eq 'pickaxe') {
5978 git_print_page_nav('','', $hash,$co{'tree'},$hash);
5979 git_print_header_div('commit', esc_html($co{'title'}), $hash);
5981 print "<table class=\"pickaxe search\">\n";
5984 open my $fd, '-|', git_cmd(), '--no-pager', 'log', @diff_opts,
5985 '--pretty=format:%H', '--no-abbrev', '--raw', "-S$searchtext",
5986 ($search_use_regexp ? '--pickaxe-regex' : ());
5989 while (my $line = <$fd>) {
5993 my %set = parse_difftree_raw_line($line);
5994 if (defined $set{'commit'}) {
5995 # finish previous commit
5998 "<td class=\"link\">" .
5999 $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") .
6001 $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree");
6007 print "<tr class=\"dark\">\n";
6009 print "<tr class=\"light\">\n";
6012 %co = parse_commit($set{'commit'});
6013 my $author = chop_and_escape_str($co{'author_name'}, 15, 5);
6014 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
6015 "<td><i>$author</i></td>\n" .
6017 $cgi->a({-href => href(action=>"commit", hash=>$co{'id'}),
6018 -class => "list subject"},
6019 chop_and_escape_str($co{'title'}, 50) . "<br/>");
6020 } elsif (defined $set{'to_id'}) {
6021 next if ($set{'to_id'} =~ m/^0{40}$/);
6023 print $cgi->a({-href => href(action=>"blob", hash_base=>$co{'id'},
6024 hash=>$set{'to_id'}, file_name=>$set{'to_file'}),
6026 "<span class=\"match\">" . esc_path($set{'file'}) . "</span>") .
6032 # finish last commit (warning: repetition!)
6035 "<td class=\"link\">" .
6036 $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") .
6038 $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree");
6046 if ($searchtype eq 'grep') {
6047 git_print_page_nav('','', $hash,$co{'tree'},$hash);
6048 git_print_header_div('commit', esc_html($co{'title'}), $hash);
6050 print "<table class=\"grep_search\">\n";
6054 open my $fd, "-|", git_cmd(), 'grep', '-n',
6055 $search_use_regexp ? ('-E', '-i') : '-F',
6056 $searchtext, $co{'tree'};
6058 while (my $line = <$fd>) {
6060 my ($file, $lno, $ltext, $binary);
6061 last if ($matches++ > 1000);
6062 if ($line =~ /^Binary file (.+) matches$/) {
6066 (undef, $file, $lno, $ltext) = split(/:/, $line, 4);
6068 if ($file ne $lastfile) {
6069 $lastfile and print "</td></tr>\n";
6071 print "<tr class=\"dark\">\n";
6073 print "<tr class=\"light\">\n";
6075 print "<td class=\"list\">".
6076 $cgi->a({-href => href(action=>"blob", hash=>$co{'hash'},
6077 file_name=>"$file"),
6078 -class => "list"}, esc_path($file));
6079 print "</td><td>\n";
6083 print "<div class=\"binary\">Binary file</div>\n";
6085 $ltext = untabify($ltext);
6086 if ($ltext =~ m/^(.*)($search_regexp)(.*)$/i) {
6087 $ltext = esc_html($1, -nbsp=>1);
6088 $ltext .= '<span class="match">';
6089 $ltext .= esc_html($2, -nbsp=>1);
6090 $ltext .= '</span>';
6091 $ltext .= esc_html($3, -nbsp=>1);
6093 $ltext = esc_html($ltext, -nbsp=>1);
6095 print "<div class=\"pre\">" .
6096 $cgi->a({-href => href(action=>"blob", hash=>$co{'hash'},
6097 file_name=>"$file").'#l'.$lno,
6098 -class => "linenr"}, sprintf('%4i', $lno))
6099 . ' ' . $ltext . "</div>\n";
6103 print "</td></tr>\n";
6104 if ($matches > 1000) {
6105 print "<div class=\"diff nodifferences\">Too many matches, listing trimmed</div>\n";
6108 print "<div class=\"diff nodifferences\">No matches found</div>\n";
6117 sub git_search_help {
6119 git_print_page_nav('','', $hash,$hash,$hash);
6121 <p><strong>Pattern</strong> is by default a normal string that is matched precisely (but without
6122 regard to case, except in the case of pickaxe). However, when you check the <em>re</em> checkbox,
6123 the pattern entered is recognized as the POSIX extended
6124 <a href="http://en.wikipedia.org/wiki/Regular_expression">regular expression</a> (also case
6127 <dt><b>commit</b></dt>
6128 <dd>The commit messages and authorship information will be scanned for the given pattern.</dd>
6130 my $have_grep = gitweb_check_feature('grep');
6133 <dt><b>grep</b></dt>
6134 <dd>All files in the currently selected tree (HEAD unless you are explicitly browsing
6135 a different one) are searched for the given pattern. On large trees, this search can take
6136 a while and put some strain on the server, so please use it with some consideration. Note that
6137 due to git-grep peculiarity, currently if regexp mode is turned off, the matches are
6138 case-sensitive.</dd>
6142 <dt><b>author</b></dt>
6143 <dd>Name and e-mail of the change author and date of birth of the patch will be scanned for the given pattern.</dd>
6144 <dt><b>committer</b></dt>
6145 <dd>Name and e-mail of the committer and date of commit will be scanned for the given pattern.</dd>
6147 my $have_pickaxe = gitweb_check_feature('pickaxe');
6148 if ($have_pickaxe) {
6150 <dt><b>pickaxe</b></dt>
6151 <dd>All commits that caused the string to appear or disappear from any file (changes that
6152 added, removed or "modified" the string) will be listed. This search can take a while and
6153 takes a lot of strain on the server, so please use it wisely. Note that since you may be
6154 interested even in changes just changing the case as well, this search is case sensitive.</dd>
6162 my $head = git_get_head_hash($project);
6163 if (!defined $hash) {
6166 if (!defined $page) {
6169 my $refs = git_get_references();
6171 my $commit_hash = $hash;
6172 if (defined $hash_parent) {
6173 $commit_hash = "$hash_parent..$hash";
6175 my @commitlist = parse_commits($commit_hash, 101, (100 * $page));
6177 my $paging_nav = format_paging_nav('shortlog', $hash, $head, $page, $#commitlist >= 100);
6179 if ($#commitlist >= 100) {
6181 $cgi->a({-href => href(-replay=>1, page=>$page+1),
6182 -accesskey => "n", -title => "Alt-n"}, "next");
6184 my $patch_max = gitweb_check_feature('patches');
6186 if ($patch_max < 0 || @commitlist <= $patch_max) {
6187 $paging_nav .= " ⋅ " .
6188 $cgi->a({-href => href(action=>"patches", -replay=>1)},
6194 git_print_page_nav('shortlog','', $hash,$hash,$hash, $paging_nav);
6195 git_print_header_div('summary', $project);
6197 git_shortlog_body(\@commitlist, 0, 99, $refs, $next_link);
6202 ## ......................................................................
6203 ## feeds (RSS, Atom; OPML)
6206 my $format = shift || 'atom';
6207 my $have_blame = gitweb_check_feature('blame');
6209 # Atom: http://www.atomenabled.org/developers/syndication/
6210 # RSS: http://www.notestips.com/80256B3A007F2692/1/NAMO5P9UPQ
6211 if ($format ne 'rss' && $format ne 'atom') {
6212 die_error(400, "Unknown web feed format");
6215 # log/feed of current (HEAD) branch, log of given branch, history of file/directory
6216 my $head = $hash || 'HEAD';
6217 my @commitlist = parse_commits($head, 150, 0, $file_name);
6221 my $content_type = "application/$format+xml";
6222 if (defined $cgi->http('HTTP_ACCEPT') &&
6223 $cgi->Accept('text/xml') > $cgi->Accept($content_type)) {
6224 # browser (feed reader) prefers text/xml
6225 $content_type = 'text/xml';
6227 if (defined($commitlist[0])) {
6228 %latest_commit = %{$commitlist[0]};
6229 my $latest_epoch = $latest_commit{'committer_epoch'};
6230 %latest_date = parse_date($latest_epoch);
6231 my $if_modified = $cgi->http('IF_MODIFIED_SINCE');
6232 if (defined $if_modified) {
6234 if (eval { require HTTP::Date; 1; }) {
6235 $since = HTTP::Date::str2time($if_modified);
6236 } elsif (eval { require Time::ParseDate; 1; }) {
6237 $since = Time::ParseDate::parsedate($if_modified, GMT => 1);
6239 if (defined $since && $latest_epoch <= $since) {
6241 -type => $content_type,
6242 -charset => 'utf-8',
6243 -last_modified => $latest_date{'rfc2822'},
6244 -status => '304 Not Modified');
6249 -type => $content_type,
6250 -charset => 'utf-8',
6251 -last_modified => $latest_date{'rfc2822'});
6254 -type => $content_type,
6255 -charset => 'utf-8');
6258 # Optimization: skip generating the body if client asks only
6259 # for Last-Modified date.
6260 return if ($cgi->request_method() eq 'HEAD');
6263 my $title = "$site_name - $project/$action";
6264 my $feed_type = 'log';
6265 if (defined $hash) {
6266 $title .= " - '$hash'";
6267 $feed_type = 'branch log';
6268 if (defined $file_name) {
6269 $title .= " :: $file_name";
6270 $feed_type = 'history';
6272 } elsif (defined $file_name) {
6273 $title .= " - $file_name";
6274 $feed_type = 'history';
6276 $title .= " $feed_type";
6277 my $descr = git_get_project_description($project);
6278 if (defined $descr) {
6279 $descr = esc_html($descr);
6281 $descr = "$project " .
6282 ($format eq 'rss' ? 'RSS' : 'Atom') .
6285 my $owner = git_get_project_owner($project);
6286 $owner = esc_html($owner);
6290 if (defined $file_name) {
6291 $alt_url = href(-full=>1, action=>"history", hash=>$hash, file_name=>$file_name);
6292 } elsif (defined $hash) {
6293 $alt_url = href(-full=>1, action=>"log", hash=>$hash);
6295 $alt_url = href(-full=>1, action=>"summary");
6297 print qq!<?xml version="1.0" encoding="utf-8"?>\n!;
6298 if ($format eq 'rss') {
6300 <rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
6303 print "<title>$title</title>\n" .
6304 "<link>$alt_url</link>\n" .
6305 "<description>$descr</description>\n" .
6306 "<language>en</language>\n" .
6307 # project owner is responsible for 'editorial' content
6308 "<managingEditor>$owner</managingEditor>\n";
6309 if (defined $logo || defined $favicon) {
6310 # prefer the logo to the favicon, since RSS
6311 # doesn't allow both
6312 my $img = esc_url($logo || $favicon);
6314 "<url>$img</url>\n" .
6315 "<title>$title</title>\n" .
6316 "<link>$alt_url</link>\n" .
6320 print "<pubDate>$latest_date{'rfc2822'}</pubDate>\n";
6321 print "<lastBuildDate>$latest_date{'rfc2822'}</lastBuildDate>\n";
6323 print "<generator>gitweb v.$version/$git_version</generator>\n";
6324 } elsif ($format eq 'atom') {
6326 <feed xmlns="http://www.w3.org/2005/Atom">
6328 print "<title>$title</title>\n" .
6329 "<subtitle>$descr</subtitle>\n" .
6330 '<link rel="alternate" type="text/html" href="' .
6331 $alt_url . '" />' . "\n" .
6332 '<link rel="self" type="' . $content_type . '" href="' .
6333 $cgi->self_url() . '" />' . "\n" .
6334 "<id>" . href(-full=>1) . "</id>\n" .
6335 # use project owner for feed author
6336 "<author><name>$owner</name></author>\n";
6337 if (defined $favicon) {
6338 print "<icon>" . esc_url($favicon) . "</icon>\n";
6340 if (defined $logo_url) {
6341 # not twice as wide as tall: 72 x 27 pixels
6342 print "<logo>" . esc_url($logo) . "</logo>\n";
6344 if (! %latest_date) {
6345 # dummy date to keep the feed valid until commits trickle in:
6346 print "<updated>1970-01-01T00:00:00Z</updated>\n";
6348 print "<updated>$latest_date{'iso-8601'}</updated>\n";
6350 print "<generator version='$version/$git_version'>gitweb</generator>\n";
6354 for (my $i = 0; $i <= $#commitlist; $i++) {
6355 my %co = %{$commitlist[$i]};
6356 my $commit = $co{'id'};
6357 # we read 150, we always show 30 and the ones more recent than 48 hours
6358 if (($i >= 20) && ((time - $co{'author_epoch'}) > 48*60*60)) {
6361 my %cd = parse_date($co{'author_epoch'});
6363 # get list of changed files
6364 open my $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
6365 $co{'parent'} || "--root",
6366 $co{'id'}, "--", (defined $file_name ? $file_name : ())
6368 my @difftree = map { chomp; $_ } <$fd>;
6372 # print element (entry, item)
6373 my $co_url = href(-full=>1, action=>"commitdiff", hash=>$commit);
6374 if ($format eq 'rss') {
6376 "<title>" . esc_html($co{'title'}) . "</title>\n" .
6377 "<author>" . esc_html($co{'author'}) . "</author>\n" .
6378 "<pubDate>$cd{'rfc2822'}</pubDate>\n" .
6379 "<guid isPermaLink=\"true\">$co_url</guid>\n" .
6380 "<link>$co_url</link>\n" .
6381 "<description>" . esc_html($co{'title'}) . "</description>\n" .
6382 "<content:encoded>" .
6384 } elsif ($format eq 'atom') {
6386 "<title type=\"html\">" . esc_html($co{'title'}) . "</title>\n" .
6387 "<updated>$cd{'iso-8601'}</updated>\n" .
6389 " <name>" . esc_html($co{'author_name'}) . "</name>\n";
6390 if ($co{'author_email'}) {
6391 print " <email>" . esc_html($co{'author_email'}) . "</email>\n";
6393 print "</author>\n" .
6394 # use committer for contributor
6396 " <name>" . esc_html($co{'committer_name'}) . "</name>\n";
6397 if ($co{'committer_email'}) {
6398 print " <email>" . esc_html($co{'committer_email'}) . "</email>\n";
6400 print "</contributor>\n" .
6401 "<published>$cd{'iso-8601'}</published>\n" .
6402 "<link rel=\"alternate\" type=\"text/html\" href=\"$co_url\" />\n" .
6403 "<id>$co_url</id>\n" .
6404 "<content type=\"xhtml\" xml:base=\"" . esc_url($my_url) . "\">\n" .
6405 "<div xmlns=\"http://www.w3.org/1999/xhtml\">\n";
6407 my $comment = $co{'comment'};
6409 foreach my $line (@$comment) {
6410 $line = esc_html($line);
6413 print "</pre><ul>\n";
6414 foreach my $difftree_line (@difftree) {
6415 my %difftree = parse_difftree_raw_line($difftree_line);
6416 next if !$difftree{'from_id'};
6418 my $file = $difftree{'file'} || $difftree{'to_file'};
6422 $cgi->a({-href => href(-full=>1, action=>"blobdiff",
6423 hash=>$difftree{'to_id'}, hash_parent=>$difftree{'from_id'},
6424 hash_base=>$co{'id'}, hash_parent_base=>$co{'parent'},
6425 file_name=>$file, file_parent=>$difftree{'from_file'}),
6426 -title => "diff"}, 'D');
6428 print $cgi->a({-href => href(-full=>1, action=>"blame",
6429 file_name=>$file, hash_base=>$commit),
6430 -title => "blame"}, 'B');
6432 # if this is not a feed of a file history
6433 if (!defined $file_name || $file_name ne $file) {
6434 print $cgi->a({-href => href(-full=>1, action=>"history",
6435 file_name=>$file, hash=>$commit),
6436 -title => "history"}, 'H');
6438 $file = esc_path($file);
6442 if ($format eq 'rss') {
6443 print "</ul>]]>\n" .
6444 "</content:encoded>\n" .
6446 } elsif ($format eq 'atom') {
6447 print "</ul>\n</div>\n" .
6454 if ($format eq 'rss') {
6455 print "</channel>\n</rss>\n";
6456 } elsif ($format eq 'atom') {
6470 my @list = git_get_projects_list();
6473 -type => 'text/xml',
6474 -charset => 'utf-8',
6475 -content_disposition => 'inline; filename="opml.xml"');
6478 <?xml version="1.0" encoding="utf-8"?>
6479 <opml version="1.0">
6481 <title>$site_name OPML Export</title>
6484 <outline text="git RSS feeds">
6487 foreach my $pr (@list) {
6489 my $head = git_get_head_hash($proj{'path'});
6490 if (!defined $head) {
6493 $git_dir = "$projectroot/$proj{'path'}";
6494 my %co = parse_commit($head);
6499 my $path = esc_html(chop_str($proj{'path'}, 25, 5));
6500 my $rss = href('project' => $proj{'path'}, 'action' => 'rss', -full => 1);
6501 my $html = href('project' => $proj{'path'}, 'action' => 'summary', -full => 1);
6502 print "<outline type=\"rss\" text=\"$path\" title=\"$path\" xmlUrl=\"$rss\" htmlUrl=\"$html\"/>\n";