#!/usr/bin/perl
# $Id: cvsweb.cgi,v 4.5 2019/11/09 09:24:13 schwarze Exp $
# $knu: cvsweb.cgi,v 1.299 2010/11/13 16:37:18 simon
#
# cvsweb - a CGI interface to CVS trees.
#
# Written in their spare time by
# Bill Fenner The server on which the CVS tree lives is probably down. Please try again in a few minutes.');
}
#
# Short-circuit forbidden things. Note that $fullname should not change
# after this, because the rest of the code assumes this check has already
# been done.
#
fatal('403 Forbidden', 'Access to %s forbidden.', $where)
if forbidden($fullname);
#
# Handle tarball downloads before any headers are output.
#
if ($input{tarball}) {
fatal('403 Forbidden', 'Downloading tarballs is prohibited.')
unless $allow_tar;
my ($module) = ($where =~ m,^/?(.*),); # untaint
$module =~ s,/([^/]*)$,,;
my ($ext) = ($1 =~ /(\.t(?:ar\.)?gz|\.zip)$/);
my ($basedir) = ($module =~ m,([^/]+)$,);
if ($basedir eq '' || $module eq '') {
fatal('500 Internal Error',
'You cannot download the top level directory.');
}
my $istar = ($ext eq '.tar.gz' || $ext eq '.tgz');
if ($istar) {
fatal('500 Internal Error', 'tar command not found.') unless $CMD{tar};
fatal('500 Internal Error', 'gzip command not found.') unless $CMD{gzip};
}
my $iszip = ($ext eq '.zip');
if ($iszip && !$CMD{zip}) {
fatal('500 Internal Error', 'zip command not found.');
}
if (!$istar && !$iszip) {
fatal('500 Internal Error', 'Unsupported archive type.');
}
my $tmpexportdir;
eval {
local $SIG{__DIE__};
# Don't use the CLEANUP argument to tempdir() here, since we might be under
# mod_perl (the process runs for a long time), unlink explicitly later.
$tmpexportdir = tempdir('.cvsweb.XXXXXXXX', TMPDIR => 1);
};
if ($@) {
fatal('500 Internal Error', 'Unable to make temporary directory: %s', $@);
}
if (!chdir($tmpexportdir)) {
fatal('500 Internal Error',
"Can't cd to temporary directory %s: %s", $tmpexportdir, $!);
}
my @fatal;
my $tag = $input{only_with_tag} || 'HEAD';
$tag = 'HEAD' if ($tag eq 'MAIN');
my @cmd =
($CMD{cvs}, @cvs_options, '-Qd', $cvsroot, 'export', '-r', $tag,
'-d', $basedir, $module);
my $export_err;
my ($errcode, $err) = runproc(\@cmd, '2>', \$export_err);
if ($errcode) {
@fatal =
('500 Internal Error',
'Export failure (exit status %s), output: \n";
while ( Current directory: ', clickablePath($where, 0), '';
if ($cvshistory_url) {
(my $d = $where) =~ s|^/*(.*?)/*$|$1|;
print ' - ', history_link($d, '');
}
print " Current tag: ", htmlquote($input{only_with_tag}), "%s
',
$ENV{PATH_INFO});
}
if ($ENV{SCRIPT_NAME}) {
($scriptname) = ($ENV{SCRIPT_NAME} =~ VALID_PATH)
or fatal('500 Internal Error',
'Illegal SCRIPT_NAME in environment: %s
',
$ENV{SCRIPT_NAME});
}
$scriptname = '' unless defined($scriptname);
$where = $pathinfo;
$doCheckout = $where =~ s|^/$CheckoutMagic/|/|o;
$where =~ s|^/||;
$scriptname =~ s|^/*|/|;
# Let's workaround thttpd's stupidity..
if ($scriptname =~ m|/$|) {
$pathinfo .= '/';
my $re = quotemeta $pathinfo;
$scriptname =~ s/$re$//;
}
# $scriptname : the URI escaped path to this script
# $where : the path in the CVS repository (without leading /, or only /)
# $scriptwhere: the URI escaped $scriptname + '/' + $where
$scriptname = uri_escape_path($scriptname);
$scriptwhere = join('/', $scriptname, uri_escape_path($where));
$where = '/' if ($where eq '');
# In text-based browsers, it's very annoying to have two links per file;
# skip linking the image for them.
$Browser = $ENV{HTTP_USER_AGENT} || '';
$is_links = ($Browser =~ m`^E?Links `);
$is_lynx = ($Browser =~ m`^Lynx/`i);
$is_w3m = ($Browser =~ m`^w3m/`i);
$is_msie = ($Browser =~ m`MSIE`);
$is_mozilla3 = ($Browser =~ m`^Mozilla/[3-9]`);
$is_textbased = ($is_links || $is_lynx || $is_w3m);
$nofilelinks = $is_textbased;
# newer browsers accept gzip content encoding
# and state this in a header
# (netscape did always but didn't state it)
# It has been reported that these
# braindamaged MS-Internet Exploders claim that they
# accept gzip .. but don't in fact and
# display garbage then :-/
# Turn off gzip if running under mod_perl and no zlib is available,
# piping does not work as expected inside the server.
$maycompress = (
((defined($ENV{HTTP_ACCEPT_ENCODING})
&& $ENV{HTTP_ACCEPT_ENCODING} =~ /gzip/)
|| $is_mozilla3)
&& !$is_msie
&& !(defined($ENV{MOD_PERL}) && !HAS_ZLIB)
);
# Parameters that will be sticky in all constructed links/query strings.
@stickyvars =
qw(cvsroot hideattic ignorecase sortby logsort f only_with_tag ln
hidecvsroot hidenonreadable);
#
# Load configuration.
#
if (-f $config) {
do "$config" or config_error($config, $@);
} else {
fatal("500 Internal Error",
'Configuration not found. Set the parameter $config
in cvsweb.cgi to your cvsweb.conf configuration file first.');
}
# Try to find a readable dir where we can cd into. Some abs_path()
# implementations as well as various cvs operations require such a dir to
# work properly.
{
local $^W = 0;
for my $dir (tmpdir(), rootdir()) {
last if (-r $dir && chdir($dir));
}
}
$CSS = $cssurl ?
sprintf("\n",
htmlquote($cssurl)) : '';
# --- input parameters
my %query = ();
if (defined($ENV{QUERY_STRING})) {
for my $p (split(/[;&]+/, $ENV{QUERY_STRING})) {
next unless $p;
$p =~ y/+/ /;
my ($key, $val) = split(/=/, $p, 2);
next unless defined($key);
$val = 1 unless defined($val);
($key = uri_unescape($key)) =~ /[[:graph:]]/ or next;
($val = uri_unescape($val)) =~ /[[:graph:]]/ or next;
$query{$key} = $val;
}
}
undef %input;
my $t;
for my $p (qw(graph hideattic hidecvsroot hidenonreadable ignorecase ln copt
makeimage options tarball)) {
$t = $query{$p};
if (defined($t)) {
($input{$p}) = ($t =~ /^([01]|on)$/)
or fatal('500 Internal Error',
'Invalid boolean value: %s=%s
', $p, $t);
}
}
for my $p (qw(annotate r1 r2 rev tr1 tr2)) {
$t = $query{$p};
if (defined($t)) {
if (($p eq 'r1' || $p eq 'r2') && $t eq 'text') {
# Special case for the "Use text field" option in the log view diff form.
$input{$p} = $t;
next;
} elsif (($p eq 'rev' || $p eq 'annotate') && ($t eq '.' || $t eq 'HEAD')){
# Another special case, allow linking to latest revision using these.
$input{$p} = '.';
next;
}
my ($rev, $tag) = split(/:/, $t, 2);
($input{$p}) = ($rev =~ /^(\d+(?:\.\d+)*)$/)
or fatal('500 Internal Error',
'Invalid revision: %s=%s
', $p, $t);
if (defined($tag)) {
($tag) = ($tag =~ VALID_TAG1)
or fatal('500 Internal Error',
'Invalid tag/branch name in revision: %s=%s
',
$p, $t);
($tag) = ($tag =~ VALID_TAG2)
or fatal('500 Internal Error',
'Invalid tag/branch name in revision: %s=%s
',
$p, $t);
$input{$p} .= ':' . $tag;
}
}
}
$t = defined($query{only_with_tag}) ?
$query{only_with_tag} : $query{only_on_branch}; # Backwards compatibility.
if (defined($t)) {
($input{only_with_tag}) = ($t =~ VALID_TAG1)
or fatal('500 Internal Error',
'Invalid tag/branch name: %s
', $t);
($input{only_with_tag}) = ($t =~ VALID_TAG2)
or fatal('500 Internal Error',
'Invalid tag/branch name: %s
', $t);
}
$t = $query{logsort};
if (defined($t)) {
($input{logsort}) = ($t =~ /^(cvs|date|rev)$/)
or fatal('500 Internal Error',
'Unsupported log sort key: %s
', $t);
}
$t = $query{f};
if (defined($t)) {
($input{f}) = ($t =~ /^(([hH]|[ucs]c?)|ext\d*)$/)
or fatal('500 Internal Error',
'Unsupported diff format: %s
', $t);
}
$t = $query{sortby};
if (defined($t)) {
($input{sortby}) = ($t =~ /^(file|date|rev|author|log)$/)
or fatal('500 Internal Error',
'Unsupported dir sort key: %s
', $t);
}
$t = $query{'content-type'};
if (defined($t)) {
($input{'content-type'}) = ($t =~ m|^([-0-9A-Za-z]+/[-0-9A-Za-z\.\+]+)$|)
or fatal('500 Internal Error',
'Unsupported content type: %s
', $t);
}
$t = $query{cvsroot};
if (defined($t)) {
($input{cvsroot}) = ($t =~ /^([[:print:]]+)$/)
or fatal('500 Internal Error',
'Invalid symbolic CVS root name: %s
', $t);
}
$t = $query{path};
if (defined($t)) {
($input{path}) = ($t =~ VALID_PATH)
or fatal('500 Internal Error',
'Invalid path: %s
', $t);
}
undef($t);
undef(%query);
# --- end input parameters
#
# CVS roots
#
my $rootfound = 0;
for (my $i = 0; $i < scalar(@CVSrepositories); $i += 2) {
my $key = $CVSrepositories[$i];
my ($descr, $root) = @{$CVSrepositories[$i+1]};
$root = canonpath($root);
unless (-d $root) {
warn("Root '$root' defined in \@CVSrepositories is not a directory, " .
'entry ignored');
next;
}
$rootfound ||= 1;
$cvstreedefault = $key unless defined($cvstreedefault);
$CVSROOTdescr{$key} = $descr;
$CVSROOT{$key} = $root;
push(@CVSROOT, $key);
}
unless ($rootfound) {
fatal('500 Internal Error',
'No valid CVS roots found! See @CVSrepositories
in ' .
'the configuration file (%s
).',
$config);
}
undef $rootfound;
#
# Default CVS root
#
if (!defined($CVSROOT{$cvstreedefault})) {
fatal("500 Internal Error",
'$cvstreedefault
points to a repository (%s) not ' .
'defined in @CVSrepositories
in your configuration ' .
'file (%s
).',
$cvstreedefault,
$config);
}
$DEFAULTVALUE{cvsroot} = $cvstreedefault;
while (my ($key, $defval) = each %DEFAULTVALUE) {
# Replace not given parameters with defaults.
next unless (defined($defval) && $defval =~ /\S/ && !defined($input{$key}));
# Empty checkboxes in forms return nothing, so we define a helper parameter
# in these forms (copt) which indicates that we just set parameters with a
# checkbox.
if ($input{copt}) {
# 'copt' is set -> the result of empty input checkbox
# -> set to zero (disable) if default is a boolean (0|1).
$input{$key} = 0 if ($defval eq '0' || $defval eq '1');
} else {
# 'copt' isn't set --> empty input is not the result
# of empty input checkbox --> set default.
$input{$key} = $defval;
}
}
$barequery = "";
my @barequery;
foreach (@stickyvars) {
# construct a query string with the sticky non default parameters set
if (defined($input{$_})
&& !(defined($DEFAULTVALUE{$_}) && $input{$_} eq $DEFAULTVALUE{$_}))
{
push(@barequery, join('=', uri_escape($_), uri_escape($input{$_})));
}
}
if ($allow_enscript) {
push(@DIFFTYPES, qw(uc cc));
@DIFFTYPES{qw(uc cc)} = (
{
'descr' => 'unified, colored',
'opts' => ['-u'],
'colored' => 0,
},
{
'descr' => 'context, colored',
'opts' => ['-c'],
'colored' => 0,
},
);
} else {
# No Enscript -> respect difftype, but don't offer colorization.
if ($input{f} && $input{f} =~ /^([ucs])c$/) {
$input{f} = $1;
}
}
# is there any query ?
if (@barequery) {
$barequery = join (';', @barequery);
$query = "?$barequery";
$barequery = ";$barequery";
} else {
$query = "";
}
undef @barequery;
if (defined($input{path})) {
redirect("$scriptname/$input{path}$query");
}
# get actual parameters
{
my $sortby = $input{sortby} || 'file';
$bydate = 0;
$byrev = 0;
$byauthor = 0;
$bylog = 0;
$byfile = 0;
if ($sortby eq 'date') {
$bydate = 1;
} elsif ($sortby eq 'rev') {
$byrev = 1;
} elsif ($sortby eq 'author') {
$byauthor = 1;
} elsif ($sortby eq 'log') {
$bylog = 1;
} else {
$byfile = 1;
}
}
$defaultDiffType = $input{f};
$logsort = $input{logsort};
# alternate CVS-Tree, configured in cvsweb.conf
if ($input{cvsroot} && $CVSROOT{$input{cvsroot}}) {
$cvstree = $input{cvsroot};
} else {
$cvstree = $cvstreedefault;
}
$cvsroot = $CVSROOT{$cvstree};
# create icons out of description
foreach my $k (keys %ICONS) {
my ($itxt, $ipath, $iwidth, $iheight) = @{$ICONS{$k}};
no strict 'refs';
if ($ipath) {
${"${k}icon"} =
sprintf('',
htmlquote($ipath), htmlquote($itxt), $iwidth, $iheight);
} else {
${"${k}icon"} = $itxt;
}
}
my $config_cvstree = "$config-$cvstree";
# Do some special configuration for cvstrees
if (-f $config_cvstree) {
do "$config_cvstree"
or fatal("500 Internal Error",
'Error in loading configuration file: %s
%s
',
$config_cvstree, $@);
}
undef $config_cvstree;
$re_prcategories = '(?:' . join ('|', @prcategories) . ')' if @prcategories;
$re_prkeyword = quotemeta($prkeyword) if defined($prkeyword);
$prcgi .= '%s' if defined($prcgi) && $prcgi !~ /%s/;
$fullname = catfile($cvsroot, $where);
my $rewrite = 0;
if ($pathinfo =~ m|//|) {
$pathinfo =~ y|/|/|s;
$rewrite = 1;
}
if (-d $fullname) {
if ($pathinfo !~ m|/$|) {
$pathinfo .= '/';
$rewrite = 1;
}
} elsif ($pathinfo =~ m|/$|) {
chop $pathinfo;
$rewrite = 1;
}
if ($rewrite) {
redirect($scriptname . uri_escape_path($pathinfo) . $query, 1);
}
undef $rewrite;
undef $pathinfo;
if (!-d $cvsroot) {
fatal("500 Internal Error",
'$CVSROOT not found!%s
',
$errcode, $err || $export_err);
} else {
$| = 1; # Essential to get the buffering right.
local (*TAR_OUT);
my (@cmd, $ctype);
if ($istar) {
my @tar = ($CMD{tar}, @tar_options, '-cf', '-', $basedir);
my @gzip = ($CMD{gzip}, @gzip_options, '-c');
push(@cmd, \@tar, '|', \@gzip);
$ctype = 'application/x-gzip';
} elsif ($iszip) {
my @zip = ($CMD{zip}, @zip_options, '-r', '-', $basedir);
push(@cmd, \@zip, \'');
$ctype = 'application/zip';
}
push(@cmd, '>pipe', \*TAR_OUT);
my ($h, $err) = startproc(@cmd);
if ($h) {
print "Content-Type: $ctype\r\n\r\n";
local $/ = undef;
print %s
',
$istar ? 'Tar' : 'Zip', $? >> 8 || -1, $err);
}
}
# Clean up.
rmtree($tmpexportdir);
&fatal(@fatal) if @fatal;
exit;
}
##############################
# View a directory
###############################
if (-d $fullname) {
my $dh = do { local (*DH); };
opendir($dh, $fullname) or fatal("404 Not Found", '%s: %s', $where, $!);
my @dir = grep(!forbidden(catfile($fullname, $_)), readdir($dh));
closedir($dh);
my @subLevelFiles = findLastModifiedSubdirs(@dir) if $show_subdir_lastmod;
my @unreadable = getDirLogs($cvsroot, $where, @subLevelFiles);
if ($where eq '/') {
html_header($defaulttitle);
$long_intro =~ s/!!CVSROOTdescr!!/$CVSROOTdescr{$cvstree}/g;
print $long_intro;
} else {
html_header($where);
my $html = (-f catfile($fullname, 'README.cvs.html,v') ||
-f catfile($fullname, 'Attic', 'README.cvs.html,v'));
my $text = (!$html &&
(-f catfile($fullname, 'README.cvs,v') ||
-f catfile($fullname, 'Attic', 'README.cvs,v')));
if ($html || $text) {
my $rev = $input{only_with_tag} || 'HEAD';
my $cr = abs_path($cvsroot) || $cvsroot;
my $co = "$where/README.cvs.html" if $html;
$co ||= "$where/README.cvs" if $text;
# abs_path() taints when run as a CGI...
if ($cr =~ VALID_PATH) {
$cr = $1;
} else {
fatal('500 Internal Error', 'Illegal CVS root: %s
', $cr);
}
my @cmd = ($CMD{cvs}, @cvs_options, '-d', $cr, 'co', '-p', "-r$rev",$co);
local (*CVS_OUT, *CVS_ERR);
my ($h, $err) = startproc(\@cmd, \"", '>pipe', \*CVS_OUT,
'2>pipe', \*CVS_ERR);
fatal('500 Internal Error', $err) unless $h;
if ($html) {
local $/ = undef;
print
';
}
print "
\n";
my $infocols = 1;
printf(<
EOF
printf(' \n";
my $dirrow = 0;
my $i;
lookingforattic:
for ($i = 0; $i <= $#dir; $i++) {
if ($dir[$i] eq "Attic") {
last lookingforattic;
}
}
if (!$input{hideattic}
&& ($i <= $#dir)
&& opendir($dh, $fullname . '/Attic'))
{
splice(@dir, $i, 1, grep((s|^|Attic/|, !m|/\.|), readdir($dh)));
closedir($dh);
}
my $hideAtticToggleLink =
$input{hideattic}
? ''
: &link('[hide]', sprintf('./%s#dirlist', &toggleQuery('hideattic')));
# Sort without the Attic/ pathname.
# place directories first
my $filesexists;
my $filesfound;
foreach my $file (sort { &fileSortCmp } @dir) {
next if ($file eq curdir());
# ignore CVS lock and stale NFS files
next if ($file =~ /^\#cvs\.|^,|^\.nfs/); # \# for XEmacs cperl-mode...
# Check whether to show the CVSROOT path
next if ($input{hidecvsroot} && $where eq '/' && $file eq 'CVSROOT');
# Is it a directory?
my $isdir = -d catdir($fullname, $file);
# Ignore non-readable files and directories?
next if ($input{hidenonreadable} && (! -r _ || ($isdir && ! -x _)));
my $attic = '';
if ($file =~ s|^Attic/||) {
$attic = ' (in the Attic) ' .
$hideAtticToggleLink . '';
}
if ($file eq updir() || $isdir) {
next if ($file eq updir() && $where eq '/');
my ($rev, $date, $log, $author, $filename, $keywordsubst) =
@{$fileinfo{$file}} if (defined($fileinfo{$file}));
printf "', ($byfile ? ' class="sorted"' : ''));
if ($byfile) {
print 'File';
} else {
print &link('File',
sprintf('./%s#dirlist', toggleQuery('sortby', 'file')));
}
print " \n";
# Do not display the other column headers if we do not have any files
# with revision information.
if (scalar(%fileinfo)) {
$infocols++;
printf('', ($byrev ? ' class="sorted"' : ''));
if ($byrev) {
print 'Rev.';
} else {
print &link('Rev.',
sprintf('./%s#dirlist', toggleQuery('sortby', 'rev')));
}
print " \n";
$infocols++;
printf('', ($bydate ? ' class="sorted"' : ''));
if ($bydate) {
print 'Age';
} else {
print &link('Age',
sprintf('./%s#dirlist', toggleQuery('sortby', 'date')));
}
print " \n";
if ($show_author) {
$infocols++;
printf('', ($byauthor ? ' class="sorted"' : ''));
if ($byauthor) {
print 'Author';
} else {
print
&link('Author',
sprintf('./%s#dirlist', toggleQuery('sortby', 'author')));
}
print " \n";
}
$infocols++;
printf('', ($bylog ? ' class="sorted"' : ''));
if ($bylog) {
print 'Last log entry';
} else {
print &link('Last log entry',
sprintf('./%s#dirlist', toggleQuery('sortby', 'log')));
}
print " \n";
} elsif ($use_descriptions) {
print "Description \n";
$infocols++;
}
print "\n \n";
$dirrow++;
} elsif ($file =~ s/,v$//) {
my $fileurl = ($attic ? 'Attic/' : '') . uri_escape_path($file);
my $url = './' . $fileurl . $query;
$filesexists++;
next if (!defined($fileinfo{$file}));
my ($rev, $date, $log, $author, $filename, $keywordsubst) =
@{$fileinfo{$file}};
my $isbinary = $keywordsubst eq 'b' ? 1 : 0;
$filesfound++;
printf "",
($dirrow % 2) ? 'even' : 'odd';
if ($file eq updir()) {
my $url = "../$query";
print $nofilelinks ? $backicon : &link($backicon, $url);
print ' ', &link("Parent Directory", $url);
} else {
my $url = './' . uri_escape_path($file) . "/$query";
print '';
print $nofilelinks ? $diricon : &link($diricon, $url);
print ' ', &link(htmlquote("$file/"), $url), $attic;
if ($file eq "Attic") {
print ' ',
&link('[show]',
sprintf('./%s#dirlist', &toggleQuery('hideattic'))),
'';
}
}
# Show last change in dir
if ($filename) {
my $ageclass = 'age';
my $age = '';
if ($date) {
$age = readableTime(time() - $date, 0);
$ageclass .= " $1" if ($age =~ /^\d+ ([a-z]+)/);
}
print " \n \n$age";
print " \n", htmlquote($author)
if $show_author;
print " \n";
$filename =~ s%^[^/]+/%%;
print &link(htmlquote("$filename/$rev"),
sprintf('%s/%s%s#rev%s',
uri_escape($file), uri_escape($filename),
$query, $rev)), '
';
if ($log) {
print htmlify(substr($log, 0, $shortLogLen), $allow_dir_extra);
print '...' if (length($log) > 80);
}
} else {
my $dwhere = ($where ne '/' ? $where : '') . $file;
if ($use_descriptions && defined $descriptions{$dwhere}) {
print '';
print $descriptions{$dwhere};
} elsif ($infocols > 1) {
# close the row with the appropriate number of
# columns, so that the vertical seperators are visible
my ($cols) = $infocols;
while ($cols > 1) {
print " \n ";
$cols--;
}
}
}
print " \n\n", ($dirrow % 2) ? 'even' : 'odd';
printf ' ";
$dirrow++;
}
print "\n";
}
print "\n";
if ((my $num = scalar(@unreadable)) && ! $input{hidenonreadable}) {
printf(<', $allow_cvsgraph ? '' : ' colspan="2"';
my $icon = $isbinary ? $binfileicon : $fileicon;
print $nofilelinks ? $icon : &link($icon, $url);
print ' ', &link(htmlquote($file), $url), $attic;
print ' ', graph_link($fileurl) if $allow_cvsgraph;
print " \n", display_link($fileurl, $rev);
my $ageclass = 'age';
my $age = '';
if ($date) {
$age = readableTime(time() - $date, 0);
$ageclass .= " $1" if ($age =~ /^\d+ ([a-z]+)/);
}
print " \n$age";
print " \n", htmlquote($author) if $show_author;
print " \n";
if ($log) {
print htmlify(substr($log, 0, $shortLogLen), $allow_dir_extra);
print '...' if (length $log > 80);
}
print " \n
%s
%s
":This document has moved ", &link('here', $url), ".
\n"; html_footer(); exit(1); } sub safeglob($) { my ($filename) = @_; (my $dirname = $filename) =~ s|/[^/]+$||; $filename =~ s|.*/||; my @results; my $dh = do { local (*DH); }; if (opendir($dh, $dirname)) { my $glob = $filename; my $t; # transform filename from glob to regex. Deal with: # [, {, ?, * as glob chars # make sure to escape all other regex chars $glob =~ s/([\.\(\)\|\+])/\\$1/g; $glob =~ s/\*/.*/g; $glob =~ s/\?/./g; $glob =~ s/{([^}]+)}/($t = $1) =~ s-,-|-g; "($t)"/eg; $glob = qr/^$glob$/; foreach (readdir($dh)) { if ($_ =~ $glob && $_ =~ VALID_PATH) { push(@results, catfile($dirname, $1)); # untaint } } closedir($dh); } return @results; } # # Searches @command_path for the given executable file. # sub search_path($) { my ($command) = @_; for my $d (@command_path) { my $cmd = catfile($d, $command); return $cmd if (-x $cmd && !-d _); } return ''; } # # Gets the enscript(1) highlight mode corresponding to the given filename, # or undef if unsupported. # sub getEnscriptHL($) { return undef unless $allow_enscript; my ($filename) = @_; while (my ($hl, $regex) = each %enscript_types) { return $hl if ($filename =~ $regex); } return undef; } # # Gets the MIME type for the given file name. # sub getMimeType($;$) { my ($fullname, $binary) = @_; $binary = ($keywordsubstitution && $keywordsubstitution =~ /b/) unless defined($binary); (my $suffix = $fullname) =~ s/^.*\.([^.]*)$/$1/; my $mimetype = $MTYPES{$suffix}; $mimetype ||= $MimeTypes->mimeTypeOf($fullname) if defined($MimeTypes); if (!$mimetype && $suffix ne '*' && -f $mime_types && -r _) { my $fh = do { local (*FH); }; if (open($fh, $mime_types)) { my $re = sprintf('^\s*(\S+\/\S+)\s.+\b%s\b', quotemeta($suffix)); $re = qr/$re/; while (my $line = <$fh>) { if ($line =~ $re) { $mimetype = $1; $MTYPES{$suffix} = $mimetype; last; } } close($fh); } else { warn("Can't open MIME types file $mime_types for reading: $!"); } } $mimetype ||= $MTYPES{'*'}; $mimetype ||= $binary ? 'application/octet-stream' : 'text/plain'; return $mimetype; } ############################### # read first lines like head(1) ############################### sub head($;$) { my ($fh, $linecount) = @_; $linecount ||= 10; my @buf; if ($linecount > 0) { for (my $i = 0; !eof($fh) && $i < $linecount; $i++) { push @buf, scalar <$fh>; } } else { @buf = <$fh>; } return @buf; } ############################### # scan vim and Emacs directives ############################### sub scan_directives(@) { my $ts = undef; for (@_) { $ts = $1 if /\b(?:ts|tabstop|tab-width)[:=]\s*([1-9]\d*)\b/; } ('tabstop' => $ts); } sub openOutputFilter() { return unless $output_filter; open(STDOUT, "|-") and return; # child of child open(STDERR, '>', devnull()) unless $DEBUG; exec($output_filter) or exit -1; } ############################### # show Annotation ############################### sub doAnnotate($$) { my ($rev, $tag) = @_; $rev = $tag || 'HEAD' if ($rev eq '.'); (my $pathname = $where) =~ s|((?<=/)Attic/)?[^/]*$||; (my $filename = $where) =~ s|^.*/||; # This annotate version is based on the cvs annotate-demo Perl script by # Cyclic Software. It was written by Cyclic Software, # http://www.cyclic.com/, and is in the public domain. # We could abandon the use of rlog, rcsdiff and co using # the cvs server in a similiar way one day (..after rewrite). local (*CVS_IN, *CVS_OUT); my $annotate_err; my ($h, $err) = startproc([ $CMD{cvs}, @annotate_options, 'server' ], '%s', $? >> 8 || -1, $err) unless $h; # OK, first send the request to the server. A simplified example is: # Root /home/kingdon/zwork/cvsroot # Argument foo/xx # Directory foo # /home/kingdon/zwork/cvsroot/foo # Directory . # /home/kingdon/zwork/cvsroot # annotate # although as you can see there are a few more details. print CVS_IN "Root $cvsroot\n"; print CVS_IN "Valid-responses ok error Valid-requests Checked-in Updated Merged Removed M E\n"; # Don't worry about sending valid-requests, the server just needs to # support "annotate" and if it doesn't, there isn't anything to be done. print CVS_IN "UseUnchanged\n"; print CVS_IN "Argument -r\n"; print CVS_IN "Argument $rev\n"; print CVS_IN "Argument $where\n"; # The protocol requires us to fully fake a working directory (at # least to the point of including the directories down to the one # containing the file in question). # So if $where is "dir/sdir/file", then dirs will be ("dir","sdir","file") my $path = ''; foreach my $dir (split('/', $where)) { if ($path eq "") { # In our example, $dir is "dir". $path = $dir; } else { print CVS_IN "Directory $path\n"; print CVS_IN "$cvsroot/$path\n"; # In our example, $_ is "sdir" and $path becomes "dir/sdir" # And the next time, "file" and "dir/sdir/file" (which then gets # ignored, because we don't need to send Directory for the file). $path .= "/$dir"; } } undef $path; # And the last "Directory" before "annotate" is the top level. print CVS_IN "Directory .\n"; print CVS_IN "$cvsroot\n"; print CVS_IN "annotate\n"; # OK, we've sent our command to the server. Thing to do is to # close the writer side and get all the responses. if (!close(CVS_IN)) { $h->finish(); fatal('500 Internal Error', 'Annotate failure (exit status %s):
%s
, output: ' .
'%s', $? >> 8, $!, $annotate_err); } navigateHeader($scriptwhere, $pathname, $filename, $rev, 'annotate'); my $revtype = ($rev =~ /\./) ? 'revision' : 'tag'; # TODO: tag -> branch/tag? print '
"; } # prefetch several lines my @buf = head(*CVS_OUT); my %d = scan_directives(@buf); while (@buf || !eof(*CVS_OUT)) { $_ = @buf ? shift @buf :"; } html_footer(); } ############################### # make Checkout ############################### sub doCheckout($$$) { my ($fullname, $rev, $tag) = @_; $rev = $tag || undef if (!$rev || $rev eq '.'); # Start resolving whether we will do a markup view or not. my $do_markup = undef; my $want_type = $input{'content-type'}; # No markup if markup disallowed. $do_markup = 0 unless $allow_markup; # No markup if checkout magic cookie in URL. $do_markup = 0 if (!defined($do_markup) && $doCheckout); # Do markup if explicitly asked using cvsweb-markup content type. If the # asked content type is anything else, no markup. if (!defined($do_markup) && $want_type) { if ($want_type =~ CVSWEBMARKUP) { $want_type = undef; $do_markup = 1; } else { $do_markup = 0; } } # Ok, if $do_markup is still undefined, we know that a download has not been # explicitly asked. For the last check further down below we'll need to # know if the file is binary, and possibly run a log on it. my $needlog = $do_markup || $use_moddate; my $moddate = undef; my $revopt; if (defined($rev)) { $revopt = "-r$rev"; if ($needlog) { readLog($fullname, $rev); $moddate = $date{$rev}; # TODO: even this does not work for branch tags, but only normal tags :( $moddate ||= $date{$symrev{$rev}} if defined($symrev{$rev}); } } else { $revopt = "-rHEAD"; if ($needlog) { readLog($fullname); $moddate = $date{$symrev{HEAD}}; } } my $cr = abs_path($cvsroot) || $cvsroot; # abs_path() taints when run as a CGI... if ($cr =~ VALID_PATH) { $cr = $1; } else { fatal('500 Internal Error', 'Illegal CVS root:; my @words = split; # Adding one is for the (single) space which follows $words[0]. my $rest = substr($_, length($words[0]) + 1); if ($words[0] eq "E") { next; } elsif ($words[0] eq "M") { $lineNr++; (my $lrev = substr($_, 2, 13)) =~ y/ //d; (my $lusr = substr($_, 16, 9)) =~ y/ //d; my $line = substr($_, 36); # TODO: this does not work for branch/tag revisions. my $isCurrentRev = ($rev eq $lrev); # we should parse the date here .. if ($lrev eq $oldLrev) { $revprint = sprintf('%-8s', ''); } else { $revprint = sprintf('%-8s', $lrev); $revprint =~ s|(\S+)|&link($1, uri_escape($filename)."$query#rev$1")|e; $oldLusr = ''; } $usrprint = ($lusr eq $oldLusr) ? '' : $lusr; $oldLrev = $lrev; $oldLusr = $lusr; print $is_textbased ? '' : '' if $isCurrentRev; $usrprint = sprintf('%-8s', $usrprint); printf '%s%s %s %4d:', $revprint, $isCurrentRev ? '!' : ' ', htmlquote($usrprint), $lineNr; print spacedHtmlText($line, $d{tabstop}); print $is_textbased ? '' : '' if $isCurrentRev; } elsif ($words[0] eq "ok") { # We could complain about any text received after this, like the # CVS command line client. But for simplicity, we don't. } elsif ($words[0] eq "error") { fatal("500 Internal Error", 'Error occured during annotate: %s', $_); } } $h->finish(); if ($annTable) { print ""; } else { print "
%s
', $cr);
}
# Use abs_path() to work around a bug of cvs -p; expand symlinks if we can.
my @cmd = ($CMD{cvs}, @cvs_options, '-d', $cr, 'co', '-p', $revopt, $where);
local (*CVS_OUT, *CVS_ERR);
my ($h, $err) =
startproc(\@cmd, \"", '>pipe', \*CVS_OUT, '2>pipe', \*CVS_ERR);
fatal('500 Internal Error',
'Checkout failure (exit status %s), output: %s', $? >> 8 || -1, $err) unless $h; if (eof(CVS_ERR)) { $h->finish(); fatal("404 Not Found", '%s is not (any longer) pertinent', $where); } #=================================================================== #Checking out squid/src/ftp.c #RCS: /usr/src/CVS/squid/src/ftp.c,v #VERS: 1.1.1.28.6.2 #*************** # Parse CVS header my ($revision, $filename, $cvsheader); $filename = ""; while (
%s' . '(expected "
%s
" but got "%s
")',
$cvsheader, $where, $filename);
}
# Last checks whether we'll do markup or not.
my $isbin = $keywordsubstitution && $keywordsubstitution =~ /b/;
my $mimetype = getMimeType($fullname, $isbin);
# If we still are not sure whether to do markup or not:
# if the MIME type is "viewable" or this is not a binary file, do.
$do_markup = !$isbin || viewable($mimetype) unless defined($do_markup);
if ($do_markup) {
# If this is something we'll be linking to in the markup view, we are
# done with this particular output from "cvs co" and must discard it.
my $linked = $mimetype =~ m{^image/|application/pdf$}i;
if ($linked) {
close(CVS_OUT);
$h->finish();
}
# Here we know the last modified date, but don't know if tags have been
# added afterwards (those are shown in the markup view): no last-modified.
cvswebMarkup(\*CVS_OUT, $fullname, $revision, $isbin, $mimetype, $needlog);
$h->finish() unless $linked;
} else {
http_header($want_type || $mimetype, $moddate);
local $/ = undef;
print \n"; my $linenumbers = $input{ln} || 0; if (my $enscript_hl = getEnscriptHL($filename)) { doEnscript($filehandle, $enscript_hl, $linenumbers); } else { my $ln = 0; my @buf = (); my $ts = undef; if ($preformat_in_markup) { # prefetch several lines @buf = head($filehandle); my %d = scan_directives(@buf); $ts = $d{tabstop}; } while (@buf || !eof($filehandle)) { $_ = @buf ? shift @buf : <$filehandle>; if ($linenumbers) { $ln++; printf '%5d: ', ($ln) x 2; } print $preformat_in_markup ? spacedHtmlText($_, $ts) : htmlquote($_); } } print "\n"; } html_footer(); } sub viewable($) { return shift =~ m{^((text|image)/|application/pdf)}i; } ############################### # Show Colored Diff ############################### sub doDiff($$$$$$) { my ($fullname, $r1, $tr1, $r2, $tr2, $f) = @_; if (forbidden($fullname)) { fatal('403 Forbidden', 'Access to %s forbidden.', $where); } my ($rev1, $sym1); if ($r1 =~ /([^:]+)(:(.+))?/) { $rev1 = $1; $sym1 = $3; } if ($r1 eq 'text') { $rev1 = $tr1; $sym1 = ""; } my ($rev2, $sym2); if ($r2 =~ /([^:]+)(:(.+))?/) { $rev2 = $1; $sym2 = $3; } if ($r2 eq 'text') { $rev2 = $tr2; $sym2 = ""; } # # rev1 and rev2 are now both numeric revisions. # Thus we do a DWIM here and swap them if rev1 is after rev2. # XXX should we warn about the fact that we do this? if (&revcmp($rev1, $rev2) > 0) { my ($tmp1, $tmp2) = ($rev1, $sym1); ($rev1, $sym1) = ($rev2, $sym2); ($rev2, $sym2) = ($tmp1, $tmp2); } my $mimetype = getMimeType($fullname); # # Check for per-MIME type diff commands. # my $diffcmd = undef; if (my $diffcmds = $DIFF_COMMANDS{lc($mimetype)}) { if ($f =~ /^ext(\d*)$/) { my $n = $1 || 0; $diffcmd = $diffcmds->[$n]; } } if ($diffcmd && $diffcmd->{cmd} && $diffcmd->{name}) { if ($diffcmd->{args} && ref($diffcmd->{args}) ne 'ARRAY') { fatal('500 Internal Error', 'Configuration error: arguments to external diff tools must ' . 'be given as array refs. See "
%s
" in ' .
'%%DIFF_COMMANDS
.',
$diffcmd->{name});
}
(my $cvsname = $where) =~ s/\.diff$//;
# Create two temporary files with the two revisions
my $temp_fn1 = checkout_to_temp($cvsroot, $cvsname, $rev1);
my $temp_fn2 = checkout_to_temp($cvsroot, $cvsname, $rev2);
# Execute chosen diff binary.
local (*DIFF_OUT);
my @cmd = ($diffcmd->{cmd});
push(@cmd, @{$diffcmd->{args}}) if $diffcmd->{args};
push(@cmd, $temp_fn1, $temp_fn2);
my ($h, $err) = startproc(\@cmd, \"", '>pipe', \*DIFF_OUT);
if (!$h) {
unlink($temp_fn1);
unlink($temp_fn2);
fatal('500 Internal Error',
'Diff failure (exit status %s), output: %s', $? >> 8 || -1, $err); } http_header($diffcmd->{type} || 'text/plain'); local $/ = undef; print
EOF doEnscript(\$fh, $hl, 0, 'cvsweb_diff'); print <
\n"; html_footer(); gzipclose(); exit; } else { # # Plain diff. # http_header("text/plain"); } # #=================================================================== #RCS file: /home/ncvs/src/sys/netinet/tcp_output.c,v #retrieving revision 1.16 #retrieving revision 1.17 #diff -c -r1.16 -r1.17 #*** /home/ncvs/src/sys/netinet/tcp_output.c 1995/11/03 22:08:08 1.16 #--- /home/ncvs/src/sys/netinet/tcp_output.c 1995/12/05 17:46:35 1.17 # # Ideas: # - nuke the stderr output if it's what we expect it to be # - Add "no differences found" if the diff command supplied no output. # #*** src/sys/netinet/tcp_output.c 1995/11/03 22:08:08 1.16 #--- src/sys/netinet/tcp_output.c 1995/12/05 17:46:35 1.17 RELENG_2_1_0 # (bogus example, but...) # my ($f1, $f2); if (grep { $_ eq '-u' } @difftype) { $f1 = '---'; $f2 = '\+\+\+'; } else { $f1 = '\*\*\*'; $f2 = '---'; } while (<$fh>) { if (m|^$f1 $cvsroot|o) { s|$cvsroot/||o; if ($sym1) { chop; $_ .= " $sym1\n"; } } elsif (m|^$f2 $cvsroot|o) { s|$cvsroot/||o; if ($sym2) { chop; $_ .= " $sym2\n"; } } print $_; } close($fh); } ############################### # Show Logs .. ############################### sub getDirLogs($$@) { my ($cvsroot, $dirname, @otherFiles) = @_; my $tag = $input{only_with_tag}; my $DirName = catdir($cvsroot, $where); my @files = &safeglob("$DirName/*,v"); push (@files, &safeglob("$DirName/Attic/*,v")) unless $input{hideattic}; foreach my $file (@otherFiles) { push(@files, catfile($DirName, $file)); } # Weed out unreadable files. my $i = 0; my @unreadable = (); while ($i < scalar(@files)) { # Note: last modified files from subdirs returned by # findLastModifiedSubdirs() come without the ,v suffix so they're not # found here, but have already been checked for readability. *cough* if (-r $files[$i] || !-e _) { $i++; } else { push(@unreadable, splice(@files, $i, 1)); } } # If there are no files, we're done. return @unreadable unless @files; my @cmd = ($CMD{rlog}); # Can't use -ras '-' is allowed in tagnames, # but misinterpreted by rlog. push(@cmd, '-r') unless defined($tag); my $fh = do { local (*FH); }; if (!open($fh, '-|')) { # Child open(STDERR, '>', devnull()) unless $DEBUG; # Ignore rlog's complaints. openOutputFilter(); if ($file_list_len && $file_list_len > 1) { while (scalar(@files) > $file_list_len) { # Process files in chunks. system(@cmd, splice(@files, 0, $file_list_len)) == 0 or exit -1; } } exec(@cmd, @files) or exit -1; } undef @cmd; my $state = 'start'; my ($date, $branchpoint, $branch, $log, @filetags); my ($rev, $revision, $revwanted, $filename, $head, $author, $keywordsubst); while (<$fh>) { if ($state eq "start") { #Next file. Initialize file variables $rev = ''; $revwanted = ''; $branch = ''; $branchpoint = ''; $filename = ''; $log = ''; $revision = ''; %symrev = (); @filetags = (); $keywordsubst= ''; #jump to head state $state = "head"; } again: if ($state eq "head") { #$rcsfile = $1 if (/^RCS file: (.+)$/); #not used (yet) if (/^Working file: (.+)$/) { $filename = $1; } elsif (/^head: (.+)$/) { $head = $1; } elsif (/^branch: (.+)$/) { $branch = $1; } elsif (/^keyword substitution: (.+)$/) { $keywordsubst = $1; } elsif (/^symbolic names:/) { $state = "tags"; ($branch = $head) =~ s/\.\d+$// if $branch eq ''; $branch =~ s/(\d+)$/0.$1/; $symrev{MAIN} = $branch; $symrev{HEAD} = $branch; $alltags{MAIN} = 1; $alltags{HEAD} = 1; push (@filetags, "MAIN", "HEAD"); } elsif ($_ =~ LOG_REVSEPR) { $state = "log"; $rev = ''; $date = ''; $log = ''; # Try to reconstruct the relative filename if RCS spits out a full path $filename =~ s%^\Q$DirName\E/%%; } next; } if ($state eq "tags") { if (/^\s+([^:]+):\s+([\d\.]+)\s*$/) { push (@filetags, $1); $symrev{$1} = $2; $alltags{$1} = 1; next; } elsif (/^\S/) { if (defined($tag)) { if (defined($symrev{$tag}) || $tag eq "HEAD") { $revwanted = $symrev{$tag eq "HEAD" ? "MAIN" : $tag}; ($branch = $revwanted) =~ s/\b0\.//; ($branchpoint = $branch) =~ s/\.?\d+$//; $revwanted = '' if ($revwanted ne $branch); } elsif ($tag ne "HEAD") { $state = "skip"; next; } } foreach my $tagfound (@filetags) { $tags{$tagfound} = 1; } $state = "head"; goto again; } } if ($state eq "log") { if ($_ =~ LOG_REVSEPR || $_ =~ LOG_FILESEPR) { # End of a log entry. my $revbranch = $rev; $revbranch =~ s/\.\d+$//; if ($revwanted eq '' && $branch ne '' && $branch eq $revbranch || !defined($tag)) { $revwanted = $rev; } if ($revwanted ne '' ? $rev eq $revwanted : $branchpoint ne '' ? $rev eq $branchpoint : 0 && ($rev eq $head)) { # Don't think head is needed here.. my @finfo = ($rev, $date, $log, $author, $filename, $keywordsubst); (my $name = $filename) =~ s%/.*%%; $fileinfo{$name} = [@finfo]; $state = "done" if ($rev eq $revwanted); } $rev = ''; $date = ''; $log = ''; } elsif ($date eq '' && m|^date:\s+(\d+)/(\d+)/(\d+)\s+(\d+):(\d+):(\d+);|) { my $yr = $1; $yr -= 1900 if ($yr > 100); # Damn 2-digit year routines :-) $date = timegm($6, $5, $4, $3, $2 - 1, $yr); ($author) = /author: ([^;]+)/; $state = 'log'; $log = ''; next; } elsif ($rev eq '' && /^revision (\d+(?:\.\d+)+).*$/) { $rev = $1; # .*$ eats up the locker(lockers?) info, if any next; } else { $log .= $_; } } if ($_ =~ LOG_FILESEPR) { $state = "start"; next; } } my $linesread = $. || 0; close($fh); if ($linesread == 0) { fatal('500 Internal Error', 'Failed to spawn GNU rlog on "%s".
Did you set the@command_path
in your configuration file correctly? (Currently: "%s
")', htmlquote(join(', ', @files)), join(':', @command_path)); } return @unreadable; } sub readLog($;$) { my ($fullname, $revision) = @_; my ($symnames, $head, $rev, $br, $brp, $branch, $branchrev); undef %symrev; undef %revsym; undef @allrevisions; undef %date; undef %author; undef %state; undef %difflines; undef %log; $keywordsubstitution = ''; my $fh = do { local (*FH); }; if (!open($fh, "-|")) { # child openOutputFilter(); $revision = defined($revision) ? "-r$revision" : ''; if ($revision =~ /\./) { # Normal revision, not a branch/tag name. exec($CMD{rlog}, $revision, $fullname) or exit -1; } else { exec($CMD{rlog}, $fullname) or exit -1; } } my $curbranch = undef; while (<$fh>) { if ($symnames) { if (/^\s+([^:]+):\s+([\d\.]+)/) { $symrev{$1} = $2; next; } else { $symnames = 0; } } if (/^head:\s+([\d\.]+)/) { $head = $1; } elsif (/^branch:\s+([\d\.]+)/) { $curbranch = $1; } elsif (/^symbolic names/) { $symnames = 1; } elsif (/^keyword substitution: (.+)$/) { $keywordsubstitution = $1; } elsif (/^-----/) { last; } } ($curbranch = $head) =~ s/\.\d+$// if (!defined($curbranch)); # each log entry is of the form: # ---------------------------- # revision 3.7.1.1 # date: 1995/11/29 22:15:52; author: fenner; state: Exp; lines: +5 -3 # log info # ---------------------------- # For a locked revision, the first line after the separator # becomes smth like # revision 9.19 locked by: vassilii; logentry: while ($_ !~ LOG_FILESEPR) { $_ = <$fh>; last logentry if (!defined($_)); # EOF if (/^revision (\d+(?:\.\d+)+)/) { $rev = $1; unshift(@allrevisions, $rev); } elsif ($_ =~ LOG_FILESEPR || $_ =~ LOG_REVSEPR) { next logentry; } else { # The rlog output is syntactically ambiguous. We must # have guessed wrong about where the end of the last log # message was. # Since this is likely to happen when people put rlog output # in their commit messages, don't even bother keeping # these lines since we don't know what revision they go with # any more. next logentry; } $_ = <$fh>; if ( m|^date:\s+(\d+)/(\d+)/(\d+)\s+(\d+):(\d+):(\d+);\s+author:\s+(\S+);\s+state:\s+(\S+);\s+(lines:\s+([0-9\s+-]+))?| ) { my $yr = $1; $yr -= 1900 if ($yr > 100); # Damn 2-digit year routines :-) $date{$rev} = timegm($6, $5, $4, $3, $2 - 1, $yr); $author{$rev} = $7; $state{$rev} = $8; $difflines{$rev} = $10; } else { fatal("500 Internal Error", 'Error parsing RCS output: %s', $_); } line: while (<$fh>) { next line if (/^branches:\s/); last line if ($_ =~ LOG_FILESEPR || $_ =~ LOG_REVSEPR); $log{$rev} .= $_; } } close($fh); @revorder = reverse sort { revcmp($a, $b) } @allrevisions; # # HEAD is an artificial tag which is simply the highest tag number on the main # branch, unless there is a branch tag in the RCS file in which case it's the # highest revision on that branch. Find it by looking through @revorder; it # is the first commit listed on the appropriate branch. # This is not neccesary the same revision as marked as head in the RCS file. my $headrev = $curbranch || "1"; ($symrev{MAIN} = $headrev) =~ s/(\d+)$/0.$1/; foreach $rev (@revorder) { if ($rev =~ /^(\S*)\.\d+$/ && $headrev eq $1) { $symrev{HEAD} = $rev; last; } } ($symrev{HEAD} = $headrev) =~ s/\.\d+$// unless defined($symrev{HEAD}); # # Now that we know all of the revision numbers, we can associate # absolute revision numbers with all of the symbolic names, and # pass them to the form so that the same association doesn't have # to be built then. # undef @branchnames; undef %branchpoint; undef $sel; foreach (reverse sort keys %symrev) { $rev = $symrev{$_}; if ($rev =~ /^((.*)\.)?\b0\.(\d+)$/) { push (@branchnames, $_); # # A revision number of A.B.0.D really translates into # "the highest current revision on branch A.B.D". # # If there is no branch A.B.D, then it translates into # the head A.B . # # This reasoning also applies to the main branch A.B, # with the branch number 0.A, with the exception that # it has no head to translate to if there is nothing on # the branch, but I guess this can never happen? # # (the code below gracefully forgets about the branch # if it should happen) # $head = defined($2) ? $2 : ""; $branch = $3; $branchrev = $head . ($head ne "" ? "." : "") . $branch; $rev = $head; my $regex = '^' . quotemeta($branchrev) . '\b'; $regex = qr/$regex/; foreach my $r (@revorder) { if ($r =~ $regex) { $rev = $branchrev; last; } } next if ($rev eq ""); if ($rev ne $head && $head ne "") { $branchpoint{$head} .= ', ' if ($branchpoint{$head}); $branchpoint{$head} .= $_; } } $revsym{$rev} .= ", " if ($revsym{$rev}); $revsym{$rev} .= $_; $sel .= sprintf("\n", htmlquote($rev), (htmlquote($_)) x 2); } my ($onlyonbranch, $onlybranchpoint); if ($onlyonbranch = $input{only_with_tag}) { $onlyonbranch = $symrev{$onlyonbranch}; if ($onlyonbranch && $onlyonbranch =~ s/\b0\.//) { ($onlybranchpoint = $onlyonbranch) =~ s/\.\d+$//; } else { $onlybranchpoint = $onlyonbranch; } if (!defined($onlyonbranch) || $onlybranchpoint eq "") { fatal("404 Tag not found", 'Tag "%s
" is not defined.', $input{only_with_tag}); } } undef @revisions; foreach (@allrevisions) { ($br = $_) =~ s/\.\d+$//; ($brp = $br) =~ s/\.\d+$//; next if ($onlyonbranch && $br ne $onlyonbranch && $_ ne $onlybranchpoint); unshift(@revisions, $_); } if ($logsort eq "date") { # Sort the revisions in commit order an secondary sort on revision # (secondary sort needed for imported sources, or the first main # revision gets before the same revision on the 1.1.1 branch) @revdisplayorder = sort { $date{$b} <=> $date{$a} || -revcmp($a, $b) } @revisions; } elsif ($logsort eq "rev") { # Sort the revisions in revision order, highest first @revdisplayorder = reverse sort { revcmp($a, $b) } @revisions; } else { # No sorting. Present in the same order as rlog / cvs log @revdisplayorder = @revisions; } return $curbranch; } sub getDiffLinks($$$) { my ($url, $mimetype, $isbin) = @_; my @links = (); if (!$isbin) { # Offer ordinary diff only for non-binary files. push(@links, &link('preferred', $url)); for my $difftype ($DIFFTYPES{$defaultDiffType}{colored} ? qw(u) : qw(h)) { my $f = $difftype eq $defaultDiffType ? '' : $difftype; push(@links, &link(htmlquote(lc($DIFFTYPES{$difftype}{descr})), "$url;f=$f")); } } if (my $extdiffs = $DIFF_COMMANDS{lc($mimetype)}) { for my $i (0 .. scalar(@$extdiffs)-1) { my $extdiff = $extdiffs->[$i]; push(@links, &link(htmlquote($extdiff->{name}), "$url;f=ext$i")) if ($extdiff->{cmd} && $extdiff->{name}); } } return @links; } sub printLog($$$;$$) { # inlogview: 1 if in log view, otherwise in markup view. ($_, my $mimetype, my $isbin, my $inlogview, my $isSelected) = @_; (my $br = $_) =~ s/\.\d+$//; (my $brp = $br) =~ s/\.?\d+$//; print ""; if (defined($revsym{$_})) { foreach my $sym (split(", ", $revsym{$_})) { print ''; } } if ($revsym{$br} && !defined($nameprinted{$br})) { foreach my $sym (split(", ", $revsym{$br})) { print ''; } $nameprinted{$br} = 1; } print "\n Revision $_"; if (/^1\.1\.1\.\d+$/) { print " (vendor branch)"; } (my $filename = $where) =~ s|^.*/||; my $fileurl = uri_escape($filename); undef $filename; my $isDead = ($state{$_} eq 'dead'); if (!$isDead) { print ': ', download_link($fileurl, $_, 'download', $mimetype); my @vlinks = (); push(@vlinks, display_link($fileurl, $_, 'text', 'text/plain')) unless $isbin; push(@vlinks, display_link($fileurl, $_, 'markup', 'text/x-cvsweb-markup')) if ($allow_markup && $inlogview && (!$isbin || viewable($mimetype))); if (!$isbin && $allow_annotate) { push(@vlinks, &link('annotated', sprintf('%s?annotate=%s%s', $fileurl, $_, $barequery))); } print ' - view: ', join(', ', @vlinks) if @vlinks; undef @vlinks; if (!$isbin && $allow_version_select) { print ' - '; if ($isSelected) { print '[selected for diffs]'; } else { print &link('select for diffs', sprintf('%s?r1=%s%s#rev%s', $fileurl, $_, $barequery, $_)); } } print ' - ', graph_link('', 'revision graph') if (!$inlogview && $allow_cvsgraph); } print "
\n"; print ''; if (@mytz) { my ($est) = $mytz[(localtime($date{$_}))[8]]; print scalar localtime($date{$_}), " $est ("; } else { print scalar gmtime($date{$_}), " UTC ("; } print readableTime(time() - $date{$_}, 1), ' ago)'; print ' by ', htmlquote($author{$_}), "
\n"; printf("Branches: %s
\n", link_tags($revsym{$br})) if $revsym{$br}; printf("CVS tags: %s
\n", link_tags($revsym{$_})) if $revsym{$_}; printf("Branch point for: %s
\n", link_tags($branchpoint{$_})) if $branchpoint{$_}; # Find the previous revision my $prev; my @prevrev = split(/\./, $_); do { if (--$prevrev[$#prevrev] <= 0) { # If it was X.Y.Z.1, just make it X.Y pop (@prevrev); pop (@prevrev); } $prev = join (".", @prevrev); } until (defined($date{$prev}) || $prev eq ""); if ($isDead) { print "FILE REMOVED
\n"; } else { my %diffrev = (); $diffrev{$_} = 1; $diffrev{""} = 1; my $diff = 'Diff to:'; my $printed = 0; # # Offer diff to previous revision if ($prev) { $diffrev{$prev} = 1; my $url = sprintf('%s.diff?r1=%s;r2=%s%s', $fileurl, $prev, $_, $barequery); if (my @dlinks = getDiffLinks($url, $mimetype, $isbin)) { print $diff, ' previous ', $prev, ': ', join(', ', @dlinks); $diff = ';'; $printed = 1; } } # # Plus, if it's on a branch, and it's not a vendor branch, # offer a diff with the branch point. if ($revsym{$brp} && !/^1\.1\.1\.\d+$/ && !defined($diffrev{$brp})) { my $url = sprintf('%s.diff?r1=%s;r2=%s%s', $fileurl, $brp, $_, $barequery); if (my @dlinks = getDiffLinks($url, $mimetype, $isbin)) { print $diff, ' branchpoint ', $brp, ': ', join(', ', @dlinks); $diff = ';'; $printed = 1; } } # # Plus, if it's on a branch, and it's not a vendor branch, # offer to diff with the next revision of the higher branch. # (e.g. change gets committed and then brought # over to -stable) if (/^\d+\.\d+\.\d+/ && !/^1\.1\.1\.\d+$/) { my ($i, $nextmain); for ($i = 0; $i < $#revorder && $revorder[$i] ne $_; $i++) { } my @tmp2 = split(/\./, $_); for ($nextmain = ""; $i > 0; $i--) { my $next = $revorder[$i - 1]; my @tmp1 = split(/\./, $next); if (@tmp1 < @tmp2) { $nextmain = $next; last; } # Only the highest version on a branch should have # a diff for the "next MAIN". last if (@tmp1 - 1 <= @tmp2 && join (".", @tmp1[0 .. $#tmp1 - 1]) eq join (".", @tmp2[0 .. $#tmp1 - 1])); } if (!defined($diffrev{$nextmain})) { $diffrev{$nextmain} = 1; my $url = sprintf('%s.diff?r1=%s;r2=%s%s', $fileurl, $nextmain, $_, $barequery); if (my @dlinks = getDiffLinks($url, $mimetype, $isbin)) { print $diff, ' next MAIN ', $nextmain, ': ', join(', ', @dlinks); $diff = ';'; $printed = 1; } } } # Plus if user has selected only r1, then present a link # to make a diff to that revision if (defined($input{r1}) && !defined($diffrev{$input{r1}})) { $diffrev{$input{r1}} = 1; my $url = sprintf('%s.diff?r1=%s;r2=%s%s', $fileurl, $input{r1}, $_, $barequery); if (my @dlinks = getDiffLinks($url, $mimetype, $isbin)) { print $diff, ' selected ', $input{r1}, ': ', join(', ', @dlinks); $diff = ';'; $printed = 1; } } print "
\n" if $printed; } if ($prev ne "" && $difflines{$_}) { printf "Changes since revision %s: %s lines
\n", htmlquote($prev), htmlquote($difflines{$_}); } print "\n"; print &htmlify($log{$_}, $allow_log_extra); print "\n"; } # # Generates the HTML view for CvsGraph. # sub doGraphView() { (my $pathname = $where) =~ s|[^/]*$||; (my $filename = $where) =~ s|^.*/||; navigateHeader($scriptwhere, $pathname, $filename, undef, 'graph'); my $title = 'Revision graph of ' . htmlquote($pathname . $filename); my $mapname = 'CvsGraphMap'; printf(<%s EOF # Remove any pre-existing tag/branch names from branch links. (my $notag_query = $barequery) =~ s/;+only_with_tag=.*?(?=;|$)//g; my @graph_cmd = ($CMD{cvsgraph}, '-r', $cvsroot, '-m', $pathname, '-i', '-M', $mapname, '-x', 'x', "-Omap_branch_href=\"href=\\\"./?only_with_tag=%(%t%)$notag_query\\\"\"", "-Omap_rev_href=\"href=\\\"?rev=%(%R%)$barequery\\\"\"", "-Omap_diff_href=\"href=\\\"%(%F%).diff" . "?r1=%(%P%);r2=%(%R%)$barequery\\\"\"", ); push(@graph_cmd, '-c', $cvsgraph_config) if $cvsgraph_config; push(@graph_cmd, $filename . ',v'); local *CVSGRAPH_OUT; my ($h, $err) = startproc(\@graph_cmd, \"", '>pipe', \*CVSGRAPH_OUT); fatal('500 Internal Error', $err) unless $h; # Browser compatibility kludge: many browsers do not support client side # image maps where the\n"; html_footer(); } # # Generates a graph using CvsGraph. # sub doGraph() { (my $pathname = $where) =~ s|[^/]*$||; (my $filename = $where) =~ s|^.*/||; http_header('image/png'); my @graph_cmd = ($CMD{cvsgraph}, '-r', $cvsroot, '-m', $pathname); push(@graph_cmd, '-c', $cvsgraph_config) if $cvsgraph_config; push(@graph_cmd, $filename . ',v'); local *CVSGRAPH_OUT; my ($h, $err) = startproc(\@graph_cmd, \"", '>pipe', \*CVSGRAPH_OUT); fatal('500 Internal Error', $err) unless $h; { local $/ = undef; binmode(\*STDOUT); print; } $h->finish(); } sub doLog($) { my ($fullname) = @_; my $curbranch = readLog($fullname); html_header("CVS log for $where"); my $upwhere = $where; (my $filename = $where) =~ s|^.*/||; my $backurl = "./$query#" . uri_escape($filename); if ($where =~ m|^(.*?)((?<=/)Attic/)?[^/]+$|) { $upwhere = $1; $backurl = ".$backurl" if $2; # skip over Attic } my $isbin = $keywordsubstitution =~ /b/; my $mimetype = getMimeType($filename, $isbin); print " \n "; print &link($backicon, $backurl), " Up to ", &clickablePath($upwhere, 1), "\n
\n"; print "\n "; print &link('Request diff between arbitrary revisions', '#diff'); print ' - ', &graph_link('', 'Display revisions graphically') if $allow_cvsgraph; if ($cvshistory_url) { (my $d = $upwhere) =~ s|/+$||; print ' - ', history_link($d, $filename); } print "\n
\n
\n"; print "\n"; my $explain = $isbin ? ' (i.e.: CVS considers this a binary file)' : ''; print "Keyword substitution: $keywordsubstitution$explain
\n"; undef %nameprinted; for my $r (@revdisplayorder) { print "
\n"; if ($curbranch) { print "Default branch: ", ($revsym{$curbranch} || $curbranch); } else { print "No default branch"; } print "
\n"; print 'Current tag: ', htmlquote($input{only_with_tag}), "
\n" if $input{only_with_tag}; print "
\n"; my $sel = (defined($input{r1}) && $input{r1} eq $r); print "\n" if $sel; printLog($r, $mimetype, $isbin, 1, $sel); print "\n" if $sel; } printf(<\n"; html_footer(); } sub flush_diff_rows($$$$) { my ($leftColRef, $rightColRef, $leftRow, $rightRow) = @_; return unless defined($state); if ($state eq "PreChangeRemove") { # we just got remove-lines before for (my $j = 0; $j < $leftRow; $j++) { printf(< %s EOF } } elsif ($state eq "PreChange") { # state eq "PreChange" # we got removes with subsequent adds if (HAS_EDIFF) { # construct the suffix tree my $left_diff = join("\n", @$leftColRef[0..$leftRow-1]); my $right_diff = join("\n", @$rightColRef[0..$rightRow-1]); my $diff_str = String::Ediff::ediff($left_diff, $right_diff); my @diff_str = split(/ /, $diff_str); my $INFINITY = 10000000; push(@diff_str, ($INFINITY) x 8); my ($idx, $b1, $e1, $lb1, $le1, $b2, $e2, $lb2, $le2) = (0, @diff_str[0..7]); my ($l_cul, $r_cul) = (0, 0); my ($ldx, $rdx) = (0, 0); my (@left_html, @right_html); for (my $j = 0; $j < $leftRow; $j++) { my $line_len = length(@$leftColRef[$j]); my $line = @$leftColRef[$j]; $l_cul += length($line) + 1; # includes "\n" my $l_culx = $l_cul - 1; # not includes "\n" if ($j < $lb1) { $line = spacedHtmlText($line); push(@left_html, " $line "); } elsif ($lb1 == $j) { my $html_line; while ($lb1 == $j) { my $begin_char = $l_culx - $b1; $line =~ /^(.*)(.{$begin_char})$/; $html_line .= spacedHtmlText($1) . ''; $line = $2; last if ($j != $le1); my $end_char = $l_culx - $e1; $line =~ /^(.*)(.{$end_char})$/; $html_line .= spacedHtmlText($1) . ''; $line = $2; $idx++; my ($tb1, $te1, $tlb1, $tle1, $tb2, $te2, $tlb2, $tle2) = ($b1, $e1, $lb1, $le1, $b2, $e2, $lb2, $le2); ($b1, $e1, $lb1, $le1, $b2, $e2, $lb2, $le2) = @diff_str[$idx*8..($idx+1)*8-1]; $lb1 = $INFINITY if ($lb1 < 0); $lb2 = $INFINITY if ($lb2 < 0); $le1 = $INFINITY if ($le1 < 0); $le2 = $INFINITY if ($le2 < 0); if ($te1 > $b1) { ($b1, $lb1) = ($te1, $tle1); } if ($te2 > $b2) { ($b2, $lb2) = ($te2, $tle2); } } push(@left_html, sprintf('%s%s ', $html_line, spacedHtmlText($line))); } elsif ($le1 == $j) { my $html_line; while ($le1 == $j) { my $end_char = $l_culx - $e1; $line =~ /^(.*)(.{$end_char})$/; $html_line .= spacedHtmlText($1) . ''; $line = $2; $idx++; my ($tb1, $te1, $tlb1, $tle1, $tb2, $te2, $tlb2, $tle2) = ($b1, $e1, $lb1, $le1, $b2, $e2, $lb2, $le2); ($b1, $e1, $lb1, $le1, $b2, $e2, $lb2, $le2) = @diff_str[$idx*8..($idx+1)*8-1]; $lb1 = $INFINITY if ($lb1 < 0); $lb2 = $INFINITY if ($lb2 < 0); $le1 = $INFINITY if ($le1 < 0); $le2 = $INFINITY if ($le2 < 0); if ($te1 > $b1) { ($b1, $lb1) = ($te1, $tle1); } if ($te2 > $b2) { ($b2, $lb2) = ($te2, $tle2); } last if ($lb1 != $j); my $begin_char = $l_culx - $b1; $line =~ /^(.*)(.{$begin_char})$/; $html_line .= spacedHtmlText($1) . ''; $line = $2; } push(@left_html, sprintf('%s%s ', $html_line, spacedHtmlText($line))); } else { $line = spacedHtmlText($line); push(@left_html, "$line "); } } ($idx, $b1, $e1, $lb1, $le1, $b2, $e2, $lb2, $le2) = (0, @diff_str[0..7]); $lb1 = $INFINITY if ($lb1 < 0); $lb2 = $INFINITY if ($lb2 < 0); $le1 = $INFINITY if ($le1 < 0); $le2 = $INFINITY if ($le2 < 0); for (my $j = 0; $j < $rightRow; $j++) { my $line_len = length(@$rightColRef[$j]); my $line = @$rightColRef[$j]; $r_cul += length($line) + 1; # includes "\n" my $r_culx = $r_cul - 1; # not includes "\n" if ($j < $lb2) { $line = spacedHtmlText($line); push(@right_html, "$line "); } elsif ($lb2 == $j) { my $html_line; while ($lb2 == $j) { my $begin_char = $r_culx - $b2; $line =~ /^(.*)(.{$begin_char})$/; $html_line .= spacedHtmlText($1) . ''; $line = $2; last if ($j != $le2); my $end_char = $r_culx - $e2; $line =~ /^(.*)(.{$end_char})$/; $html_line .= spacedHtmlText($1) . ''; $line = $2; $idx++; my ($tb1, $te1, $tlb1, $tle1, $tb2, $te2, $tlb2, $tle2) = ($b1, $e1, $lb1, $le1, $b2, $e2, $lb2, $le2); ($b1, $e1, $lb1, $le1, $b2, $e2, $lb2, $le2) = @diff_str[$idx*8..($idx+1)*8-1]; $lb1 = $INFINITY if ($lb1 < 0); $lb2 = $INFINITY if ($lb2 < 0); $le1 = $INFINITY if ($le1 < 0); $le2 = $INFINITY if ($le2 < 0); if ($te1 > $b1) { ($b1, $lb1) = ($te1, $tle1); } if ($te2 > $b2) { ($b2, $lb2) = ($te2, $tle2); } } push(@right_html, sprintf('%s%s ', $html_line, spacedHtmlText($line))); } elsif ($le2 == $j) { my $html_line; while ($le2 == $j) { my $end_char = $r_culx - $e2; $line =~ /^(.*)(.{$end_char})$/; $html_line .= spacedHtmlText($1) . ''; $line = $2; $idx++; my ($tb1, $te1, $tlb1, $tle1, $tb2, $te2, $tlb2, $tle2) = ($b1, $e1, $lb1, $le1, $b2, $e2, $lb2, $le2); ($b1, $e1, $lb1, $le1, $b2, $e2, $lb2, $le2) = @diff_str[$idx*8..($idx+1)*8-1]; $lb1 = $INFINITY if ($lb1 < 0); $lb2 = $INFINITY if ($lb2 < 0); $le1 = $INFINITY if ($le1 < 0); $le2 = $INFINITY if ($le2 < 0); if ($te1 > $b1) { ($b1, $lb1) = ($te1, $tle1); } if ($te2 > $b2) { ($b2, $lb2) = ($te2, $tle2); } last if ($lb2 != $j); my $begin_char = $r_culx - $b2; $line =~ /^(.*)(.{$begin_char})$/; $html_line .= spacedHtmlText($1) . ''; $line = $2; } push(@right_html, sprintf('%s%s ', $html_line, spacedHtmlText($line))); } else { $line = spacedHtmlText ($line); push @right_html, "$line "; } } for (my $j = 0; $j < $leftRow || $j < $rightRow ; $j++) { # dump out both cols print ''; if ($j < $leftRow) { print $left_html[$j]; } else { print ' \n"; } } else { for (my $j = 0; $j < $leftRow || $j < $rightRow; $j++) { # dump both cols print "'; } if ($j < $rightRow) { print $right_html[$j]; } else { print ' '; } print " \n"; if ($j < $leftRow) { print ' \n"; } } } } # # Generates "human readable", HTMLified diffs. # sub human_readable_diff($$) { my ($fh, $rev) = @_; (my $where_nd = $where) =~ s|\.diff$||; (my $filename = $where_nd) =~ s|^.*/||; (my $pathname = $where_nd) =~ s|((?<=/)Attic/)?[^/]*$||; (my $scriptwhere_nd = $scriptwhere) =~ s|\.diff$||; navigateHeader($scriptwhere_nd, $pathname, $filename, $rev, 'diff'); # Read header to pick up read revision and date, if possible. my ($r1d, $r1r, $r2d, $r2r); while (<$fh>) { ($r1d, $r1r) = /\t(.*)\t(.*)$/ if (/^--- /); ($r2d, $r2r) = /\t(.*)\t(.*)$/ if (/^\+\+\+ /); last if (/^\+\+\+ /); } my ($rev1, $date1); if (defined($r1r) && $r1r =~ /^(\d+\.)+\d+$/) { $rev1 = $r1r; $date1 = $r1d; } my ($rev2, $date2); if (defined($r2r) && $r2r =~ /^(\d+\.)+\d+$/) { $rev2 = $r2r; $date2 = $r2d; } $rev1 = $input{r1} unless defined($rev1); $rev1 = $input{tr1} if (defined($rev1) && $rev1 eq 'text'); $rev1 = 'unknown-left' unless defined($rev1); $rev2 = $input{r2} unless defined($rev2); $rev2 = $input{tr2} if (defined($rev2) && $rev2 eq 'text'); $rev2 = 'unknown-right' unless defined($rev2); $date1 = defined($date1) ? ', ' . htmlquote($date1) : ''; $date2 = defined($date2) ? ', ' . htmlquote($date2) : ''; my $link = uri_escape($filename) . ($query ? "$query;" : '?'); # Using' . spacedHtmlText(@$leftColRef[$j]) . ' '; } else { print ''; } print "\n"; if ($j < $rightRow) { print ' ' . spacedHtmlText(@$rightColRef[$j]) . ' '; } else { print ''; } print "\n Diff for /$where_nd between versions $rev1 and $rev2
EOF # Process diff text # prefetch several lines my @buf = head($fh); my %d = scan_directives(@buf); my $leftRow = 0; my $rightRow = 0; my ($difftxt, @rightCol, @leftCol, $oldline, $newline, $funname); $link .= 'content-type=text%2Fx-cvsweb-markup;'; $link .= 'ln=1;' unless ($link =~ /\?.*\bln=1\b/); while (@buf || !eof($fh)) { $difftxt = @buf ? shift @buf : <$fh>; if ($difftxt =~ /^@@/) { ($oldline, $newline, $funname) = $difftxt =~ /@@ \-([0-9]+).*\+([0-9]+).*@@(.*)/; $funname = htmlquote($funname); $funname =~ s/\s/ /go; $funname &&= " $funname"; my $ol = $oldline || 1; my $nl = $newline || 1; print < version $rev1$date1 version $rev2$date2 Line $oldline$funname Line $newline$funname EOF $state = "dump"; $leftRow = 0; $rightRow = 0; } else { my ($diffcode, $rest) = $difftxt =~ /^([-+ ])(.*)/; $diffcode = '' unless defined($diffcode); $_ = $rest; ######### # little state machine to parse unified-diff output (Hen, zeller@think.de) # in order to get some nice 'ediff'-mode output # states: # "dump" - just dump the value # "PreChangeRemove" - we began with '-' .. so this could be the start of a 'change' area or just remove # "PreChange" - okey, we got several '-' lines and moved to '+' lines -> this is a change block ########## if ($diffcode eq '+') { if ($state eq "dump") { # 'change' never begins with '+': just dump out value $_ = spacedHtmlText($rest, $d{tabstop}); printf(<%s EOF } else { # we got minus before $state = "PreChange"; $rightCol[$rightRow++] = $_; } } elsif ($diffcode eq '-') { $state = "PreChangeRemove"; $leftCol[$leftRow++] = $_; } else { # empty diffcode flush_diff_rows \@leftCol, \@rightCol, $leftRow, $rightRow; $_ = spacedHtmlText($rest, $d{tabstop}); printf(<%s %s EOF $state = "dump"; $leftRow = 0; $rightRow = 0; } } } close($fh); flush_diff_rows \@leftCol, \@rightCol, $leftRow, $rightRow; # state is empty if we didn't have any change if (!$state) { print <EOF } printf(< - No viewable change -
EOF } sub doEnscript($$$;$) { my ($filehandle, $highlight, $linenumbers, $lang) = @_; $lang ||= 'cvsweb'; my @cmd = ($CMD{enscript}, @enscript_options, '-q', "--language=$lang", '-o', '-', "--highlight=$highlight"); local *ENSCRIPT_OUT; my ($h, $err) = startproc(\@cmd, $filehandle, '>pipe', \*ENSCRIPT_OUT); fatal('500 Internal Error', $err) unless $h; # We could short-circuit and have enscript output directly to STDOUT above, # but that doesn't work with mod_perl (at least some 1.99 versions). if ($linenumbers) { my $ln = 0; while () { printf '%5d: ', (++$ln) x 2; print $_; } } else { local $/ = undef; print ; } $h->finish(); } # # The passed in $path and $filename should not be URI escaped, and $swhere # *should* be. # sub navigateHeader($$$$$;$) { my ($swhere, $path, $filename, $rev, $title, $moddate) = @_; $swhere = "" if ($swhere eq $scriptwhere); $swhere = './' . uri_escape($filename) if ($swhere eq ""); my $qfile = htmlquote($filename); my $qpath = htmlquote($path); my $trev = $rev ? " - " . htmlquote($rev) : ''; http_header('text/html', $moddate); print < $qpath$qfile - $title$trev $HTML_META$CSSEOF } sub plural_write($$) { my ($num, $text) = @_; if ($num != 1) { $text .= "s"; } if ($num > 0) { return join (' ', $num, $text); } else { return ""; } } ## # print readable timestamp in terms of # '..time ago' # H. Zeller
EOF my $frag = ''; if ($rev) { $frag = '#'; $frag .= 'rev' if ($rev =~ /\./); # Normal revision: prefix with "rev". $frag .= $rev; # Append revision/branch/tag. } my $backurl = "$swhere$query$frag"; print &link($backicon, $backurl); printf 'Return to %s CVS log', &link($qfile, $backurl); print " $fileicon "; printf(<%s Up to %s ## sub readableTime($$) { my ($secs, $long) = @_; # This function works correctly for time >= 2 seconds. return 'very little time' if ($secs < 2); my %desc = ( 1 => 'second', 60 => 'minute', 3600 => 'hour', 86400 => 'day', 604800 => 'week', 2628000 => 'month', 31536000 => 'year' ); my @breaks = sort { $a <=> $b } keys %desc; my $i = 0; while ($i <= $#breaks && $secs >= 2 * $breaks[$i]) { $i++; } $i--; my $break = $breaks[$i]; my $retval = plural_write(int($secs / $break), $desc{$break}); if ($long == 1 && $i > 0) { my $rest = $secs % $break; $i--; $break = $breaks[$i]; my $resttime = plural_write(int($rest / $break), $desc{$break}); if ($resttime) { $retval .= ", $resttime"; } } return $retval; } # # Returns a htmlified path where each directory is a link for faster # navigation. $clickLast controls whether the basename # (last directory/file) is a link as well. The passed in $pathname should # *not* be URI escaped. # sub clickablePath($$) { my ($pathname, $clickLast) = @_; my $root = '[' . htmlquote($CVSROOTdescr{$cvstree} || $cvstree) . ']'; # This should never happen (see chooseCVSRoot()), but let's be sure... return $root if ($pathname eq '/'); my $retval = ' ' . &link($root, sprintf('%s/%s#dirlist', $scriptname, $query)); my $wherepath = ''; my ($lastslash) = $pathname =~ m|/$|; foreach (split(m|/|, $pathname)) { $retval .= ' / '; $wherepath .= "/$_"; my $last = "$wherepath/" eq "/$pathname" || $wherepath eq "/$pathname"; if ($clickLast || !$last) { $retval .= &link(htmlquote($_), join ('', $scriptname, uri_escape_path($wherepath), (!$last || $lastslash ? '/' : ''), $query, (!$last || $lastslash ? "#dirlist" : ""))); } else { # do not make a link to the current dir $retval .= htmlquote($_); } } return $retval; } sub chooseCVSRoot() { print " EOF } sub chooseMirror() { # This code comes from the original BSD-cvsweb # and may not be useful for your site; If you don't # set %MIRRORS this won't show up, anyway. scalar(%MIRRORS) or return; # Should perhaps exclude the current site somehow... print "\n \nThis CVSweb is mirrored in\n"; my @tmp = map(&link(htmlquote($_), $MIRRORS{$_}), sort keys %MIRRORS); my $tmp = pop (@tmp); if (scalar(@tmp)) { print join (', ', @tmp), ' and '; } print "$tmp.\n
\n"; } sub fileSortCmp() { (my $af = $a) =~ s/,v$//; (my $bf = $b) =~ s/,v$//; my ($rev1, $date1, $log1, $author1, $filename1) = @{$fileinfo{$af}} if (defined($fileinfo{$af})); my ($rev2, $date2, $log2, $author2, $filename2) = @{$fileinfo{$bf}} if (defined($fileinfo{$bf})); my $comp = 0; if (defined($filename1) && defined($filename2) && $af eq $filename1 && $bf eq $filename2) { # Two files $comp = -revcmp($rev1, $rev2) if ($byrev && $rev1 && $rev2); $comp = ($date2 <=> $date1) if ($bydate && $date1 && $date2); if ($input{ignorecase}) { $comp = (uc($log1) cmp uc($log2)) if ($bylog && $log1 && $log2); $comp = (uc($author1) cmp uc($author2)) if ($byauthor && $author1 && $author2); } else { $comp = ($log1 cmp $log2) if ($bylog && $log1 && $log2); $comp = ($author1 cmp $author2) if ($byauthor && $author1 && $author2); } } if ($comp == 0) { # Directories first, then files under version control, # then other, "rogue" files. # Sort by filename if no other criteria available. my $ad = ( (-d "$fullname/$a") ? 'D' : (defined($fileinfo{$af}) ? 'F' : 'R') ); my $bd = ( (-d "$fullname/$b") ? 'D' : (defined($fileinfo{$bf}) ? 'F' : 'R') ); (my $c = $a) =~ s|.*/||; (my $d = $b) =~ s|.*/||; my ($l, $r) = ("$ad$c", "$bd$d"); $comp = $input{ignorecase} ? (uc($l) cmp uc($r)) : ($l cmp $r); # Parent dir is always first, then Attic. if ($comp != 0) { if ($l eq 'D..') { $comp = -1; } elsif ($r eq 'D..') { $comp = 1; } elsif ($l eq 'DAttic') { $comp = -1; } elsif ($r eq 'DAttic') { $comp = 1; } } } return $comp; } # # Returns a URL to download the selected revision. # Expects the passed in URL to be URI escaped, relative, and without a query # string. # sub download_url($$;$) { my ($url, $revision, $mimetype) = @_; my @dots = $revision =~ /\./g; $revision =~ s/\b0\.(?=\d+$)// if (scalar(@dots) & 1); if (!defined($mimetype) || $mimetype !~ CVSWEBMARKUP) { my $path = $where; $path =~ s|[^/]+$||; $url = "$scriptname/$CheckoutMagic/$path$url"; } $url .= '?rev=' . uri_escape($revision); $url .= ';content-type=' . uri_escape($mimetype) if $mimetype; return $url; } # # Returns a link to download the selected revision. # Expects the passed in URL to be URI escaped, relative, # and without a query string. # sub download_link($$$;$) { my ($url, $revision, $textlink, $mimetype) = @_; return sprintf('%s', download_url($url, $revision, $mimetype) . $barequery, htmlquote($textlink)); } # # Returns a URL to display the selected revision. # Expects the passed in URL to be URI escaped, and without a query string. # sub display_url($$;$) { my ($url, $revision, $mimetype) = @_; $url .= '?rev=' . uri_escape($revision); $url .= ';content-type=' . uri_escape($mimetype) if $mimetype; return $url; } # # Returns a link to display the selected revision. # Expects the passed in URL to be URI escaped, and without a query string. # sub display_link($$;$$) { my ($url, $revision, $textlink, $mtype) = @_; $textlink = $revision unless defined($textlink); return sprintf('%s', display_url($url, $revision, $mtype) . $barequery, htmlquote($textlink)); } # # Expects the passed in URL to be URI escaped, and without a query string. # The passed in link text should be already HTML escaped as appropriate. # sub graph_link($;$) { my ($url, $text) = @_; $text ||= $graphicon; return sprintf('%s', $url, $barequery, $text); } # # Returns a link to CVSHistory for the given directory and filename. # sub history_link($$;$) { my ($dir, $file, $text) = @_; $dir ||= ''; $file ||= ''; $text ||= 'History'; return &link($text, sprintf('%s?cvsroot=%s;dsearch=%s;fsearch=%s;limit=1', $cvshistory_url, uri_escape($input{cvsroot} || ''), uri_escape($dir), uri_escape($file))); } # Returns a Query string with the # specified parameter toggled sub toggleQuery($;$) { my ($toggle, $value) = @_; my %vars = %input; if (defined($value)) { $vars{$toggle} = $value; } else { $vars{$toggle} = $vars{$toggle} ? 0 : 1; } # Build a new query of non-default paramenters my $newquery = ""; foreach my $var (@stickyvars) { my ($value) = defined($vars{$var}) ? $vars{$var} : ""; my ($default) = defined($DEFAULTVALUE{$var}) ? $DEFAULTVALUE{$var} : ""; if ($value ne $default) { $newquery .= ';' if ($newquery ne ""); $newquery .= uri_escape($var) . '=' . uri_escape($value); } } if ($newquery) { return '?' . $newquery; } return ""; } sub htmlquote($) { local ($_) = @_; # Special Characters; RFC 1866 s/&/&/g; s/\"/"/g; s/</g; s/>/>/g; return $_; } sub htmlunquote($) { local ($_) = @_; # Special Characters; RFC 1866 s/"/\"/g; s/<//g; s/&/&/g; return $_; } sub uri_escape_path($) { return join('/', map(uri_escape($_), split(m|/+|, shift, -1))); } sub http_header(;$$) { my ($content_type, $moddate) = @_; $content_type ||= 'text/html'; $content_type .= "; charset=$charset" if ($charset && $content_type =~ m,^text/,); # Note that in the following, we explicitly join() and concatenate the # headers instead of printing them as an array. This is because some # systems, eg. early versions of mod_perl 2 don't quite get it if the # last \r\n\r\n isn't included in the last "payload" header print(). my @headers = (); # TODO: ctime(3) from scalar gmtime() isn't HTTP compliant, see HTTP::Date. push(@headers, 'Last-Modified: ' . scalar gmtime($moddate) . ' GMT') if $moddate; push(@headers, 'Content-Type: ' . $content_type); if ($allow_compress && $maycompress) { if (HAS_ZLIB || (defined($CMD{gzip}) && open(GZIP, "| $CMD{gzip} -1 -c"))) { push(@headers, 'Content-Encoding: gzip'); push(@headers, 'Vary: Accept-Encoding'); # RFC 2616, 14.44 print join("\r\n", @headers) . "\r\n\r\n"; $| = 1; $| = 0; # Flush header output. tie(*GZIP, __PACKAGE__, \*STDOUT) if HAS_ZLIB; select(GZIP); $gzip_open = 1; } else { print join("\r\n", @headers) . "\r\n\r\n"; printf 'Unable to find gzip binary in the $command_path (%s
) to compress output
', htmlquote(join(':', @command_path)); } } else { print join("\r\n", @headers) . "\r\n\r\n"; } } sub html_header($;$) { my ($title, $moddate) = @_; $title = htmlquote($title); my $l = $logo || ''; my $css = $CSS || ''; http_header('text/html', $moddate); print <$title $HTML_META$css $l$title
EOH } sub html_footer() { print "
\n$address\n" if $address; print "\n\n"; } sub link_tags($) { my ($tags) = @_; (my $filename = $where) =~ s|^.*/||; my $fileurl = './' . uri_escape($filename); my $ret = ""; foreach my $sym (split(", ", $tags)) { $ret .= ",\n" if ($ret ne ""); $ret .= &link(htmlquote($sym), $fileurl . toggleQuery('only_with_tag', $sym)); } return $ret; } # # See if a file/dir is listed in the config file's @ForbiddenFiles list. # Takes a full file system path or one relative to $cvsroot, and strips the # trailing ",v" if present, then compares. Returns 1 if forbidden, else 0. # sub forbidden($) { (my $path = canonpath(shift)) =~ s/,v$//; $path =~ s|^$cvsroot/+||; for my $forbidden_re (@ForbiddenFiles) { return 1 if ($path =~ $forbidden_re); } return 0; } # # Starts a process using IPC::Run. All arguments are passed to # IPC::Run::start() as-is. Returns an array ($harness, $error) where # $harness is from IPC::Run if start() succeeds, undef otherwise. In case # of an error, $error contains the error message. # sub startproc(@) { my $h = my $err = undef; eval { local $SIG{__DIE__}; $h = IPC::Run::start(@_) or die("return code: $?"); }; if ($@) { $h->finish() if $h; $h = undef; $err = "'@{$_[0]}' failed: $@"; } return ($h, $err); } # # Runs a process using IPC::Run. All arguments are passed to # IPC::Run::run() as-is. Returns an array ($exitcode, $errormsg). # sub runproc(@) { eval { local $SIG{__DIE__}; IPC::Run::run(@_); }; my $exitcode = $? >> 8; my $errormsg = undef; if ($@) { $exitcode ||= -1; $errormsg = "'@{$_[0]}' failed: $@"; } return ($exitcode, $errormsg); } # # Check out a file to a temporary file. # sub checkout_to_temp($$$) { my ($cvsroot, $cvsname, $rev) = @_; # Pipe given cvs file into a temporary place. my ($temp_fh, $temp_fn) = tempfile('.cvsweb.XXXXXXXX', DIR => tmpdir()); my @cmd = ($CMD{cvs}, @cvs_options, '-Qd', $cvsroot, 'co', '-p', "-r$rev", $cvsname); local (*DIFF_OUT); my ($h, $err) = startproc(\@cmd, \"", '>pipe', \*DIFF_OUT); if ($h) { local $/ = undef; print $temp_fh; $h->finish(); close($temp_fh); } else { close($temp_fh); unlink($temp_fn); fatal('500 Internal Error', 'Checkout failure (exit status %s), output: %s', $? >> 8 || -1, $err); } return $temp_fn; } # # Close the GZIP handle, and remove the tie. # sub gzipclose { if ($gzip_open) { select(STDOUT); close(GZIP); untie *GZIP; $gzip_open = 0; } } # implement a gzipped file handle via the Compress:Zlib compression # library. sub MAGIC1() { 0x1f } sub MAGIC2() { 0x8b } sub OSCODE() { 3 } sub TIEHANDLE { my ($class, $out) = @_; my ($d) = Compress::Zlib::deflateInit( -Level => Compress::Zlib::Z_BEST_COMPRESSION(), -WindowBits => -Compress::Zlib::MAX_WBITS() ) or return undef; my ($o) = { handle => $out, dh => $d, crc => 0, len => 0, }; my ($header) = pack("c10", MAGIC1, MAGIC2, Compress::Zlib::Z_DEFLATED(), 0, 0, 0, 0, 0, 0, OSCODE); print {$o->{handle}} $header; return bless($o, $class); } sub PRINT { my ($o) = shift; my ($buf) = join (defined($,) ? $, : "", @_); my ($len) = length($buf); my ($compressed, $status) = $o->{dh}->deflate($buf); print {$o->{handle}} $compressed if defined($compressed); $o->{crc} = Compress::Zlib::crc32($buf, $o->{crc}); $o->{len} += $len; return $len; } sub PRINTF { my ($o) = shift; my ($fmt) = shift; my ($buf) = sprintf($fmt, @_); my ($len) = length($buf); my ($compressed, $status) = $o->{dh}->deflate($buf); print {$o->{handle}} $compressed if defined($compressed); $o->{crc} = Compress::Zlib::crc32($buf, $o->{crc}); $o->{len} += $len; return $len; } sub WRITE { my ($o, $buf, $len, $off) = @_; my ($compressed, $status) = $o->{dh}->deflate(substr($buf, 0, $len)); print {$o->{handle}} $compressed if defined($compressed); $o->{crc} = Compress::Zlib::crc32(substr($buf, 0, $len), $o->{crc}); $o->{len} += $len; return $len; } sub CLOSE { my ($o) = @_; return if !defined($o->{dh}); my ($buf) = $o->{dh}->flush(); $buf .= pack("V V", $o->{crc}, $o->{len}); print {$o->{handle}} $buf; undef $o->{dh}; } sub DESTROY { my ($o) = @_; CLOSE($o); } # Local variables: # indent-tabs-mode: nil # cperl-indent-level: 2 # End: