use Fcntl ':mode';
use File::Find qw();
use File::Basename qw(basename);
+use Time::HiRes qw(gettimeofday tv_interval);
binmode STDOUT, ':utf8';
-our $t0;
-if (eval { require Time::HiRes; 1; }) {
- $t0 = [Time::HiRes::gettimeofday()];
-}
+our $t0 = [ gettimeofday() ];
our $number_of_git_cmds = 0;
BEGIN {
# the gitweb domain.
our $prevent_xss = 0;
+# Path to the highlight executable to use (must be the one from
+# http://www.andre-simon.de due to assumptions about parameters and output).
+# Useful if highlight is not installed on your webserver's PATH.
+# [Default: highlight]
+our $highlight_bin = "++HIGHLIGHT_BIN++";
+
# information about snapshot formats that gitweb is capable of serving
our %known_snapshot_formats = (
# name => {
# main extensions, defining name of syntax;
# see files in /usr/share/highlight/langDefs/ directory
map { $_ => $_ }
- qw(py c cpp rb java css php sh pl js tex bib xml awk bat ini spec tcl),
+ qw(py c cpp rb java css php sh pl js tex bib xml awk bat ini spec tcl sql make),
# alternate extensions, see /etc/highlight/filetypes.conf
'h' => 'c',
+ map { $_ => 'sh' } qw(bash zsh ksh),
map { $_ => 'cpp' } qw(cxx c++ cc),
- map { $_ => 'php' } qw(php3 php4),
+ map { $_ => 'php' } qw(php3 php4 php5 phps),
map { $_ => 'pl' } qw(perl pm), # perhaps also 'cgi'
- 'mak' => 'make',
+ map { $_ => 'make'} qw(mak mk),
map { $_ => 'xml' } qw(xhtml html htm),
);
'sub' => sub { feature_bool('highlight', @_) },
'override' => 0,
'default' => [0]},
+
+ # Enable displaying of remote heads in the heads list
+
+ # To enable system wide have in $GITWEB_CONFIG
+ # $feature{'remote_heads'}{'default'} = [1];
+ # To have project specific config enable override in $GITWEB_CONFIG
+ # $feature{'remote_heads'}{'override'} = 1;
+ # and in project config gitweb.remote_heads = 0|1;
+ 'remote_heads' => {
+ 'sub' => sub { feature_bool('remote_heads', @_) },
+ 'override' => 0,
+ 'default' => [0]},
);
sub gitweb_get_feature {
!$known_snapshot_formats{$_}{'disabled'}} @fmts;
}
+# If it is set to code reference, it is code that it is to be run once per
+# request, allowing updating configurations that change with each request,
+# while running other code in config file only once.
+#
+# Otherwise, if it is false then gitweb would process config file only once;
+# if it is true then gitweb config would be run for each request.
+our $per_request_config = 1;
+
our ($GITWEB_CONFIG, $GITWEB_CONFIG_SYSTEM);
sub evaluate_gitweb_config {
our $GITWEB_CONFIG = $ENV{'GITWEB_CONFIG'} || "++GITWEB_CONFIG++";
"log" => \&git_log,
"patch" => \&git_patch,
"patches" => \&git_patches,
+ "remotes" => \&git_remotes,
"rss" => \&git_rss,
"atom" => \&git_atom,
"search" => \&git_search,
'history',
);
- # we want to catch
+ # we want to catch, among others
# [$hash_parent_base[:$file_parent]..]$hash_parent[:$file_name]
my ($parentrefname, $parentpathname, $refname, $pathname) =
- ($path_info =~ /^(?:(.+?)(?::(.+))?\.\.)?(.+?)(?::(.+))?$/);
+ ($path_info =~ /^(?:(.+?)(?::(.+))?\.\.)?([^:]+?)?(?::(.+))?$/);
# first, analyze the 'current' part
if (defined $pathname) {
# hash_base instead. It should also be noted that hand-crafted
# links having 'history' as an action and no pathname or hash
# set will fail, but that happens regardless of PATH_INFO.
- $input_params{'action'} ||= "shortlog";
- if (grep { $_ eq $input_params{'action'} } @wants_base) {
+ if (defined $parentrefname) {
+ # if there is parent let the default be 'shortlog' action
+ # (for http://git.example.com/repo.git/A..B links); if there
+ # is no parent, dispatch will detect type of object and set
+ # action appropriately if required (if action is not set)
+ $input_params{'action'} ||= "shortlog";
+ }
+ if ($input_params{'action'} &&
+ grep { $_ eq $input_params{'action'} } @wants_base) {
$input_params{'hash_base'} ||= $refname;
} else {
$input_params{'hash'} ||= $refname;
}
sub reset_timer {
- our $t0 = [Time::HiRes::gettimeofday()]
+ our $t0 = [ gettimeofday() ]
if defined $t0;
our $number_of_git_cmds = 0;
}
+our $first_request = 1;
sub run_request {
reset_timer();
evaluate_uri();
- evaluate_gitweb_config();
+ if ($first_request) {
+ evaluate_gitweb_config();
+ evaluate_git_version();
+ }
+ if ($per_request_config) {
+ if (ref($per_request_config) eq 'CODE') {
+ $per_request_config->();
+ } elsif (!$first_request) {
+ evaluate_gitweb_config();
+ }
+ }
check_loadavg();
# $projectroot and $projects_list might be set in gitweb config file
sub run {
evaluate_argv();
- evaluate_git_version();
+ $first_request = 1;
$pre_listen_hook->()
if $pre_listen_hook;
$post_dispatch_hook->()
if $post_dispatch_hook;
+ $first_request = 0;
last REQUEST if ($is_last_request->());
}
$href =~ s,/$,,;
# Then add the project name, if present
- $href .= "/".esc_url($params{'project'});
+ $href .= "/".esc_path_info($params{'project'});
delete $params{'project'};
# since we destructively absorb parameters, we keep this
# Summary just uses the project path URL, any other action is
# added to the URL
if (defined $params{'action'}) {
- $href .= "/".esc_url($params{'action'}) unless $params{'action'} eq 'summary';
+ $href .= "/".esc_path_info($params{'action'})
+ unless $params{'action'} eq 'summary';
delete $params{'action'};
}
|| $params{'hash_parent'} || $params{'hash'});
if (defined $params{'hash_base'}) {
if (defined $params{'hash_parent_base'}) {
- $href .= esc_url($params{'hash_parent_base'});
+ $href .= esc_path_info($params{'hash_parent_base'});
# skip the file_parent if it's the same as the file_name
if (defined $params{'file_parent'}) {
if (defined $params{'file_name'} && $params{'file_parent'} eq $params{'file_name'}) {
delete $params{'file_parent'};
} elsif ($params{'file_parent'} !~ /\.\./) {
- $href .= ":/".esc_url($params{'file_parent'});
+ $href .= ":/".esc_path_info($params{'file_parent'});
delete $params{'file_parent'};
}
}
delete $params{'hash_parent'};
delete $params{'hash_parent_base'};
} elsif (defined $params{'hash_parent'}) {
- $href .= esc_url($params{'hash_parent'}). "..";
+ $href .= esc_path_info($params{'hash_parent'}). "..";
delete $params{'hash_parent'};
}
- $href .= esc_url($params{'hash_base'});
+ $href .= esc_path_info($params{'hash_base'});
if (defined $params{'file_name'} && $params{'file_name'} !~ /\.\./) {
- $href .= ":/".esc_url($params{'file_name'});
+ $href .= ":/".esc_path_info($params{'file_name'});
delete $params{'file_name'};
}
delete $params{'hash'};
delete $params{'hash_base'};
} elsif (defined $params{'hash'}) {
- $href .= esc_url($params{'hash'});
+ $href .= esc_path_info($params{'hash'});
delete $params{'hash'};
}
}
$href .= "?" . join(';', @result) if scalar @result;
+ # final transformation: trailing spaces must be escaped (URI-encoded)
+ $href =~ s/(\s+)$/CGI::escape($1)/e;
+
return $href;
}
return $str;
}
+# the quoting rules for path_info fragment are slightly different
+sub esc_path_info {
+ my $str = shift;
+ return undef unless defined $str;
+
+ # path_info doesn't treat '+' as space (specially), but '?' must be escaped
+ $str =~ s/([^A-Za-z0-9\-_.~();\/;:@&= +]+)/CGI::escape($1)/eg;
+
+ return $str;
+}
+
# quote unsafe chars in whole URL, so some characters cannot be quoted
sub esc_url {
my $str = shift;
return (undef, undef);
}
+# Implementation note: when a single remote is wanted, we cannot use 'git
+# remote show -n' because that command always work (assuming it's a remote URL
+# if it's not defined), and we cannot use 'git remote show' because that would
+# try to make a network roundtrip. So the only way to find if that particular
+# remote is defined is to walk the list provided by 'git remote -v' and stop if
+# and when we find what we want.
+sub git_get_remotes_list {
+ my $wanted = shift;
+ my %remotes = ();
+
+ open my $fd, '-|' , git_cmd(), 'remote', '-v';
+ return unless $fd;
+ while (my $remote = <$fd>) {
+ chomp $remote;
+ $remote =~ s!\t(.*?)\s+\((\w+)\)$!!;
+ next if $wanted and not $remote eq $wanted;
+ my ($url, $key) = ($1, $2);
+
+ $remotes{$remote} ||= { 'heads' => () };
+ $remotes{$remote}{$key} = $url;
+ }
+ close $fd or return;
+ return wantarray ? %remotes : \%remotes;
+}
+
+# Takes a hash of remotes as first parameter and fills it by adding the
+# available remote heads for each of the indicated remotes.
+sub fill_remote_heads {
+ my $remotes = shift;
+ my @heads = map { "remotes/$_" } keys %$remotes;
+ my @remoteheads = git_get_heads_list(undef, @heads);
+ foreach my $remote (keys %$remotes) {
+ $remotes->{$remote}{'heads'} = [ grep {
+ $_->{'name'} =~ s!^$remote/!!
+ } @remoteheads ];
+ }
+}
+
sub git_get_references {
my $type = shift || "";
my %refs;
## parse to array of hashes functions
sub git_get_heads_list {
- my $limit = shift;
+ my ($limit, @classes) = @_;
+ @classes = ('heads') unless @classes;
+ my @patterns = map { "refs/$_" } @classes;
my @headslist;
open my $fd, '-|', git_cmd(), 'for-each-ref',
($limit ? '--count='.($limit+1) : ()), '--sort=-committerdate',
'--format=%(objectname) %(refname) %(subject)%00%(committer)',
- 'refs/heads'
+ @patterns
or return;
while (my $line = <$fd>) {
my %ref_item;
my ($committer, $epoch, $tz) =
($committerinfo =~ /^(.*) ([0-9]+) (.*)$/);
$ref_item{'fullname'} = $name;
- $name =~ s!^refs/heads/!!;
+ $name =~ s!^refs/(?:head|remote)s/!!;
$ref_item{'name'} = $name;
$ref_item{'id'} = $hash;
my ($fd, $highlight, $syntax) = @_;
return $fd unless ($highlight && defined $syntax);
- close $fd
- or die_error(404, "Reading blob failed");
+ close $fd;
open $fd, quote_command(git_cmd(), "cat-file", "blob", $hash)." | ".
- "highlight --xhtml --fragment --syntax $syntax |"
+ quote_command($highlight_bin).
+ " --fragment --syntax $syntax |"
or die_error(500, "Couldn't open file or run syntax highlighter");
return $fd;
}
$href_params{'-title'} = 'log';
}
- foreach my $format qw(RSS Atom) {
+ foreach my $format (qw(RSS Atom)) {
my $type = lc($format);
my %link_attr = (
'-rel' => 'alternate',
insert_file($site_header);
}
- print "<div class=\"page_header\">\n" .
- $cgi->a({-href => esc_url($logo_url),
- -title => $logo_label},
- qq(<img src=").esc_url($logo).qq(" width="72" height="27" alt="git" class="logo"/>));
+ print "<div class=\"page_header\">\n";
+ if (defined $logo) {
+ print $cgi->a({-href => esc_url($logo_url),
+ -title => $logo_label},
+ $cgi->img({-src => esc_url($logo),
+ -width => 72, -height => 27,
+ -alt => "git",
+ -class => "logo"}));
+ }
print $cgi->a({-href => esc_url($home_link)}, $home_link_str) . " / ";
if (defined $project) {
print $cgi->a({-href => href(action=>"summary")}, esc_html($project));
if (defined $action) {
- print " / $action";
+ my $action_print = $action ;
+ if (defined $opts{-action_extra}) {
+ $action_print = $cgi->a({-href => href(action=>$action)},
+ $action);
+ }
+ print " / $action_print";
+ }
+ if (defined $opts{-action_extra}) {
+ print " / $opts{-action_extra}";
}
print "\n";
}
}
$href_params{'-title'} ||= 'log';
- foreach my $format qw(RSS Atom) {
+ foreach my $format (qw(RSS Atom)) {
$href_params{'action'} = lc($format);
print $cgi->a({-href => href(%href_params),
-title => "$href_params{'-title'} $format feed",
print "<div id=\"generating_info\">\n";
print 'This page took '.
'<span id="generating_time" class="time_span">'.
- Time::HiRes::tv_interval($t0, [Time::HiRes::gettimeofday()]).
+ tv_interval($t0, [ gettimeofday() ]).
' seconds </span>'.
' and '.
'<span id="generating_cmd">'.
"</div>\n";
}
+# returns a submenu for the nagivation of the refs views (tags, heads,
+# remotes) with the current view disabled and the remotes view only
+# available if the feature is enabled
+sub format_ref_views {
+ my ($current) = @_;
+ my @ref_views = qw{tags heads};
+ push @ref_views, 'remotes' if gitweb_check_feature('remote_heads');
+ return join " | ", map {
+ $_ eq $current ? $_ :
+ $cgi->a({-href => href(action=>$_)}, $_)
+ } @ref_views
+}
+
sub format_paging_nav {
my ($action, $page, $has_next_link) = @_;
my $paging_nav;
"\n</div>\n";
}
+sub format_repo_url {
+ my ($name, $url) = @_;
+ return "<tr class=\"metadata_url\"><td>$name</td><td>$url</td></tr>\n";
+}
+
+# Group output by placing it in a DIV element and adding a header.
+# Options for start_div() can be provided by passing a hash reference as the
+# first parameter to the function.
+# Options to git_print_header_div() can be provided by passing an array
+# reference. This must follow the options to start_div if they are present.
+# The content can be a scalar, which is output as-is, a scalar reference, which
+# is output after html escaping, an IO handle passed either as *handle or
+# *handle{IO}, or a function reference. In the latter case all following
+# parameters will be taken as argument to the content function call.
+sub git_print_section {
+ my ($div_args, $header_args, $content);
+ my $arg = shift;
+ if (ref($arg) eq 'HASH') {
+ $div_args = $arg;
+ $arg = shift;
+ }
+ if (ref($arg) eq 'ARRAY') {
+ $header_args = $arg;
+ $arg = shift;
+ }
+ $content = $arg;
+
+ print $cgi->start_div($div_args);
+ git_print_header_div(@$header_args);
+
+ if (ref($content) eq 'CODE') {
+ $content->(@_);
+ } elsif (ref($content) eq 'SCALAR') {
+ print esc_html($$content);
+ } elsif (ref($content) eq 'GLOB' or ref($content) eq 'IO::Handle') {
+ print <$content>;
+ } elsif (!ref($content) && defined($content)) {
+ print $content;
+ }
+
+ print $cgi->end_div;
+}
+
sub print_local_time {
print format_local_time(@_);
}
}
if ($diff->{'from_mode'} ne ('0' x 6)) {
$from_mode_oct = oct $diff->{'from_mode'};
- if (S_ISREG($to_mode_oct)) { # only for regular file
+ if (S_ISREG($from_mode_oct)) { # only for regular file
$from_mode_str = sprintf("%04o", $from_mode_oct & 0777); # permission bits
}
$from_file_type = file_type($diff->{'from_mode'});
"<td class=\"link\">" .
$cgi->a({-href => href(action=>"shortlog", hash=>$ref{'fullname'})}, "shortlog") . " | " .
$cgi->a({-href => href(action=>"log", hash=>$ref{'fullname'})}, "log") . " | " .
- $cgi->a({-href => href(action=>"tree", hash=>$ref{'fullname'}, hash_base=>$ref{'name'})}, "tree") .
+ $cgi->a({-href => href(action=>"tree", hash=>$ref{'fullname'}, hash_base=>$ref{'fullname'})}, "tree") .
"</td>\n" .
"</tr>";
}
print "</table>\n";
}
+# Display a single remote block
+sub git_remote_block {
+ my ($remote, $rdata, $limit, $head) = @_;
+
+ my $heads = $rdata->{'heads'};
+ my $fetch = $rdata->{'fetch'};
+ my $push = $rdata->{'push'};
+
+ my $urls_table = "<table class=\"projects_list\">\n" ;
+
+ if (defined $fetch) {
+ if ($fetch eq $push) {
+ $urls_table .= format_repo_url("URL", $fetch);
+ } else {
+ $urls_table .= format_repo_url("Fetch URL", $fetch);
+ $urls_table .= format_repo_url("Push URL", $push) if defined $push;
+ }
+ } elsif (defined $push) {
+ $urls_table .= format_repo_url("Push URL", $push);
+ } else {
+ $urls_table .= format_repo_url("", "No remote URL");
+ }
+
+ $urls_table .= "</table>\n";
+
+ my $dots;
+ if (defined $limit && $limit < @$heads) {
+ $dots = $cgi->a({-href => href(action=>"remotes", hash=>$remote)}, "...");
+ }
+
+ print $urls_table;
+ git_heads_body($heads, $head, 0, $limit, $dots);
+}
+
+# Display a list of remote names with the respective fetch and push URLs
+sub git_remotes_list {
+ my ($remotedata, $limit) = @_;
+ print "<table class=\"heads\">\n";
+ my $alternate = 1;
+ my @remotes = sort keys %$remotedata;
+
+ my $limited = $limit && $limit < @remotes;
+
+ $#remotes = $limit - 1 if $limited;
+
+ while (my $remote = shift @remotes) {
+ my $rdata = $remotedata->{$remote};
+ my $fetch = $rdata->{'fetch'};
+ my $push = $rdata->{'push'};
+ if ($alternate) {
+ print "<tr class=\"dark\">\n";
+ } else {
+ print "<tr class=\"light\">\n";
+ }
+ $alternate ^= 1;
+ print "<td>" .
+ $cgi->a({-href=> href(action=>'remotes', hash=>$remote),
+ -class=> "list name"},esc_html($remote)) .
+ "</td>";
+ print "<td class=\"link\">" .
+ (defined $fetch ? $cgi->a({-href=> $fetch}, "fetch") : "fetch") .
+ " | " .
+ (defined $push ? $cgi->a({-href=> $push}, "push") : "push") .
+ "</td>";
+
+ print "</tr>\n";
+ }
+
+ if ($limited) {
+ print "<tr>\n" .
+ "<td colspan=\"3\">" .
+ $cgi->a({-href => href(action=>"remotes")}, "...") .
+ "</td>\n" . "</tr>\n";
+ }
+
+ print "</table>";
+}
+
+# Display remote heads grouped by remote, unless there are too many
+# remotes, in which case we only display the remote names
+sub git_remotes_body {
+ my ($remotedata, $limit, $head) = @_;
+ if ($limit and $limit < keys %$remotedata) {
+ git_remotes_list($remotedata, $limit);
+ } else {
+ fill_remote_heads($remotedata);
+ while (my ($remote, $rdata) = each %$remotedata) {
+ git_print_section({-class=>"remote", -id=>$remote},
+ ["remotes", $remote, $remote], sub {
+ git_remote_block($remote, $rdata, $limit, $head);
+ });
+ }
+ }
+}
+
sub git_search_grep_body {
my ($commitlist, $from, $to, $extra) = @_;
$from = 0 unless defined $from;
my %co = parse_commit("HEAD");
my %cd = %co ? parse_date($co{'committer_epoch'}, $co{'committer_tz'}) : ();
my $head = $co{'id'};
+ my $remote_heads = gitweb_check_feature('remote_heads');
my $owner = git_get_project_owner($project);
# there are more ...
my @taglist = git_get_tags_list(16);
my @headlist = git_get_heads_list(16);
+ my %remotedata = $remote_heads ? git_get_remotes_list() : ();
my @forklist;
my $check_forks = gitweb_check_feature('forks');
@url_list = map { "$_/$project" } @git_base_url_list unless @url_list;
foreach my $git_url (@url_list) {
next unless $git_url;
- print "<tr class=\"metadata_url\"><td>$url_tag</td><td>$git_url</td></tr>\n";
+ print format_repo_url($url_tag, $git_url);
$url_tag = "";
}
$cgi->a({-href => href(action=>"heads")}, "..."));
}
+ if (%remotedata) {
+ git_print_header_div('remotes');
+ git_remotes_body(\%remotedata, 15, $head);
+ }
+
if (@forklist) {
git_print_header_div('forks');
git_project_list_body(\@forklist, 'age', 0, 15,
print 'END';
if (defined $t0 && gitweb_check_feature('timed')) {
print ' '.
- Time::HiRes::tv_interval($t0, [Time::HiRes::gettimeofday()]).
+ tv_interval($t0, [ gettimeofday() ]).
' '.$number_of_git_cmds;
}
print "\n";
sub git_tags {
my $head = git_get_head_hash($project);
git_header_html();
- git_print_page_nav('','', $head,undef,$head);
+ git_print_page_nav('','', $head,undef,$head,format_ref_views('tags'));
git_print_header_div('summary', $project);
my @tagslist = git_get_tags_list();
sub git_heads {
my $head = git_get_head_hash($project);
git_header_html();
- git_print_page_nav('','', $head,undef,$head);
+ git_print_page_nav('','', $head,undef,$head,format_ref_views('heads'));
git_print_header_div('summary', $project);
my @headslist = git_get_heads_list();
git_footer_html();
}
+# used both for single remote view and for list of all the remotes
+sub git_remotes {
+ gitweb_check_feature('remote_heads')
+ or die_error(403, "Remote heads view is disabled");
+
+ my $head = git_get_head_hash($project);
+ my $remote = $input_params{'hash'};
+
+ my $remotedata = git_get_remotes_list($remote);
+ die_error(500, "Unable to get remote information") unless defined $remotedata;
+
+ unless (%$remotedata) {
+ die_error(404, defined $remote ?
+ "Remote $remote not found" :
+ "No remotes found");
+ }
+
+ git_header_html(undef, undef, -action_extra => $remote);
+ git_print_page_nav('', '', $head, undef, $head,
+ format_ref_views($remote ? '' : 'remotes'));
+
+ fill_remote_heads($remotedata);
+ if (defined $remote) {
+ git_print_header_div('remotes', "$remote remote for $project");
+ git_remote_block($remote, $remotedata->{$remote}, undef, $head);
+ } else {
+ git_print_header_div('summary', "$project remotes");
+ git_remotes_body($remotedata, undef, $head);
+ }
+
+ git_footer_html();
+}
+
sub git_blob_plain {
my $type = shift;
my $expires;
if (defined $favicon) {
print "<icon>" . esc_url($favicon) . "</icon>\n";
}
- if (defined $logo_url) {
+ if (defined $logo) {
# not twice as wide as tall: 72 x 27 pixels
print "<logo>" . esc_url($logo) . "</logo>\n";
}