#our $projectroot = "/pub/scm";
our $projectroot = "++GITWEB_PROJECTROOT++";
+# fs traversing limit for getting project list
+# the number is relative to the projectroot
+our $project_maxdepth = "++GITWEB_PROJECT_MAXDEPTH++";
+
# target of the home link on top of all pages
our $home_link = $my_uri || "/";
# source of projects list
our $projects_list = "++GITWEB_LIST++";
+# the width (in characters) of the projects list "Description" column
+our $projects_list_description_width = 25;
+
# default order of projects list
# valid values are none, project, descr, owner, and age
our $default_projects_order = "project";
# could be even 'utf-8' for the old behavior)
our $fallback_encoding = 'latin1';
+# rename detection options for git-diff and git-diff-tree
+# - default is '-M', with the cost proportional to
+# (number of removed files) * (number of new files).
+# - more costly is '-C' (which implies '-M'), with the cost proportional to
+# (number of changed files + number of removed files) * (number of new files)
+# - even more costly is '-C', '--find-copies-harder' with cost
+# (number of files in the original tree) * (number of new files)
+# - one might want to include '-B' option, e.g. '-B', '-M'
+our @diff_opts = ('-M'); # taken from git_commit
+
+# information about snapshot formats that gitweb is capable of serving
+our %known_snapshot_formats = (
+ # name => {
+ # 'display' => display name,
+ # 'type' => mime type,
+ # 'suffix' => filename suffix,
+ # 'format' => --format for git-archive,
+ # 'compressor' => [compressor command and arguments]
+ # (array reference, optional)}
+ #
+ 'tgz' => {
+ 'display' => 'tar.gz',
+ 'type' => 'application/x-gzip',
+ 'suffix' => '.tar.gz',
+ 'format' => 'tar',
+ 'compressor' => ['gzip']},
+
+ 'tbz2' => {
+ 'display' => 'tar.bz2',
+ 'type' => 'application/x-bzip2',
+ 'suffix' => '.tar.bz2',
+ 'format' => 'tar',
+ 'compressor' => ['bzip2']},
+
+ 'zip' => {
+ 'display' => 'zip',
+ 'type' => 'application/x-zip',
+ 'suffix' => '.zip',
+ 'format' => 'zip'},
+);
+
+# Aliases so we understand old gitweb.snapshot values in repository
+# configuration.
+our %known_snapshot_format_aliases = (
+ 'gzip' => 'tgz',
+ 'bzip2' => 'tbz2',
+
+ # backward compatibility: legacy gitweb config support
+ 'x-gzip' => undef, 'gz' => undef,
+ 'x-bzip2' => undef, 'bz2' => undef,
+ 'x-zip' => undef, '' => undef,
+);
+
# You define site-wide feature defaults here; override them with
# $GITWEB_CONFIG as necessary.
our %feature = (
'override' => 0,
'default' => [0]},
- # Enable the 'snapshot' link, providing a compressed tarball of any
+ # Enable the 'snapshot' link, providing a compressed archive of any
# tree. This can potentially generate high traffic if you have large
# project.
+ # Value is a list of formats defined in %known_snapshot_formats that
+ # you wish to offer.
# To disable system wide have in $GITWEB_CONFIG
- # $feature{'snapshot'}{'default'} = [undef];
+ # $feature{'snapshot'}{'default'} = [];
# To have project specific config enable override in $GITWEB_CONFIG
# $feature{'snapshot'}{'override'} = 1;
- # and in project config gitweb.snapshot = none|gzip|bzip2|zip;
+ # and in project config, a comma-separated list of formats or "none"
+ # to disable. Example: gitweb.snapshot = tbz2,zip;
'snapshot' => {
'sub' => \&feature_snapshot,
'override' => 0,
- # => [content-encoding, suffix, program]
- 'default' => ['x-gzip', 'gz', 'gzip']},
+ 'default' => ['tgz']},
# Enable text search, which will list the commits which match author,
# committer or commit text to a given string. Enabled by default.
}
sub feature_snapshot {
- my ($ctype, $suffix, $command) = @_;
+ my (@fmts) = @_;
my ($val) = git_get_project_config('snapshot');
- if ($val eq 'gzip') {
- return ('x-gzip', 'gz', 'gzip');
- } elsif ($val eq 'bzip2') {
- return ('x-bzip2', 'bz2', 'bzip2');
- } elsif ($val eq 'zip') {
- return ('x-zip', 'zip', '');
- } elsif ($val eq 'none') {
- return ();
+ if ($val) {
+ @fmts = ($val eq 'none' ? () : split /\s*[,\s]\s*/, $val);
}
- return ($ctype, $suffix, $command);
-}
-
-sub gitweb_have_snapshot {
- my ($ctype, $suffix, $command) = gitweb_check_feature('snapshot');
- my $have_snapshot = (defined $ctype && defined $suffix);
-
- return $have_snapshot;
+ return @fmts;
}
sub feature_grep {
(!$export_ok || -e "$dir/$export_ok"));
}
-# rename detection options for git-diff and git-diff-tree
-# - default is '-M', with the cost proportional to
-# (number of removed files) * (number of new files).
-# - more costly is '-C' (or '-C', '-M'), with the cost proportional to
-# (number of changed files + number of removed files) * (number of new files)
-# - even more costly is '-C', '--find-copies-harder' with cost
-# (number of files in the original tree) * (number of new files)
-# - one might want to include '-B' option, e.g. '-B', '-M'
-our @diff_opts = ('-M'); # taken from git_commit
+# process alternate names for backward compatibility
+# filter out unsupported (unknown) snapshot formats
+sub filter_snapshot_fmts {
+ my @fmts = @_;
+
+ @fmts = map {
+ exists $known_snapshot_format_aliases{$_} ?
+ $known_snapshot_format_aliases{$_} : $_} @fmts;
+ @fmts = grep(exists $known_snapshot_formats{$_}, @fmts);
+
+}
our $GITWEB_CONFIG = $ENV{'GITWEB_CONFIG'} || "++GITWEB_CONFIG++";
do $GITWEB_CONFIG if -e $GITWEB_CONFIG;
}
}
+my %allowed_options = (
+ "--no-merges" => [ qw(rss atom log shortlog history) ],
+);
+
+our @extra_options = $cgi->param('opt');
+if (defined @extra_options) {
+ foreach my $opt (@extra_options) {
+ if (not exists $allowed_options{$opt}) {
+ die_error(undef, "Invalid option parameter");
+ }
+ if (not grep(/^$action$/, @{$allowed_options{$opt}})) {
+ die_error(undef, "Invalid option parameter for this action");
+ }
+ }
+}
+
our $hash_parent_base = $cgi->param('hpb');
if (defined $hash_parent_base) {
if (!validate_refname($hash_parent_base)) {
our $searchtext = $cgi->param('s');
our $search_regexp;
if (defined $searchtext) {
- if ($searchtype ne 'grep' and $searchtype ne 'pickaxe' and $searchtext =~ m/[^a-zA-Z0-9_\.\/\-\+\:\@ ]/) {
- die_error(undef, "Invalid search parameter");
- }
if (length($searchtext) < 2) {
die_error(undef, "At least two characters are required for search parameter");
}
order => "o",
searchtext => "s",
searchtype => "st",
+ snapshot_format => "sf",
+ extra_options => "opt",
);
my %mapping = @mapping;
for (my $i = 0; $i < @mapping; $i += 2) {
my ($name, $symbol) = ($mapping[$i], $mapping[$i+1]);
if (defined $params{$name}) {
- push @result, $symbol . "=" . esc_param($params{$name});
+ if (ref($params{$name}) eq "ARRAY") {
+ foreach my $par (@{$params{$name}}) {
+ push @result, $symbol . "=" . esc_param($par);
+ }
+ } else {
+ push @result, $symbol . "=" . esc_param($params{$name});
+ }
}
}
$href .= "?" . join(';', @result) if scalar @result;
return "$body$tail";
}
+# takes the same arguments as chop_str, but also wraps a <span> around the
+# result with a title attribute if it does get chopped. Additionally, the
+# string is HTML-escaped.
+sub chop_and_escape_str {
+ my $str = shift;
+ my $len = shift;
+ my $add_len = shift || 10;
+
+ my $chopped = chop_str($str, $len, $add_len);
+ if ($chopped eq $str) {
+ return esc_html($chopped);
+ } else {
+ return qq{<span title="} . esc_html($str) . qq{">} .
+ esc_html($chopped) . qq{</span>};
+ }
+}
+
## ----------------------------------------------------------------------
## functions returning short strings
return $age_str;
}
+use constant {
+ S_IFINVALID => 0030000,
+ S_IFGITLINK => 0160000,
+};
+
+# submodule/subproject, a commit object reference
+sub S_ISGITLINK($) {
+ my $mode = shift;
+
+ return (($mode & S_IFMT) == S_IFGITLINK)
+}
+
# convert file mode in octal to symbolic file mode string
sub mode_str {
my $mode = oct shift;
- if (S_ISDIR($mode & S_IFMT)) {
+ if (S_ISGITLINK($mode)) {
+ return 'm---------';
+ } elsif (S_ISDIR($mode & S_IFMT)) {
return 'drwxr-xr-x';
} elsif (S_ISLNK($mode)) {
return 'lrwxrwxrwx';
$mode = oct $mode;
}
- if (S_ISDIR($mode & S_IFMT)) {
+ if (S_ISGITLINK($mode)) {
+ return "submodule";
+ } elsif (S_ISDIR($mode & S_IFMT)) {
return "directory";
} elsif (S_ISLNK($mode)) {
return "symlink";
$mode = oct $mode;
}
- if (S_ISDIR($mode & S_IFMT)) {
+ if (S_ISGITLINK($mode)) {
+ return "submodule";
+ } elsif (S_ISDIR($mode & S_IFMT)) {
return "directory";
} elsif (S_ISLNK($mode)) {
return "symlink";
return "<div class=\"diff$diff_class\">" . esc_html($line, -nbsp=>1) . "</div>\n";
}
+# Generates undef or something like "_snapshot_" or "snapshot (_tbz2_ _zip_)",
+# linked. Pass the hash of the tree/commit to snapshot.
+sub format_snapshot_links {
+ my ($hash) = @_;
+ my @snapshot_fmts = gitweb_check_feature('snapshot');
+ @snapshot_fmts = filter_snapshot_fmts(@snapshot_fmts);
+ my $num_fmts = @snapshot_fmts;
+ if ($num_fmts > 1) {
+ # A parenthesized list of links bearing format names.
+ # e.g. "snapshot (_tar.gz_ _zip_)"
+ return "snapshot (" . join(' ', map
+ $cgi->a({
+ -href => href(
+ action=>"snapshot",
+ hash=>$hash,
+ snapshot_format=>$_
+ )
+ }, $known_snapshot_formats{$_}{'display'})
+ , @snapshot_fmts) . ")";
+ } elsif ($num_fmts == 1) {
+ # A single "snapshot" link whose tooltip bears the format name.
+ # i.e. "_snapshot_"
+ my ($fmt) = @snapshot_fmts;
+ return
+ $cgi->a({
+ -href => href(
+ action=>"snapshot",
+ hash=>$hash,
+ snapshot_format=>$fmt
+ ),
+ -title => "in format: $known_snapshot_formats{$fmt}{'display'}"
+ }, "snapshot");
+ } else { # $num_fmts == 0
+ return undef;
+ }
+}
+
## ----------------------------------------------------------------------
## git utility subroutines, invoking git commands
# remove the trailing "/"
$dir =~ s!/+$!!;
my $pfxlen = length("$dir");
+ my $pfxdepth = ($dir =~ tr!/!!);
File::Find::find({
follow_fast => 1, # follow symbolic links
+ follow_skip => 2, # ignore duplicates
dangling_symlinks => 0, # ignore dangling symlinks, silently
wanted => sub {
# skip project-list toplevel, if we get it.
return if (m!^[/.]$!);
# only directories can be git repositories
return unless (-d $_);
+ # don't traverse too deep (Find is super slow on os x)
+ if (($File::Find::name =~ tr!/!!) - $pfxdepth > $project_maxdepth) {
+ $File::Find::prune = 1;
+ return;
+ }
my $subdir = substr($File::Find::name, $pfxlen + 1);
# we check related file in $projectroot
($arg ? ($arg) : ()),
("--max-count=" . $maxcount),
("--skip=" . $skip),
+ @extra_options,
$commit_id,
"--",
($filename ? ($filename) : ())
return wantarray ? %res : \%res;
}
+# wrapper: return parsed line of git-diff-tree "raw" output
+# (the argument might be raw line, or parsed info)
+sub parsed_difftree_line {
+ my $line_or_ref = shift;
+
+ if (ref($line_or_ref) eq "HASH") {
+ # pre-parsed (or generated by hand)
+ return $line_or_ref;
+ } else {
+ return parse_difftree_raw_line($line_or_ref);
+ }
+}
+
# parse line of git-ls-tree output
sub parse_ls_tree_line ($;%) {
my $line = shift;
}
}
} else {
+ # ordinary (not combined) diff
$from->{'file'} = $diffinfo->{'from_file'} || $diffinfo->{'file'};
if ($diffinfo->{'status'} ne "A") { # not new (added) file
$from->{'href'} = href(action=>"blob", hash_base=>$hash_parent,
printf('<link rel="alternate" title="%s log RSS feed" '.
'href="%s" type="application/rss+xml" />'."\n",
esc_param($project), href(action=>"rss"));
+ printf('<link rel="alternate" title="%s log RSS feed (no merges)" '.
+ 'href="%s" type="application/rss+xml" />'."\n",
+ esc_param($project), href(action=>"rss",
+ extra_options=>"--no-merges"));
printf('<link rel="alternate" title="%s log Atom feed" '.
'href="%s" type="application/atom+xml" />'."\n",
esc_param($project), href(action=>"atom"));
+ printf('<link rel="alternate" title="%s log Atom feed (no merges)" '.
+ 'href="%s" type="application/atom+xml" />'."\n",
+ esc_param($project), href(action=>"atom",
+ extra_options=>"--no-merges"));
} else {
printf('<link rel="alternate" title="%s projects list" '.
'href="%s" type="text/plain; charset=utf-8"/>'."\n",
"history");
}
print "</td>\n";
+ } else {
+ # unknown object: we can only present history for it
+ # (this includes 'commit' object, i.e. submodule support)
+ print "<td class=\"list\">" .
+ esc_path($t->{'name'}) .
+ "</td>\n";
+ print "<td class=\"link\">";
+ if (defined $hash_base) {
+ print $cgi->a({-href => href(action=>"history",
+ hash_base=>$hash_base,
+ file_name=>"$basedir$t->{'name'}")},
+ "history");
+ }
+ print "</td>\n";
}
}
## ......................................................................
## functions printing large fragments of HTML
+# get pre-image filenames for merge (combined) diff
sub fill_from_file_info {
my ($diff, @parents) = @_;
return $diff;
}
-# parameters can be strings, or references to arrays of strings
-sub from_ids_eq {
- my ($a, $b) = @_;
-
- if (ref($a) eq "ARRAY" && ref($b) eq "ARRAY" && @$a == @$b) {
- for (my $i = 0; $i < @$a; ++$i) {
- return 0 unless ($a->[$i] eq $b->[$i]);
- }
- return 1;
- } elsif (!ref($a) && !ref($b)) {
- return $a eq $b;
- } else {
- return 0;
- }
-}
-
+# is current raw difftree line of file deletion
sub is_deleted {
my $diffinfo = shift;
return $diffinfo->{'to_id'} eq ('0' x 40);
}
+# does patch correspond to [previous] difftree raw line
+# $diffinfo - hashref of parsed raw diff format
+# $patchinfo - hashref of parsed patch diff format
+# (the same keys as in $diffinfo)
+sub is_patch_split {
+ my ($diffinfo, $patchinfo) = @_;
+
+ return defined $diffinfo && defined $patchinfo
+ && ($diffinfo->{'to_file'} || $diffinfo->{'file'}) eq $patchinfo->{'to_file'};
+}
+
+
sub git_difftree_body {
my ($difftree, $hash, @parents) = @_;
my ($parent) = $parents[0];
"diff_tree\">\n";
# header only for combined diff in 'commitdiff' view
- my $has_header = @parents > 1 && $action eq 'commitdiff';
+ my $has_header = @$difftree && @parents > 1 && $action eq 'commitdiff';
if ($has_header) {
# table header
print "<thead><tr>\n" .
my $alternate = 1;
my $patchno = 0;
foreach my $line (@{$difftree}) {
- my $diff;
- if (ref($line) eq "HASH") {
- # pre-parsed (or generated by hand)
- $diff = $line;
- } else {
- $diff = parse_difftree_raw_line($line);
- }
+ my $diff = parsed_difftree_line($line);
if ($alternate) {
print "<tr class=\"dark\">\n";
my ($fd, $difftree, $hash, @hash_parents) = @_;
my ($hash_parent) = $hash_parents[0];
+ my $is_combined = (@hash_parents > 1);
my $patch_idx = 0;
my $patch_number = 0;
my $patch_line;
my $diffinfo;
+ my $to_name;
my (%from, %to);
print "<div class=\"patchset\">\n";
PATCH:
while ($patch_line) {
- my @diff_header;
- my ($from_id, $to_id);
- # git diff header
- #assert($patch_line =~ m/^diff /) if DEBUG;
- #assert($patch_line !~ m!$/$!) if DEBUG; # is chomp-ed
- $patch_number++;
- push @diff_header, $patch_line;
-
- # extended diff header
- EXTENDED_HEADER:
- while ($patch_line = <$fd>) {
- chomp $patch_line;
-
- last EXTENDED_HEADER if ($patch_line =~ m/^--- |^diff /);
-
- if ($patch_line =~ m/^index ([0-9a-fA-F]{40})..([0-9a-fA-F]{40})/) {
- $from_id = $1;
- $to_id = $2;
- } elsif ($patch_line =~ m/^index ((?:[0-9a-fA-F]{40},)+[0-9a-fA-F]{40})..([0-9a-fA-F]{40})/) {
- $from_id = [ split(',', $1) ];
- $to_id = $2;
- }
-
- push @diff_header, $patch_line;
+ # parse "git diff" header line
+ if ($patch_line =~ m/^diff --git (\"(?:[^\\\"]*(?:\\.[^\\\"]*)*)\"|[^ "]*) (.*)$/) {
+ # $1 is from_name, which we do not use
+ $to_name = unquote($2);
+ $to_name =~ s!^b/!!;
+ } elsif ($patch_line =~ m/^diff --(cc|combined) ("?.*"?)$/) {
+ # $1 is 'cc' or 'combined', which we do not use
+ $to_name = unquote($2);
+ } else {
+ $to_name = undef;
}
- my $last_patch_line = $patch_line;
# check if current patch belong to current raw line
# and parse raw git-diff line if needed
- if (defined $diffinfo &&
- defined $from_id && defined $to_id &&
- from_ids_eq($diffinfo->{'from_id'}, $from_id) &&
- $diffinfo->{'to_id'} eq $to_id) {
+ if (is_patch_split($diffinfo, { 'to_file' => $to_name })) {
# this is continuation of a split patch
print "<div class=\"patch cont\">\n";
} else {
# advance raw git-diff output if needed
$patch_idx++ if defined $diffinfo;
- # compact combined diff output can have some patches skipped
- # find which patch (using pathname of result) we are at now
- my $to_name;
- if ($diff_header[0] =~ m!^diff --cc "?(.*)"?$!) {
- $to_name = $1;
- }
-
- do {
- # read and prepare patch information
- if (ref($difftree->[$patch_idx]) eq "HASH") {
- # pre-parsed (or generated by hand)
- $diffinfo = $difftree->[$patch_idx];
- } else {
- $diffinfo = parse_difftree_raw_line($difftree->[$patch_idx]);
- }
+ # read and prepare patch information
+ $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
- # check if current raw line has no patch (it got simplified)
- if (defined $to_name && $to_name ne $diffinfo->{'to_file'}) {
+ # compact combined diff output can have some patches skipped
+ # find which patch (using pathname of result) we are at now;
+ if ($is_combined) {
+ while ($to_name ne $diffinfo->{'to_file'}) {
print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n" .
format_diff_cc_simplified($diffinfo, @hash_parents) .
"</div>\n"; # class="patch"
$patch_idx++;
$patch_number++;
+
+ last if $patch_idx > $#$difftree;
+ $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
}
- } until (!defined $to_name || $to_name eq $diffinfo->{'to_file'} ||
- $patch_idx > $#$difftree);
+ }
+
# modifies %from, %to hashes
parse_from_to_diffinfo($diffinfo, \%from, \%to, @hash_parents);
- if ($diffinfo->{'nparents'}) {
- # combined diff
- $from{'file'} = [];
- $from{'href'} = [];
- fill_from_file_info($diffinfo, @hash_parents)
- unless exists $diffinfo->{'from_file'};
- for (my $i = 0; $i < $diffinfo->{'nparents'}; $i++) {
- $from{'file'}[$i] = $diffinfo->{'from_file'}[$i] || $diffinfo->{'to_file'};
- if ($diffinfo->{'status'}[$i] ne "A") { # not new (added) file
- $from{'href'}[$i] = href(action=>"blob",
- hash_base=>$hash_parents[$i],
- hash=>$diffinfo->{'from_id'}[$i],
- file_name=>$from{'file'}[$i]);
- } else {
- $from{'href'}[$i] = undef;
- }
- }
- } else {
- $from{'file'} = $diffinfo->{'from_file'} || $diffinfo->{'file'};
- if ($diffinfo->{'status'} ne "A") { # not new (added) file
- $from{'href'} = href(action=>"blob", hash_base=>$hash_parent,
- hash=>$diffinfo->{'from_id'},
- file_name=>$from{'file'});
- } else {
- delete $from{'href'};
- }
- }
- $to{'file'} = $diffinfo->{'to_file'} || $diffinfo->{'file'};
- if (!is_deleted($diffinfo)) { # file exists in result
- $to{'href'} = href(action=>"blob", hash_base=>$hash,
- hash=>$diffinfo->{'to_id'},
- file_name=>$to{'file'});
- } else {
- delete $to{'href'};
- }
# this is first patch for raw difftree line with $patch_idx index
# we index @$difftree array from 0, but number patches from 1
print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n";
}
+ # git diff header
+ #assert($patch_line =~ m/^diff /) if DEBUG;
+ #assert($patch_line !~ m!$/$!) if DEBUG; # is chomp-ed
+ $patch_number++;
# print "git diff" header
- $patch_line = shift @diff_header;
print format_git_diff_header_line($patch_line, $diffinfo,
\%from, \%to);
# print extended diff header
- print "<div class=\"diff extended_header\">\n" if (@diff_header > 0);
+ print "<div class=\"diff extended_header\">\n";
EXTENDED_HEADER:
- foreach $patch_line (@diff_header) {
+ while ($patch_line = <$fd>) {
+ chomp $patch_line;
+
+ last EXTENDED_HEADER if ($patch_line =~ m/^--- |^diff /);
+
print format_extended_diff_header_line($patch_line, $diffinfo,
\%from, \%to);
}
- print "</div>\n" if (@diff_header > 0); # class="diff extended_header"
+ print "</div>\n"; # class="diff extended_header"
# from-file/to-file diff header
- $patch_line = $last_patch_line;
if (! $patch_line) {
print "</div>\n"; # class="patch"
last PATCH;
}
next PATCH if ($patch_line =~ m/^diff /);
#assert($patch_line =~ m/^---/) if DEBUG;
- #assert($patch_line eq $last_patch_line) if DEBUG;
+ my $last_patch_line = $patch_line;
$patch_line = <$fd>;
chomp $patch_line;
#assert($patch_line =~ m/^\+\+\+/) if DEBUG;
# for compact combined (--cc) format, with chunk and patch simpliciaction
# patchset might be empty, but there might be unprocessed raw lines
- for ($patch_idx++ if $patch_number > 0;
+ for (++$patch_idx if $patch_number > 0;
$patch_idx < @$difftree;
- $patch_idx++) {
+ ++$patch_idx) {
# read and prepare patch information
- if (ref($difftree->[$patch_idx]) eq "HASH") {
- # pre-parsed (or generated by hand)
- $diffinfo = $difftree->[$patch_idx];
- } else {
- $diffinfo = parse_difftree_raw_line($difftree->[$patch_idx]);
- }
+ $diffinfo = parsed_difftree_line($difftree->[$patch_idx]);
# generate anchor for "patch" links in difftree / whatchanged part
print "<div class=\"patch\" id=\"patch". ($patch_idx+1) ."\">\n" .
if (!defined $pr->{'descr'}) {
my $descr = git_get_project_description($pr->{'path'}) || "";
$pr->{'descr_long'} = to_utf8($descr);
- $pr->{'descr'} = chop_str($descr, 25, 5);
+ $pr->{'descr'} = chop_str($descr, $projects_list_description_width, 5);
}
if (!defined $pr->{'owner'}) {
$pr->{'owner'} = git_get_project_owner("$pr->{'path'}") || "";
"<td>" . $cgi->a({-href => href(project=>$pr->{'path'}, action=>"summary"),
-class => "list", -title => $pr->{'descr_long'}},
esc_html($pr->{'descr'})) . "</td>\n" .
- "<td><i>" . chop_str($pr->{'owner'}, 15) . "</i></td>\n";
+ "<td><i>" . chop_and_escape_str($pr->{'owner'}, 15) . "</i></td>\n";
print "<td class=\"". age_class($pr->{'age'}) . "\">" .
(defined $pr->{'age_string'} ? $pr->{'age_string'} : "No commits") . "</td>\n" .
"<td class=\"link\">" .
# uses global variable $project
my ($commitlist, $from, $to, $refs, $extra) = @_;
- my $have_snapshot = gitweb_have_snapshot();
-
$from = 0 unless defined $from;
$to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
print "<tr class=\"light\">\n";
}
$alternate ^= 1;
+ my $author = chop_and_escape_str($co{'author_name'}, 10);
# git_summary() used print "<td><i>$co{'age_string'}</i></td>\n" .
print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
- "<td><i>" . esc_html(chop_str($co{'author_name'}, 10)) . "</i></td>\n" .
+ "<td><i>" . $author . "</i></td>\n" .
"<td>";
print format_subject_html($co{'title'}, $co{'title_short'},
href(action=>"commit", hash=>$commit), $ref);
$cgi->a({-href => href(action=>"commit", hash=>$commit)}, "commit") . " | " .
$cgi->a({-href => href(action=>"commitdiff", hash=>$commit)}, "commitdiff") . " | " .
$cgi->a({-href => href(action=>"tree", hash=>$commit, hash_base=>$commit)}, "tree");
- if ($have_snapshot) {
- print " | " . $cgi->a({-href => href(action=>"snapshot", hash=>$commit)}, "snapshot");
+ my $snapshot_links = format_snapshot_links($commit);
+ if (defined $snapshot_links) {
+ print " | " . $snapshot_links;
}
print "</td>\n" .
"</tr>\n";
print "<tr class=\"light\">\n";
}
$alternate ^= 1;
+ # shortlog uses chop_str($co{'author_name'}, 10)
+ my $author = chop_and_escape_str($co{'author_name'}, 15, 3);
print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
- # shortlog uses chop_str($co{'author_name'}, 10)
- "<td><i>" . esc_html(chop_str($co{'author_name'}, 15, 3)) . "</i></td>\n" .
+ "<td><i>" . $author . "</i></td>\n" .
"<td>";
# originally git_history used chop_str($co{'title'}, 50)
print format_subject_html($co{'title'}, $co{'title_short'},
print "<tr class=\"light\">\n";
}
$alternate ^= 1;
+ my $author = chop_and_escape_str($co{'author_name'}, 15, 5);
print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
- "<td><i>" . esc_html(chop_str($co{'author_name'}, 15, 5)) . "</i></td>\n" .
+ "<td><i>" . $author . "</i></td>\n" .
"<td>" .
$cgi->a({-href => href(action=>"commit", hash=>$co{'id'}), -class => "list subject"},
- esc_html(chop_str($co{'title'}, 50)) . "<br/>");
+ chop_and_escape_str($co{'title'}, 50) . "<br/>");
my $comment = $co{'comment'};
foreach my $line (@$comment) {
if ($line =~ m/^(.*)($search_regexp)(.*)$/i) {
print "<div class=\"title\"> </div>\n";
print "<table cellspacing=\"0\">\n" .
"<tr><td>description</td><td>" . esc_html($descr) . "</td></tr>\n" .
- "<tr><td>owner</td><td>$owner</td></tr>\n";
+ "<tr><td>owner</td><td>" . esc_html($owner) . "</td></tr>\n";
if (defined $cd{'rfc2822'}) {
print "<tr><td>last change</td><td>$cd{'rfc2822'}</td></tr>\n";
}
}
sub git_tree {
- my $have_snapshot = gitweb_have_snapshot();
-
if (!defined $hash_base) {
$hash_base = "HEAD";
}
hash_base=>"HEAD", file_name=>$file_name)},
"HEAD"),
}
- if ($have_snapshot) {
+ my $snapshot_links = format_snapshot_links($hash);
+ if (defined $snapshot_links) {
# FIXME: Should be available when we have no hash base as well.
- push @views_nav,
- $cgi->a({-href => href(action=>"snapshot", hash=>$hash)},
- "snapshot");
+ push @views_nav, $snapshot_links;
}
git_print_page_nav('tree','', $hash_base, undef, undef, join(' | ', @views_nav));
git_print_header_div('commit', esc_html($co{'title'}) . $ref, $hash_base);
}
sub git_snapshot {
- my ($ctype, $suffix, $command) = gitweb_check_feature('snapshot');
- my $have_snapshot = (defined $ctype && defined $suffix);
- if (!$have_snapshot) {
+ my @supported_fmts = gitweb_check_feature('snapshot');
+ @supported_fmts = filter_snapshot_fmts(@supported_fmts);
+
+ my $format = $cgi->param('sf');
+ if (!@supported_fmts) {
die_error('403 Permission denied', "Permission denied");
}
+ # default to first supported snapshot format
+ $format ||= $supported_fmts[0];
+ if ($format !~ m/^[a-z0-9]+$/) {
+ die_error(undef, "Invalid snapshot format parameter");
+ } elsif (!exists($known_snapshot_formats{$format})) {
+ die_error(undef, "Unknown snapshot format");
+ } elsif (!grep($_ eq $format, @supported_fmts)) {
+ die_error(undef, "Unsupported snapshot format");
+ }
if (!defined $hash) {
$hash = git_get_head_hash($project);
}
- my $git = git_cmd_str();
+ my $git_command = git_cmd_str();
my $name = $project;
$name =~ s,([^/])/*\.git$,$1,;
$name = basename($name);
my $filename = to_utf8($name);
$name =~ s/\047/\047\\\047\047/g;
my $cmd;
- if ($suffix eq 'zip') {
- $filename .= "-$hash.$suffix";
- $cmd = "$git archive --format=zip --prefix=\'$name\'/ $hash";
- } else {
- $filename .= "-$hash.tar.$suffix";
- $cmd = "$git archive --format=tar --prefix=\'$name\'/ $hash | $command";
+ $filename .= "-$hash$known_snapshot_formats{$format}{'suffix'}";
+ $cmd = "$git_command archive " .
+ "--format=$known_snapshot_formats{$format}{'format'} " .
+ "--prefix=\'$name\'/ $hash";
+ if (exists $known_snapshot_formats{$format}{'compressor'}) {
+ $cmd .= ' | ' . join ' ', @{$known_snapshot_formats{$format}{'compressor'}};
}
print $cgi->header(
- -type => "application/$ctype",
+ -type => $known_snapshot_formats{$format}{'type'},
-content_disposition => 'inline; filename="' . "$filename" . '"',
-status => '200 OK');
print <$fd>;
binmode STDOUT, ':utf8'; # as set at the beginning of gitweb.cgi
close $fd;
-
}
sub git_log {
my $refs = git_get_references();
my $ref = format_ref_marker($refs, $co{'id'});
- my $have_snapshot = gitweb_have_snapshot();
-
git_header_html(undef, $expires);
git_print_page_nav('commit', '',
$hash, $co{'tree'}, $hash,
"<td class=\"link\">" .
$cgi->a({-href => href(action=>"tree", hash=>$co{'tree'}, hash_base=>$hash)},
"tree");
- if ($have_snapshot) {
- print " | " .
- $cgi->a({-href => href(action=>"snapshot", hash=>$hash)}, "snapshot");
+ my $snapshot_links = format_snapshot_links($hash);
+ if (defined $snapshot_links) {
+ print " | " . $snapshot_links;
}
print "</td>" .
"</tr>\n";
print "<tr class=\"light\">\n";
}
$alternate ^= 1;
+ my $author = chop_and_escape_str($co{'author_name'}, 15, 5);
print "<td title=\"$co{'age_string_age'}\"><i>$co{'age_string_date'}</i></td>\n" .
- "<td><i>" . esc_html(chop_str($co{'author_name'}, 15, 5)) . "</i></td>\n" .
+ "<td><i>" . $author . "</i></td>\n" .
"<td>" .
$cgi->a({-href => href(action=>"commit", hash=>$co{'id'}),
-class => "list subject"},
- esc_html(chop_str($co{'title'}, 50)) . "<br/>");
+ chop_and_escape_str($co{'title'}, 50) . "<br/>");
while (my $setref = shift @files) {
my %set = %$setref;
print $cgi->a({-href => href(action=>"blob", hash_base=>$co{'id'},
# log/feed of current (HEAD) branch, log of given branch, history of file/directory
my $head = $hash || 'HEAD';
- my @commitlist = parse_commits($head, 150);
+ my @commitlist = parse_commits($head, 150, 0, undef, $file_name);
my %latest_commit;
my %latest_date;