3 # gitweb - simple web interface to track changes in git repositories
5 # (C) 2005-2006, Kay Sievers <kay.sievers@vrfy.org>
6 # (C) 2005, Christian Gierke
8 # This program is licensed under the GPLv2
12 use CGI qw(:standard :escapeHTML -nosticky);
13 use CGI::Util qw(unescape);
14 use CGI::Carp qw(fatalsToBrowser);
18 use File::Basename qw(basename);
19 binmode STDOUT, ':utf8';
22 if (eval { require Time::HiRes; 1; }) {
23 $t0 = [Time::HiRes::gettimeofday()];
25 our $number_of_git_cmds = 0;
28 CGI->compile() if $ENV{'MOD_PERL'};
32 our $version = "++GIT_VERSION++";
33 our $my_url = $cgi->url();
34 our $my_uri = $cgi->url(-absolute => 1);
36 # Base URL for relative URLs in gitweb ($logo, $favicon, ...),
37 # needed and used only for URLs with nonempty PATH_INFO
38 our $base_url = $my_url;
40 # When the script is used as DirectoryIndex, the URL does not contain the name
41 # of the script file itself, and $cgi->url() fails to strip PATH_INFO, so we
42 # have to do it ourselves. We make $path_info global because it's also used
45 # Another issue with the script being the DirectoryIndex is that the resulting
46 # $my_url data is not the full script URL: this is good, because we want
47 # generated links to keep implying the script name if it wasn't explicitly
48 # indicated in the URL we're handling, but it means that $my_url cannot be used
50 # Therefore, if we needed to strip PATH_INFO, then we know that we have
51 # to build the base URL ourselves:
52 our $path_info = $ENV{"PATH_INFO"};
54 if ($my_url =~ s,\Q$path_info\E$,, &&
55 $my_uri =~ s,\Q$path_info\E$,, &&
56 defined $ENV{'SCRIPT_NAME'}) {
57 $base_url = $cgi->url(-base => 1) . $ENV{'SCRIPT_NAME'};
61 # core git executable to use
62 # this can just be "git" if your webserver has a sensible PATH
63 our $GIT = "++GIT_BINDIR++/git";
65 # absolute fs-path which will be prepended to the project path
66 #our $projectroot = "/pub/scm";
67 our $projectroot = "++GITWEB_PROJECTROOT++";
69 # fs traversing limit for getting project list
70 # the number is relative to the projectroot
71 our $project_maxdepth = "++GITWEB_PROJECT_MAXDEPTH++";
73 # target of the home link on top of all pages
74 our $home_link = $my_uri || "/";
76 # string of the home link on top of all pages
77 our $home_link_str = "++GITWEB_HOME_LINK_STR++";
79 # name of your site or organization to appear in page titles
80 # replace this with something more descriptive for clearer bookmarks
81 our $site_name = "++GITWEB_SITENAME++"
82 || ($ENV{'SERVER_NAME'} || "Untitled") . " Git";
84 # filename of html text to include at top of each page
85 our $site_header = "++GITWEB_SITE_HEADER++";
86 # html text to include at home page
87 our $home_text = "++GITWEB_HOMETEXT++";
88 # filename of html text to include at bottom of each page
89 our $site_footer = "++GITWEB_SITE_FOOTER++";
92 our @stylesheets = ("++GITWEB_CSS++");
93 # URI of a single stylesheet, which can be overridden in GITWEB_CONFIG.
94 our $stylesheet = undef;
95 # URI of GIT logo (72x27 size)
96 our $logo = "++GITWEB_LOGO++";
97 # URI of GIT favicon, assumed to be image/png type
98 our $favicon = "++GITWEB_FAVICON++";
99 # URI of gitweb.js (JavaScript code for gitweb)
100 our $javascript = "++GITWEB_JS++";
102 # URI and label (title) of GIT logo link
103 #our $logo_url = "http://www.kernel.org/pub/software/scm/git/docs/";
104 #our $logo_label = "git documentation";
105 our $logo_url = "http://git-scm.com/";
106 our $logo_label = "git homepage";
108 # source of projects list
109 our $projects_list = "++GITWEB_LIST++";
111 # the width (in characters) of the projects list "Description" column
112 our $projects_list_description_width = 25;
114 # default order of projects list
115 # valid values are none, project, descr, owner, and age
116 our $default_projects_order = "project";
118 # show repository only if this file exists
119 # (only effective if this variable evaluates to true)
120 our $export_ok = "++GITWEB_EXPORT_OK++";
122 # show repository only if this subroutine returns true
123 # when given the path to the project, for example:
124 # sub { return -e "$_[0]/git-daemon-export-ok"; }
125 our $export_auth_hook = undef;
127 # only allow viewing of repositories also shown on the overview page
128 our $strict_export = "++GITWEB_STRICT_EXPORT++";
130 # list of git base URLs used for URL to where fetch project from,
131 # i.e. full URL is "$git_base_url/$project"
132 our @git_base_url_list = grep { $_ ne '' } ("++GITWEB_BASE_URL++");
134 # default blob_plain mimetype and default charset for text/plain blob
135 our $default_blob_plain_mimetype = 'text/plain';
136 our $default_text_plain_charset = undef;
138 # file to use for guessing MIME types before trying /etc/mime.types
139 # (relative to the current git repository)
140 our $mimetypes_file = undef;
142 # assume this charset if line contains non-UTF-8 characters;
143 # it should be valid encoding (see Encoding::Supported(3pm) for list),
144 # for which encoding all byte sequences are valid, for example
145 # 'iso-8859-1' aka 'latin1' (it is decoded without checking, so it
146 # could be even 'utf-8' for the old behavior)
147 our $fallback_encoding = 'latin1';
149 # rename detection options for git-diff and git-diff-tree
150 # - default is '-M', with the cost proportional to
151 # (number of removed files) * (number of new files).
152 # - more costly is '-C' (which implies '-M'), with the cost proportional to
153 # (number of changed files + number of removed files) * (number of new files)
154 # - even more costly is '-C', '--find-copies-harder' with cost
155 # (number of files in the original tree) * (number of new files)
156 # - one might want to include '-B' option, e.g. '-B', '-M'
157 our @diff_opts = ('-M'); # taken from git_commit
159 # Disables features that would allow repository owners to inject script into
161 our $prevent_xss = 0;
163 # information about snapshot formats that gitweb is capable of serving
164 our %known_snapshot_formats = (
166 # 'display' => display name,
167 # 'type' => mime type,
168 # 'suffix' => filename suffix,
169 # 'format' => --format for git-archive,
170 # 'compressor' => [compressor command and arguments]
171 # (array reference, optional)}
174 'display' => 'tar.gz',
175 'type' => 'application/x-gzip',
176 'suffix' => '.tar.gz',
178 'compressor' => ['gzip']},
181 'display' => 'tar.bz2',
182 'type' => 'application/x-bzip2',
183 'suffix' => '.tar.bz2',
185 'compressor' => ['bzip2']},
189 'type' => 'application/x-zip',
194 # Aliases so we understand old gitweb.snapshot values in repository
196 our %known_snapshot_format_aliases = (
200 # backward compatibility: legacy gitweb config support
201 'x-gzip' => undef, 'gz' => undef,
202 'x-bzip2' => undef, 'bz2' => undef,
203 'x-zip' => undef, '' => undef,
206 # Pixel sizes for icons and avatars. If the default font sizes or lineheights
207 # are changed, it may be appropriate to change these values too via
214 # You define site-wide feature defaults here; override them with
215 # $GITWEB_CONFIG as necessary.
218 # 'sub' => feature-sub (subroutine),
219 # 'override' => allow-override (boolean),
220 # 'default' => [ default options...] (array reference)}
222 # if feature is overridable (it means that allow-override has true value),
223 # then feature-sub will be called with default options as parameters;
224 # return value of feature-sub indicates if to enable specified feature
226 # if there is no 'sub' key (no feature-sub), then feature cannot be
229 # use gitweb_get_feature(<feature>) to retrieve the <feature> value
230 # (an array) or gitweb_check_feature(<feature>) to check if <feature>
233 # Enable the 'blame' blob view, showing the last commit that modified
234 # each line in the file. This can be very CPU-intensive.
236 # To enable system wide have in $GITWEB_CONFIG
237 # $feature{'blame'}{'default'} = [1];
238 # To have project specific config enable override in $GITWEB_CONFIG
239 # $feature{'blame'}{'override'} = 1;
240 # and in project config gitweb.blame = 0|1;
242 'sub' => sub { feature_bool('blame', @_) },
246 # Enable the 'snapshot' link, providing a compressed archive of any
247 # tree. This can potentially generate high traffic if you have large
250 # Value is a list of formats defined in %known_snapshot_formats that
252 # To disable system wide have in $GITWEB_CONFIG
253 # $feature{'snapshot'}{'default'} = [];
254 # To have project specific config enable override in $GITWEB_CONFIG
255 # $feature{'snapshot'}{'override'} = 1;
256 # and in project config, a comma-separated list of formats or "none"
257 # to disable. Example: gitweb.snapshot = tbz2,zip;
259 'sub' => \&feature_snapshot,
261 'default' => ['tgz']},
263 # Enable text search, which will list the commits which match author,
264 # committer or commit text to a given string. Enabled by default.
265 # Project specific override is not supported.
270 # Enable grep search, which will list the files in currently selected
271 # tree containing the given string. Enabled by default. This can be
272 # potentially CPU-intensive, of course.
274 # To enable system wide have in $GITWEB_CONFIG
275 # $feature{'grep'}{'default'} = [1];
276 # To have project specific config enable override in $GITWEB_CONFIG
277 # $feature{'grep'}{'override'} = 1;
278 # and in project config gitweb.grep = 0|1;
280 'sub' => sub { feature_bool('grep', @_) },
284 # Enable the pickaxe search, which will list the commits that modified
285 # a given string in a file. This can be practical and quite faster
286 # alternative to 'blame', but still potentially CPU-intensive.
288 # To enable system wide have in $GITWEB_CONFIG
289 # $feature{'pickaxe'}{'default'} = [1];
290 # To have project specific config enable override in $GITWEB_CONFIG
291 # $feature{'pickaxe'}{'override'} = 1;
292 # and in project config gitweb.pickaxe = 0|1;
294 'sub' => sub { feature_bool('pickaxe', @_) },
298 # Make gitweb use an alternative format of the URLs which can be
299 # more readable and natural-looking: project name is embedded
300 # directly in the path and the query string contains other
301 # auxiliary information. All gitweb installations recognize
302 # URL in either format; this configures in which formats gitweb
305 # To enable system wide have in $GITWEB_CONFIG
306 # $feature{'pathinfo'}{'default'} = [1];
307 # Project specific override is not supported.
309 # Note that you will need to change the default location of CSS,
310 # favicon, logo and possibly other files to an absolute URL. Also,
311 # if gitweb.cgi serves as your indexfile, you will need to force
312 # $my_uri to contain the script name in your $GITWEB_CONFIG.
317 # Make gitweb consider projects in project root subdirectories
318 # to be forks of existing projects. Given project $projname.git,
319 # projects matching $projname/*.git will not be shown in the main
320 # projects list, instead a '+' mark will be added to $projname
321 # there and a 'forks' view will be enabled for the project, listing
322 # all the forks. If project list is taken from a file, forks have
323 # to be listed after the main project.
325 # To enable system wide have in $GITWEB_CONFIG
326 # $feature{'forks'}{'default'} = [1];
327 # Project specific override is not supported.
332 # Insert custom links to the action bar of all project pages.
333 # This enables you mainly to link to third-party scripts integrating
334 # into gitweb; e.g. git-browser for graphical history representation
335 # or custom web-based repository administration interface.
337 # The 'default' value consists of a list of triplets in the form
338 # (label, link, position) where position is the label after which
339 # to insert the link and link is a format string where %n expands
340 # to the project name, %f to the project path within the filesystem,
341 # %h to the current hash (h gitweb parameter) and %b to the current
342 # hash base (hb gitweb parameter); %% expands to %.
344 # To enable system wide have in $GITWEB_CONFIG e.g.
345 # $feature{'actions'}{'default'} = [('graphiclog',
346 # '/git-browser/by-commit.html?r=%n', 'summary')];
347 # Project specific override is not supported.
352 # Allow gitweb scan project content tags described in ctags/
353 # of project repository, and display the popular Web 2.0-ish
354 # "tag cloud" near the project list. Note that this is something
355 # COMPLETELY different from the normal Git tags.
357 # gitweb by itself can show existing tags, but it does not handle
358 # tagging itself; you need an external application for that.
359 # For an example script, check Girocco's cgi/tagproj.cgi.
360 # You may want to install the HTML::TagCloud Perl module to get
361 # a pretty tag cloud instead of just a list of tags.
363 # To enable system wide have in $GITWEB_CONFIG
364 # $feature{'ctags'}{'default'} = ['path_to_tag_script'];
365 # Project specific override is not supported.
370 # The maximum number of patches in a patchset generated in patch
371 # view. Set this to 0 or undef to disable patch view, or to a
372 # negative number to remove any limit.
374 # To disable system wide have in $GITWEB_CONFIG
375 # $feature{'patches'}{'default'} = [0];
376 # To have project specific config enable override in $GITWEB_CONFIG
377 # $feature{'patches'}{'override'} = 1;
378 # and in project config gitweb.patches = 0|n;
379 # where n is the maximum number of patches allowed in a patchset.
381 'sub' => \&feature_patches,
385 # Avatar support. When this feature is enabled, views such as
386 # shortlog or commit will display an avatar associated with
387 # the email of the committer(s) and/or author(s).
389 # Currently available providers are gravatar and picon.
390 # If an unknown provider is specified, the feature is disabled.
392 # Gravatar depends on Digest::MD5.
393 # Picon currently relies on the indiana.edu database.
395 # To enable system wide have in $GITWEB_CONFIG
396 # $feature{'avatar'}{'default'} = ['<provider>'];
397 # where <provider> is either gravatar or picon.
398 # To have project specific config enable override in $GITWEB_CONFIG
399 # $feature{'avatar'}{'override'} = 1;
400 # and in project config gitweb.avatar = <provider>;
402 'sub' => \&feature_avatar,
406 # Enable displaying how much time and how many git commands
407 # it took to generate and display page. Disabled by default.
408 # Project specific override is not supported.
414 sub gitweb_get_feature {
416 return unless exists $feature{$name};
417 my ($sub, $override, @defaults) = (
418 $feature{$name}{'sub'},
419 $feature{$name}{'override'},
420 @{$feature{$name}{'default'}});
421 if (!$override) { return @defaults; }
423 warn "feature $name is not overrideable";
426 return $sub->(@defaults);
429 # A wrapper to check if a given feature is enabled.
430 # With this, you can say
432 # my $bool_feat = gitweb_check_feature('bool_feat');
433 # gitweb_check_feature('bool_feat') or somecode;
437 # my ($bool_feat) = gitweb_get_feature('bool_feat');
438 # (gitweb_get_feature('bool_feat'))[0] or somecode;
440 sub gitweb_check_feature {
441 return (gitweb_get_feature(@_))[0];
447 my ($val) = git_get_project_config($key, '--bool');
451 } elsif ($val eq 'true') {
453 } elsif ($val eq 'false') {
458 sub feature_snapshot {
461 my ($val) = git_get_project_config('snapshot');
464 @fmts = ($val eq 'none' ? () : split /\s*[,\s]\s*/, $val);
470 sub feature_patches {
471 my @val = (git_get_project_config('patches', '--int'));
481 my @val = (git_get_project_config('avatar'));
483 return @val ? @val : @_;
486 # checking HEAD file with -e is fragile if the repository was
487 # initialized long time ago (i.e. symlink HEAD) and was pack-ref'ed
489 sub check_head_link {
491 my $headfile = "$dir/HEAD";
492 return ((-e $headfile) ||
493 (-l $headfile && readlink($headfile) =~ /^refs\/heads\//));
496 sub check_export_ok {
498 return (check_head_link($dir) &&
499 (!$export_ok || -e "$dir/$export_ok") &&
500 (!$export_auth_hook || $export_auth_hook->($dir)));
503 # process alternate names for backward compatibility
504 # filter out unsupported (unknown) snapshot formats
505 sub filter_snapshot_fmts {
509 exists $known_snapshot_format_aliases{$_} ?
510 $known_snapshot_format_aliases{$_} : $_} @fmts;
512 exists $known_snapshot_formats{$_} } @fmts;
515 our $GITWEB_CONFIG = $ENV{'GITWEB_CONFIG'} || "++GITWEB_CONFIG++";
516 if (-e $GITWEB_CONFIG) {
519 our $GITWEB_CONFIG_SYSTEM = $ENV{'GITWEB_CONFIG_SYSTEM'} || "++GITWEB_CONFIG_SYSTEM++";
520 do $GITWEB_CONFIG_SYSTEM if -e $GITWEB_CONFIG_SYSTEM;
523 # version of the core git binary
524 our $git_version = qx("$GIT" --version) =~ m/git version (.*)$/ ? $1 : "unknown";
525 $number_of_git_cmds++;
527 $projects_list ||= $projectroot;
529 # ======================================================================
530 # input validation and dispatch
532 # input parameters can be collected from a variety of sources (presently, CGI
533 # and PATH_INFO), so we define an %input_params hash that collects them all
534 # together during validation: this allows subsequent uses (e.g. href()) to be
535 # agnostic of the parameter origin
537 our %input_params = ();
539 # input parameters are stored with the long parameter name as key. This will
540 # also be used in the href subroutine to convert parameters to their CGI
541 # equivalent, and since the href() usage is the most frequent one, we store
542 # the name -> CGI key mapping here, instead of the reverse.
544 # XXX: Warning: If you touch this, check the search form for updating,
547 our @cgi_param_mapping = (
555 hash_parent_base => "hpb",
560 snapshot_format => "sf",
561 extra_options => "opt",
562 search_use_regexp => "sr",
563 # this must be last entry (for manipulation from JavaScript)
566 our %cgi_param_mapping = @cgi_param_mapping;
568 # we will also need to know the possible actions, for validation
570 "blame" => \&git_blame,
571 "blame_incremental" => \&git_blame_incremental,
572 "blame_data" => \&git_blame_data,
573 "blobdiff" => \&git_blobdiff,
574 "blobdiff_plain" => \&git_blobdiff_plain,
575 "blob" => \&git_blob,
576 "blob_plain" => \&git_blob_plain,
577 "commitdiff" => \&git_commitdiff,
578 "commitdiff_plain" => \&git_commitdiff_plain,
579 "commit" => \&git_commit,
580 "forks" => \&git_forks,
581 "heads" => \&git_heads,
582 "history" => \&git_history,
584 "patch" => \&git_patch,
585 "patches" => \&git_patches,
587 "atom" => \&git_atom,
588 "search" => \&git_search,
589 "search_help" => \&git_search_help,
590 "shortlog" => \&git_shortlog,
591 "summary" => \&git_summary,
593 "tags" => \&git_tags,
594 "tree" => \&git_tree,
595 "snapshot" => \&git_snapshot,
596 "object" => \&git_object,
597 # those below don't need $project
598 "opml" => \&git_opml,
599 "project_list" => \&git_project_list,
600 "project_index" => \&git_project_index,
603 # finally, we have the hash of allowed extra_options for the commands that
605 our %allowed_options = (
606 "--no-merges" => [ qw(rss atom log shortlog history) ],
609 # fill %input_params with the CGI parameters. All values except for 'opt'
610 # should be single values, but opt can be an array. We should probably
611 # build an array of parameters that can be multi-valued, but since for the time
612 # being it's only this one, we just single it out
613 while (my ($name, $symbol) = each %cgi_param_mapping) {
614 if ($symbol eq 'opt') {
615 $input_params{$name} = [ $cgi->param($symbol) ];
617 $input_params{$name} = $cgi->param($symbol);
621 # now read PATH_INFO and update the parameter list for missing parameters
622 sub evaluate_path_info {
623 return if defined $input_params{'project'};
624 return if !$path_info;
625 $path_info =~ s,^/+,,;
626 return if !$path_info;
628 # find which part of PATH_INFO is project
629 my $project = $path_info;
631 while ($project && !check_head_link("$projectroot/$project")) {
632 $project =~ s,/*[^/]*$,,;
634 return unless $project;
635 $input_params{'project'} = $project;
637 # do not change any parameters if an action is given using the query string
638 return if $input_params{'action'};
639 $path_info =~ s,^\Q$project\E/*,,;
641 # next, check if we have an action
642 my $action = $path_info;
644 if (exists $actions{$action}) {
645 $path_info =~ s,^$action/*,,;
646 $input_params{'action'} = $action;
649 # list of actions that want hash_base instead of hash, but can have no
650 # pathname (f) parameter
657 # [$hash_parent_base[:$file_parent]..]$hash_parent[:$file_name]
658 my ($parentrefname, $parentpathname, $refname, $pathname) =
659 ($path_info =~ /^(?:(.+?)(?::(.+))?\.\.)?(.+?)(?::(.+))?$/);
661 # first, analyze the 'current' part
662 if (defined $pathname) {
663 # we got "branch:filename" or "branch:dir/"
664 # we could use git_get_type(branch:pathname), but:
665 # - it needs $git_dir
666 # - it does a git() call
667 # - the convention of terminating directories with a slash
668 # makes it superfluous
669 # - embedding the action in the PATH_INFO would make it even
671 $pathname =~ s,^/+,,;
672 if (!$pathname || substr($pathname, -1) eq "/") {
673 $input_params{'action'} ||= "tree";
676 # the default action depends on whether we had parent info
678 if ($parentrefname) {
679 $input_params{'action'} ||= "blobdiff_plain";
681 $input_params{'action'} ||= "blob_plain";
684 $input_params{'hash_base'} ||= $refname;
685 $input_params{'file_name'} ||= $pathname;
686 } elsif (defined $refname) {
687 # we got "branch". In this case we have to choose if we have to
688 # set hash or hash_base.
690 # Most of the actions without a pathname only want hash to be
691 # set, except for the ones specified in @wants_base that want
692 # hash_base instead. It should also be noted that hand-crafted
693 # links having 'history' as an action and no pathname or hash
694 # set will fail, but that happens regardless of PATH_INFO.
695 $input_params{'action'} ||= "shortlog";
696 if (grep { $_ eq $input_params{'action'} } @wants_base) {
697 $input_params{'hash_base'} ||= $refname;
699 $input_params{'hash'} ||= $refname;
703 # next, handle the 'parent' part, if present
704 if (defined $parentrefname) {
705 # a missing pathspec defaults to the 'current' filename, allowing e.g.
706 # someproject/blobdiff/oldrev..newrev:/filename
707 if ($parentpathname) {
708 $parentpathname =~ s,^/+,,;
709 $parentpathname =~ s,/$,,;
710 $input_params{'file_parent'} ||= $parentpathname;
712 $input_params{'file_parent'} ||= $input_params{'file_name'};
714 # we assume that hash_parent_base is wanted if a path was specified,
715 # or if the action wants hash_base instead of hash
716 if (defined $input_params{'file_parent'} ||
717 grep { $_ eq $input_params{'action'} } @wants_base) {
718 $input_params{'hash_parent_base'} ||= $parentrefname;
720 $input_params{'hash_parent'} ||= $parentrefname;
724 # for the snapshot action, we allow URLs in the form
725 # $project/snapshot/$hash.ext
726 # where .ext determines the snapshot and gets removed from the
727 # passed $refname to provide the $hash.
729 # To be able to tell that $refname includes the format extension, we
730 # require the following two conditions to be satisfied:
731 # - the hash input parameter MUST have been set from the $refname part
732 # of the URL (i.e. they must be equal)
733 # - the snapshot format MUST NOT have been defined already (e.g. from
735 # It's also useless to try any matching unless $refname has a dot,
736 # so we check for that too
737 if (defined $input_params{'action'} &&
738 $input_params{'action'} eq 'snapshot' &&
739 defined $refname && index($refname, '.') != -1 &&
740 $refname eq $input_params{'hash'} &&
741 !defined $input_params{'snapshot_format'}) {
742 # We loop over the known snapshot formats, checking for
743 # extensions. Allowed extensions are both the defined suffix
744 # (which includes the initial dot already) and the snapshot
745 # format key itself, with a prepended dot
746 while (my ($fmt, $opt) = each %known_snapshot_formats) {
748 unless ($hash =~ s/(\Q$opt->{'suffix'}\E|\Q.$fmt\E)$//) {
752 # a valid suffix was found, so set the snapshot format
753 # and reset the hash parameter
754 $input_params{'snapshot_format'} = $fmt;
755 $input_params{'hash'} = $hash;
756 # we also set the format suffix to the one requested
757 # in the URL: this way a request for e.g. .tgz returns
758 # a .tgz instead of a .tar.gz
759 $known_snapshot_formats{$fmt}{'suffix'} = $sfx;
764 evaluate_path_info();
766 our $action = $input_params{'action'};
767 if (defined $action) {
768 if (!validate_action($action)) {
769 die_error(400, "Invalid action parameter");
773 # parameters which are pathnames
774 our $project = $input_params{'project'};
775 if (defined $project) {
776 if (!validate_project($project)) {
778 die_error(404, "No such project");
782 our $file_name = $input_params{'file_name'};
783 if (defined $file_name) {
784 if (!validate_pathname($file_name)) {
785 die_error(400, "Invalid file parameter");
789 our $file_parent = $input_params{'file_parent'};
790 if (defined $file_parent) {
791 if (!validate_pathname($file_parent)) {
792 die_error(400, "Invalid file parent parameter");
796 # parameters which are refnames
797 our $hash = $input_params{'hash'};
799 if (!validate_refname($hash)) {
800 die_error(400, "Invalid hash parameter");
804 our $hash_parent = $input_params{'hash_parent'};
805 if (defined $hash_parent) {
806 if (!validate_refname($hash_parent)) {
807 die_error(400, "Invalid hash parent parameter");
811 our $hash_base = $input_params{'hash_base'};
812 if (defined $hash_base) {
813 if (!validate_refname($hash_base)) {
814 die_error(400, "Invalid hash base parameter");
818 our @extra_options = @{$input_params{'extra_options'}};
819 # @extra_options is always defined, since it can only be (currently) set from
820 # CGI, and $cgi->param() returns the empty array in array context if the param
822 foreach my $opt (@extra_options) {
823 if (not exists $allowed_options{$opt}) {
824 die_error(400, "Invalid option parameter");
826 if (not grep(/^$action$/, @{$allowed_options{$opt}})) {
827 die_error(400, "Invalid option parameter for this action");
831 our $hash_parent_base = $input_params{'hash_parent_base'};
832 if (defined $hash_parent_base) {
833 if (!validate_refname($hash_parent_base)) {
834 die_error(400, "Invalid hash parent base parameter");
839 our $page = $input_params{'page'};
841 if ($page =~ m/[^0-9]/) {
842 die_error(400, "Invalid page parameter");
846 our $searchtype = $input_params{'searchtype'};
847 if (defined $searchtype) {
848 if ($searchtype =~ m/[^a-z]/) {
849 die_error(400, "Invalid searchtype parameter");
853 our $search_use_regexp = $input_params{'search_use_regexp'};
855 our $searchtext = $input_params{'searchtext'};
857 if (defined $searchtext) {
858 if (length($searchtext) < 2) {
859 die_error(403, "At least two characters are required for search parameter");
861 $search_regexp = $search_use_regexp ? $searchtext : quotemeta $searchtext;
864 # path to the current git repository
866 $git_dir = "$projectroot/$project" if $project;
868 # list of supported snapshot formats
869 our @snapshot_fmts = gitweb_get_feature('snapshot');
870 @snapshot_fmts = filter_snapshot_fmts(@snapshot_fmts);
872 # check that the avatar feature is set to a known provider name,
873 # and for each provider check if the dependencies are satisfied.
874 # if the provider name is invalid or the dependencies are not met,
875 # reset $git_avatar to the empty string.
876 our ($git_avatar) = gitweb_get_feature('avatar');
877 if ($git_avatar eq 'gravatar') {
878 $git_avatar = '' unless (eval { require Digest::MD5; 1; });
879 } elsif ($git_avatar eq 'picon') {
886 if (!defined $action) {
888 $action = git_get_type($hash);
889 } elsif (defined $hash_base && defined $file_name) {
890 $action = git_get_type("$hash_base:$file_name");
891 } elsif (defined $project) {
894 $action = 'project_list';
897 if (!defined($actions{$action})) {
898 die_error(400, "Unknown action");
900 if ($action !~ m/^(?:opml|project_list|project_index)$/ &&
902 die_error(400, "Project needed");
904 $actions{$action}->();
907 ## ======================================================================
912 # default is to use -absolute url() i.e. $my_uri
913 my $href = $params{-full} ? $my_url : $my_uri;
915 $params{'project'} = $project unless exists $params{'project'};
917 if ($params{-replay}) {
918 while (my ($name, $symbol) = each %cgi_param_mapping) {
919 if (!exists $params{$name}) {
920 $params{$name} = $input_params{$name};
925 my $use_pathinfo = gitweb_check_feature('pathinfo');
926 if ($use_pathinfo and defined $params{'project'}) {
927 # try to put as many parameters as possible in PATH_INFO:
930 # - hash_parent or hash_parent_base:/file_parent
931 # - hash or hash_base:/filename
932 # - the snapshot_format as an appropriate suffix
934 # When the script is the root DirectoryIndex for the domain,
935 # $href here would be something like http://gitweb.example.com/
936 # Thus, we strip any trailing / from $href, to spare us double
937 # slashes in the final URL
940 # Then add the project name, if present
941 $href .= "/".esc_url($params{'project'});
942 delete $params{'project'};
944 # since we destructively absorb parameters, we keep this
945 # boolean that remembers if we're handling a snapshot
946 my $is_snapshot = $params{'action'} eq 'snapshot';
948 # Summary just uses the project path URL, any other action is
950 if (defined $params{'action'}) {
951 $href .= "/".esc_url($params{'action'}) unless $params{'action'} eq 'summary';
952 delete $params{'action'};
955 # Next, we put hash_parent_base:/file_parent..hash_base:/file_name,
956 # stripping nonexistent or useless pieces
957 $href .= "/" if ($params{'hash_base'} || $params{'hash_parent_base'}
958 || $params{'hash_parent'} || $params{'hash'});
959 if (defined $params{'hash_base'}) {
960 if (defined $params{'hash_parent_base'}) {
961 $href .= esc_url($params{'hash_parent_base'});
962 # skip the file_parent if it's the same as the file_name
963 delete $params{'file_parent'} if $params{'file_parent'} eq $params{'file_name'};
964 if (defined $params{'file_parent'} && $params{'file_parent'} !~ /\.\./) {
965 $href .= ":/".esc_url($params{'file_parent'});
966 delete $params{'file_parent'};
969 delete $params{'hash_parent'};
970 delete $params{'hash_parent_base'};
971 } elsif (defined $params{'hash_parent'}) {
972 $href .= esc_url($params{'hash_parent'}). "..";
973 delete $params{'hash_parent'};
976 $href .= esc_url($params{'hash_base'});
977 if (defined $params{'file_name'} && $params{'file_name'} !~ /\.\./) {
978 $href .= ":/".esc_url($params{'file_name'});
979 delete $params{'file_name'};
981 delete $params{'hash'};
982 delete $params{'hash_base'};
983 } elsif (defined $params{'hash'}) {
984 $href .= esc_url($params{'hash'});
985 delete $params{'hash'};
988 # If the action was a snapshot, we can absorb the
989 # snapshot_format parameter too
991 my $fmt = $params{'snapshot_format'};
992 # snapshot_format should always be defined when href()
993 # is called, but just in case some code forgets, we
994 # fall back to the default
995 $fmt ||= $snapshot_fmts[0];
996 $href .= $known_snapshot_formats{$fmt}{'suffix'};
997 delete $params{'snapshot_format'};
1001 # now encode the parameters explicitly
1003 for (my $i = 0; $i < @cgi_param_mapping; $i += 2) {
1004 my ($name, $symbol) = ($cgi_param_mapping[$i], $cgi_param_mapping[$i+1]);
1005 if (defined $params{$name}) {
1006 if (ref($params{$name}) eq "ARRAY") {
1007 foreach my $par (@{$params{$name}}) {
1008 push @result, $symbol . "=" . esc_param($par);
1011 push @result, $symbol . "=" . esc_param($params{$name});
1015 $href .= "?" . join(';', @result) if scalar @result;
1021 ## ======================================================================
1022 ## validation, quoting/unquoting and escaping
1024 sub validate_action {
1025 my $input = shift || return undef;
1026 return undef unless exists $actions{$input};
1030 sub validate_project {
1031 my $input = shift || return undef;
1032 if (!validate_pathname($input) ||
1033 !(-d "$projectroot/$input") ||
1034 !check_export_ok("$projectroot/$input") ||
1035 ($strict_export && !project_in_list($input))) {
1042 sub validate_pathname {
1043 my $input = shift || return undef;
1045 # no '.' or '..' as elements of path, i.e. no '.' nor '..'
1046 # at the beginning, at the end, and between slashes.
1047 # also this catches doubled slashes
1048 if ($input =~ m!(^|/)(|\.|\.\.)(/|$)!) {
1051 # no null characters
1052 if ($input =~ m!\0!) {
1058 sub validate_refname {
1059 my $input = shift || return undef;
1061 # textual hashes are O.K.
1062 if ($input =~ m/^[0-9a-fA-F]{40}$/) {
1065 # it must be correct pathname
1066 $input = validate_pathname($input)
1068 # restrictions on ref name according to git-check-ref-format
1069 if ($input =~ m!(/\.|\.\.|[\000-\040\177 ~^:?*\[]|/$)!) {
1075 # decode sequences of octets in utf8 into Perl's internal form,
1076 # which is utf-8 with utf8 flag set if needed. gitweb writes out
1077 # in utf-8 thanks to "binmode STDOUT, ':utf8'" at beginning
1080 if (utf8::valid($str)) {
1084 return decode($fallback_encoding, $str, Encode::FB_DEFAULT);
1088 # quote unsafe chars, but keep the slash, even when it's not
1089 # correct, but quoted slashes look too horrible in bookmarks
1092 $str =~ s/([^A-Za-z0-9\-_.~()\/:@])/sprintf("%%%02X", ord($1))/eg;
1098 # quote unsafe chars in whole URL, so some charactrs cannot be quoted
1101 $str =~ s/([^A-Za-z0-9\-_.~();\/;?:@&=])/sprintf("%%%02X", ord($1))/eg;
1107 # replace invalid utf8 character with SUBSTITUTION sequence
1112 $str = to_utf8($str);
1113 $str = $cgi->escapeHTML($str);
1114 if ($opts{'-nbsp'}) {
1115 $str =~ s/ / /g;
1117 $str =~ s|([[:cntrl:]])|(($1 ne "\t") ? quot_cec($1) : $1)|eg;
1121 # quote control characters and escape filename to HTML
1126 $str = to_utf8($str);
1127 $str = $cgi->escapeHTML($str);
1128 if ($opts{'-nbsp'}) {
1129 $str =~ s/ / /g;
1131 $str =~ s|([[:cntrl:]])|quot_cec($1)|eg;
1135 # Make control characters "printable", using character escape codes (CEC)
1139 my %es = ( # character escape codes, aka escape sequences
1140 "\t" => '\t', # tab (HT)
1141 "\n" => '\n', # line feed (LF)
1142 "\r" => '\r', # carrige return (CR)
1143 "\f" => '\f', # form feed (FF)
1144 "\b" => '\b', # backspace (BS)
1145 "\a" => '\a', # alarm (bell) (BEL)
1146 "\e" => '\e', # escape (ESC)
1147 "\013" => '\v', # vertical tab (VT)
1148 "\000" => '\0', # nul character (NUL)
1150 my $chr = ( (exists $es{$cntrl})
1152 : sprintf('\%2x', ord($cntrl)) );
1153 if ($opts{-nohtml}) {
1156 return "<span class=\"cntrl\">$chr</span>";
1160 # Alternatively use unicode control pictures codepoints,
1161 # Unicode "printable representation" (PR)
1166 my $chr = sprintf('&#%04d;', 0x2400+ord($cntrl));
1167 if ($opts{-nohtml}) {
1170 return "<span class=\"cntrl\">$chr</span>";
1174 # git may return quoted and escaped filenames
1180 my %es = ( # character escape codes, aka escape sequences
1181 't' => "\t", # tab (HT, TAB)
1182 'n' => "\n", # newline (NL)
1183 'r' => "\r", # return (CR)
1184 'f' => "\f", # form feed (FF)
1185 'b' => "\b", # backspace (BS)
1186 'a' => "\a", # alarm (bell) (BEL)
1187 'e' => "\e", # escape (ESC)
1188 'v' => "\013", # vertical tab (VT)
1191 if ($seq =~ m/^[0-7]{1,3}$/) {
1192 # octal char sequence
1193 return chr(oct($seq));
1194 } elsif (exists $es{$seq}) {
1195 # C escape sequence, aka character escape code
1198 # quoted ordinary character
1202 if ($str =~ m/^"(.*)"$/) {
1205 $str =~ s/\\([^0-7]|[0-7]{1,3})/unq($1)/eg;
1210 # escape tabs (convert tabs to spaces)
1214 while ((my $pos = index($line, "\t")) != -1) {
1215 if (my $count = (8 - ($pos % 8))) {
1216 my $spaces = ' ' x $count;
1217 $line =~ s/\t/$spaces/;
1224 sub project_in_list {
1225 my $project = shift;
1226 my @list = git_get_projects_list();
1227 return @list && scalar(grep { $_->{'path'} eq $project } @list);
1230 ## ----------------------------------------------------------------------
1231 ## HTML aware string manipulation
1233 # Try to chop given string on a word boundary between position
1234 # $len and $len+$add_len. If there is no word boundary there,
1235 # chop at $len+$add_len. Do not chop if chopped part plus ellipsis
1236 # (marking chopped part) would be longer than given string.
1240 my $add_len = shift || 10;
1241 my $where = shift || 'right'; # 'left' | 'center' | 'right'
1243 # Make sure perl knows it is utf8 encoded so we don't
1244 # cut in the middle of a utf8 multibyte char.
1245 $str = to_utf8($str);
1247 # allow only $len chars, but don't cut a word if it would fit in $add_len
1248 # if it doesn't fit, cut it if it's still longer than the dots we would add
1249 # remove chopped character entities entirely
1251 # when chopping in the middle, distribute $len into left and right part
1252 # return early if chopping wouldn't make string shorter
1253 if ($where eq 'center') {
1254 return $str if ($len + 5 >= length($str)); # filler is length 5
1257 return $str if ($len + 4 >= length($str)); # filler is length 4
1260 # regexps: ending and beginning with word part up to $add_len
1261 my $endre = qr/.{$len}\w{0,$add_len}/;
1262 my $begre = qr/\w{0,$add_len}.{$len}/;
1264 if ($where eq 'left') {
1265 $str =~ m/^(.*?)($begre)$/;
1266 my ($lead, $body) = ($1, $2);
1267 if (length($lead) > 4) {
1268 $body =~ s/^[^;]*;// if ($lead =~ m/&[^;]*$/);
1271 return "$lead$body";
1273 } elsif ($where eq 'center') {
1274 $str =~ m/^($endre)(.*)$/;
1275 my ($left, $str) = ($1, $2);
1276 $str =~ m/^(.*?)($begre)$/;
1277 my ($mid, $right) = ($1, $2);
1278 if (length($mid) > 5) {
1279 $left =~ s/&[^;]*$//;
1280 $right =~ s/^[^;]*;// if ($mid =~ m/&[^;]*$/);
1283 return "$left$mid$right";
1286 $str =~ m/^($endre)(.*)$/;
1289 if (length($tail) > 4) {
1290 $body =~ s/&[^;]*$//;
1293 return "$body$tail";
1297 # takes the same arguments as chop_str, but also wraps a <span> around the
1298 # result with a title attribute if it does get chopped. Additionally, the
1299 # string is HTML-escaped.
1300 sub chop_and_escape_str {
1303 my $chopped = chop_str(@_);
1304 if ($chopped eq $str) {
1305 return esc_html($chopped);
1307 $str =~ s/[[:cntrl:]]/?/g;
1308 return $cgi->span({-title=>$str}, esc_html($chopped));
1312 ## ----------------------------------------------------------------------
1313 ## functions returning short strings
1315 # CSS class for given age value (in seconds)
1319 if (!defined $age) {
1321 } elsif ($age < 60*60*2) {
1323 } elsif ($age < 60*60*24*2) {
1330 # convert age in seconds to "nn units ago" string
1335 if ($age > 60*60*24*365*2) {
1336 $age_str = (int $age/60/60/24/365);
1337 $age_str .= " years ago";
1338 } elsif ($age > 60*60*24*(365/12)*2) {
1339 $age_str = int $age/60/60/24/(365/12);
1340 $age_str .= " months ago";
1341 } elsif ($age > 60*60*24*7*2) {
1342 $age_str = int $age/60/60/24/7;
1343 $age_str .= " weeks ago";
1344 } elsif ($age > 60*60*24*2) {
1345 $age_str = int $age/60/60/24;
1346 $age_str .= " days ago";
1347 } elsif ($age > 60*60*2) {
1348 $age_str = int $age/60/60;
1349 $age_str .= " hours ago";
1350 } elsif ($age > 60*2) {
1351 $age_str = int $age/60;
1352 $age_str .= " min ago";
1353 } elsif ($age > 2) {
1354 $age_str = int $age;
1355 $age_str .= " sec ago";
1357 $age_str .= " right now";
1363 S_IFINVALID => 0030000,
1364 S_IFGITLINK => 0160000,
1367 # submodule/subproject, a commit object reference
1371 return (($mode & S_IFMT) == S_IFGITLINK)
1374 # convert file mode in octal to symbolic file mode string
1376 my $mode = oct shift;
1378 if (S_ISGITLINK($mode)) {
1379 return 'm---------';
1380 } elsif (S_ISDIR($mode & S_IFMT)) {
1381 return 'drwxr-xr-x';
1382 } elsif (S_ISLNK($mode)) {
1383 return 'lrwxrwxrwx';
1384 } elsif (S_ISREG($mode)) {
1385 # git cares only about the executable bit
1386 if ($mode & S_IXUSR) {
1387 return '-rwxr-xr-x';
1389 return '-rw-r--r--';
1392 return '----------';
1396 # convert file mode in octal to file type string
1400 if ($mode !~ m/^[0-7]+$/) {
1406 if (S_ISGITLINK($mode)) {
1408 } elsif (S_ISDIR($mode & S_IFMT)) {
1410 } elsif (S_ISLNK($mode)) {
1412 } elsif (S_ISREG($mode)) {
1419 # convert file mode in octal to file type description string
1420 sub file_type_long {
1423 if ($mode !~ m/^[0-7]+$/) {
1429 if (S_ISGITLINK($mode)) {
1431 } elsif (S_ISDIR($mode & S_IFMT)) {
1433 } elsif (S_ISLNK($mode)) {
1435 } elsif (S_ISREG($mode)) {
1436 if ($mode & S_IXUSR) {
1437 return "executable";
1447 ## ----------------------------------------------------------------------
1448 ## functions returning short HTML fragments, or transforming HTML fragments
1449 ## which don't belong to other sections
1451 # format line of commit message.
1452 sub format_log_line_html {
1455 $line = esc_html($line, -nbsp=>1);
1456 $line =~ s{\b([0-9a-fA-F]{8,40})\b}{
1457 $cgi->a({-href => href(action=>"object", hash=>$1),
1458 -class => "text"}, $1);
1464 # format marker of refs pointing to given object
1466 # the destination action is chosen based on object type and current context:
1467 # - for annotated tags, we choose the tag view unless it's the current view
1468 # already, in which case we go to shortlog view
1469 # - for other refs, we keep the current view if we're in history, shortlog or
1470 # log view, and select shortlog otherwise
1471 sub format_ref_marker {
1472 my ($refs, $id) = @_;
1475 if (defined $refs->{$id}) {
1476 foreach my $ref (@{$refs->{$id}}) {
1477 # this code exploits the fact that non-lightweight tags are the
1478 # only indirect objects, and that they are the only objects for which
1479 # we want to use tag instead of shortlog as action
1480 my ($type, $name) = qw();
1481 my $indirect = ($ref =~ s/\^\{\}$//);
1482 # e.g. tags/v2.6.11 or heads/next
1483 if ($ref =~ m!^(.*?)s?/(.*)$!) {
1492 $class .= " indirect" if $indirect;
1494 my $dest_action = "shortlog";
1497 $dest_action = "tag" unless $action eq "tag";
1498 } elsif ($action =~ /^(history|(short)?log)$/) {
1499 $dest_action = $action;
1503 $dest .= "refs/" unless $ref =~ m!^refs/!;
1506 my $link = $cgi->a({
1508 action=>$dest_action,
1512 $markers .= " <span class=\"$class\" title=\"$ref\">" .
1518 return ' <span class="refs">'. $markers . '</span>';
1524 # format, perhaps shortened and with markers, title line
1525 sub format_subject_html {
1526 my ($long, $short, $href, $extra) = @_;
1527 $extra = '' unless defined($extra);
1529 if (length($short) < length($long)) {
1530 $long =~ s/[[:cntrl:]]/?/g;
1531 return $cgi->a({-href => $href, -class => "list subject",
1532 -title => to_utf8($long)},
1533 esc_html($short) . $extra);
1535 return $cgi->a({-href => $href, -class => "list subject"},
1536 esc_html($long) . $extra);
1540 # Rather than recomputing the url for an email multiple times, we cache it
1541 # after the first hit. This gives a visible benefit in views where the avatar
1542 # for the same email is used repeatedly (e.g. shortlog).
1543 # The cache is shared by all avatar engines (currently gravatar only), which
1544 # are free to use it as preferred. Since only one avatar engine is used for any
1545 # given page, there's no risk for cache conflicts.
1546 our %avatar_cache = ();
1548 # Compute the picon url for a given email, by using the picon search service over at
1549 # http://www.cs.indiana.edu/picons/search.html
1551 my $email = lc shift;
1552 if (!$avatar_cache{$email}) {
1553 my ($user, $domain) = split('@', $email);
1554 $avatar_cache{$email} =
1555 "http://www.cs.indiana.edu/cgi-pub/kinzler/piconsearch.cgi/" .
1557 "users+domains+unknown/up/single";
1559 return $avatar_cache{$email};
1562 # Compute the gravatar url for a given email, if it's not in the cache already.
1563 # Gravatar stores only the part of the URL before the size, since that's the
1564 # one computationally more expensive. This also allows reuse of the cache for
1565 # different sizes (for this particular engine).
1567 my $email = lc shift;
1569 $avatar_cache{$email} ||=
1570 "http://www.gravatar.com/avatar/" .
1571 Digest::MD5::md5_hex($email) . "?s=";
1572 return $avatar_cache{$email} . $size;
1575 # Insert an avatar for the given $email at the given $size if the feature
1577 sub git_get_avatar {
1578 my ($email, %opts) = @_;
1579 my $pre_white = ($opts{-pad_before} ? " " : "");
1580 my $post_white = ($opts{-pad_after} ? " " : "");
1581 $opts{-size} ||= 'default';
1582 my $size = $avatar_size{$opts{-size}} || $avatar_size{'default'};
1584 if ($git_avatar eq 'gravatar') {
1585 $url = gravatar_url($email, $size);
1586 } elsif ($git_avatar eq 'picon') {
1587 $url = picon_url($email);
1589 # Other providers can be added by extending the if chain, defining $url
1590 # as needed. If no variant puts something in $url, we assume avatars
1591 # are completely disabled/unavailable.
1594 "<img width=\"$size\" " .
1595 "class=\"avatar\" " .
1604 # format the author name of the given commit with the given tag
1605 # the author name is chopped and escaped according to the other
1606 # optional parameters (see chop_str).
1607 sub format_author_html {
1610 my $author = chop_and_escape_str($co->{'author_name'}, @_);
1611 return "<$tag class=\"author\">" .
1612 git_get_avatar($co->{'author_email'}, -pad_after => 1) .
1613 $author . "</$tag>";
1616 # format git diff header line, i.e. "diff --(git|combined|cc) ..."
1617 sub format_git_diff_header_line {
1619 my $diffinfo = shift;
1620 my ($from, $to) = @_;
1622 if ($diffinfo->{'nparents'}) {
1624 $line =~ s!^(diff (.*?) )"?.*$!$1!;
1625 if ($to->{'href'}) {
1626 $line .= $cgi->a({-href => $to->{'href'}, -class => "path"},
1627 esc_path($to->{'file'}));
1628 } else { # file was deleted (no href)
1629 $line .= esc_path($to->{'file'});
1633 $line =~ s!^(diff (.*?) )"?a/.*$!$1!;
1634 if ($from->{'href'}) {
1635 $line .= $cgi->a({-href => $from->{'href'}, -class => "path"},
1636 'a/' . esc_path($from->{'file'}));
1637 } else { # file was added (no href)
1638 $line .= 'a/' . esc_path($from->{'file'});
1641 if ($to->{'href'}) {
1642 $line .= $cgi->a({-href => $to->{'href'}, -class => "path"},
1643 'b/' . esc_path($to->{'file'}));
1644 } else { # file was deleted
1645 $line .= 'b/' . esc_path($to->{'file'});
1649 return "<div class=\"diff header\">$line</div>\n";
1652 # format extended diff header line, before patch itself
1653 sub format_extended_diff_header_line {
1655 my $diffinfo = shift;
1656 my ($from, $to) = @_;
1659 if ($line =~ s!^((copy|rename) from ).*$!$1! && $from->{'href'}) {
1660 $line .= $cgi->a({-href=>$from->{'href'}, -class=>"path"},
1661 esc_path($from->{'file'}));
1663 if ($line =~ s!^((copy|rename) to ).*$!$1! && $to->{'href'}) {
1664 $line .= $cgi->a({-href=>$to->{'href'}, -class=>"path"},
1665 esc_path($to->{'file'}));
1667 # match single <mode>
1668 if ($line =~ m/\s(\d{6})$/) {
1669 $line .= '<span class="info"> (' .
1670 file_type_long($1) .
1674 if ($line =~ m/^index [0-9a-fA-F]{40},[0-9a-fA-F]{40}/) {
1675 # can match only for combined diff
1677 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
1678 if ($from->{'href'}[$i]) {
1679 $line .= $cgi->a({-href=>$from->{'href'}[$i],
1681 substr($diffinfo->{'from_id'}[$i],0,7));
1686 $line .= ',' if ($i < $diffinfo->{'nparents'} - 1);
1689 if ($to->{'href'}) {
1690 $line .= $cgi->a({-href=>$to->{'href'}, -class=>"hash"},
1691 substr($diffinfo->{'to_id'},0,7));
1696 } elsif ($line =~ m/^index [0-9a-fA-F]{40}..[0-9a-fA-F]{40}/) {
1697 # can match only for ordinary diff
1698 my ($from_link, $to_link);
1699 if ($from->{'href'}) {
1700 $from_link = $cgi->a({-href=>$from->{'href'}, -class=>"hash"},
1701 substr($diffinfo->{'from_id'},0,7));
1703 $from_link = '0' x 7;
1705 if ($to->{'href'}) {
1706 $to_link = $cgi->a({-href=>$to->{'href'}, -class=>"hash"},
1707 substr($diffinfo->{'to_id'},0,7));
1711 my ($from_id, $to_id) = ($diffinfo->{'from_id'}, $diffinfo->{'to_id'});
1712 $line =~ s!$from_id\.\.$to_id!$from_link..$to_link!;
1715 return $line . "<br/>\n";
1718 # format from-file/to-file diff header
1719 sub format_diff_from_to_header {
1720 my ($from_line, $to_line, $diffinfo, $from, $to, @parents) = @_;
1725 #assert($line =~ m/^---/) if DEBUG;
1726 # no extra formatting for "^--- /dev/null"
1727 if (! $diffinfo->{'nparents'}) {
1728 # ordinary (single parent) diff
1729 if ($line =~ m!^--- "?a/!) {
1730 if ($from->{'href'}) {
1732 $cgi->a({-href=>$from->{'href'}, -class=>"path"},
1733 esc_path($from->{'file'}));
1736 esc_path($from->{'file'});
1739 $result .= qq!<div class="diff from_file">$line</div>\n!;
1742 # combined diff (merge commit)
1743 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
1744 if ($from->{'href'}[$i]) {
1746 $cgi->a({-href=>href(action=>"blobdiff",
1747 hash_parent=>$diffinfo->{'from_id'}[$i],
1748 hash_parent_base=>$parents[$i],
1749 file_parent=>$from->{'file'}[$i],
1750 hash=>$diffinfo->{'to_id'},
1752 file_name=>$to->{'file'}),
1754 -title=>"diff" . ($i+1)},
1757 $cgi->a({-href=>$from->{'href'}[$i], -class=>"path"},
1758 esc_path($from->{'file'}[$i]));
1760 $line = '--- /dev/null';
1762 $result .= qq!<div class="diff from_file">$line</div>\n!;
1767 #assert($line =~ m/^\+\+\+/) if DEBUG;
1768 # no extra formatting for "^+++ /dev/null"
1769 if ($line =~ m!^\+\+\+ "?b/!) {
1770 if ($to->{'href'}) {
1772 $cgi->a({-href=>$to->{'href'}, -class=>"path"},
1773 esc_path($to->{'file'}));
1776 esc_path($to->{'file'});
1779 $result .= qq!<div class="diff to_file">$line</div>\n!;
1784 # create note for patch simplified by combined diff
1785 sub format_diff_cc_simplified {
1786 my ($diffinfo, @parents) = @_;
1789 $result .= "<div class=\"diff header\">" .
1791 if (!is_deleted($diffinfo)) {
1792 $result .= $cgi->a({-href => href(action=>"blob",
1794 hash=>$diffinfo->{'to_id'},
1795 file_name=>$diffinfo->{'to_file'}),
1797 esc_path($diffinfo->{'to_file'}));
1799 $result .= esc_path($diffinfo->{'to_file'});
1801 $result .= "</div>\n" . # class="diff header"
1802 "<div class=\"diff nodifferences\">" .
1804 "</div>\n"; # class="diff nodifferences"
1809 # format patch (diff) line (not to be used for diff headers)
1810 sub format_diff_line {
1812 my ($from, $to) = @_;
1813 my $diff_class = "";
1817 if ($from && $to && ref($from->{'href'}) eq "ARRAY") {
1819 my $prefix = substr($line, 0, scalar @{$from->{'href'}});
1820 if ($line =~ m/^\@{3}/) {
1821 $diff_class = " chunk_header";
1822 } elsif ($line =~ m/^\\/) {
1823 $diff_class = " incomplete";
1824 } elsif ($prefix =~ tr/+/+/) {
1825 $diff_class = " add";
1826 } elsif ($prefix =~ tr/-/-/) {
1827 $diff_class = " rem";
1830 # assume ordinary diff
1831 my $char = substr($line, 0, 1);
1833 $diff_class = " add";
1834 } elsif ($char eq '-') {
1835 $diff_class = " rem";
1836 } elsif ($char eq '@') {
1837 $diff_class = " chunk_header";
1838 } elsif ($char eq "\\") {
1839 $diff_class = " incomplete";
1842 $line = untabify($line);
1843 if ($from && $to && $line =~ m/^\@{2} /) {
1844 my ($from_text, $from_start, $from_lines, $to_text, $to_start, $to_lines, $section) =
1845 $line =~ m/^\@{2} (-(\d+)(?:,(\d+))?) (\+(\d+)(?:,(\d+))?) \@{2}(.*)$/;
1847 $from_lines = 0 unless defined $from_lines;
1848 $to_lines = 0 unless defined $to_lines;
1850 if ($from->{'href'}) {
1851 $from_text = $cgi->a({-href=>"$from->{'href'}#l$from_start",
1852 -class=>"list"}, $from_text);
1854 if ($to->{'href'}) {
1855 $to_text = $cgi->a({-href=>"$to->{'href'}#l$to_start",
1856 -class=>"list"}, $to_text);
1858 $line = "<span class=\"chunk_info\">@@ $from_text $to_text @@</span>" .
1859 "<span class=\"section\">" . esc_html($section, -nbsp=>1) . "</span>";
1860 return "<div class=\"diff$diff_class\">$line</div>\n";
1861 } elsif ($from && $to && $line =~ m/^\@{3}/) {
1862 my ($prefix, $ranges, $section) = $line =~ m/^(\@+) (.*?) \@+(.*)$/;
1863 my (@from_text, @from_start, @from_nlines, $to_text, $to_start, $to_nlines);
1865 @from_text = split(' ', $ranges);
1866 for (my $i = 0; $i < @from_text; ++$i) {
1867 ($from_start[$i], $from_nlines[$i]) =
1868 (split(',', substr($from_text[$i], 1)), 0);
1871 $to_text = pop @from_text;
1872 $to_start = pop @from_start;
1873 $to_nlines = pop @from_nlines;
1875 $line = "<span class=\"chunk_info\">$prefix ";
1876 for (my $i = 0; $i < @from_text; ++$i) {
1877 if ($from->{'href'}[$i]) {
1878 $line .= $cgi->a({-href=>"$from->{'href'}[$i]#l$from_start[$i]",
1879 -class=>"list"}, $from_text[$i]);
1881 $line .= $from_text[$i];
1885 if ($to->{'href'}) {
1886 $line .= $cgi->a({-href=>"$to->{'href'}#l$to_start",
1887 -class=>"list"}, $to_text);
1891 $line .= " $prefix</span>" .
1892 "<span class=\"section\">" . esc_html($section, -nbsp=>1) . "</span>";
1893 return "<div class=\"diff$diff_class\">$line</div>\n";
1895 return "<div class=\"diff$diff_class\">" . esc_html($line, -nbsp=>1) . "</div>\n";
1898 # Generates undef or something like "_snapshot_" or "snapshot (_tbz2_ _zip_)",
1899 # linked. Pass the hash of the tree/commit to snapshot.
1900 sub format_snapshot_links {
1902 my $num_fmts = @snapshot_fmts;
1903 if ($num_fmts > 1) {
1904 # A parenthesized list of links bearing format names.
1905 # e.g. "snapshot (_tar.gz_ _zip_)"
1906 return "snapshot (" . join(' ', map
1913 }, $known_snapshot_formats{$_}{'display'})
1914 , @snapshot_fmts) . ")";
1915 } elsif ($num_fmts == 1) {
1916 # A single "snapshot" link whose tooltip bears the format name.
1918 my ($fmt) = @snapshot_fmts;
1924 snapshot_format=>$fmt
1926 -title => "in format: $known_snapshot_formats{$fmt}{'display'}"
1928 } else { # $num_fmts == 0
1933 ## ......................................................................
1934 ## functions returning values to be passed, perhaps after some
1935 ## transformation, to other functions; e.g. returning arguments to href()
1937 # returns hash to be passed to href to generate gitweb URL
1938 # in -title key it returns description of link
1940 my $format = shift || 'Atom';
1941 my %res = (action => lc($format));
1943 # feed links are possible only for project views
1944 return unless (defined $project);
1945 # some views should link to OPML, or to generic project feed,
1946 # or don't have specific feed yet (so they should use generic)
1947 return if ($action =~ /^(?:tags|heads|forks|tag|search)$/x);
1950 # branches refs uses 'refs/heads/' prefix (fullname) to differentiate
1951 # from tag links; this also makes possible to detect branch links
1952 if ((defined $hash_base && $hash_base =~ m!^refs/heads/(.*)$!) ||
1953 (defined $hash && $hash =~ m!^refs/heads/(.*)$!)) {
1956 # find log type for feed description (title)
1958 if (defined $file_name) {
1959 $type = "history of $file_name";
1960 $type .= "/" if ($action eq 'tree');
1961 $type .= " on '$branch'" if (defined $branch);
1963 $type = "log of $branch" if (defined $branch);
1966 $res{-title} = $type;
1967 $res{'hash'} = (defined $branch ? "refs/heads/$branch" : undef);
1968 $res{'file_name'} = $file_name;
1973 ## ----------------------------------------------------------------------
1974 ## git utility subroutines, invoking git commands
1976 # returns path to the core git executable and the --git-dir parameter as list
1978 $number_of_git_cmds++;
1979 return $GIT, '--git-dir='.$git_dir;
1982 # quote the given arguments for passing them to the shell
1983 # quote_command("command", "arg 1", "arg with ' and ! characters")
1984 # => "'command' 'arg 1' 'arg with '\'' and '\!' characters'"
1985 # Try to avoid using this function wherever possible.
1988 map { my $a = $_; $a =~ s/(['!])/'\\$1'/g; "'$a'" } @_ );
1991 # get HEAD ref of given project as hash
1992 sub git_get_head_hash {
1993 my $project = shift;
1994 my $o_git_dir = $git_dir;
1996 $git_dir = "$projectroot/$project";
1997 if (open my $fd, "-|", git_cmd(), "rev-parse", "--verify", "HEAD") {
2000 if (defined $head && $head =~ /^([0-9a-fA-F]{40})$/) {
2004 if (defined $o_git_dir) {
2005 $git_dir = $o_git_dir;
2010 # get type of given object
2014 open my $fd, "-|", git_cmd(), "cat-file", '-t', $hash or return;
2016 close $fd or return;
2021 # repository configuration
2022 our $config_file = '';
2025 # store multiple values for single key as anonymous array reference
2026 # single values stored directly in the hash, not as [ <value> ]
2027 sub hash_set_multi {
2028 my ($hash, $key, $value) = @_;
2030 if (!exists $hash->{$key}) {
2031 $hash->{$key} = $value;
2032 } elsif (!ref $hash->{$key}) {
2033 $hash->{$key} = [ $hash->{$key}, $value ];
2035 push @{$hash->{$key}}, $value;
2039 # return hash of git project configuration
2040 # optionally limited to some section, e.g. 'gitweb'
2041 sub git_parse_project_config {
2042 my $section_regexp = shift;
2047 open my $fh, "-|", git_cmd(), "config", '-z', '-l',
2050 while (my $keyval = <$fh>) {
2052 my ($key, $value) = split(/\n/, $keyval, 2);
2054 hash_set_multi(\%config, $key, $value)
2055 if (!defined $section_regexp || $key =~ /^(?:$section_regexp)\./o);
2062 # convert config value to boolean: 'true' or 'false'
2063 # no value, number > 0, 'true' and 'yes' values are true
2064 # rest of values are treated as false (never as error)
2065 sub config_to_bool {
2068 return 1 if !defined $val; # section.key
2070 # strip leading and trailing whitespace
2074 return (($val =~ /^\d+$/ && $val) || # section.key = 1
2075 ($val =~ /^(?:true|yes)$/i)); # section.key = true
2078 # convert config value to simple decimal number
2079 # an optional value suffix of 'k', 'm', or 'g' will cause the value
2080 # to be multiplied by 1024, 1048576, or 1073741824
2084 # strip leading and trailing whitespace
2088 if (my ($num, $unit) = ($val =~ /^([0-9]*)([kmg])$/i)) {
2090 # unknown unit is treated as 1
2091 return $num * ($unit eq 'g' ? 1073741824 :
2092 $unit eq 'm' ? 1048576 :
2093 $unit eq 'k' ? 1024 : 1);
2098 # convert config value to array reference, if needed
2099 sub config_to_multi {
2102 return ref($val) ? $val : (defined($val) ? [ $val ] : []);
2105 sub git_get_project_config {
2106 my ($key, $type) = @_;
2109 return unless ($key);
2110 $key =~ s/^gitweb\.//;
2111 return if ($key =~ m/\W/);
2114 if (defined $type) {
2117 unless ($type eq 'bool' || $type eq 'int');
2121 if (!defined $config_file ||
2122 $config_file ne "$git_dir/config") {
2123 %config = git_parse_project_config('gitweb');
2124 $config_file = "$git_dir/config";
2127 # check if config variable (key) exists
2128 return unless exists $config{"gitweb.$key"};
2131 if (!defined $type) {
2132 return $config{"gitweb.$key"};
2133 } elsif ($type eq 'bool') {
2134 # backward compatibility: 'git config --bool' returns true/false
2135 return config_to_bool($config{"gitweb.$key"}) ? 'true' : 'false';
2136 } elsif ($type eq 'int') {
2137 return config_to_int($config{"gitweb.$key"});
2139 return $config{"gitweb.$key"};
2142 # get hash of given path at given ref
2143 sub git_get_hash_by_path {
2145 my $path = shift || return undef;
2150 open my $fd, "-|", git_cmd(), "ls-tree", $base, "--", $path
2151 or die_error(500, "Open git-ls-tree failed");
2153 close $fd or return undef;
2155 if (!defined $line) {
2156 # there is no tree or hash given by $path at $base
2160 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'
2161 $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t/;
2162 if (defined $type && $type ne $2) {
2163 # type doesn't match
2169 # get path of entry with given hash at given tree-ish (ref)
2170 # used to get 'from' filename for combined diff (merge commit) for renames
2171 sub git_get_path_by_hash {
2172 my $base = shift || return;
2173 my $hash = shift || return;
2177 open my $fd, "-|", git_cmd(), "ls-tree", '-r', '-t', '-z', $base
2179 while (my $line = <$fd>) {
2182 #'040000 tree 595596a6a9117ddba9fe379b6b012b558bac8423 gitweb'
2183 #'100644 blob e02e90f0429be0d2a69b76571101f20b8f75530f gitweb/README'
2184 if ($line =~ m/(?:[0-9]+) (?:.+) $hash\t(.+)$/) {
2193 ## ......................................................................
2194 ## git utility functions, directly accessing git repository
2196 sub git_get_project_description {
2199 $git_dir = "$projectroot/$path";
2200 open my $fd, '<', "$git_dir/description"
2201 or return git_get_project_config('description');
2204 if (defined $descr) {
2210 sub git_get_project_ctags {
2214 $git_dir = "$projectroot/$path";
2215 opendir my $dh, "$git_dir/ctags"
2217 foreach (grep { -f $_ } map { "$git_dir/ctags/$_" } readdir($dh)) {
2218 open my $ct, '<', $_ or next;
2222 my $ctag = $_; $ctag =~ s#.*/##;
2223 $ctags->{$ctag} = $val;
2229 sub git_populate_project_tagcloud {
2232 # First, merge different-cased tags; tags vote on casing
2234 foreach (keys %$ctags) {
2235 $ctags_lc{lc $_}->{count} += $ctags->{$_};
2236 if (not $ctags_lc{lc $_}->{topcount}
2237 or $ctags_lc{lc $_}->{topcount} < $ctags->{$_}) {
2238 $ctags_lc{lc $_}->{topcount} = $ctags->{$_};
2239 $ctags_lc{lc $_}->{topname} = $_;
2244 if (eval { require HTML::TagCloud; 1; }) {
2245 $cloud = HTML::TagCloud->new;
2246 foreach (sort keys %ctags_lc) {
2247 # Pad the title with spaces so that the cloud looks
2249 my $title = $ctags_lc{$_}->{topname};
2250 $title =~ s/ / /g;
2251 $title =~ s/^/ /g;
2252 $title =~ s/$/ /g;
2253 $cloud->add($title, $home_link."?by_tag=".$_, $ctags_lc{$_}->{count});
2256 $cloud = \%ctags_lc;
2261 sub git_show_project_tagcloud {
2262 my ($cloud, $count) = @_;
2263 print STDERR ref($cloud)."..\n";
2264 if (ref $cloud eq 'HTML::TagCloud') {
2265 return $cloud->html_and_css($count);
2267 my @tags = sort { $cloud->{$a}->{count} <=> $cloud->{$b}->{count} } keys %$cloud;
2268 return '<p align="center">' . join (', ', map {
2269 "<a href=\"$home_link?by_tag=$_\">$cloud->{$_}->{topname}</a>"
2270 } splice(@tags, 0, $count)) . '</p>';
2274 sub git_get_project_url_list {
2277 $git_dir = "$projectroot/$path";
2278 open my $fd, '<', "$git_dir/cloneurl"
2279 or return wantarray ?
2280 @{ config_to_multi(git_get_project_config('url')) } :
2281 config_to_multi(git_get_project_config('url'));
2282 my @git_project_url_list = map { chomp; $_ } <$fd>;
2285 return wantarray ? @git_project_url_list : \@git_project_url_list;
2288 sub git_get_projects_list {
2293 $filter =~ s/\.git$//;
2295 my $check_forks = gitweb_check_feature('forks');
2297 if (-d $projects_list) {
2298 # search in directory
2299 my $dir = $projects_list . ($filter ? "/$filter" : '');
2300 # remove the trailing "/"
2302 my $pfxlen = length("$dir");
2303 my $pfxdepth = ($dir =~ tr!/!!);
2306 follow_fast => 1, # follow symbolic links
2307 follow_skip => 2, # ignore duplicates
2308 dangling_symlinks => 0, # ignore dangling symlinks, silently
2310 # skip project-list toplevel, if we get it.
2311 return if (m!^[/.]$!);
2312 # only directories can be git repositories
2313 return unless (-d $_);
2314 # don't traverse too deep (Find is super slow on os x)
2315 if (($File::Find::name =~ tr!/!!) - $pfxdepth > $project_maxdepth) {
2316 $File::Find::prune = 1;
2320 my $subdir = substr($File::Find::name, $pfxlen + 1);
2321 # we check related file in $projectroot
2322 my $path = ($filter ? "$filter/" : '') . $subdir;
2323 if (check_export_ok("$projectroot/$path")) {
2324 push @list, { path => $path };
2325 $File::Find::prune = 1;
2330 } elsif (-f $projects_list) {
2331 # read from file(url-encoded):
2332 # 'git%2Fgit.git Linus+Torvalds'
2333 # 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin'
2334 # 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman'
2336 open my $fd, '<', $projects_list or return;
2338 while (my $line = <$fd>) {
2340 my ($path, $owner) = split ' ', $line;
2341 $path = unescape($path);
2342 $owner = unescape($owner);
2343 if (!defined $path) {
2346 if ($filter ne '') {
2347 # looking for forks;
2348 my $pfx = substr($path, 0, length($filter));
2349 if ($pfx ne $filter) {
2352 my $sfx = substr($path, length($filter));
2353 if ($sfx !~ /^\/.*\.git$/) {
2356 } elsif ($check_forks) {
2358 foreach my $filter (keys %paths) {
2359 # looking for forks;
2360 my $pfx = substr($path, 0, length($filter));
2361 if ($pfx ne $filter) {
2364 my $sfx = substr($path, length($filter));
2365 if ($sfx !~ /^\/.*\.git$/) {
2368 # is a fork, don't include it in
2373 if (check_export_ok("$projectroot/$path")) {
2376 owner => to_utf8($owner),
2379 (my $forks_path = $path) =~ s/\.git$//;
2380 $paths{$forks_path}++;
2388 our $gitweb_project_owner = undef;
2389 sub git_get_project_list_from_file {
2391 return if (defined $gitweb_project_owner);
2393 $gitweb_project_owner = {};
2394 # read from file (url-encoded):
2395 # 'git%2Fgit.git Linus+Torvalds'
2396 # 'libs%2Fklibc%2Fklibc.git H.+Peter+Anvin'
2397 # 'linux%2Fhotplug%2Fudev.git Greg+Kroah-Hartman'
2398 if (-f $projects_list) {
2399 open(my $fd, '<', $projects_list);
2400 while (my $line = <$fd>) {
2402 my ($pr, $ow) = split ' ', $line;
2403 $pr = unescape($pr);
2404 $ow = unescape($ow);
2405 $gitweb_project_owner->{$pr} = to_utf8($ow);
2411 sub git_get_project_owner {
2412 my $project = shift;
2415 return undef unless $project;
2416 $git_dir = "$projectroot/$project";
2418 if (!defined $gitweb_project_owner) {
2419 git_get_project_list_from_file();
2422 if (exists $gitweb_project_owner->{$project}) {
2423 $owner = $gitweb_project_owner->{$project};
2425 if (!defined $owner){
2426 $owner = git_get_project_config('owner');
2428 if (!defined $owner) {
2429 $owner = get_file_owner("$git_dir");
2435 sub git_get_last_activity {
2439 $git_dir = "$projectroot/$path";
2440 open($fd, "-|", git_cmd(), 'for-each-ref',
2441 '--format=%(committer)',
2442 '--sort=-committerdate',
2444 'refs/heads') or return;
2445 my $most_recent = <$fd>;
2446 close $fd or return;
2447 if (defined $most_recent &&
2448 $most_recent =~ / (\d+) [-+][01]\d\d\d$/) {
2450 my $age = time - $timestamp;
2451 return ($age, age_string($age));
2453 return (undef, undef);
2456 sub git_get_references {
2457 my $type = shift || "";
2459 # 5dc01c595e6c6ec9ccda4f6f69c131c0dd945f8c refs/tags/v2.6.11
2460 # c39ae07f393806ccf406ef966e9a15afc43cc36a refs/tags/v2.6.11^{}
2461 open my $fd, "-|", git_cmd(), "show-ref", "--dereference",
2462 ($type ? ("--", "refs/$type") : ()) # use -- <pattern> if $type
2465 while (my $line = <$fd>) {
2467 if ($line =~ m!^([0-9a-fA-F]{40})\srefs/($type.*)$!) {
2468 if (defined $refs{$1}) {
2469 push @{$refs{$1}}, $2;
2475 close $fd or return;
2479 sub git_get_rev_name_tags {
2480 my $hash = shift || return undef;
2482 open my $fd, "-|", git_cmd(), "name-rev", "--tags", $hash
2484 my $name_rev = <$fd>;
2487 if ($name_rev =~ m|^$hash tags/(.*)$|) {
2490 # catches also '$hash undefined' output
2495 ## ----------------------------------------------------------------------
2496 ## parse to hash functions
2500 my $tz = shift || "-0000";
2503 my @months = ("Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec");
2504 my @days = ("Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat");
2505 my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($epoch);
2506 $date{'hour'} = $hour;
2507 $date{'minute'} = $min;
2508 $date{'mday'} = $mday;
2509 $date{'day'} = $days[$wday];
2510 $date{'month'} = $months[$mon];
2511 $date{'rfc2822'} = sprintf "%s, %d %s %4d %02d:%02d:%02d +0000",
2512 $days[$wday], $mday, $months[$mon], 1900+$year, $hour ,$min, $sec;
2513 $date{'mday-time'} = sprintf "%d %s %02d:%02d",
2514 $mday, $months[$mon], $hour ,$min;
2515 $date{'iso-8601'} = sprintf "%04d-%02d-%02dT%02d:%02d:%02dZ",
2516 1900+$year, 1+$mon, $mday, $hour ,$min, $sec;
2518 $tz =~ m/^([+\-][0-9][0-9])([0-9][0-9])$/;
2519 my $local = $epoch + ((int $1 + ($2/60)) * 3600);
2520 ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($local);
2521 $date{'hour_local'} = $hour;
2522 $date{'minute_local'} = $min;
2523 $date{'tz_local'} = $tz;
2524 $date{'iso-tz'} = sprintf("%04d-%02d-%02d %02d:%02d:%02d %s",
2525 1900+$year, $mon+1, $mday,
2526 $hour, $min, $sec, $tz);
2535 open my $fd, "-|", git_cmd(), "cat-file", "tag", $tag_id or return;
2536 $tag{'id'} = $tag_id;
2537 while (my $line = <$fd>) {
2539 if ($line =~ m/^object ([0-9a-fA-F]{40})$/) {
2540 $tag{'object'} = $1;
2541 } elsif ($line =~ m/^type (.+)$/) {
2543 } elsif ($line =~ m/^tag (.+)$/) {
2545 } elsif ($line =~ m/^tagger (.*) ([0-9]+) (.*)$/) {
2546 $tag{'author'} = $1;
2547 $tag{'author_epoch'} = $2;
2548 $tag{'author_tz'} = $3;
2549 if ($tag{'author'} =~ m/^([^<]+) <([^>]*)>/) {
2550 $tag{'author_name'} = $1;
2551 $tag{'author_email'} = $2;
2553 $tag{'author_name'} = $tag{'author'};
2555 } elsif ($line =~ m/--BEGIN/) {
2556 push @comment, $line;
2558 } elsif ($line eq "") {
2562 push @comment, <$fd>;
2563 $tag{'comment'} = \@comment;
2564 close $fd or return;
2565 if (!defined $tag{'name'}) {
2571 sub parse_commit_text {
2572 my ($commit_text, $withparents) = @_;
2573 my @commit_lines = split '\n', $commit_text;
2576 pop @commit_lines; # Remove '\0'
2578 if (! @commit_lines) {
2582 my $header = shift @commit_lines;
2583 if ($header !~ m/^[0-9a-fA-F]{40}/) {
2586 ($co{'id'}, my @parents) = split ' ', $header;
2587 while (my $line = shift @commit_lines) {
2588 last if $line eq "\n";
2589 if ($line =~ m/^tree ([0-9a-fA-F]{40})$/) {
2591 } elsif ((!defined $withparents) && ($line =~ m/^parent ([0-9a-fA-F]{40})$/)) {
2593 } elsif ($line =~ m/^author (.*) ([0-9]+) (.*)$/) {
2595 $co{'author_epoch'} = $2;
2596 $co{'author_tz'} = $3;
2597 if ($co{'author'} =~ m/^([^<]+) <([^>]*)>/) {
2598 $co{'author_name'} = $1;
2599 $co{'author_email'} = $2;
2601 $co{'author_name'} = $co{'author'};
2603 } elsif ($line =~ m/^committer (.*) ([0-9]+) (.*)$/) {
2604 $co{'committer'} = $1;
2605 $co{'committer_epoch'} = $2;
2606 $co{'committer_tz'} = $3;
2607 $co{'committer_name'} = $co{'committer'};
2608 if ($co{'committer'} =~ m/^([^<]+) <([^>]*)>/) {
2609 $co{'committer_name'} = $1;
2610 $co{'committer_email'} = $2;
2612 $co{'committer_name'} = $co{'committer'};
2616 if (!defined $co{'tree'}) {
2619 $co{'parents'} = \@parents;
2620 $co{'parent'} = $parents[0];
2622 foreach my $title (@commit_lines) {
2625 $co{'title'} = chop_str($title, 80, 5);
2626 # remove leading stuff of merges to make the interesting part visible
2627 if (length($title) > 50) {
2628 $title =~ s/^Automatic //;
2629 $title =~ s/^merge (of|with) /Merge ... /i;
2630 if (length($title) > 50) {
2631 $title =~ s/(http|rsync):\/\///;
2633 if (length($title) > 50) {
2634 $title =~ s/(master|www|rsync)\.//;
2636 if (length($title) > 50) {
2637 $title =~ s/kernel.org:?//;
2639 if (length($title) > 50) {
2640 $title =~ s/\/pub\/scm//;
2643 $co{'title_short'} = chop_str($title, 50, 5);
2647 if (! defined $co{'title'} || $co{'title'} eq "") {
2648 $co{'title'} = $co{'title_short'} = '(no commit message)';
2650 # remove added spaces
2651 foreach my $line (@commit_lines) {
2654 $co{'comment'} = \@commit_lines;
2656 my $age = time - $co{'committer_epoch'};
2658 $co{'age_string'} = age_string($age);
2659 my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday) = gmtime($co{'committer_epoch'});
2660 if ($age > 60*60*24*7*2) {
2661 $co{'age_string_date'} = sprintf "%4i-%02u-%02i", 1900 + $year, $mon+1, $mday;
2662 $co{'age_string_age'} = $co{'age_string'};
2664 $co{'age_string_date'} = $co{'age_string'};
2665 $co{'age_string_age'} = sprintf "%4i-%02u-%02i", 1900 + $year, $mon+1, $mday;
2671 my ($commit_id) = @_;
2676 open my $fd, "-|", git_cmd(), "rev-list",
2682 or die_error(500, "Open git-rev-list failed");
2683 %co = parse_commit_text(<$fd>, 1);
2690 my ($commit_id, $maxcount, $skip, $filename, @args) = @_;
2698 open my $fd, "-|", git_cmd(), "rev-list",
2701 ("--max-count=" . $maxcount),
2702 ("--skip=" . $skip),
2706 ($filename ? ($filename) : ())
2707 or die_error(500, "Open git-rev-list failed");
2708 while (my $line = <$fd>) {
2709 my %co = parse_commit_text($line);
2714 return wantarray ? @cos : \@cos;
2717 # parse line of git-diff-tree "raw" output
2718 sub parse_difftree_raw_line {
2722 # ':100644 100644 03b218260e99b78c6df0ed378e59ed9205ccc96d 3b93d5e7cc7f7dd4ebed13a5cc1a4ad976fc94d8 M ls-files.c'
2723 # ':100644 100644 7f9281985086971d3877aca27704f2aaf9c448ce bc190ebc71bbd923f2b728e505408f5e54bd073a M rev-tree.c'
2724 if ($line =~ m/^:([0-7]{6}) ([0-7]{6}) ([0-9a-fA-F]{40}) ([0-9a-fA-F]{40}) (.)([0-9]{0,3})\t(.*)$/) {
2725 $res{'from_mode'} = $1;
2726 $res{'to_mode'} = $2;
2727 $res{'from_id'} = $3;
2729 $res{'status'} = $5;
2730 $res{'similarity'} = $6;
2731 if ($res{'status'} eq 'R' || $res{'status'} eq 'C') { # renamed or copied
2732 ($res{'from_file'}, $res{'to_file'}) = map { unquote($_) } split("\t", $7);
2734 $res{'from_file'} = $res{'to_file'} = $res{'file'} = unquote($7);
2737 # '::100755 100755 100755 60e79ca1b01bc8b057abe17ddab484699a7f5fdb 94067cc5f73388f33722d52ae02f44692bc07490 94067cc5f73388f33722d52ae02f44692bc07490 MR git-gui/git-gui.sh'
2738 # combined diff (for merge commit)
2739 elsif ($line =~ s/^(::+)((?:[0-7]{6} )+)((?:[0-9a-fA-F]{40} )+)([a-zA-Z]+)\t(.*)$//) {
2740 $res{'nparents'} = length($1);
2741 $res{'from_mode'} = [ split(' ', $2) ];
2742 $res{'to_mode'} = pop @{$res{'from_mode'}};
2743 $res{'from_id'} = [ split(' ', $3) ];
2744 $res{'to_id'} = pop @{$res{'from_id'}};
2745 $res{'status'} = [ split('', $4) ];
2746 $res{'to_file'} = unquote($5);
2748 # 'c512b523472485aef4fff9e57b229d9d243c967f'
2749 elsif ($line =~ m/^([0-9a-fA-F]{40})$/) {
2750 $res{'commit'} = $1;
2753 return wantarray ? %res : \%res;
2756 # wrapper: return parsed line of git-diff-tree "raw" output
2757 # (the argument might be raw line, or parsed info)
2758 sub parsed_difftree_line {
2759 my $line_or_ref = shift;
2761 if (ref($line_or_ref) eq "HASH") {
2762 # pre-parsed (or generated by hand)
2763 return $line_or_ref;
2765 return parse_difftree_raw_line($line_or_ref);
2769 # parse line of git-ls-tree output
2770 sub parse_ls_tree_line {
2775 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'
2776 $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t(.+)$/s;
2784 $res{'name'} = unquote($4);
2787 return wantarray ? %res : \%res;
2790 # generates _two_ hashes, references to which are passed as 2 and 3 argument
2791 sub parse_from_to_diffinfo {
2792 my ($diffinfo, $from, $to, @parents) = @_;
2794 if ($diffinfo->{'nparents'}) {
2796 $from->{'file'} = [];
2797 $from->{'href'} = [];
2798 fill_from_file_info($diffinfo, @parents)
2799 unless exists $diffinfo->{'from_file'};
2800 for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
2801 $from->{'file'}[$i] =
2802 defined $diffinfo->{'from_file'}[$i] ?
2803 $diffinfo->{'from_file'}[$i] :
2804 $diffinfo->{'to_file'};
2805 if ($diffinfo->{'status'}[$i] ne "A") { # not new (added) file
2806 $from->{'href'}[$i] = href(action=>"blob",
2807 hash_base=>$parents[$i],
2808 hash=>$diffinfo->{'from_id'}[$i],
2809 file_name=>$from->{'file'}[$i]);
2811 $from->{'href'}[$i] = undef;
2815 # ordinary (not combined) diff
2816 $from->{'file'} = $diffinfo->{'from_file'};
2817 if ($diffinfo->{'status'} ne "A") { # not new (added) file
2818 $from->{'href'} = href(action=>"blob", hash_base=>$hash_parent,
2819 hash=>$diffinfo->{'from_id'},
2820 file_name=>$from->{'file'});
2822 delete $from->{'href'};
2826 $to->{'file'} = $diffinfo->{'to_file'};
2827 if (!is_deleted($diffinfo)) { # file exists in result
2828 $to->{'href'} = href(action=>"blob", hash_base=>$hash,
2829 hash=>$diffinfo->{'to_id'},
2830 file_name=>$to->{'file'});
2832 delete $to->{'href'};
2836 ## ......................................................................
2837 ## parse to array of hashes functions
2839 sub git_get_heads_list {
2843 open my $fd, '-|', git_cmd(), 'for-each-ref',
2844 ($limit ? '--count='.($limit+1) : ()), '--sort=-committerdate',
2845 '--format=%(objectname) %(refname) %(subject)%00%(committer)',
2848 while (my $line = <$fd>) {
2852 my ($refinfo, $committerinfo) = split(/\0/, $line);
2853 my ($hash, $name, $title) = split(' ', $refinfo, 3);
2854 my ($committer, $epoch, $tz) =
2855 ($committerinfo =~ /^(.*) ([0-9]+) (.*)$/);
2856 $ref_item{'fullname'} = $name;
2857 $name =~ s!^refs/heads/!!;
2859 $ref_item{'name'} = $name;
2860 $ref_item{'id'} = $hash;
2861 $ref_item{'title'} = $title || '(no commit message)';
2862 $ref_item{'epoch'} = $epoch;
2864 $ref_item{'age'} = age_string(time - $ref_item{'epoch'});
2866 $ref_item{'age'} = "unknown";
2869 push @headslist, \%ref_item;
2873 return wantarray ? @headslist : \@headslist;
2876 sub git_get_tags_list {
2880 open my $fd, '-|', git_cmd(), 'for-each-ref',
2881 ($limit ? '--count='.($limit+1) : ()), '--sort=-creatordate',
2882 '--format=%(objectname) %(objecttype) %(refname) '.
2883 '%(*objectname) %(*objecttype) %(subject)%00%(creator)',
2886 while (my $line = <$fd>) {
2890 my ($refinfo, $creatorinfo) = split(/\0/, $line);
2891 my ($id, $type, $name, $refid, $reftype, $title) = split(' ', $refinfo, 6);
2892 my ($creator, $epoch, $tz) =
2893 ($creatorinfo =~ /^(.*) ([0-9]+) (.*)$/);
2894 $ref_item{'fullname'} = $name;
2895 $name =~ s!^refs/tags/!!;
2897 $ref_item{'type'} = $type;
2898 $ref_item{'id'} = $id;
2899 $ref_item{'name'} = $name;
2900 if ($type eq "tag") {
2901 $ref_item{'subject'} = $title;
2902 $ref_item{'reftype'} = $reftype;
2903 $ref_item{'refid'} = $refid;
2905 $ref_item{'reftype'} = $type;
2906 $ref_item{'refid'} = $id;
2909 if ($type eq "tag" || $type eq "commit") {
2910 $ref_item{'epoch'} = $epoch;
2912 $ref_item{'age'} = age_string(time - $ref_item{'epoch'});
2914 $ref_item{'age'} = "unknown";
2918 push @tagslist, \%ref_item;
2922 return wantarray ? @tagslist : \@tagslist;
2925 ## ----------------------------------------------------------------------
2926 ## filesystem-related functions
2928 sub get_file_owner {
2931 my ($dev, $ino, $mode, $nlink, $st_uid, $st_gid, $rdev, $size) = stat($path);
2932 my ($name, $passwd, $uid, $gid, $quota, $comment, $gcos, $dir, $shell) = getpwuid($st_uid);
2933 if (!defined $gcos) {
2937 $owner =~ s/[,;].*$//;
2938 return to_utf8($owner);
2941 # assume that file exists
2943 my $filename = shift;
2945 open my $fd, '<', $filename;
2946 print map { to_utf8($_) } <$fd>;
2950 ## ......................................................................
2951 ## mimetype related functions
2953 sub mimetype_guess_file {
2954 my $filename = shift;
2955 my $mimemap = shift;
2956 -r $mimemap or return undef;
2959 open(my $mh, '<', $mimemap) or return undef;
2961 next if m/^#/; # skip comments
2962 my ($mimetype, $exts) = split(/\t+/);
2963 if (defined $exts) {
2964 my @exts = split(/\s+/, $exts);
2965 foreach my $ext (@exts) {
2966 $mimemap{$ext} = $mimetype;
2972 $filename =~ /\.([^.]*)$/;
2973 return $mimemap{$1};
2976 sub mimetype_guess {
2977 my $filename = shift;
2979 $filename =~ /\./ or return undef;
2981 if ($mimetypes_file) {
2982 my $file = $mimetypes_file;
2983 if ($file !~ m!^/!) { # if it is relative path
2984 # it is relative to project
2985 $file = "$projectroot/$project/$file";
2987 $mime = mimetype_guess_file($filename, $file);
2989 $mime ||= mimetype_guess_file($filename, '/etc/mime.types');
2995 my $filename = shift;
2998 my $mime = mimetype_guess($filename);
2999 $mime and return $mime;
3003 return $default_blob_plain_mimetype unless $fd;
3006 return 'text/plain';
3007 } elsif (! $filename) {
3008 return 'application/octet-stream';
3009 } elsif ($filename =~ m/\.png$/i) {
3011 } elsif ($filename =~ m/\.gif$/i) {
3013 } elsif ($filename =~ m/\.jpe?g$/i) {
3014 return 'image/jpeg';
3016 return 'application/octet-stream';
3020 sub blob_contenttype {
3021 my ($fd, $file_name, $type) = @_;
3023 $type ||= blob_mimetype($fd, $file_name);
3024 if ($type eq 'text/plain' && defined $default_text_plain_charset) {
3025 $type .= "; charset=$default_text_plain_charset";
3031 ## ======================================================================
3032 ## functions printing HTML: header, footer, error page
3034 sub git_header_html {
3035 my $status = shift || "200 OK";
3036 my $expires = shift;
3038 my $title = "$site_name";
3039 if (defined $project) {
3040 $title .= " - " . to_utf8($project);
3041 if (defined $action) {
3042 $title .= "/$action";
3043 if (defined $file_name) {
3044 $title .= " - " . esc_path($file_name);
3045 if ($action eq "tree" && $file_name !~ m|/$|) {
3052 # require explicit support from the UA if we are to send the page as
3053 # 'application/xhtml+xml', otherwise send it as plain old 'text/html'.
3054 # we have to do this because MSIE sometimes globs '*/*', pretending to
3055 # support xhtml+xml but choking when it gets what it asked for.
3056 if (defined $cgi->http('HTTP_ACCEPT') &&
3057 $cgi->http('HTTP_ACCEPT') =~ m/(,|;|\s|^)application\/xhtml\+xml(,|;|\s|$)/ &&
3058 $cgi->Accept('application/xhtml+xml') != 0) {
3059 $content_type = 'application/xhtml+xml';
3061 $content_type = 'text/html';
3063 print $cgi->header(-type=>$content_type, -charset => 'utf-8',
3064 -status=> $status, -expires => $expires);
3065 my $mod_perl_version = $ENV{'MOD_PERL'} ? " $ENV{'MOD_PERL'}" : '';
3067 <?xml version="1.0" encoding="utf-8"?>
3068 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
3069 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-US" lang="en-US">
3070 <!-- git web interface version $version, (C) 2005-2006, Kay Sievers <kay.sievers\@vrfy.org>, Christian Gierke -->
3071 <!-- git core binaries version $git_version -->
3073 <meta http-equiv="content-type" content="$content_type; charset=utf-8"/>
3074 <meta name="generator" content="gitweb/$version git/$git_version$mod_perl_version"/>
3075 <meta name="robots" content="index, nofollow"/>
3076 <title>$title</title>
3078 # the stylesheet, favicon etc urls won't work correctly with path_info
3079 # unless we set the appropriate base URL
3080 if ($ENV{'PATH_INFO'}) {
3081 print "<base href=\"".esc_url($base_url)."\" />\n";
3083 # print out each stylesheet that exist, providing backwards capability
3084 # for those people who defined $stylesheet in a config file
3085 if (defined $stylesheet) {
3086 print '<link rel="stylesheet" type="text/css" href="'.$stylesheet.'"/>'."\n";
3088 foreach my $stylesheet (@stylesheets) {
3089 next unless $stylesheet;
3090 print '<link rel="stylesheet" type="text/css" href="'.$stylesheet.'"/>'."\n";
3093 if (defined $project) {
3094 my %href_params = get_feed_info();
3095 if (!exists $href_params{'-title'}) {
3096 $href_params{'-title'} = 'log';
3099 foreach my $format qw(RSS Atom) {
3100 my $type = lc($format);
3102 '-rel' => 'alternate',
3103 '-title' => "$project - $href_params{'-title'} - $format feed",
3104 '-type' => "application/$type+xml"
3107 $href_params{'action'} = $type;
3108 $link_attr{'-href'} = href(%href_params);
3110 "rel=\"$link_attr{'-rel'}\" ".
3111 "title=\"$link_attr{'-title'}\" ".
3112 "href=\"$link_attr{'-href'}\" ".
3113 "type=\"$link_attr{'-type'}\" ".
3116 $href_params{'extra_options'} = '--no-merges';
3117 $link_attr{'-href'} = href(%href_params);
3118 $link_attr{'-title'} .= ' (no merges)';
3120 "rel=\"$link_attr{'-rel'}\" ".
3121 "title=\"$link_attr{'-title'}\" ".
3122 "href=\"$link_attr{'-href'}\" ".
3123 "type=\"$link_attr{'-type'}\" ".
3128 printf('<link rel="alternate" title="%s projects list" '.
3129 'href="%s" type="text/plain; charset=utf-8" />'."\n",
3130 $site_name, href(project=>undef, action=>"project_index"));
3131 printf('<link rel="alternate" title="%s projects feeds" '.
3132 'href="%s" type="text/x-opml" />'."\n",
3133 $site_name, href(project=>undef, action=>"opml"));
3135 if (defined $favicon) {
3136 print qq(<link rel="shortcut icon" href="$favicon" type="image/png" />\n);
3142 if (-f $site_header) {
3143 insert_file($site_header);
3146 print "<div class=\"page_header\">\n" .
3147 $cgi->a({-href => esc_url($logo_url),
3148 -title => $logo_label},
3149 qq(<img src="$logo" width="72" height="27" alt="git" class="logo"/>));
3150 print $cgi->a({-href => esc_url($home_link)}, $home_link_str) . " / ";
3151 if (defined $project) {
3152 print $cgi->a({-href => href(action=>"summary")}, esc_html($project));
3153 if (defined $action) {
3160 my $have_search = gitweb_check_feature('search');
3161 if (defined $project && $have_search) {
3162 if (!defined $searchtext) {
3166 if (defined $hash_base) {
3167 $search_hash = $hash_base;
3168 } elsif (defined $hash) {
3169 $search_hash = $hash;
3171 $search_hash = "HEAD";
3173 my $action = $my_uri;
3174 my $use_pathinfo = gitweb_check_feature('pathinfo');
3175 if ($use_pathinfo) {
3176 $action .= "/".esc_url($project);
3178 print $cgi->startform(-method => "get", -action => $action) .
3179 "<div class=\"search\">\n" .
3181 $cgi->input({-name=>"p", -value=>$project, -type=>"hidden"}) . "\n") .
3182 $cgi->input({-name=>"a", -value=>"search", -type=>"hidden"}) . "\n" .
3183 $cgi->input({-name=>"h", -value=>$search_hash, -type=>"hidden"}) . "\n" .
3184 $cgi->popup_menu(-name => 'st', -default => 'commit',
3185 -values => ['commit', 'grep', 'author', 'committer', 'pickaxe']) .
3186 $cgi->sup($cgi->a({-href => href(action=>"search_help")}, "?")) .
3188 $cgi->textfield(-name => "s", -value => $searchtext) . "\n" .
3189 "<span title=\"Extended regular expression\">" .
3190 $cgi->checkbox(-name => 'sr', -value => 1, -label => 're',
3191 -checked => $search_use_regexp) .
3194 $cgi->end_form() . "\n";
3198 sub git_footer_html {
3199 my $feed_class = 'rss_logo';
3201 print "<div class=\"page_footer\">\n";
3202 if (defined $project) {
3203 my $descr = git_get_project_description($project);
3204 if (defined $descr) {
3205 print "<div class=\"page_footer_text\">" . esc_html($descr) . "</div>\n";
3208 my %href_params = get_feed_info();
3209 if (!%href_params) {
3210 $feed_class .= ' generic';
3212 $href_params{'-title'} ||= 'log';
3214 foreach my $format qw(RSS Atom) {
3215 $href_params{'action'} = lc($format);
3216 print $cgi->a({-href => href(%href_params),
3217 -title => "$href_params{'-title'} $format feed",
3218 -class => $feed_class}, $format)."\n";
3222 print $cgi->a({-href => href(project=>undef, action=>"opml"),
3223 -class => $feed_class}, "OPML") . " ";
3224 print $cgi->a({-href => href(project=>undef, action=>"project_index"),
3225 -class => $feed_class}, "TXT") . "\n";
3227 print "</div>\n"; # class="page_footer"
3229 if (defined $t0 && gitweb_check_feature('timed')) {
3230 print "<div id=\"generating_info\">\n";
3231 print 'This page took '.
3232 '<span id="generating_time" class="time_span">'.
3233 Time::HiRes::tv_interval($t0, [Time::HiRes::gettimeofday()]).
3236 '<span id="generating_cmd">'.
3237 $number_of_git_cmds.
3238 '</span> git commands '.
3240 print "</div>\n"; # class="page_footer"
3243 if (-f $site_footer) {
3244 insert_file($site_footer);
3247 print qq!<script type="text/javascript" src="$javascript"></script>\n!;
3248 if ($action eq 'blame_incremental') {
3249 print qq!<script type="text/javascript">\n!.
3250 qq!startBlame("!. href(action=>"blame_data", -replay=>1) .qq!",\n!.
3251 qq! "!. href() .qq!");\n!.
3254 print qq!<script type="text/javascript">\n!.
3255 qq!window.onload = fixLinks;\n!.
3263 # die_error(<http_status_code>, <error_message>)
3264 # Example: die_error(404, 'Hash not found')
3265 # By convention, use the following status codes (as defined in RFC 2616):
3266 # 400: Invalid or missing CGI parameters, or
3267 # requested object exists but has wrong type.
3268 # 403: Requested feature (like "pickaxe" or "snapshot") not enabled on
3269 # this server or project.
3270 # 404: Requested object/revision/project doesn't exist.
3271 # 500: The server isn't configured properly, or
3272 # an internal error occurred (e.g. failed assertions caused by bugs), or
3273 # an unknown error occurred (e.g. the git binary died unexpectedly).
3275 my $status = shift || 500;
3276 my $error = shift || "Internal server error";
3278 my %http_responses = (400 => '400 Bad Request',
3279 403 => '403 Forbidden',
3280 404 => '404 Not Found',
3281 500 => '500 Internal Server Error');
3282 git_header_html($http_responses{$status});
3284 <div class="page_body">
3294 ## ----------------------------------------------------------------------
3295 ## functions printing or outputting HTML: navigation
3297 sub git_print_page_nav {
3298 my ($current, $suppress, $head, $treehead, $treebase, $extra) = @_;
3299 $extra = '' if !defined $extra; # pager or formats
3301 my @navs = qw(summary shortlog log commit commitdiff tree);
3303 @navs = grep { $_ ne $suppress } @navs;
3306 my %arg = map { $_ => {action=>$_} } @navs;
3307 if (defined $head) {
3308 for (qw(commit commitdiff)) {
3309 $arg{$_}{'hash'} = $head;
3311 if ($current =~ m/^(tree | log | shortlog | commit | commitdiff | search)$/x) {
3312 for (qw(shortlog log)) {
3313 $arg{$_}{'hash'} = $head;
3318 $arg{'tree'}{'hash'} = $treehead if defined $treehead;
3319 $arg{'tree'}{'hash_base'} = $treebase if defined $treebase;
3321 my @actions = gitweb_get_feature('actions');
3324 'n' => $project, # project name
3325 'f' => $git_dir, # project path within filesystem
3326 'h' => $treehead || '', # current hash ('h' parameter)
3327 'b' => $treebase || '', # hash base ('hb' parameter)
3330 my ($label, $link, $pos) = splice(@actions,0,3);
3332 @navs = map { $_ eq $pos ? ($_, $label) : $_ } @navs;
3334 $link =~ s/%([%nfhb])/$repl{$1}/g;
3335 $arg{$label}{'_href'} = $link;
3338 print "<div class=\"page_nav\">\n" .
3340 map { $_ eq $current ?
3341 $_ : $cgi->a({-href => ($arg{$_}{_href} ? $arg{$_}{_href} : href(%{$arg{$_}}))}, "$_")
3343 print "<br/>\n$extra<br/>\n" .
3347 sub format_paging_nav {
3348 my ($action, $hash, $head, $page, $has_next_link) = @_;
3352 if ($hash ne $head || $page) {
3353 $paging_nav .= $cgi->a({-href => href(action=>$action)}, "HEAD");
3355 $paging_nav .= "HEAD";
3359 $paging_nav .= " ⋅ " .
3360 $cgi->a({-href => href(-replay=>1, page=>$page-1),
3361 -accesskey => "p", -title => "Alt-p"}, "prev");
3363 $paging_nav .= " ⋅ prev";
3366 if ($has_next_link) {
3367 $paging_nav .= " ⋅ " .
3368 $cgi->a({-href => href(-replay=>1, page=>$page+1),
3369 -accesskey => "n", -title => "Alt-n"}, "next");
3371 $paging_nav .= " ⋅ next";
3377 ## ......................................................................
3378 ## functions printing or outputting HTML: div
3380 sub git_print_header_div {
3381 my ($action, $title, $hash, $hash_base) = @_;
3384 $args{'action'} = $action;
3385 $args{'hash'} = $hash if $hash;
3386 $args{'hash_base'} = $hash_base if $hash_base;
3388 print "<div class=\"header\">\n" .
3389 $cgi->a({-href => href(%args), -class => "title"},
3390 $title ? $title : $action) .
3394 sub print_local_time {
3396 if ($date{'hour_local'} < 6) {
3397 printf(" (<span class=\"atnight\">%02d:%02d</span> %s)",
3398 $date{'hour_local'}, $date{'minute_local'}, $date{'tz_local'});
3400 printf(" (%02d:%02d %s)",
3401 $date{'hour_local'}, $date{'minute_local'}, $date{'tz_local'});
3405 # Outputs the author name and date in long form
3406 sub git_print_authorship {
3409 my $tag = $opts{-tag} || 'div';
3411 my %ad = parse_date($co->{'author_epoch'}, $co->{'author_tz'});
3412 print "<$tag class=\"author_date\">" .
3413 esc_html($co->{'author_name'}) .
3415 print_local_time(%ad) if ($opts{-localtime});
3416 print "]" . git_get_avatar($co->{'author_email'}, -pad_before => 1)
3420 # Outputs table rows containing the full author or committer information,
3421 # in the format expected for 'commit' view (& similia).
3422 # Parameters are a commit hash reference, followed by the list of people
3423 # to output information for. If the list is empty it defalts to both
3424 # author and committer.
3425 sub git_print_authorship_rows {
3427 # too bad we can't use @people = @_ || ('author', 'committer')
3429 @people = ('author', 'committer') unless @people;
3430 foreach my $who (@people) {
3431 my %wd = parse_date($co->{"${who}_epoch"}, $co->{"${who}_tz"});
3432 print "<tr><td>$who</td><td>" . esc_html($co->{$who}) . "</td>" .
3433 "<td rowspan=\"2\">" .
3434 git_get_avatar($co->{"${who}_email"}, -size => 'double') .
3437 "<td></td><td> $wd{'rfc2822'}";
3438 print_local_time(%wd);
3444 sub git_print_page_path {
3450 print "<div class=\"page_path\">";
3451 print $cgi->a({-href => href(action=>"tree", hash_base=>$hb),
3452 -title => 'tree root'}, to_utf8("[$project]"));
3454 if (defined $name) {
3455 my @dirname = split '/', $name;
3456 my $basename = pop @dirname;
3459 foreach my $dir (@dirname) {
3460 $fullname .= ($fullname ? '/' : '') . $dir;
3461 print $cgi->a({-href => href(action=>"tree", file_name=>$fullname,
3463 -title => $fullname}, esc_path($dir));
3466 if (defined $type && $type eq 'blob') {
3467 print $cgi->a({-href => href(action=>"blob_plain", file_name=>$file_name,
3469 -title => $name}, esc_path($basename));
3470 } elsif (defined $type && $type eq 'tree') {
3471 print $cgi->a({-href => href(action=>"tree", file_name=>$file_name,
3473 -title => $name}, esc_path($basename));
3476 print esc_path($basename);
3479 print "<br/></div>\n";
3486 if ($opts{'-remove_title'}) {
3487 # remove title, i.e. first line of log
3490 # remove leading empty lines
3491 while (defined $log->[0] && $log->[0] eq "") {
3498 foreach my $line (@$log) {
3499 if ($line =~ m/^ *(signed[ \-]off[ \-]by[ :]|acked[ \-]by[ :]|cc[ :])/i) {
3502 if (! $opts{'-remove_signoff'}) {
3503 print "<span class=\"signoff\">" . esc_html($line) . "</span><br/>\n";
3506 # remove signoff lines
3513 # print only one empty line
3514 # do not print empty line after signoff
3516 next if ($empty || $signoff);
3522 print format_log_line_html($line) . "<br/>\n";
3525 if ($opts{'-final_empty_line'}) {
3526 # end with single empty line
3527 print "<br/>\n" unless $empty;
3531 # return link target (what link points to)
3532 sub git_get_link_target {
3537 open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
3541 $link_target = <$fd>;
3546 return $link_target;
3549 # given link target, and the directory (basedir) the link is in,
3550 # return target of link relative to top directory (top tree);
3551 # return undef if it is not possible (including absolute links).
3552 sub normalize_link_target {
3553 my ($link_target, $basedir) = @_;
3555 # absolute symlinks (beginning with '/') cannot be normalized
3556 return if (substr($link_target, 0, 1) eq '/');
3558 # normalize link target to path from top (root) tree (dir)
3561 $path = $basedir . '/' . $link_target;
3563 # we are in top (root) tree (dir)
3564 $path = $link_target;
3567 # remove //, /./, and /../
3569 foreach my $part (split('/', $path)) {
3570 # discard '.' and ''
3571 next if (!$part || $part eq '.');
3573 if ($part eq '..') {
3577 # link leads outside repository (outside top dir)
3581 push @path_parts, $part;
3584 $path = join('/', @path_parts);
3589 # print tree entry (row of git_tree), but without encompassing <tr> element
3590 sub git_print_tree_entry {
3591 my ($t, $basedir, $hash_base, $have_blame) = @_;
3594 $base_key{'hash_base'} = $hash_base if defined $hash_base;
3596 # The format of a table row is: mode list link. Where mode is
3597 # the mode of the entry, list is the name of the entry, an href,
3598 # and link is the action links of the entry.
3600 print "<td class=\"mode\">" . mode_str($t->{'mode'}) . "</td>\n";
3601 if ($t->{'type'} eq "blob") {
3602 print "<td class=\"list\">" .
3603 $cgi->a({-href => href(action=>"blob", hash=>$t->{'hash'},
3604 file_name=>"$basedir$t->{'name'}", %base_key),
3605 -class => "list"}, esc_path($t->{'name'}));
3606 if (S_ISLNK(oct $t->{'mode'})) {
3607 my $link_target = git_get_link_target($t->{'hash'});
3609 my $norm_target = normalize_link_target($link_target, $basedir);
3610 if (defined $norm_target) {
3612 $cgi->a({-href => href(action=>"object", hash_base=>$hash_base,
3613 file_name=>$norm_target),
3614 -title => $norm_target}, esc_path($link_target));
3616 print " -> " . esc_path($link_target);
3621 print "<td class=\"link\">";
3622 print $cgi->a({-href => href(action=>"blob", hash=>$t->{'hash'},
3623 file_name=>"$basedir$t->{'name'}", %base_key)},
3627 $cgi->a({-href => href(action=>"blame", hash=>$t->{'hash'},
3628 file_name=>"$basedir$t->{'name'}", %base_key)},
3631 if (defined $hash_base) {
3633 $cgi->a({-href => href(action=>"history", hash_base=>$hash_base,
3634 hash=>$t->{'hash'}, file_name=>"$basedir$t->{'name'}")},
3638 $cgi->a({-href => href(action=>"blob_plain", hash_base=>$hash_base,
3639 file_name=>"$basedir$t->{'name'}")},
3643 } elsif ($t->{'type'} eq "tree") {
3644 print "<td class=\"list\">";
3645 print $cgi->a({-href => href(action=>"tree", hash=>$t->{'hash'},
3646 file_name=>"$basedir$t->{'name'}", %base_key)},
3647 esc_path($t->{'name'}));
3649 print "<td class=\"link\">";
3650 print $cgi->a({-href => href(action=>"tree", hash=>$t->{'hash'},
3651 file_name=>"$basedir$t->{'name'}", %base_key)},
3653 if (defined $hash_base) {
3655 $cgi->a({-href => href(action=>"history", hash_base=>$hash_base,
3656 file_name=>"$basedir$t->{'name'}")},
3661 # unknown object: we can only present history for it
3662 # (this includes 'commit' object, i.e. submodule support)
3663 print "<td class=\"list\">" .
3664 esc_path($t->{'name'}) .
3666 print "<td class=\"link\">";
3667 if (defined $hash_base) {
3668 print $cgi->a({-href => href(action=>"history",
3669 hash_base=>$hash_base,
3670 file_name=>"$basedir$t->{'name'}")},
3677 ## ......................................................................
3678 ## functions printing large fragments of HTML
3680 # get pre-image filenames for merge (combined) diff
3681 sub fill_from_file_info {
3682 my ($diff, @parents) = @_;
3684 $diff->{'from_file'} = [ ];
3685 $diff->{'from_file'}[$diff->{'nparents'} - 1] = undef;
3686 for (my $i = 0; $i < $diff->{'nparents'}; $i++) {
3687 if ($diff->{'status'}[$i] eq 'R' ||
3688 $diff->{'status'}[$i] eq 'C') {
3689 $diff->{'from_file'}[$i] =
3690 git_get_path_by_hash($parents[$i], $diff->{'from_id'}[$i]);
3697 # is current raw difftree line of file deletion
3699 my $diffinfo = shift;
3701 return $diffinfo->{'to_id'} eq ('0' x 40);
3704 # does patch correspond to [previous] difftree raw line
3705 # $diffinfo - hashref of parsed raw diff format
3706 # $patchinfo - hashref of parsed patch diff format
3707 # (the same keys as in $diffinfo)
3708 sub is_patch_split {
3709 my ($diffinfo, $patchinfo) = @_;
3711 return defined $diffinfo && defined $patchinfo
3712 && $diffinfo->{'to_file'} eq $patchinfo->{'to_file'};
3716 sub git_difftree_body {
3717 my ($difftree, $hash, @parents) = @_;
3718 my ($parent) = $parents[0];
3719 my $have_blame = gitweb_check_feature('blame');
3720 print "<div class=\"list_head\">\n";
3721 if ($#{$difftree} > 10) {
3722 print(($#{$difftree} + 1) . " files changed:\n");
3726 print "<table class=\"" .
3727 (@parents > 1 ? "combined " : "") .
3730 # header only for combined diff in 'commitdiff' view
3731 my $has_header = @$difftree && @parents > 1 && $action eq 'commitdiff';
3734 print "<thead><tr>\n" .
3735 "<th></th><th></th>\n"; # filename, patchN link
3736 for (my $i = 0; $i < @parents; $i++) {
3737 my $par = $parents[$i];
3739 $cgi->a({-href => href(action=>"commitdiff",
3740 hash=>$hash, hash_parent=>$par),
3741 -title => 'commitdiff to parent number ' .
3742 ($i+1) . ': ' . substr($par,0,7)},
3746 print "</tr></thead>\n<tbody>\n";
3751 foreach my $line (@{$difftree}) {
3752 my $diff = parsed_difftree_line($line);
3755 print "<tr class=\"dark\">\n";
3757 print "<tr class=\"light\">\n";
3761 if (exists $diff->{'nparents'}) { # combined diff
3763 fill_from_file_info($diff, @parents)
3764 unless exists $diff->{'from_file'};
3766 if (!is_deleted($diff)) {
3767 # file exists in the result (child) commit
3769 $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3770 file_name=>$diff->{'to_file'},
3772 -class => "list"}, esc_path($diff->{'to_file'})) .
3776 esc_path($diff->{'to_file'}) .
3780 if ($action eq 'commitdiff') {
3783 print "<td class=\"link\">" .
3784 $cgi->a({-href => "#patch$patchno"}, "patch") .
3789 my $has_history = 0;
3790 my $not_deleted = 0;
3791 for (my $i = 0; $i < $diff->{'nparents'}; $i++) {
3792 my $hash_parent = $parents[$i];
3793 my $from_hash = $diff->{'from_id'}[$i];
3794 my $from_path = $diff->{'from_file'}[$i];
3795 my $status = $diff->{'status'}[$i];
3797 $has_history ||= ($status ne 'A');
3798 $not_deleted ||= ($status ne 'D');
3800 if ($status eq 'A') {
3801 print "<td class=\"link\" align=\"right\"> | </td>\n";
3802 } elsif ($status eq 'D') {
3803 print "<td class=\"link\">" .
3804 $cgi->a({-href => href(action=>"blob",
3807 file_name=>$from_path)},
3811 if ($diff->{'to_id'} eq $from_hash) {
3812 print "<td class=\"link nochange\">";
3814 print "<td class=\"link\">";
3816 print $cgi->a({-href => href(action=>"blobdiff",
3817 hash=>$diff->{'to_id'},
3818 hash_parent=>$from_hash,
3820 hash_parent_base=>$hash_parent,
3821 file_name=>$diff->{'to_file'},
3822 file_parent=>$from_path)},
3828 print "<td class=\"link\">";
3830 print $cgi->a({-href => href(action=>"blob",
3831 hash=>$diff->{'to_id'},
3832 file_name=>$diff->{'to_file'},
3835 print " | " if ($has_history);
3838 print $cgi->a({-href => href(action=>"history",
3839 file_name=>$diff->{'to_file'},
3846 next; # instead of 'else' clause, to avoid extra indent
3848 # else ordinary diff
3850 my ($to_mode_oct, $to_mode_str, $to_file_type);
3851 my ($from_mode_oct, $from_mode_str, $from_file_type);
3852 if ($diff->{'to_mode'} ne ('0' x 6)) {
3853 $to_mode_oct = oct $diff->{'to_mode'};
3854 if (S_ISREG($to_mode_oct)) { # only for regular file
3855 $to_mode_str = sprintf("%04o", $to_mode_oct & 0777); # permission bits
3857 $to_file_type = file_type($diff->{'to_mode'});
3859 if ($diff->{'from_mode'} ne ('0' x 6)) {
3860 $from_mode_oct = oct $diff->{'from_mode'};
3861 if (S_ISREG($to_mode_oct)) { # only for regular file
3862 $from_mode_str = sprintf("%04o", $from_mode_oct & 0777); # permission bits
3864 $from_file_type = file_type($diff->{'from_mode'});
3867 if ($diff->{'status'} eq "A") { # created
3868 my $mode_chng = "<span class=\"file_status new\">[new $to_file_type";
3869 $mode_chng .= " with mode: $to_mode_str" if $to_mode_str;
3870 $mode_chng .= "]</span>";
3872 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3873 hash_base=>$hash, file_name=>$diff->{'file'}),
3874 -class => "list"}, esc_path($diff->{'file'}));
3876 print "<td>$mode_chng</td>\n";
3877 print "<td class=\"link\">";
3878 if ($action eq 'commitdiff') {
3881 print $cgi->a({-href => "#patch$patchno"}, "patch");
3884 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3885 hash_base=>$hash, file_name=>$diff->{'file'})},
3889 } elsif ($diff->{'status'} eq "D") { # deleted
3890 my $mode_chng = "<span class=\"file_status deleted\">[deleted $from_file_type]</span>";
3892 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'from_id'},
3893 hash_base=>$parent, file_name=>$diff->{'file'}),
3894 -class => "list"}, esc_path($diff->{'file'}));
3896 print "<td>$mode_chng</td>\n";
3897 print "<td class=\"link\">";
3898 if ($action eq 'commitdiff') {
3901 print $cgi->a({-href => "#patch$patchno"}, "patch");
3904 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'from_id'},
3905 hash_base=>$parent, file_name=>$diff->{'file'})},
3908 print $cgi->a({-href => href(action=>"blame", hash_base=>$parent,
3909 file_name=>$diff->{'file'})},
3912 print $cgi->a({-href => href(action=>"history", hash_base=>$parent,
3913 file_name=>$diff->{'file'})},
3917 } elsif ($diff->{'status'} eq "M" || $diff->{'status'} eq "T") { # modified, or type changed
3918 my $mode_chnge = "";
3919 if ($diff->{'from_mode'} != $diff->{'to_mode'}) {
3920 $mode_chnge = "<span class=\"file_status mode_chnge\">[changed";
3921 if ($from_file_type ne $to_file_type) {
3922 $mode_chnge .= " from $from_file_type to $to_file_type";
3924 if (($from_mode_oct & 0777) != ($to_mode_oct & 0777)) {
3925 if ($from_mode_str && $to_mode_str) {
3926 $mode_chnge .= " mode: $from_mode_str->$to_mode_str";
3927 } elsif ($to_mode_str) {
3928 $mode_chnge .= " mode: $to_mode_str";
3931 $mode_chnge .= "]</span>\n";
3934 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3935 hash_base=>$hash, file_name=>$diff->{'file'}),
3936 -class => "list"}, esc_path($diff->{'file'}));
3938 print "<td>$mode_chnge</td>\n";
3939 print "<td class=\"link\">";
3940 if ($action eq 'commitdiff') {
3943 print $cgi->a({-href => "#patch$patchno"}, "patch") .
3945 } elsif ($diff->{'to_id'} ne $diff->{'from_id'}) {
3946 # "commit" view and modified file (not onlu mode changed)
3947 print $cgi->a({-href => href(action=>"blobdiff",
3948 hash=>$diff->{'to_id'}, hash_parent=>$diff->{'from_id'},
3949 hash_base=>$hash, hash_parent_base=>$parent,
3950 file_name=>$diff->{'file'})},
3954 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
3955 hash_base=>$hash, file_name=>$diff->{'file'})},
3958 print $cgi->a({-href => href(action=>"blame", hash_base=>$hash,
3959 file_name=>$diff->{'file'})},
3962 print $cgi->a({-href => href(action=>"history", hash_base=>$hash,
3963 file_name=>$diff->{'file'})},
3967 } elsif ($diff->{'status'} eq "R" || $diff->{'status'} eq "C") { # renamed or copied
3968 my %status_name = ('R' => 'moved', 'C' => 'copied');
3969 my $nstatus = $status_name{$diff->{'status'}};
3971 if ($diff->{'from_mode'} != $diff->{'to_mode'}) {
3972 # mode also for directories, so we cannot use $to_mode_str
3973 $mode_chng = sprintf(", mode: %04o", $to_mode_oct & 0777);
3976 $cgi->a({-href => href(action=>"blob", hash_base=>$hash,
3977 hash=>$diff->{'to_id'}, file_name=>$diff->{'to_file'}),
3978 -class => "list"}, esc_path($diff->{'to_file'})) . "</td>\n" .
3979 "<td><span class=\"file_status $nstatus\">[$nstatus from " .
3980 $cgi->a({-href => href(action=>"blob", hash_base=>$parent,
3981 hash=>$diff->{'from_id'}, file_name=>$diff->{'from_file'}),
3982 -class => "list"}, esc_path($diff->{'from_file'})) .
3983 " with " . (int $diff->{'similarity'}) . "% similarity$mode_chng]</span></td>\n" .
3984 "<td class=\"link\">";
3985 if ($action eq 'commitdiff') {
3988 print $cgi->a({-href => "#patch$patchno"}, "patch") .
3990 } elsif ($diff->{'to_id'} ne $diff->{'from_id'}) {
3991 # "commit" view and modified file (not only pure rename or copy)
3992 print $cgi->a({-href => href(action=>"blobdiff",
3993 hash=>$diff->{'to_id'}, hash_parent=>$diff->{'from_id'},
3994 hash_base=>$hash, hash_parent_base=>$parent,
3995 file_name=>$diff->{'to_file'}, file_parent=>$diff->{'from_file'})},
3999 print $cgi->a({-href => href(action=>"blob", hash=>$diff->{'to_id'},
4000 hash_base=>$parent, file_name=>$diff->{'to_file'})},
4003 print $cgi->a({-href => href(action=>"blame", hash_base=>$hash,
4004 file_name=>$diff->{'to_file'})},
4007 print $cgi->a({-href => href(action=>"history", hash_base=>$hash,
4008 file_name=>$diff->{'to_file'})},
4012 } # we should not encounter Unmerged (U) or Unknown (X) status
4015 print "</tbody>" if $has_header;
4019 sub git_patchset_body {
4020 my ($fd, $difftree, $hash, @hash_parents) = @_;
4021 my ($hash_parent) = $hash_parents[0];
4023 my $is_combined = (@hash_parents > 1);
4025 my $patch_number = 0;
4031 print "<div class=\"patchset\">\n";
4033 # skip to first patch
4034 while ($patch_line = <$fd>) {
4037 last if ($patch_line =~ m/^diff /);
4041 while ($patch_line) {
4043 # parse "git diff" header line
4044 if ($patch_line =~ m/^diff --git (\"(?:[^\\\"]*(?:\\.[^\\\"]*)*)\"|[^ "]*) (.*)$/) {
4045 # $1 is from_name, which we do not use
4046 $to_name = unquote($2);
4047 $to_name =~ s!^b/!!;
4048 } elsif ($patch_line =~ m/^diff --(cc|combined) ("?.*"?)$/) {
4049 # $1 is 'cc' or 'combined', which we do not use
4050 $to_name = unquote($2);
4055 # check if current patch belong to current raw line
4056 # and parse raw git-diff line if needed
4057 if (is_patch_split($diffinfo, { 'to_file' => $to_name })) {
4058 # this is continuation of a split patch
4059 print "<div class=\"patch cont\">\n";
4061 # advance raw git-diff output if needed
4062 $patch_idx++ if defined $diffinfo;
4064 # read and prepare patch information
4065 $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
4067 # compact combined diff output can have some patches skipped
4068 # find which patch (using pathname of result) we are at now;
4070 while ($to_name ne $diffinfo->{'to_file'}) {
4071 print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n" .
4072 format_diff_cc_simplified($diffinfo, @hash_parents) .
4073 "</div>\n"; # class="patch"
4078 last if $patch_idx > $#$difftree;
4079 $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
4083 # modifies %from, %to hashes
4084 parse_from_to_diffinfo($diffinfo, \%from, \%to, @hash_parents);
4086 # this is first patch for raw difftree line with $patch_idx index
4087 # we index @$difftree array from 0, but number patches from 1
4088 print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n";
4092 #assert($patch_line =~ m/^diff /) if DEBUG;
4093 #assert($patch_line !~ m!$/$!) if DEBUG; # is chomp-ed
4095 # print "git diff" header
4096 print format_git_diff_header_line($patch_line, $diffinfo,
4099 # print extended diff header
4100 print "<div class=\"diff extended_header\">\n";
4102 while ($patch_line = <$fd>) {
4105 last EXTENDED_HEADER if ($patch_line =~ m/^--- |^diff /);
4107 print format_extended_diff_header_line($patch_line, $diffinfo,
4110 print "</div>\n"; # class="diff extended_header"
4112 # from-file/to-file diff header
4113 if (! $patch_line) {
4114 print "</div>\n"; # class="patch"
4117 next PATCH if ($patch_line =~ m/^diff /);
4118 #assert($patch_line =~ m/^---/) if DEBUG;
4120 my $last_patch_line = $patch_line;
4121 $patch_line = <$fd>;
4123 #assert($patch_line =~ m/^\+\+\+/) if DEBUG;
4125 print format_diff_from_to_header($last_patch_line, $patch_line,
4126 $diffinfo, \%from, \%to,
4131 while ($patch_line = <$fd>) {
4134 next PATCH if ($patch_line =~ m/^diff /);
4136 print format_diff_line($patch_line, \%from, \%to);
4140 print "</div>\n"; # class="patch"
4143 # for compact combined (--cc) format, with chunk and patch simpliciaction
4144 # patchset might be empty, but there might be unprocessed raw lines
4145 for (++$patch_idx if $patch_number > 0;
4146 $patch_idx < @$difftree;
4148 # read and prepare patch information
4149 $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
4151 # generate anchor for "patch" links in difftree / whatchanged part
4152 print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n" .
4153 format_diff_cc_simplified($diffinfo, @hash_parents) .
4154 "</div>\n"; # class="patch"
4159 if ($patch_number == 0) {
4160 if (@hash_parents > 1) {
4161 print "<div class=\"diff nodifferences\">Trivial merge</div>\n";
4163 print "<div class=\"diff nodifferences\">No differences found</div>\n";
4167 print "</div>\n"; # class="patchset"
4170 # . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
4172 # fills project list info (age, description, owner, forks) for each
4173 # project in the list, removing invalid projects from returned list
4174 # NOTE: modifies $projlist, but does not remove entries from it
4175 sub fill_project_list_info {
4176 my ($projlist, $check_forks) = @_;
4179 my $show_ctags = gitweb_check_feature('ctags');
4181 foreach my $pr (@$projlist) {
4182 my (@activity) = git_get_last_activity($pr->{'path'});
4183 unless (@activity) {
4186 ($pr->{'age'}, $pr->{'age_string'}) = @activity;
4187 if (!defined $pr->{'descr'}) {
4188 my $descr = git_get_project_description($pr->{'path'}) || "";
4189 $descr = to_utf8($descr);
4190 $pr->{'descr_long'} = $descr;
4191 $pr->{'descr'} = chop_str($descr, $projects_list_description_width, 5);
4193 if (!defined $pr->{'owner'}) {
4194 $pr->{'owner'} = git_get_project_owner("$pr->{'path'}") || "";
4197 my $pname = $pr->{'path'};
4198 if (($pname =~ s/\.git$//) &&
4199 ($pname !~ /\/$/) &&
4200 (-d "$projectroot/$pname")) {
4201 $pr->{'forks'} = "-d $projectroot/$pname";
4206 $show_ctags and $pr->{'ctags'} = git_get_project_ctags($pr->{'path'});
4207 push @projects, $pr;
4213 # print 'sort by' <th> element, generating 'sort by $name' replay link
4214 # if that order is not selected
4216 my ($name, $order, $header) = @_;
4217 $header ||= ucfirst($name);
4219 if ($order eq $name) {
4220 print "<th>$header</th>\n";
4223 $cgi->a({-href => href(-replay=>1, order=>$name),
4224 -class => "header"}, $header) .
4229 sub git_project_list_body {
4230 # actually uses global variable $project
4231 my ($projlist, $order, $from, $to, $extra, $no_header) = @_;
4233 my $check_forks = gitweb_check_feature('forks');
4234 my @projects = fill_project_list_info($projlist, $check_forks);
4236 $order ||= $default_projects_order;
4237 $from = 0 unless defined $from;
4238 $to = $#projects if (!defined $to || $#projects < $to);
4241 project => { key => 'path', type => 'str' },
4242 descr => { key => 'descr_long', type => 'str' },
4243 owner => { key => 'owner', type => 'str' },
4244 age => { key => 'age', type => 'num' }
4246 my $oi = $order_info{$order};
4247 if ($oi->{'type'} eq 'str') {
4248 @projects = sort {$a->{$oi->{'key'}} cmp $b->{$oi->{'key'}}} @projects;
4250 @projects = sort {$a->{$oi->{'key'}} <=> $b->{$oi->{'key'}}} @projects;
4253 my $show_ctags = gitweb_check_feature('ctags');
4256 foreach my $p (@projects) {
4257 foreach my $ct (keys %{$p->{'ctags'}}) {
4258 $ctags{$ct} += $p->{'ctags'}->{$ct};
4261 my $cloud = git_populate_project_tagcloud(\%ctags);
4262 print git_show_project_tagcloud($cloud, 64);
4265 print "<table class=\"project_list\">\n";
4266 unless ($no_header) {
4269 print "<th></th>\n";
4271 print_sort_th('project', $order, 'Project');
4272 print_sort_th('descr', $order, 'Description');
4273 print_sort_th('owner', $order, 'Owner');
4274 print_sort_th('age', $order, 'Last Change');
4275 print "<th></th>\n" . # for links
4279 my $tagfilter = $cgi->param('by_tag');
4280 for (my $i = $from; $i <= $to; $i++) {
4281 my $pr = $projects[$i];
4283 next if $tagfilter and $show_ctags and not grep { lc $_ eq lc $tagfilter } keys %{$pr->{'ctags'}};
4284 next if $searchtext and not $pr->{'path'} =~ /$searchtext/
4285 and not $pr->{'descr_long'} =~ /$searchtext/;
4286 # Weed out forks or non-matching entries of search
4288 my $forkbase = $project; $forkbase ||= ''; $forkbase =~ s#\.git$#/#;
4289 $forkbase="^$forkbase" if $forkbase;
4290 next if not $searchtext and not $tagfilter and $show_ctags
4291 and $pr->{'path'} =~ m#$forkbase.*/.*#; # regexp-safe
4295 print "<tr class=\"dark\">\n";
4297 print "<tr class=\"light\">\n";
4302 if ($pr->{'forks'}) {
4303 print "<!-- $pr->{'forks'} -->\n";
4304 print $cgi->a({-href => href(project=>$pr->{'path'}, action=>"forks")}, "+");
4308 print "<td>" . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary"),
4309 -class => "list"}, esc_html($pr->{'path'})) . "</td>\n" .
4310 "<td>" . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary"),
4311 -class => "list", -title => $pr->{'descr_long'}},
4312 esc_html($pr->{'descr'})) . "</td>\n" .
4313 "<td><i>" . chop_and_escape_str($pr->{'owner'}, 15) . "</i></td>\n";
4314 print "<td class=\"". age_class($pr->{'age'}) . "\">" .
4315 (defined $pr->{'age_string'} ? $pr->{'age_string'} : "No commits") . "</td>\n" .
4316 "<td class=\"link\">" .
4317 $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary")}, "summary") . " | " .
4318 $cgi->a({-href => href(project=>$pr->{'path'}, action=>"shortlog")}, "shortlog") . " | " .
4319 $cgi->a({-href => href(project=>$pr->{'path'}, action=>"log")}, "log") . " | " .
4320 $cgi->a({-href => href(project=>$pr->{'path'}, action=>"tree")}, "tree") .
4321 ($pr->{'forks'} ? " | " . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"forks")}, "forks") : '') .
4325 if (defined $extra) {
4328 print "<td></td>\n";
4330 print "<td colspan=\"5\">$extra</td>\n" .
4336 sub git_shortlog_body {
4337 # uses global variable $project
4338 my ($commitlist, $from, $to, $refs, $extra) = @_;
4340 $from = 0 unless defined $from;
4341 $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
4343 print "<table class=\"shortlog\">\n";
4345 for (my $i = $from; $i <= $to; $i++) {
4346 my %co = %{$commitlist->[$i]};
4347 my $commit = $co{'id'};
4348 my $ref = format_ref_marker($refs, $commit);
4350 print "<tr class=\"dark\">\n";
4352 print "<tr class=\"light\">\n";
4355 # git_summary() used print "<td><i>$co{'age_string'}</i></td>\n" .
4356 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
4357 format_author_html('td', \%co, 10) . "<td>";
4358 print format_subject_html($co{'title'}, $co{'title_short'},
4359 href(action=>"commit", hash=>$commit), $ref);
4361 "<td class=\"link\">" .
4362 $cgi->a({-href => href(action=>"commit", hash=>$commit)}, "commit") . " | " .
4363 $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff") . " | " .
4364 $cgi->a({-href => href(action=>"tree", hash=>$commit, hash_base=>$commit)}, "tree");
4365 my $snapshot_links = format_snapshot_links($commit);
4366 if (defined $snapshot_links) {
4367 print " | " . $snapshot_links;
4372 if (defined $extra) {
4374 "<td colspan=\"4\">$extra</td>\n" .
4380 sub git_history_body {
4381 # Warning: assumes constant type (blob or tree) during history
4382 my ($commitlist, $from, $to, $refs, $hash_base, $ftype, $extra) = @_;
4384 $from = 0 unless defined $from;
4385 $to = $#{$commitlist} unless (defined $to && $to <= $#{$commitlist});
4387 print "<table class=\"history\">\n";
4389 for (my $i = $from; $i <= $to; $i++) {
4390 my %co = %{$commitlist->[$i]};
4394 my $commit = $co{'id'};
4396 my $ref = format_ref_marker($refs, $commit);
4399 print "<tr class=\"dark\">\n";
4401 print "<tr class=\"light\">\n";
4404 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
4405 # shortlog: format_author_html('td', \%co, 10)
4406 format_author_html('td', \%co, 15, 3) . "<td>";
4407 # originally git_history used chop_str($co{'title'}, 50)
4408 print format_subject_html($co{'title'}, $co{'title_short'},
4409 href(action=>"commit", hash=>$commit), $ref);
4411 "<td class=\"link\">" .
4412 $cgi->a({-href => href(action=>$ftype, hash_base=>$commit, file_name=>$file_name)}, $ftype) . " | " .
4413 $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff");
4415 if ($ftype eq 'blob') {
4416 my $blob_current = git_get_hash_by_path($hash_base, $file_name);
4417 my $blob_parent = git_get_hash_by_path($commit, $file_name);
4418 if (defined $blob_current && defined $blob_parent &&
4419 $blob_current ne $blob_parent) {
4421 $cgi->a({-href => href(action=>"blobdiff",
4422 hash=>$blob_current, hash_parent=>$blob_parent,
4423 hash_base=>$hash_base, hash_parent_base=>$commit,
4424 file_name=>$file_name)},
4431 if (defined $extra) {
4433 "<td colspan=\"4\">$extra</td>\n" .
4440 # uses global variable $project
4441 my ($taglist, $from, $to, $extra) = @_;
4442 $from = 0 unless defined $from;
4443 $to = $#{$taglist} if (!defined $to || $#{$taglist} < $to);
4445 print "<table class=\"tags\">\n";
4447 for (my $i = $from; $i <= $to; $i++) {
4448 my $entry = $taglist->[$i];
4450 my $comment = $tag{'subject'};
4452 if (defined $comment) {
4453 $comment_short = chop_str($comment, 30, 5);
4456 print "<tr class=\"dark\">\n";
4458 print "<tr class=\"light\">\n";
4461 if (defined $tag{'age'}) {
4462 print "<td><i>$tag{'age'}</i></td>\n";
4464 print "<td></td>\n";
4467 $cgi->a({-href => href(action=>$tag{'reftype'}, hash=>$tag{'refid'}),
4468 -class => "list name"}, esc_html($tag{'name'})) .
4471 if (defined $comment) {
4472 print format_subject_html($comment, $comment_short,
4473 href(action=>"tag", hash=>$tag{'id'}));
4476 "<td class=\"selflink\">";
4477 if ($tag{'type'} eq "tag") {
4478 print $cgi->a({-href => href(action=>"tag", hash=>$tag{'id'})}, "tag");
4483 "<td class=\"link\">" . " | " .
4484 $cgi->a({-href => href(action=>$tag{'reftype'}, hash=>$tag{'refid'})}, $tag{'reftype'});
4485 if ($tag{'reftype'} eq "commit") {
4486 print " | " . $cgi->a({-href => href(action=>"shortlog", hash=>$tag{'fullname'})}, "shortlog") .
4487 " | " . $cgi->a({-href => href(action=>"log", hash=>$tag{'fullname'})}, "log");
4488 } elsif ($tag{'reftype'} eq "blob") {
4489 print " | " . $cgi->a({-href => href(action=>"blob_plain", hash=>$tag{'refid'})}, "raw");
4494 if (defined $extra) {
4496 "<td colspan=\"5\">$extra</td>\n" .
4502 sub git_heads_body {
4503 # uses global variable $project
4504 my ($headlist, $head, $from, $to, $extra) = @_;
4505 $from = 0 unless defined $from;
4506 $to = $#{$headlist} if (!defined $to || $#{$headlist} < $to);
4508 print "<table class=\"heads\">\n";
4510 for (my $i = $from; $i <= $to; $i++) {
4511 my $entry = $headlist->[$i];
4513 my $curr = $ref{'id'} eq $head;
4515 print "<tr class=\"dark\">\n";
4517 print "<tr class=\"light\">\n";
4520 print "<td><i>$ref{'age'}</i></td>\n" .
4521 ($curr ? "<td class=\"current_head\">" : "<td>") .
4522 $cgi->a({-href => href(action=>"shortlog", hash=>$ref{'fullname'}),
4523 -class => "list name"},esc_html($ref{'name'})) .
4525 "<td class=\"link\">" .
4526 $cgi->a({-href => href(action=>"shortlog", hash=>$ref{'fullname'})}, "shortlog") . " | " .
4527 $cgi->a({-href => href(action=>"log", hash=>$ref{'fullname'})}, "log") . " | " .
4528 $cgi->a({-href => href(action=>"tree", hash=>$ref{'fullname'}, hash_base=>$ref{'name'})}, "tree") .
4532 if (defined $extra) {
4534 "<td colspan=\"3\">$extra</td>\n" .
4540 sub git_search_grep_body {
4541 my ($commitlist, $from, $to, $extra) = @_;
4542 $from = 0 unless defined $from;
4543 $to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
4545 print "<table class=\"commit_search\">\n";
4547 for (my $i = $from; $i <= $to; $i++) {
4548 my %co = %{$commitlist->[$i]};
4552 my $commit = $co{'id'};
4554 print "<tr class=\"dark\">\n";
4556 print "<tr class=\"light\">\n";
4559 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
4560 format_author_html('td', \%co, 15, 5) .
4562 $cgi->a({-href => href(action=>"commit", hash=>$co{'id'}),
4563 -class => "list subject"},
4564 chop_and_escape_str($co{'title'}, 50) . "<br/>");
4565 my $comment = $co{'comment'};
4566 foreach my $line (@$comment) {
4567 if ($line =~ m/^(.*?)($search_regexp)(.*)$/i) {
4568 my ($lead, $match, $trail) = ($1, $2, $3);
4569 $match = chop_str($match, 70, 5, 'center');
4570 my $contextlen = int((80 - length($match))/2);
4571 $contextlen = 30 if ($contextlen > 30);
4572 $lead = chop_str($lead, $contextlen, 10, 'left');
4573 $trail = chop_str($trail, $contextlen, 10, 'right');
4575 $lead = esc_html($lead);
4576 $match = esc_html($match);
4577 $trail = esc_html($trail);
4579 print "$lead<span class=\"match\">$match</span>$trail<br />";
4583 "<td class=\"link\">" .
4584 $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") .
4586 $cgi->a({-href => href(action=>"commitdiff", hash=>$co{'id'})}, "commitdiff") .
4588 $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree");
4592 if (defined $extra) {
4594 "<td colspan=\"3\">$extra</td>\n" .
4600 ## ======================================================================
4601 ## ======================================================================
4604 sub git_project_list {
4605 my $order = $input_params{'order'};
4606 if (defined $order && $order !~ m/none|project|descr|owner|age/) {
4607 die_error(400, "Unknown order parameter");
4610 my @list = git_get_projects_list();
4612 die_error(404, "No projects found");
4616 if (-f $home_text) {
4617 print "<div class=\"index_include\">\n";
4618 insert_file($home_text);
4621 print $cgi->startform(-method => "get") .
4622 "<p class=\"projsearch\">Search:\n" .
4623 $cgi->textfield(-name => "s", -value => $searchtext) . "\n" .
4625 $cgi->end_form() . "\n";
4626 git_project_list_body(\@list, $order);
4631 my $order = $input_params{'order'};
4632 if (defined $order && $order !~ m/none|project|descr|owner|age/) {
4633 die_error(400, "Unknown order parameter");
4636 my @list = git_get_projects_list($project);
4638 die_error(404, "No forks found");
4642 git_print_page_nav('','');
4643 git_print_header_div('summary', "$project forks");
4644 git_project_list_body(\@list, $order);
4648 sub git_project_index {
4649 my @projects = git_get_projects_list($project);
4652 -type => 'text/plain',
4653 -charset => 'utf-8',
4654 -content_disposition => 'inline; filename="index.aux"');
4656 foreach my $pr (@projects) {
4657 if (!exists $pr->{'owner'}) {
4658 $pr->{'owner'} = git_get_project_owner("$pr->{'path'}");
4661 my ($path, $owner) = ($pr->{'path'}, $pr->{'owner'});
4662 # quote as in CGI::Util::encode, but keep the slash, and use '+' for ' '
4663 $path =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf("%%%02X", ord($1))/eg;
4664 $owner =~ s/([^a-zA-Z0-9_.\-\/ ])/sprintf("%%%02X", ord($1))/eg;
4668 print "$path $owner\n";
4673 my $descr = git_get_project_description($project) || "none";
4674 my %co = parse_commit("HEAD");
4675 my %cd = %co ? parse_date($co{'committer_epoch'}, $co{'committer_tz'}) : ();
4676 my $head = $co{'id'};
4678 my $owner = git_get_project_owner($project);
4680 my $refs = git_get_references();
4681 # These get_*_list functions return one more to allow us to see if
4682 # there are more ...
4683 my @taglist = git_get_tags_list(16);
4684 my @headlist = git_get_heads_list(16);
4686 my $check_forks = gitweb_check_feature('forks');
4689 @forklist = git_get_projects_list($project);
4693 git_print_page_nav('summary','', $head);
4695 print "<div class=\"title\"> </div>\n";
4696 print "<table class=\"projects_list\">\n" .
4697 "<tr id=\"metadata_desc\"><td>description</td><td>" . esc_html($descr) . "</td></tr>\n" .
4698 "<tr id=\"metadata_owner\"><td>owner</td><td>" . esc_html($owner) . "</td></tr>\n";
4699 if (defined $cd{'rfc2822'}) {
4700 print "<tr id=\"metadata_lchange\"><td>last change</td><td>$cd{'rfc2822'}</td></tr>\n";
4703 # use per project git URL list in $projectroot/$project/cloneurl
4704 # or make project git URL from git base URL and project name
4705 my $url_tag = "URL";
4706 my @url_list = git_get_project_url_list($project);
4707 @url_list = map { "$_/$project" } @git_base_url_list unless @url_list;
4708 foreach my $git_url (@url_list) {
4709 next unless $git_url;
4710 print "<tr class=\"metadata_url\"><td>$url_tag</td><td>$git_url</td></tr>\n";
4715 my $show_ctags = gitweb_check_feature('ctags');
4717 my $ctags = git_get_project_ctags($project);
4718 my $cloud = git_populate_project_tagcloud($ctags);
4719 print "<tr id=\"metadata_ctags\"><td>Content tags:<br />";
4720 print "</td>\n<td>" unless %$ctags;
4721 print "<form action=\"$show_ctags\" method=\"post\"><input type=\"hidden\" name=\"p\" value=\"$project\" />Add: <input type=\"text\" name=\"t\" size=\"8\" /></form>";
4722 print "</td>\n<td>" if %$ctags;
4723 print git_show_project_tagcloud($cloud, 48);
4729 # If XSS prevention is on, we don't include README.html.
4730 # TODO: Allow a readme in some safe format.
4731 if (!$prevent_xss && -s "$projectroot/$project/README.html") {
4732 print "<div class=\"title\">readme</div>\n" .
4733 "<div class=\"readme\">\n";
4734 insert_file("$projectroot/$project/README.html");
4735 print "\n</div>\n"; # class="readme"
4738 # we need to request one more than 16 (0..15) to check if
4740 my @commitlist = $head ? parse_commits($head, 17) : ();
4742 git_print_header_div('shortlog');
4743 git_shortlog_body(\@commitlist, 0, 15, $refs,
4744 $#commitlist <= 15 ? undef :
4745 $cgi->a({-href => href(action=>"shortlog")}, "..."));
4749 git_print_header_div('tags');
4750 git_tags_body(\@taglist, 0, 15,
4751 $#taglist <= 15 ? undef :
4752 $cgi->a({-href => href(action=>"tags")}, "..."));
4756 git_print_header_div('heads');
4757 git_heads_body(\@headlist, $head, 0, 15,
4758 $#headlist <= 15 ? undef :
4759 $cgi->a({-href => href(action=>"heads")}, "..."));
4763 git_print_header_div('forks');
4764 git_project_list_body(\@forklist, 'age', 0, 15,
4765 $#forklist <= 15 ? undef :
4766 $cgi->a({-href => href(action=>"forks")}, "..."),
4774 my $head = git_get_head_hash($project);
4776 git_print_page_nav('','', $head,undef,$head);
4777 my %tag = parse_tag($hash);
4780 die_error(404, "Unknown tag object");
4783 git_print_header_div('commit', esc_html($tag{'name'}), $hash);
4784 print "<div class=\"title_text\">\n" .
4785 "<table class=\"object_header\">\n" .
4787 "<td>object</td>\n" .
4788 "<td>" . $cgi->a({-class => "list", -href => href(action=>$tag{'type'}, hash=>$tag{'object'})},
4789 $tag{'object'}) . "</td>\n" .
4790 "<td class=\"link\">" . $cgi->a({-href => href(action=>$tag{'type'}, hash=>$tag{'object'})},
4791 $tag{'type'}) . "</td>\n" .
4793 if (defined($tag{'author'})) {
4794 git_print_authorship_rows(\%tag, 'author');
4796 print "</table>\n\n" .
4798 print "<div class=\"page_body\">";
4799 my $comment = $tag{'comment'};
4800 foreach my $line (@$comment) {
4802 print esc_html($line, -nbsp=>1) . "<br/>\n";
4808 sub git_blame_common {
4809 my $format = shift || 'porcelain';
4810 if ($format eq 'porcelain' && $cgi->param('js')) {
4811 $format = 'incremental';
4812 $action = 'blame_incremental'; # for page title etc
4816 gitweb_check_feature('blame')
4817 or die_error(403, "Blame view not allowed");
4820 die_error(400, "No file name given") unless $file_name;
4821 $hash_base ||= git_get_head_hash($project);
4822 die_error(404, "Couldn't find base commit") unless $hash_base;
4823 my %co = parse_commit($hash_base)
4824 or die_error(404, "Commit not found");
4826 if (!defined $hash) {
4827 $hash = git_get_hash_by_path($hash_base, $file_name, "blob")
4828 or die_error(404, "Error looking up file");
4830 $ftype = git_get_type($hash);
4831 if ($ftype !~ "blob") {
4832 die_error(400, "Object is not a blob");
4837 if ($format eq 'incremental') {
4838 # get file contents (as base)
4839 open $fd, "-|", git_cmd(), 'cat-file', 'blob', $hash
4840 or die_error(500, "Open git-cat-file failed");
4841 } elsif ($format eq 'data') {
4842 # run git-blame --incremental
4843 open $fd, "-|", git_cmd(), "blame", "--incremental",
4844 $hash_base, "--", $file_name
4845 or die_error(500, "Open git-blame --incremental failed");
4847 # run git-blame --porcelain
4848 open $fd, "-|", git_cmd(), "blame", '-p',
4849 $hash_base, '--', $file_name
4850 or die_error(500, "Open git-blame --porcelain failed");
4853 # incremental blame data returns early
4854 if ($format eq 'data') {
4856 -type=>"text/plain", -charset => "utf-8",
4857 -status=> "200 OK");
4858 local $| = 1; # output autoflush
4861 or print "ERROR $!\n";
4864 if (defined $t0 && gitweb_check_feature('timed')) {
4866 Time::HiRes::tv_interval($t0, [Time::HiRes::gettimeofday()]).
4867 ' '.$number_of_git_cmds;
4877 $cgi->a({-href => href(action=>"blob", -replay=>1)},
4880 $cgi->a({-href => href(action=>"history", -replay=>1)},
4883 $cgi->a({-href => href(action=>$action, file_name=>$file_name)},
4885 git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
4886 git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
4887 git_print_page_path($file_name, $ftype, $hash_base);
4890 if ($format eq 'incremental') {
4891 print "<noscript>\n<div class=\"error\"><center><b>\n".
4892 "This page requires JavaScript to run.\n Use ".
4893 $cgi->a({-href => href(action=>'blame',javascript=>0,-replay=>1)},
4896 "</b></center></div>\n</noscript>\n";
4898 print qq!<div id="progress_bar" style="width: 100%; background-color: yellow"></div>\n!;
4901 print qq!<div class="page_body">\n!;
4902 print qq!<div id="progress_info">... / ...</div>\n!
4903 if ($format eq 'incremental');
4904 print qq!<table id="blame_table" class="blame" width="100%">\n!.
4905 #qq!<col width="5.5em" /><col width="2.5em" /><col width="*" />\n!.
4907 qq!<tr><th>Commit</th><th>Line</th><th>Data</th></tr>\n!.
4911 my @rev_color = qw(light dark);
4912 my $num_colors = scalar(@rev_color);
4913 my $current_color = 0;
4915 if ($format eq 'incremental') {
4916 my $color_class = $rev_color[$current_color];
4921 while (my $line = <$fd>) {
4925 print qq!<tr id="l$linenr" class="$color_class">!.
4926 qq!<td class="sha1"><a href=""> </a></td>!.
4927 qq!<td class="linenr">!.
4928 qq!<a class="linenr" href="">$linenr</a></td>!;
4929 print qq!<td class="pre">! . esc_html($line) . "</td>\n";
4933 } else { # porcelain, i.e. ordinary blame
4934 my %metainfo = (); # saves information about commits
4938 while (my $line = <$fd>) {
4940 # the header: <SHA-1> <src lineno> <dst lineno> [<lines in group>]
4941 # no <lines in group> for subsequent lines in group of lines
4942 my ($full_rev, $orig_lineno, $lineno, $group_size) =
4943 ($line =~ /^([0-9a-f]{40}) (\d+) (\d+)(?: (\d+))?$/);
4944 if (!exists $metainfo{$full_rev}) {
4945 $metainfo{$full_rev} = { 'nprevious' => 0 };
4947 my $meta = $metainfo{$full_rev};
4949 while ($data = <$fd>) {
4951 last if ($data =~ s/^\t//); # contents of line
4952 if ($data =~ /^(\S+)(?: (.*))?$/) {
4953 $meta->{$1} = $2 unless exists $meta->{$1};
4955 if ($data =~ /^previous /) {
4956 $meta->{'nprevious'}++;
4959 my $short_rev = substr($full_rev, 0, 8);
4960 my $author = $meta->{'author'};
4962 parse_date($meta->{'author-time'}, $meta->{'author-tz'});
4963 my $date = $date{'iso-tz'};
4965 $current_color = ($current_color + 1) % $num_colors;
4967 my $tr_class = $rev_color[$current_color];
4968 $tr_class .= ' boundary' if (exists $meta->{'boundary'});
4969 $tr_class .= ' no-previous' if ($meta->{'nprevious'} == 0);
4970 $tr_class .= ' multiple-previous' if ($meta->{'nprevious'} > 1);
4971 print "<tr id=\"l$lineno\" class=\"$tr_class\">\n";
4973 print "<td class=\"sha1\"";
4974 print " title=\"". esc_html($author) . ", $date\"";
4975 print " rowspan=\"$group_size\"" if ($group_size > 1);
4977 print $cgi->a({-href => href(action=>"commit",
4979 file_name=>$file_name)},
4980 esc_html($short_rev));
4981 if ($group_size >= 2) {
4982 my @author_initials = ($author =~ /\b([[:upper:]])\B/g);
4983 if (@author_initials) {
4985 esc_html(join('', @author_initials));
4991 # 'previous' <sha1 of parent commit> <filename at commit>
4992 if (exists $meta->{'previous'} &&
4993 $meta->{'previous'} =~ /^([a-fA-F0-9]{40}) (.*)$/) {
4994 $meta->{'parent'} = $1;
4995 $meta->{'file_parent'} = unquote($2);
4998 exists($meta->{'parent'}) ?
4999 $meta->{'parent'} : $full_rev;
5000 my $linenr_filename =
5001 exists($meta->{'file_parent'}) ?
5002 $meta->{'file_parent'} : unquote($meta->{'filename'});
5003 my $blamed = href(action => 'blame',
5004 file_name => $linenr_filename,
5005 hash_base => $linenr_commit);
5006 print "<td class=\"linenr\">";
5007 print $cgi->a({ -href => "$blamed#l$orig_lineno",
5008 -class => "linenr" },
5011 print "<td class=\"pre\">" . esc_html($data) . "</td>\n";
5019 "</table>\n"; # class="blame"
5020 print "</div>\n"; # class="blame_body"
5022 or print "Reading blob failed\n";
5031 sub git_blame_incremental {
5032 git_blame_common('incremental');
5035 sub git_blame_data {
5036 git_blame_common('data');
5040 my $head = git_get_head_hash($project);
5042 git_print_page_nav('','', $head,undef,$head);
5043 git_print_header_div('summary', $project);
5045 my @tagslist = git_get_tags_list();
5047 git_tags_body(\@tagslist);
5053 my $head = git_get_head_hash($project);
5055 git_print_page_nav('','', $head,undef,$head);
5056 git_print_header_div('summary', $project);
5058 my @headslist = git_get_heads_list();
5060 git_heads_body(\@headslist, $head);
5065 sub git_blob_plain {
5069 if (!defined $hash) {
5070 if (defined $file_name) {
5071 my $base = $hash_base || git_get_head_hash($project);
5072 $hash = git_get_hash_by_path($base, $file_name, "blob")
5073 or die_error(404, "Cannot find file");
5075 die_error(400, "No file name defined");
5077 } elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) {
5078 # blobs defined by non-textual hash id's can be cached
5082 open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
5083 or die_error(500, "Open git-cat-file blob '$hash' failed");
5085 # content-type (can include charset)
5086 $type = blob_contenttype($fd, $file_name, $type);
5088 # "save as" filename, even when no $file_name is given
5089 my $save_as = "$hash";
5090 if (defined $file_name) {
5091 $save_as = $file_name;
5092 } elsif ($type =~ m/^text\//) {
5096 # With XSS prevention on, blobs of all types except a few known safe
5097 # ones are served with "Content-Disposition: attachment" to make sure
5098 # they don't run in our security domain. For certain image types,
5099 # blob view writes an <img> tag referring to blob_plain view, and we
5100 # want to be sure not to break that by serving the image as an
5101 # attachment (though Firefox 3 doesn't seem to care).
5102 my $sandbox = $prevent_xss &&
5103 $type !~ m!^(?:text/plain|image/(?:gif|png|jpeg))$!;
5107 -expires => $expires,
5108 -content_disposition =>
5109 ($sandbox ? 'attachment' : 'inline')
5110 . '; filename="' . $save_as . '"');
5112 binmode STDOUT, ':raw';
5114 binmode STDOUT, ':utf8'; # as set at the beginning of gitweb.cgi
5121 if (!defined $hash) {
5122 if (defined $file_name) {
5123 my $base = $hash_base || git_get_head_hash($project);
5124 $hash = git_get_hash_by_path($base, $file_name, "blob")
5125 or die_error(404, "Cannot find file");
5127 die_error(400, "No file name defined");
5129 } elsif ($hash =~ m/^[0-9a-fA-F]{40}$/) {
5130 # blobs defined by non-textual hash id's can be cached
5134 my $have_blame = gitweb_check_feature('blame');
5135 open my $fd, "-|", git_cmd(), "cat-file", "blob", $hash
5136 or die_error(500, "Couldn't cat $file_name, $hash");
5137 my $mimetype = blob_mimetype($fd, $file_name);
5138 if ($mimetype !~ m!^(?:text/|image/(?:gif|png|jpeg)$)! && -B $fd) {
5140 return git_blob_plain($mimetype);
5142 # we can have blame only for text/* mimetype
5143 $have_blame &&= ($mimetype =~ m!^text/!);
5145 git_header_html(undef, $expires);
5146 my $formats_nav = '';
5147 if (defined $hash_base && (my %co = parse_commit($hash_base))) {
5148 if (defined $file_name) {
5151 $cgi->a({-href => href(action=>"blame", -replay=>1)},
5156 $cgi->a({-href => href(action=>"history", -replay=>1)},
5159 $cgi->a({-href => href(action=>"blob_plain", -replay=>1)},
5162 $cgi->a({-href => href(action=>"blob",
5163 hash_base=>"HEAD", file_name=>$file_name)},
5167 $cgi->a({-href => href(action=>"blob_plain", -replay=>1)},
5170 git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
5171 git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
5173 print "<div class=\"page_nav\">\n" .
5174 "<br/><br/></div>\n" .
5175 "<div class=\"title\">$hash</div>\n";
5177 git_print_page_path($file_name, "blob", $hash_base);
5178 print "<div class=\"page_body\">\n";
5179 if ($mimetype =~ m!^image/!) {
5180 print qq!<img type="$mimetype"!;
5182 print qq! alt="$file_name" title="$file_name"!;
5185 href(action=>"blob_plain", hash=>$hash,
5186 hash_base=>$hash_base, file_name=>$file_name) .
5190 while (my $line = <$fd>) {
5193 $line = untabify($line);
5194 printf "<div class=\"pre\"><a id=\"l%i\" href=\"#l%i\" class=\"linenr\">%4i</a> %s</div>\n",
5195 $nr, $nr, $nr, esc_html($line, -nbsp=>1);
5199 or print "Reading blob failed.\n";
5205 if (!defined $hash_base) {
5206 $hash_base = "HEAD";
5208 if (!defined $hash) {
5209 if (defined $file_name) {
5210 $hash = git_get_hash_by_path($hash_base, $file_name, "tree");
5215 die_error(404, "No such tree") unless defined($hash);
5220 open my $fd, "-|", git_cmd(), "ls-tree", '-z', $hash
5221 or die_error(500, "Open git-ls-tree failed");
5222 @entries = map { chomp; $_ } <$fd>;
5224 or die_error(404, "Reading tree failed");
5227 my $refs = git_get_references();
5228 my $ref = format_ref_marker($refs, $hash_base);
5231 my $have_blame = gitweb_check_feature('blame');
5232 if (defined $hash_base && (my %co = parse_commit($hash_base))) {
5234 if (defined $file_name) {
5236 $cgi->a({-href => href(action=>"history", -replay=>1)},
5238 $cgi->a({-href => href(action=>"tree",
5239 hash_base=>"HEAD", file_name=>$file_name)},
5242 my $snapshot_links = format_snapshot_links($hash);
5243 if (defined $snapshot_links) {
5244 # FIXME: Should be available when we have no hash base as well.
5245 push @views_nav, $snapshot_links;
5247 git_print_page_nav('tree','', $hash_base, undef, undef, join(' | ', @views_nav));
5248 git_print_header_div('commit', esc_html($co{'title'}) . $ref, $hash_base);
5251 print "<div class=\"page_nav\">\n";
5252 print "<br/><br/></div>\n";
5253 print "<div class=\"title\">$hash</div>\n";
5255 if (defined $file_name) {
5256 $basedir = $file_name;
5257 if ($basedir ne '' && substr($basedir, -1) ne '/') {
5260 git_print_page_path($file_name, 'tree', $hash_base);
5262 print "<div class=\"page_body\">\n";
5263 print "<table class=\"tree\">\n";
5265 # '..' (top directory) link if possible
5266 if (defined $hash_base &&
5267 defined $file_name && $file_name =~ m![^/]+$!) {
5269 print "<tr class=\"dark\">\n";
5271 print "<tr class=\"light\">\n";
5275 my $up = $file_name;
5276 $up =~ s!/?[^/]+$!!;
5277 undef $up unless $up;
5278 # based on git_print_tree_entry
5279 print '<td class="mode">' . mode_str('040000') . "</td>\n";
5280 print '<td class="list">';
5281 print $cgi->a({-href => href(action=>"tree", hash_base=>$hash_base,
5285 print "<td class=\"link\"></td>\n";
5289 foreach my $line (@entries) {
5290 my %t = parse_ls_tree_line($line, -z => 1);
5293 print "<tr class=\"dark\">\n";
5295 print "<tr class=\"light\">\n";
5299 git_print_tree_entry(\%t, $basedir, $hash_base, $have_blame);
5303 print "</table>\n" .
5309 my $format = $input_params{'snapshot_format'};
5310 if (!@snapshot_fmts) {
5311 die_error(403, "Snapshots not allowed");
5313 # default to first supported snapshot format
5314 $format ||= $snapshot_fmts[0];
5315 if ($format !~ m/^[a-z0-9]+$/) {
5316 die_error(400, "Invalid snapshot format parameter");
5317 } elsif (!exists($known_snapshot_formats{$format})) {
5318 die_error(400, "Unknown snapshot format");
5319 } elsif (!grep($_ eq $format, @snapshot_fmts)) {
5320 die_error(403, "Unsupported snapshot format");
5323 if (!defined $hash) {
5324 $hash = git_get_head_hash($project);
5327 my $name = $project;
5328 $name =~ s,([^/])/*\.git$,$1,;
5329 $name = basename($name);
5330 my $filename = to_utf8($name);
5331 $name =~ s/\047/\047\\\047\047/g;
5333 $filename .= "-$hash$known_snapshot_formats{$format}{'suffix'}";
5334 $cmd = quote_command(
5335 git_cmd(), 'archive',
5336 "--format=$known_snapshot_formats{$format}{'format'}",
5337 "--prefix=$name/", $hash);
5338 if (exists $known_snapshot_formats{$format}{'compressor'}) {
5339 $cmd .= ' | ' . quote_command(@{$known_snapshot_formats{$format}{'compressor'}});
5343 -type => $known_snapshot_formats{$format}{'type'},
5344 -content_disposition => 'inline; filename="' . "$filename" . '"',
5345 -status => '200 OK');
5347 open my $fd, "-|", $cmd
5348 or die_error(500, "Execute git-archive failed");
5349 binmode STDOUT, ':raw';
5351 binmode STDOUT, ':utf8'; # as set at the beginning of gitweb.cgi
5356 my $head = git_get_head_hash($project);
5357 if (!defined $hash) {
5360 if (!defined $page) {
5363 my $refs = git_get_references();
5365 my @commitlist = parse_commits($hash, 101, (100 * $page));
5367 my $paging_nav = format_paging_nav('log', $hash, $head, $page, $#commitlist >= 100);
5369 my ($patch_max) = gitweb_get_feature('patches');
5371 if ($patch_max < 0 || @commitlist <= $patch_max) {
5372 $paging_nav .= " ⋅ " .
5373 $cgi->a({-href => href(action=>"patches", -replay=>1)},
5379 git_print_page_nav('log','', $hash,undef,undef, $paging_nav);
5382 my %co = parse_commit($hash);
5384 git_print_header_div('summary', $project);
5385 print "<div class=\"page_body\"> Last change $co{'age_string'}.<br/><br/></div>\n";
5387 my $to = ($#commitlist >= 99) ? (99) : ($#commitlist);
5388 for (my $i = 0; $i <= $to; $i++) {
5389 my %co = %{$commitlist[$i]};
5391 my $commit = $co{'id'};
5392 my $ref = format_ref_marker($refs, $commit);
5393 my %ad = parse_date($co{'author_epoch'});
5394 git_print_header_div('commit',
5395 "<span class=\"age\">$co{'age_string'}</span>" .
5396 esc_html($co{'title'}) . $ref,
5398 print "<div class=\"title_text\">\n" .
5399 "<div class=\"log_link\">\n" .
5400 $cgi->a({-href => href(action=>"commit", hash=>$commit)}, "commit") .
5402 $cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff") .
5404 $cgi->a({-href => href(action=>"tree", hash=>$commit, hash_base=>$commit)}, "tree") .
5407 git_print_authorship(\%co, -tag => 'span');
5408 print "<br/>\n</div>\n";
5410 print "<div class=\"log_body\">\n";
5411 git_print_log($co{'comment'}, -final_empty_line=> 1);
5414 if ($#commitlist >= 100) {
5415 print "<div class=\"page_nav\">\n";
5416 print $cgi->a({-href => href(-replay=>1, page=>$page+1),
5417 -accesskey => "n", -title => "Alt-n"}, "next");
5424 $hash ||= $hash_base || "HEAD";
5425 my %co = parse_commit($hash)
5426 or die_error(404, "Unknown commit object");
5428 my $parent = $co{'parent'};
5429 my $parents = $co{'parents'}; # listref
5431 # we need to prepare $formats_nav before any parameter munging
5433 if (!defined $parent) {
5435 $formats_nav .= '(initial)';
5436 } elsif (@$parents == 1) {
5437 # single parent commit
5440 $cgi->a({-href => href(action=>"commit",
5442 esc_html(substr($parent, 0, 7))) .
5449 $cgi->a({-href => href(action=>"commit",
5451 esc_html(substr($_, 0, 7)));
5455 if (gitweb_check_feature('patches')) {
5456 $formats_nav .= " | " .
5457 $cgi->a({-href => href(action=>"patch", -replay=>1)},
5461 if (!defined $parent) {
5465 open my $fd, "-|", git_cmd(), "diff-tree", '-r', "--no-commit-id",
5467 (@$parents <= 1 ? $parent : '-c'),
5469 or die_error(500, "Open git-diff-tree failed");
5470 @difftree = map { chomp; $_ } <$fd>;
5471 close $fd or die_error(404, "Reading git-diff-tree failed");
5473 # non-textual hash id's can be cached
5475 if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
5478 my $refs = git_get_references();
5479 my $ref = format_ref_marker($refs, $co{'id'});
5481 git_header_html(undef, $expires);
5482 git_print_page_nav('commit', '',
5483 $hash, $co{'tree'}, $hash,
5486 if (defined $co{'parent'}) {
5487 git_print_header_div('commitdiff', esc_html($co{'title'}) . $ref, $hash);
5489 git_print_header_div('tree', esc_html($co{'title'}) . $ref, $co{'tree'}, $hash);
5491 print "<div class=\"title_text\">\n" .
5492 "<table class=\"object_header\">\n";
5493 git_print_authorship_rows(\%co);
5494 print "<tr><td>commit</td><td class=\"sha1\">$co{'id'}</td></tr>\n";
5497 "<td class=\"sha1\">" .
5498 $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$hash),
5499 class => "list"}, $co{'tree'}) .
5501 "<td class=\"link\">" .
5502 $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$hash)},
5504 my $snapshot_links = format_snapshot_links($hash);
5505 if (defined $snapshot_links) {
5506 print " | " . $snapshot_links;
5511 foreach my $par (@$parents) {
5514 "<td class=\"sha1\">" .
5515 $cgi->a({-href => href(action=>"commit", hash=>$par),
5516 class => "list"}, $par) .
5518 "<td class=\"link\">" .
5519 $cgi->a({-href => href(action=>"commit", hash=>$par)}, "commit") .
5521 $cgi->a({-href => href(action=>"commitdiff", hash=>$hash, hash_parent=>$par)}, "diff") .
5528 print "<div class=\"page_body\">\n";
5529 git_print_log($co{'comment'});
5532 git_difftree_body(\@difftree, $hash, @$parents);
5538 # object is defined by:
5539 # - hash or hash_base alone
5540 # - hash_base and file_name
5543 # - hash or hash_base alone
5544 if ($hash || ($hash_base && !defined $file_name)) {
5545 my $object_id = $hash || $hash_base;
5547 open my $fd, "-|", quote_command(
5548 git_cmd(), 'cat-file', '-t', $object_id) . ' 2> /dev/null'
5549 or die_error(404, "Object does not exist");
5553 or die_error(404, "Object does not exist");
5555 # - hash_base and file_name
5556 } elsif ($hash_base && defined $file_name) {
5557 $file_name =~ s,/+$,,;
5559 system(git_cmd(), "cat-file", '-e', $hash_base) == 0
5560 or die_error(404, "Base object does not exist");
5562 # here errors should not hapen
5563 open my $fd, "-|", git_cmd(), "ls-tree", $hash_base, "--", $file_name
5564 or die_error(500, "Open git-ls-tree failed");
5568 #'100644 blob 0fa3f3a66fb6a137f6ec2c19351ed4d807070ffa panic.c'
5569 unless ($line && $line =~ m/^([0-9]+) (.+) ([0-9a-fA-F]{40})\t/) {
5570 die_error(404, "File or directory for given base does not exist");
5575 die_error(400, "Not enough information to find object");
5578 print $cgi->redirect(-uri => href(action=>$type, -full=>1,
5579 hash=>$hash, hash_base=>$hash_base,
5580 file_name=>$file_name),
5581 -status => '302 Found');
5585 my $format = shift || 'html';
5592 # preparing $fd and %diffinfo for git_patchset_body
5594 if (defined $hash_base && defined $hash_parent_base) {
5595 if (defined $file_name) {
5597 open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
5598 $hash_parent_base, $hash_base,
5599 "--", (defined $file_parent ? $file_parent : ()), $file_name
5600 or die_error(500, "Open git-diff-tree failed");
5601 @difftree = map { chomp; $_ } <$fd>;
5603 or die_error(404, "Reading git-diff-tree failed");
5605 or die_error(404, "Blob diff not found");
5607 } elsif (defined $hash &&
5608 $hash =~ /[0-9a-fA-F]{40}/) {
5609 # try to find filename from $hash
5611 # read filtered raw output
5612 open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
5613 $hash_parent_base, $hash_base, "--"
5614 or die_error(500, "Open git-diff-tree failed");
5616 # ':100644 100644 03b21826... 3b93d5e7... M ls-files.c'
5618 grep { /^:[0-7]{6} [0-7]{6} [0-9a-fA-F]{40} $hash/ }
5619 map { chomp; $_ } <$fd>;
5621 or die_error(404, "Reading git-diff-tree failed");
5623 or die_error(404, "Blob diff not found");
5626 die_error(400, "Missing one of the blob diff parameters");
5629 if (@difftree > 1) {
5630 die_error(400, "Ambiguous blob diff specification");
5633 %diffinfo = parse_difftree_raw_line($difftree[0]);
5634 $file_parent ||= $diffinfo{'from_file'} || $file_name;
5635 $file_name ||= $diffinfo{'to_file'};
5637 $hash_parent ||= $diffinfo{'from_id'};
5638 $hash ||= $diffinfo{'to_id'};
5640 # non-textual hash id's can be cached
5641 if ($hash_base =~ m/^[0-9a-fA-F]{40}$/ &&
5642 $hash_parent_base =~ m/^[0-9a-fA-F]{40}$/) {
5647 open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
5648 '-p', ($format eq 'html' ? "--full-index" : ()),
5649 $hash_parent_base, $hash_base,
5650 "--", (defined $file_parent ? $file_parent : ()), $file_name
5651 or die_error(500, "Open git-diff-tree failed");
5654 # old/legacy style URI -- not generated anymore since 1.4.3.
5656 die_error('404 Not Found', "Missing one of the blob diff parameters")
5660 if ($format eq 'html') {
5662 $cgi->a({-href => href(action=>"blobdiff_plain", -replay=>1)},
5664 git_header_html(undef, $expires);
5665 if (defined $hash_base && (my %co = parse_commit($hash_base))) {
5666 git_print_page_nav('','', $hash_base,$co{'tree'},$hash_base, $formats_nav);
5667 git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
5669 print "<div class=\"page_nav\"><br/>$formats_nav<br/></div>\n";
5670 print "<div class=\"title\">$hash vs $hash_parent</div>\n";
5672 if (defined $file_name) {
5673 git_print_page_path($file_name, "blob", $hash_base);
5675 print "<div class=\"page_path\"></div>\n";
5678 } elsif ($format eq 'plain') {
5680 -type => 'text/plain',
5681 -charset => 'utf-8',
5682 -expires => $expires,
5683 -content_disposition => 'inline; filename="' . "$file_name" . '.patch"');
5685 print "X-Git-Url: " . $cgi->self_url() . "\n\n";
5688 die_error(400, "Unknown blobdiff format");
5692 if ($format eq 'html') {
5693 print "<div class=\"page_body\">\n";
5695 git_patchset_body($fd, [ \%diffinfo ], $hash_base, $hash_parent_base);
5698 print "</div>\n"; # class="page_body"
5702 while (my $line = <$fd>) {
5703 $line =~ s!a/($hash|$hash_parent)!'a/'.esc_path($diffinfo{'from_file'})!eg;
5704 $line =~ s!b/($hash|$hash_parent)!'b/'.esc_path($diffinfo{'to_file'})!eg;
5708 last if $line =~ m!^\+\+\+!;
5716 sub git_blobdiff_plain {
5717 git_blobdiff('plain');
5720 sub git_commitdiff {
5722 my $format = $params{-format} || 'html';
5724 my ($patch_max) = gitweb_get_feature('patches');
5725 if ($format eq 'patch') {
5726 die_error(403, "Patch view not allowed") unless $patch_max;
5729 $hash ||= $hash_base || "HEAD";
5730 my %co = parse_commit($hash)
5731 or die_error(404, "Unknown commit object");
5733 # choose format for commitdiff for merge
5734 if (! defined $hash_parent && @{$co{'parents'}} > 1) {
5735 $hash_parent = '--cc';
5737 # we need to prepare $formats_nav before almost any parameter munging
5739 if ($format eq 'html') {
5741 $cgi->a({-href => href(action=>"commitdiff_plain", -replay=>1)},
5744 $formats_nav .= " | " .
5745 $cgi->a({-href => href(action=>"patch", -replay=>1)},
5749 if (defined $hash_parent &&
5750 $hash_parent ne '-c' && $hash_parent ne '--cc') {
5751 # commitdiff with two commits given
5752 my $hash_parent_short = $hash_parent;
5753 if ($hash_parent =~ m/^[0-9a-fA-F]{40}$/) {
5754 $hash_parent_short = substr($hash_parent, 0, 7);
5758 for (my $i = 0; $i < @{$co{'parents'}}; $i++) {
5759 if ($co{'parents'}[$i] eq $hash_parent) {
5760 $formats_nav .= ' parent ' . ($i+1);
5764 $formats_nav .= ': ' .
5765 $cgi->a({-href => href(action=>"commitdiff",
5766 hash=>$hash_parent)},
5767 esc_html($hash_parent_short)) .
5769 } elsif (!$co{'parent'}) {
5771 $formats_nav .= ' (initial)';
5772 } elsif (scalar @{$co{'parents'}} == 1) {
5773 # single parent commit
5776 $cgi->a({-href => href(action=>"commitdiff",
5777 hash=>$co{'parent'})},
5778 esc_html(substr($co{'parent'}, 0, 7))) .
5782 if ($hash_parent eq '--cc') {
5783 $formats_nav .= ' | ' .
5784 $cgi->a({-href => href(action=>"commitdiff",
5785 hash=>$hash, hash_parent=>'-c')},
5787 } else { # $hash_parent eq '-c'
5788 $formats_nav .= ' | ' .
5789 $cgi->a({-href => href(action=>"commitdiff",
5790 hash=>$hash, hash_parent=>'--cc')},
5796 $cgi->a({-href => href(action=>"commitdiff",
5798 esc_html(substr($_, 0, 7)));
5799 } @{$co{'parents'}} ) .
5804 my $hash_parent_param = $hash_parent;
5805 if (!defined $hash_parent_param) {
5806 # --cc for multiple parents, --root for parentless
5807 $hash_parent_param =
5808 @{$co{'parents'}} > 1 ? '--cc' : $co{'parent'} || '--root';
5814 if ($format eq 'html') {
5815 open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
5816 "--no-commit-id", "--patch-with-raw", "--full-index",
5817 $hash_parent_param, $hash, "--"
5818 or die_error(500, "Open git-diff-tree failed");
5820 while (my $line = <$fd>) {
5822 # empty line ends raw part of diff-tree output
5824 push @difftree, scalar parse_difftree_raw_line($line);
5827 } elsif ($format eq 'plain') {
5828 open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
5829 '-p', $hash_parent_param, $hash, "--"
5830 or die_error(500, "Open git-diff-tree failed");
5831 } elsif ($format eq 'patch') {
5832 # For commit ranges, we limit the output to the number of
5833 # patches specified in the 'patches' feature.
5834 # For single commits, we limit the output to a single patch,
5835 # diverging from the git-format-patch default.
5836 my @commit_spec = ();
5838 if ($patch_max > 0) {
5839 push @commit_spec, "-$patch_max";
5841 push @commit_spec, '-n', "$hash_parent..$hash";
5843 if ($params{-single}) {
5844 push @commit_spec, '-1';
5846 if ($patch_max > 0) {
5847 push @commit_spec, "-$patch_max";
5849 push @commit_spec, "-n";
5851 push @commit_spec, '--root', $hash;
5853 open $fd, "-|", git_cmd(), "format-patch", '--encoding=utf8',
5854 '--stdout', @commit_spec
5855 or die_error(500, "Open git-format-patch failed");
5857 die_error(400, "Unknown commitdiff format");
5860 # non-textual hash id's can be cached
5862 if ($hash =~ m/^[0-9a-fA-F]{40}$/) {
5866 # write commit message
5867 if ($format eq 'html') {
5868 my $refs = git_get_references();
5869 my $ref = format_ref_marker($refs, $co{'id'});
5871 git_header_html(undef, $expires);
5872 git_print_page_nav('commitdiff','', $hash,$co{'tree'},$hash, $formats_nav);
5873 git_print_header_div('commit', esc_html($co{'title'}) . $ref, $hash);
5874 print "<div class=\"title_text\">\n" .
5875 "<table class=\"object_header\">\n";
5876 git_print_authorship_rows(\%co);
5879 print "<div class=\"page_body\">\n";
5880 if (@{$co{'comment'}} > 1) {
5881 print "<div class=\"log\">\n";
5882 git_print_log($co{'comment'}, -final_empty_line=> 1, -remove_title => 1);
5883 print "</div>\n"; # class="log"
5886 } elsif ($format eq 'plain') {
5887 my $refs = git_get_references("tags");
5888 my $tagname = git_get_rev_name_tags($hash);
5889 my $filename = basename($project) . "-$hash.patch";
5892 -type => 'text/plain',
5893 -charset => 'utf-8',
5894 -expires => $expires,
5895 -content_disposition => 'inline; filename="' . "$filename" . '"');
5896 my %ad = parse_date($co{'author_epoch'}, $co{'author_tz'});
5897 print "From: " . to_utf8($co{'author'}) . "\n";
5898 print "Date: $ad{'rfc2822'} ($ad{'tz_local'})\n";
5899 print "Subject: " . to_utf8($co{'title'}) . "\n";
5901 print "X-Git-Tag: $tagname\n" if $tagname;
5902 print "X-Git-Url: " . $cgi->self_url() . "\n\n";
5904 foreach my $line (@{$co{'comment'}}) {
5905 print to_utf8($line) . "\n";
5908 } elsif ($format eq 'patch') {
5909 my $filename = basename($project) . "-$hash.patch";
5912 -type => 'text/plain',
5913 -charset => 'utf-8',
5914 -expires => $expires,
5915 -content_disposition => 'inline; filename="' . "$filename" . '"');
5919 if ($format eq 'html') {
5920 my $use_parents = !defined $hash_parent ||
5921 $hash_parent eq '-c' || $hash_parent eq '--cc';
5922 git_difftree_body(\@difftree, $hash,
5923 $use_parents ? @{$co{'parents'}} : $hash_parent);
5926 git_patchset_body($fd, \@difftree, $hash,
5927 $use_parents ? @{$co{'parents'}} : $hash_parent);
5929 print "</div>\n"; # class="page_body"
5932 } elsif ($format eq 'plain') {
5936 or print "Reading git-diff-tree failed\n";
5937 } elsif ($format eq 'patch') {
5941 or print "Reading git-format-patch failed\n";
5945 sub git_commitdiff_plain {
5946 git_commitdiff(-format => 'plain');
5949 # format-patch-style patches
5951 git_commitdiff(-format => 'patch', -single=> 1);
5955 git_commitdiff(-format => 'patch');
5959 if (!defined $hash_base) {
5960 $hash_base = git_get_head_hash($project);
5962 if (!defined $page) {
5966 my %co = parse_commit($hash_base)
5967 or die_error(404, "Unknown commit object");
5969 my $refs = git_get_references();
5970 my $limit = sprintf("--max-count=%i", (100 * ($page+1)));
5972 my @commitlist = parse_commits($hash_base, 101, (100 * $page),
5973 $file_name, "--full-history")
5974 or die_error(404, "No such file or directory on given branch");
5976 if (!defined $hash && defined $file_name) {
5977 # some commits could have deleted file in question,
5978 # and not have it in tree, but one of them has to have it
5979 for (my $i = 0; $i <= @commitlist; $i++) {
5980 $hash = git_get_hash_by_path($commitlist[$i]{'id'}, $file_name);
5981 last if defined $hash;
5984 if (defined $hash) {
5985 $ftype = git_get_type($hash);
5987 if (!defined $ftype) {
5988 die_error(500, "Unknown type of object");
5991 my $paging_nav = '';
5994 $cgi->a({-href => href(action=>"history", hash=>$hash, hash_base=>$hash_base,
5995 file_name=>$file_name)},
5997 $paging_nav .= " ⋅ " .
5998 $cgi->a({-href => href(-replay=>1, page=>$page-1),
5999 -accesskey => "p", -title => "Alt-p"}, "prev");
6001 $paging_nav .= "first";
6002 $paging_nav .= " ⋅ prev";
6005 if ($#commitlist >= 100) {
6007 $cgi->a({-href => href(-replay=>1, page=>$page+1),
6008 -accesskey => "n", -title => "Alt-n"}, "next");
6009 $paging_nav .= " ⋅ $next_link";
6011 $paging_nav .= " ⋅ next";
6015 git_print_page_nav('history','', $hash_base,$co{'tree'},$hash_base, $paging_nav);
6016 git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
6017 git_print_page_path($file_name, $ftype, $hash_base);
6019 git_history_body(\@commitlist, 0, 99,
6020 $refs, $hash_base, $ftype, $next_link);
6026 gitweb_check_feature('search') or die_error(403, "Search is disabled");
6027 if (!defined $searchtext) {
6028 die_error(400, "Text field is empty");
6030 if (!defined $hash) {
6031 $hash = git_get_head_hash($project);
6033 my %co = parse_commit($hash);
6035 die_error(404, "Unknown commit object");
6037 if (!defined $page) {
6041 $searchtype ||= 'commit';
6042 if ($searchtype eq 'pickaxe') {
6043 # pickaxe may take all resources of your box and run for several minutes
6044 # with every query - so decide by yourself how public you make this feature
6045 gitweb_check_feature('pickaxe')
6046 or die_error(403, "Pickaxe is disabled");
6048 if ($searchtype eq 'grep') {
6049 gitweb_check_feature('grep')
6050 or die_error(403, "Grep is disabled");
6055 if ($searchtype eq 'commit' or $searchtype eq 'author' or $searchtype eq 'committer') {
6057 if ($searchtype eq 'commit') {
6058 $greptype = "--grep=";
6059 } elsif ($searchtype eq 'author') {
6060 $greptype = "--author=";
6061 } elsif ($searchtype eq 'committer') {
6062 $greptype = "--committer=";
6064 $greptype .= $searchtext;
6065 my @commitlist = parse_commits($hash, 101, (100 * $page), undef,
6066 $greptype, '--regexp-ignore-case',
6067 $search_use_regexp ? '--extended-regexp' : '--fixed-strings');
6069 my $paging_nav = '';
6072 $cgi->a({-href => href(action=>"search", hash=>$hash,
6073 searchtext=>$searchtext,
6074 searchtype=>$searchtype)},
6076 $paging_nav .= " ⋅ " .
6077 $cgi->a({-href => href(-replay=>1, page=>$page-1),
6078 -accesskey => "p", -title => "Alt-p"}, "prev");
6080 $paging_nav .= "first";
6081 $paging_nav .= " ⋅ prev";
6084 if ($#commitlist >= 100) {
6086 $cgi->a({-href => href(-replay=>1, page=>$page+1),
6087 -accesskey => "n", -title => "Alt-n"}, "next");
6088 $paging_nav .= " ⋅ $next_link";
6090 $paging_nav .= " ⋅ next";
6093 if ($#commitlist >= 100) {
6096 git_print_page_nav('','', $hash,$co{'tree'},$hash, $paging_nav);
6097 git_print_header_div('commit', esc_html($co{'title'}), $hash);
6098 git_search_grep_body(\@commitlist, 0, 99, $next_link);
6101 if ($searchtype eq 'pickaxe') {
6102 git_print_page_nav('','', $hash,$co{'tree'},$hash);
6103 git_print_header_div('commit', esc_html($co{'title'}), $hash);
6105 print "<table class=\"pickaxe search\">\n";
6108 open my $fd, '-|', git_cmd(), '--no-pager', 'log', @diff_opts,
6109 '--pretty=format:%H', '--no-abbrev', '--raw', "-S$searchtext",
6110 ($search_use_regexp ? '--pickaxe-regex' : ());
6113 while (my $line = <$fd>) {
6117 my %set = parse_difftree_raw_line($line);
6118 if (defined $set{'commit'}) {
6119 # finish previous commit
6122 "<td class=\"link\">" .
6123 $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") .
6125 $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree");
6131 print "<tr class=\"dark\">\n";
6133 print "<tr class=\"light\">\n";
6136 %co = parse_commit($set{'commit'});
6137 my $author = chop_and_escape_str($co{'author_name'}, 15, 5);
6138 print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
6139 "<td><i>$author</i></td>\n" .
6141 $cgi->a({-href => href(action=>"commit", hash=>$co{'id'}),
6142 -class => "list subject"},
6143 chop_and_escape_str($co{'title'}, 50) . "<br/>");
6144 } elsif (defined $set{'to_id'}) {
6145 next if ($set{'to_id'} =~ m/^0{40}$/);
6147 print $cgi->a({-href => href(action=>"blob", hash_base=>$co{'id'},
6148 hash=>$set{'to_id'}, file_name=>$set{'to_file'}),
6150 "<span class=\"match\">" . esc_path($set{'file'}) . "</span>") .
6156 # finish last commit (warning: repetition!)
6159 "<td class=\"link\">" .
6160 $cgi->a({-href => href(action=>"commit", hash=>$co{'id'})}, "commit") .
6162 $cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$co{'id'})}, "tree");
6170 if ($searchtype eq 'grep') {
6171 git_print_page_nav('','', $hash,$co{'tree'},$hash);
6172 git_print_header_div('commit', esc_html($co{'title'}), $hash);
6174 print "<table class=\"grep_search\">\n";
6178 open my $fd, "-|", git_cmd(), 'grep', '-n',
6179 $search_use_regexp ? ('-E', '-i') : '-F',
6180 $searchtext, $co{'tree'};
6182 while (my $line = <$fd>) {
6184 my ($file, $lno, $ltext, $binary);
6185 last if ($matches++ > 1000);
6186 if ($line =~ /^Binary file (.+) matches$/) {
6190 (undef, $file, $lno, $ltext) = split(/:/, $line, 4);
6192 if ($file ne $lastfile) {
6193 $lastfile and print "</td></tr>\n";
6195 print "<tr class=\"dark\">\n";
6197 print "<tr class=\"light\">\n";
6199 print "<td class=\"list\">".
6200 $cgi->a({-href => href(action=>"blob", hash=>$co{'hash'},
6201 file_name=>"$file"),
6202 -class => "list"}, esc_path($file));
6203 print "</td><td>\n";
6207 print "<div class=\"binary\">Binary file</div>\n";
6209 $ltext = untabify($ltext);
6210 if ($ltext =~ m/^(.*)($search_regexp)(.*)$/i) {
6211 $ltext = esc_html($1, -nbsp=>1);
6212 $ltext .= '<span class="match">';
6213 $ltext .= esc_html($2, -nbsp=>1);
6214 $ltext .= '</span>';
6215 $ltext .= esc_html($3, -nbsp=>1);
6217 $ltext = esc_html($ltext, -nbsp=>1);
6219 print "<div class=\"pre\">" .
6220 $cgi->a({-href => href(action=>"blob", hash=>$co{'hash'},
6221 file_name=>"$file").'#l'.$lno,
6222 -class => "linenr"}, sprintf('%4i', $lno))
6223 . ' ' . $ltext . "</div>\n";
6227 print "</td></tr>\n";
6228 if ($matches > 1000) {
6229 print "<div class=\"diff nodifferences\">Too many matches, listing trimmed</div>\n";
6232 print "<div class=\"diff nodifferences\">No matches found</div>\n";
6241 sub git_search_help {
6243 git_print_page_nav('','', $hash,$hash,$hash);
6245 <p><strong>Pattern</strong> is by default a normal string that is matched precisely (but without
6246 regard to case, except in the case of pickaxe). However, when you check the <em>re</em> checkbox,
6247 the pattern entered is recognized as the POSIX extended
6248 <a href="http://en.wikipedia.org/wiki/Regular_expression">regular expression</a> (also case
6251 <dt><b>commit</b></dt>
6252 <dd>The commit messages and authorship information will be scanned for the given pattern.</dd>
6254 my $have_grep = gitweb_check_feature('grep');
6257 <dt><b>grep</b></dt>
6258 <dd>All files in the currently selected tree (HEAD unless you are explicitly browsing
6259 a different one) are searched for the given pattern. On large trees, this search can take
6260 a while and put some strain on the server, so please use it with some consideration. Note that
6261 due to git-grep peculiarity, currently if regexp mode is turned off, the matches are
6262 case-sensitive.</dd>
6266 <dt><b>author</b></dt>
6267 <dd>Name and e-mail of the change author and date of birth of the patch will be scanned for the given pattern.</dd>
6268 <dt><b>committer</b></dt>
6269 <dd>Name and e-mail of the committer and date of commit will be scanned for the given pattern.</dd>
6271 my $have_pickaxe = gitweb_check_feature('pickaxe');
6272 if ($have_pickaxe) {
6274 <dt><b>pickaxe</b></dt>
6275 <dd>All commits that caused the string to appear or disappear from any file (changes that
6276 added, removed or "modified" the string) will be listed. This search can take a while and
6277 takes a lot of strain on the server, so please use it wisely. Note that since you may be
6278 interested even in changes just changing the case as well, this search is case sensitive.</dd>
6286 my $head = git_get_head_hash($project);
6287 if (!defined $hash) {
6290 if (!defined $page) {
6293 my $refs = git_get_references();
6295 my $commit_hash = $hash;
6296 if (defined $hash_parent) {
6297 $commit_hash = "$hash_parent..$hash";
6299 my @commitlist = parse_commits($commit_hash, 101, (100 * $page));
6301 my $paging_nav = format_paging_nav('shortlog', $hash, $head, $page, $#commitlist >= 100);
6303 if ($#commitlist >= 100) {
6305 $cgi->a({-href => href(-replay=>1, page=>$page+1),
6306 -accesskey => "n", -title => "Alt-n"}, "next");
6308 my $patch_max = gitweb_check_feature('patches');
6310 if ($patch_max < 0 || @commitlist <= $patch_max) {
6311 $paging_nav .= " ⋅ " .
6312 $cgi->a({-href => href(action=>"patches", -replay=>1)},
6318 git_print_page_nav('shortlog','', $hash,$hash,$hash, $paging_nav);
6319 git_print_header_div('summary', $project);
6321 git_shortlog_body(\@commitlist, 0, 99, $refs, $next_link);
6326 ## ......................................................................
6327 ## feeds (RSS, Atom; OPML)
6330 my $format = shift || 'atom';
6331 my $have_blame = gitweb_check_feature('blame');
6333 # Atom: http://www.atomenabled.org/developers/syndication/
6334 # RSS: http://www.notestips.com/80256B3A007F2692/1/NAMO5P9UPQ
6335 if ($format ne 'rss' && $format ne 'atom') {
6336 die_error(400, "Unknown web feed format");
6339 # log/feed of current (HEAD) branch, log of given branch, history of file/directory
6340 my $head = $hash || 'HEAD';
6341 my @commitlist = parse_commits($head, 150, 0, $file_name);
6345 my $content_type = "application/$format+xml";
6346 if (defined $cgi->http('HTTP_ACCEPT') &&
6347 $cgi->Accept('text/xml') > $cgi->Accept($content_type)) {
6348 # browser (feed reader) prefers text/xml
6349 $content_type = 'text/xml';
6351 if (defined($commitlist[0])) {
6352 %latest_commit = %{$commitlist[0]};
6353 my $latest_epoch = $latest_commit{'committer_epoch'};
6354 %latest_date = parse_date($latest_epoch);
6355 my $if_modified = $cgi->http('IF_MODIFIED_SINCE');
6356 if (defined $if_modified) {
6358 if (eval { require HTTP::Date; 1; }) {
6359 $since = HTTP::Date::str2time($if_modified);
6360 } elsif (eval { require Time::ParseDate; 1; }) {
6361 $since = Time::ParseDate::parsedate($if_modified, GMT => 1);
6363 if (defined $since && $latest_epoch <= $since) {
6365 -type => $content_type,
6366 -charset => 'utf-8',
6367 -last_modified => $latest_date{'rfc2822'},
6368 -status => '304 Not Modified');
6373 -type => $content_type,
6374 -charset => 'utf-8',
6375 -last_modified => $latest_date{'rfc2822'});
6378 -type => $content_type,
6379 -charset => 'utf-8');
6382 # Optimization: skip generating the body if client asks only
6383 # for Last-Modified date.
6384 return if ($cgi->request_method() eq 'HEAD');
6387 my $title = "$site_name - $project/$action";
6388 my $feed_type = 'log';
6389 if (defined $hash) {
6390 $title .= " - '$hash'";
6391 $feed_type = 'branch log';
6392 if (defined $file_name) {
6393 $title .= " :: $file_name";
6394 $feed_type = 'history';
6396 } elsif (defined $file_name) {
6397 $title .= " - $file_name";
6398 $feed_type = 'history';
6400 $title .= " $feed_type";
6401 my $descr = git_get_project_description($project);
6402 if (defined $descr) {
6403 $descr = esc_html($descr);
6405 $descr = "$project " .
6406 ($format eq 'rss' ? 'RSS' : 'Atom') .
6409 my $owner = git_get_project_owner($project);
6410 $owner = esc_html($owner);
6414 if (defined $file_name) {
6415 $alt_url = href(-full=>1, action=>"history", hash=>$hash, file_name=>$file_name);
6416 } elsif (defined $hash) {
6417 $alt_url = href(-full=>1, action=>"log", hash=>$hash);
6419 $alt_url = href(-full=>1, action=>"summary");
6421 print qq!<?xml version="1.0" encoding="utf-8"?>\n!;
6422 if ($format eq 'rss') {
6424 <rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
6427 print "<title>$title</title>\n" .
6428 "<link>$alt_url</link>\n" .
6429 "<description>$descr</description>\n" .
6430 "<language>en</language>\n" .
6431 # project owner is responsible for 'editorial' content
6432 "<managingEditor>$owner</managingEditor>\n";
6433 if (defined $logo || defined $favicon) {
6434 # prefer the logo to the favicon, since RSS
6435 # doesn't allow both
6436 my $img = esc_url($logo || $favicon);
6438 "<url>$img</url>\n" .
6439 "<title>$title</title>\n" .
6440 "<link>$alt_url</link>\n" .
6444 print "<pubDate>$latest_date{'rfc2822'}</pubDate>\n";
6445 print "<lastBuildDate>$latest_date{'rfc2822'}</lastBuildDate>\n";
6447 print "<generator>gitweb v.$version/$git_version</generator>\n";
6448 } elsif ($format eq 'atom') {
6450 <feed xmlns="http://www.w3.org/2005/Atom">
6452 print "<title>$title</title>\n" .
6453 "<subtitle>$descr</subtitle>\n" .
6454 '<link rel="alternate" type="text/html" href="' .
6455 $alt_url . '" />' . "\n" .
6456 '<link rel="self" type="' . $content_type . '" href="' .
6457 $cgi->self_url() . '" />' . "\n" .
6458 "<id>" . href(-full=>1) . "</id>\n" .
6459 # use project owner for feed author
6460 "<author><name>$owner</name></author>\n";
6461 if (defined $favicon) {
6462 print "<icon>" . esc_url($favicon) . "</icon>\n";
6464 if (defined $logo_url) {
6465 # not twice as wide as tall: 72 x 27 pixels
6466 print "<logo>" . esc_url($logo) . "</logo>\n";
6468 if (! %latest_date) {
6469 # dummy date to keep the feed valid until commits trickle in:
6470 print "<updated>1970-01-01T00:00:00Z</updated>\n";
6472 print "<updated>$latest_date{'iso-8601'}</updated>\n";
6474 print "<generator version='$version/$git_version'>gitweb</generator>\n";
6478 for (my $i = 0; $i <= $#commitlist; $i++) {
6479 my %co = %{$commitlist[$i]};
6480 my $commit = $co{'id'};
6481 # we read 150, we always show 30 and the ones more recent than 48 hours
6482 if (($i >= 20) && ((time - $co{'author_epoch'}) > 48*60*60)) {
6485 my %cd = parse_date($co{'author_epoch'});
6487 # get list of changed files
6488 open my $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
6489 $co{'parent'} || "--root",
6490 $co{'id'}, "--", (defined $file_name ? $file_name : ())
6492 my @difftree = map { chomp; $_ } <$fd>;
6496 # print element (entry, item)
6497 my $co_url = href(-full=>1, action=>"commitdiff", hash=>$commit);
6498 if ($format eq 'rss') {
6500 "<title>" . esc_html($co{'title'}) . "</title>\n" .
6501 "<author>" . esc_html($co{'author'}) . "</author>\n" .
6502 "<pubDate>$cd{'rfc2822'}</pubDate>\n" .
6503 "<guid isPermaLink=\"true\">$co_url</guid>\n" .
6504 "<link>$co_url</link>\n" .
6505 "<description>" . esc_html($co{'title'}) . "</description>\n" .
6506 "<content:encoded>" .
6508 } elsif ($format eq 'atom') {
6510 "<title type=\"html\">" . esc_html($co{'title'}) . "</title>\n" .
6511 "<updated>$cd{'iso-8601'}</updated>\n" .
6513 " <name>" . esc_html($co{'author_name'}) . "</name>\n";
6514 if ($co{'author_email'}) {
6515 print " <email>" . esc_html($co{'author_email'}) . "</email>\n";
6517 print "</author>\n" .
6518 # use committer for contributor
6520 " <name>" . esc_html($co{'committer_name'}) . "</name>\n";
6521 if ($co{'committer_email'}) {
6522 print " <email>" . esc_html($co{'committer_email'}) . "</email>\n";
6524 print "</contributor>\n" .
6525 "<published>$cd{'iso-8601'}</published>\n" .
6526 "<link rel=\"alternate\" type=\"text/html\" href=\"$co_url\" />\n" .
6527 "<id>$co_url</id>\n" .
6528 "<content type=\"xhtml\" xml:base=\"" . esc_url($my_url) . "\">\n" .
6529 "<div xmlns=\"http://www.w3.org/1999/xhtml\">\n";
6531 my $comment = $co{'comment'};
6533 foreach my $line (@$comment) {
6534 $line = esc_html($line);
6537 print "</pre><ul>\n";
6538 foreach my $difftree_line (@difftree) {
6539 my %difftree = parse_difftree_raw_line($difftree_line);
6540 next if !$difftree{'from_id'};
6542 my $file = $difftree{'file'} || $difftree{'to_file'};
6546 $cgi->a({-href => href(-full=>1, action=>"blobdiff",
6547 hash=>$difftree{'to_id'}, hash_parent=>$difftree{'from_id'},
6548 hash_base=>$co{'id'}, hash_parent_base=>$co{'parent'},
6549 file_name=>$file, file_parent=>$difftree{'from_file'}),
6550 -title => "diff"}, 'D');
6552 print $cgi->a({-href => href(-full=>1, action=>"blame",
6553 file_name=>$file, hash_base=>$commit),
6554 -title => "blame"}, 'B');
6556 # if this is not a feed of a file history
6557 if (!defined $file_name || $file_name ne $file) {
6558 print $cgi->a({-href => href(-full=>1, action=>"history",
6559 file_name=>$file, hash=>$commit),
6560 -title => "history"}, 'H');
6562 $file = esc_path($file);
6566 if ($format eq 'rss') {
6567 print "</ul>]]>\n" .
6568 "</content:encoded>\n" .
6570 } elsif ($format eq 'atom') {
6571 print "</ul>\n</div>\n" .
6578 if ($format eq 'rss') {
6579 print "</channel>\n</rss>\n";
6580 } elsif ($format eq 'atom') {
6594 my @list = git_get_projects_list();
6597 -type => 'text/xml',
6598 -charset => 'utf-8',
6599 -content_disposition => 'inline; filename="opml.xml"');
6602 <?xml version="1.0" encoding="utf-8"?>
6603 <opml version="1.0">
6605 <title>$site_name OPML Export</title>
6608 <outline text="git RSS feeds">
6611 foreach my $pr (@list) {
6613 my $head = git_get_head_hash($proj{'path'});
6614 if (!defined $head) {
6617 $git_dir = "$projectroot/$proj{'path'}";
6618 my %co = parse_commit($head);
6623 my $path = esc_html(chop_str($proj{'path'}, 25, 5));
6624 my $rss = href('project' => $proj{'path'}, 'action' => 'rss', -full => 1);
6625 my $html = href('project' => $proj{'path'}, 'action' => 'summary', -full => 1);
6626 print "<outline type=\"rss\" text=\"$path\" title=\"$path\" xmlUrl=\"$rss\" htmlUrl=\"$html\"/>\n";