1
0
Fork 0
mirror of https://github.com/git/git.git synced 2024-05-11 08:56:09 +02:00

Merge branch 'mo/cvs-server-updates'

Various git-cvsserver updates.

* mo/cvs-server-updates:
  t9402: Use TABs for indentation
  t9402: Rename check.cvsCount and check.list
  t9402: Simplify git ls-tree
  t9402: Add missing &&; Code style
  t9402: No space after IO-redirection
  t9402: Dont use test_must_fail cvs
  t9402: improve check_end_tree() and check_end_full_tree()
  t9402: sed -i is not portable
  cvsserver Documentation: new cvs ... -r support
  cvsserver: add t9402 to test branch and tag refs
  cvsserver: support -r and sticky tags for most operations
  cvsserver: Add version awareness to argsfromdir
  cvsserver: generalize getmeta() to recognize commit refs
  cvsserver: implement req_Sticky and related utilities
  cvsserver: add misc commit lookup, file meta data, and file listing functions
  cvsserver: define a tag name character escape mechanism
  cvsserver: cleanup extra slashes in filename arguments
  cvsserver: factor out git-log parsing logic
This commit is contained in:
Junio C Hamano 2013-01-23 21:16:38 -08:00
commit 6a3d05da55
3 changed files with 2210 additions and 329 deletions

View File

@ -359,6 +359,43 @@ Operations supported
All the operations required for normal use are supported, including
checkout, diff, status, update, log, add, remove, commit.
Most CVS command arguments that read CVS tags or revision numbers
(typically -r) work, and also support any git refspec
(tag, branch, commit ID, etc).
However, CVS revision numbers for non-default branches are not well
emulated, and cvs log does not show tags or branches at
all. (Non-main-branch CVS revision numbers superficially resemble CVS
revision numbers, but they actually encode a git commit ID directly,
rather than represent the number of revisions since the branch point.)
Note that there are two ways to checkout a particular branch.
As described elsewhere on this page, the "module" parameter
of cvs checkout is interpreted as a branch name, and it becomes
the main branch. It remains the main branch for a given sandbox
even if you temporarily make another branch sticky with
cvs update -r. Alternatively, the -r argument can indicate
some other branch to actually checkout, even though the module
is still the "main" branch. Tradeoffs (as currently
implemented): Each new "module" creates a new database on disk with
a history for the given module, and after the database is created,
operations against that main branch are fast. Or alternatively,
-r doesn't take any extra disk space, but may be significantly slower for
many operations, like cvs update.
If you want to refer to a git refspec that has characters that are
not allowed by CVS, you have two options. First, it may just work
to supply the git refspec directly to the appropriate CVS -r argument;
some CVS clients don't seem to do much sanity checking of the argument.
Second, if that fails, you can use a special character escape mechanism
that only uses characters that are valid in CVS tags. A sequence
of 4 or 5 characters of the form (underscore (`"_"`), dash (`"-"`),
one or two characters, and dash (`"-"`)) can encode various characters based
on the one or two letters: `"s"` for slash (`"/"`), `"p"` for
period (`"."`), `"u"` for underscore (`"_"`), or two hexadecimal digits
for any byte value at all (typically an ASCII number, or perhaps a part
of a UTF-8 encoded character).
Legacy monitoring operations are not supported (edit, watch and related).
Exports and tagging (tags and branches) are not supported at this stage.

View File

@ -60,6 +60,7 @@
'Valid-responses' => \&req_Validresponses,
'valid-requests' => \&req_validrequests,
'Directory' => \&req_Directory,
'Sticky' => \&req_Sticky,
'Entry' => \&req_Entry,
'Modified' => \&req_Modified,
'Unchanged' => \&req_Unchanged,
@ -470,11 +471,19 @@ sub req_Directory
{
$log->info("Setting prepend to '$state->{path}'");
$state->{prependdir} = $state->{path};
my %entries;
foreach my $entry ( keys %{$state->{entries}} )
{
$state->{entries}{$state->{prependdir} . $entry} = $state->{entries}{$entry};
delete $state->{entries}{$entry};
$entries{$state->{prependdir} . $entry} = $state->{entries}{$entry};
}
$state->{entries}=\%entries;
my %dirMap;
foreach my $dir ( keys %{$state->{dirMap}} )
{
$dirMap{$state->{prependdir} . $dir} = $state->{dirMap}{$dir};
}
$state->{dirMap}=\%dirMap;
}
if ( defined ( $state->{prependdir} ) )
@ -482,9 +491,60 @@ sub req_Directory
$log->debug("Prepending '$state->{prependdir}' to state|directory");
$state->{directory} = $state->{prependdir} . $state->{directory}
}
if ( ! defined($state->{dirMap}{$state->{directory}}) )
{
$state->{dirMap}{$state->{directory}} =
{
'names' => {}
#'tagspec' => undef
};
}
$log->debug("req_Directory : localdir=$data repository=$repository path=$state->{path} directory=$state->{directory} module=$state->{module}");
}
# Sticky tagspec \n
# Response expected: no. Tell the server that the directory most
# recently specified with Directory has a sticky tag or date
# tagspec. The first character of tagspec is T for a tag, D for
# a date, or some other character supplied by a Set-sticky
# response from a previous request to the server. The remainder
# of tagspec contains the actual tag or date, again as supplied
# by Set-sticky.
# The server should remember Static-directory and Sticky requests
# for a particular directory; the client need not resend them each
# time it sends a Directory request for a given directory. However,
# the server is not obliged to remember them beyond the context
# of a single command.
sub req_Sticky
{
my ( $cmd, $tagspec ) = @_;
my ( $stickyInfo );
if($tagspec eq "")
{
# nothing
}
elsif($tagspec=~/^T([^ ]+)\s*$/)
{
$stickyInfo = { 'tag' => $1 };
}
elsif($tagspec=~/^D([0-9.]+)\s*$/)
{
$stickyInfo= { 'date' => $1 };
}
else
{
die "Unknown tag_or_date format\n";
}
$state->{dirMap}{$state->{directory}}{stickyInfo}=$stickyInfo;
$log->debug("req_Sticky : tagspec=$tagspec repository=$state->{repository}"
. " path=$state->{path} directory=$state->{directory}"
. " module=$state->{module}");
}
# Entry entry-line \n
# Response expected: no. Tell the server what version of a file is on the
# local machine. The name in entry-line is a name relative to the directory
@ -511,6 +571,8 @@ sub req_Entry
tag_or_date => $data[5],
};
$state->{dirMap}{$state->{directory}}{names}{$data[1]} = 'F';
$log->info("Received entry line '$data' => '" . $state->{directory} . $data[1] . "'");
}
@ -549,7 +611,10 @@ sub req_add
{
$filename = filecleanup($filename);
my $meta = $updater->getmeta($filename);
# no -r, -A, or -D with add
my $stickyInfo = resolveStickyInfo($filename);
my $meta = $updater->getmeta($filename,$stickyInfo);
my $wrev = revparse($filename);
if ($wrev && $meta && ($wrev=~/^-/))
@ -572,8 +637,10 @@ sub req_add
# this is an "entries" line
my $kopts = kopts_from_path($filename,"sha1",$meta->{filehash});
$log->debug("/$filepart/$meta->{revision}//$kopts/");
print "/$filepart/$meta->{revision}//$kopts/\n";
my $entryLine = "/$filepart/$meta->{revision}//$kopts/";
$entryLine .= getStickyTagOrDate($stickyInfo);
$log->debug($entryLine);
print "$entryLine\n";
# permissions
$log->debug("SEND : u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}");
print "u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}\n";
@ -604,7 +671,8 @@ sub req_add
print "$filename\n";
my $kopts = kopts_from_path($filename,"file",
$state->{entries}{$filename}{modified_filename});
print "/$filepart/0//$kopts/\n";
print "/$filepart/0//$kopts/" .
getStickyTagOrDate($stickyInfo) . "\n";
my $requestedKopts = $state->{opt}{k};
if(defined($requestedKopts))
@ -672,7 +740,10 @@ sub req_remove
next;
}
my $meta = $updater->getmeta($filename);
# only from entries
my $stickyInfo = resolveStickyInfo($filename);
my $meta = $updater->getmeta($filename,$stickyInfo);
my $wrev = revparse($filename);
unless ( defined ( $wrev ) )
@ -702,7 +773,7 @@ sub req_remove
print "Checked-in $dirpart\n";
print "$filename\n";
my $kopts = kopts_from_path($filename,"sha1",$meta->{filehash});
print "/$filepart/-$wrev//$kopts/\n";
print "/$filepart/-$wrev//$kopts/" . getStickyTagOrDate($stickyInfo) . "\n";
$rmcount++;
}
@ -882,6 +953,9 @@ sub req_co
return 1;
}
my $stickyInfo = { 'tag' => $state->{opt}{r},
'date' => $state->{opt}{D} };
my $module = $state->{args}[0];
$state->{module} = $module;
my $checkout_path = $module;
@ -899,64 +973,32 @@ sub req_co
my $updater = GITCVS::updater->new($state->{CVSROOT}, $module, $log);
$updater->update();
my $headHash;
if( defined($stickyInfo) && defined($stickyInfo->{tag}) )
{
$headHash = $updater->lookupCommitRef($stickyInfo->{tag});
if( !defined($headHash) )
{
print "error 1 no such tag `$stickyInfo->{tag}'\n";
cleanupWorkTree();
exit;
}
}
$checkout_path =~ s|/$||; # get rid of trailing slashes
# Eclipse seems to need the Clear-sticky command
# to prepare the 'Entries' file for the new directory.
print "Clear-sticky $checkout_path/\n";
print $state->{CVSROOT} . "/$module/\n";
print "Clear-static-directory $checkout_path/\n";
print $state->{CVSROOT} . "/$module/\n";
print "Clear-sticky $checkout_path/\n"; # yes, twice
print $state->{CVSROOT} . "/$module/\n";
print "Template $checkout_path/\n";
print $state->{CVSROOT} . "/$module/\n";
print "0\n";
# instruct the client that we're checking out to $checkout_path
print "E cvs checkout: Updating $checkout_path\n";
my %seendirs = ();
my $lastdir ='';
# recursive
sub prepdir {
my ($dir, $repodir, $remotedir, $seendirs) = @_;
my $parent = dirname($dir);
$dir =~ s|/+$||;
$repodir =~ s|/+$||;
$remotedir =~ s|/+$||;
$parent =~ s|/+$||;
$log->debug("announcedir $dir, $repodir, $remotedir" );
prepDirForOutput(
".",
$state->{CVSROOT} . "/$module",
$checkout_path,
\%seendirs,
'checkout',
$state->{dirArgs} );
if ($parent eq '.' || $parent eq './') {
$parent = '';
}
# recurse to announce unseen parents first
if (length($parent) && !exists($seendirs->{$parent})) {
prepdir($parent, $repodir, $remotedir, $seendirs);
}
# Announce that we are going to modify at the parent level
if ($parent) {
print "E cvs checkout: Updating $remotedir/$parent\n";
} else {
print "E cvs checkout: Updating $remotedir\n";
}
print "Clear-sticky $remotedir/$parent/\n";
print "$repodir/$parent/\n";
print "Clear-static-directory $remotedir/$dir/\n";
print "$repodir/$dir/\n";
print "Clear-sticky $remotedir/$parent/\n"; # yes, twice
print "$repodir/$parent/\n";
print "Template $remotedir/$dir/\n";
print "$repodir/$dir/\n";
print "0\n";
$seendirs->{$dir} = 1;
}
foreach my $git ( @{$updater->gethead} )
foreach my $git ( @{$updater->getAnyHead($headHash)} )
{
# Don't want to check out deleted files
next if ( $git->{filehash} eq "deleted" );
@ -964,16 +1006,13 @@ sub req_co
my $fullName = $git->{name};
( $git->{name}, $git->{dir} ) = filenamesplit($git->{name});
if (length($git->{dir}) && $git->{dir} ne './'
&& $git->{dir} ne $lastdir ) {
unless (exists($seendirs{$git->{dir}})) {
prepdir($git->{dir}, $state->{CVSROOT} . "/$module/",
$checkout_path, \%seendirs);
$lastdir = $git->{dir};
$seendirs{$git->{dir}} = 1;
}
print "E cvs checkout: Updating /$checkout_path/$git->{dir}\n";
}
unless (exists($seendirs{$git->{dir}})) {
prepDirForOutput($git->{dir}, $state->{CVSROOT} . "/$module/",
$checkout_path, \%seendirs, 'checkout',
$state->{dirArgs} );
$lastdir = $git->{dir};
$seendirs{$git->{dir}} = 1;
}
# modification time of this file
print "Mod-time $git->{modified}\n";
@ -993,7 +1032,8 @@ sub req_co
# this is an "entries" line
my $kopts = kopts_from_path($fullName,"sha1",$git->{filehash});
print "/$git->{name}/$git->{revision}//$kopts/\n";
print "/$git->{name}/$git->{revision}//$kopts/" .
getStickyTagOrDate($stickyInfo) . "\n";
# permissions
print "u=$git->{mode},g=$git->{mode},o=$git->{mode}\n";
@ -1006,6 +1046,119 @@ sub req_co
statecleanup();
}
# used by req_co and req_update to set up directories for files
# recursively handles parents
sub prepDirForOutput
{
my ($dir, $repodir, $remotedir, $seendirs, $request, $dirArgs) = @_;
my $parent = dirname($dir);
$dir =~ s|/+$||;
$repodir =~ s|/+$||;
$remotedir =~ s|/+$||;
$parent =~ s|/+$||;
if ($parent eq '.' || $parent eq './')
{
$parent = '';
}
# recurse to announce unseen parents first
if( length($parent) &&
!exists($seendirs->{$parent}) &&
( $request eq "checkout" ||
exists($dirArgs->{$parent}) ) )
{
prepDirForOutput($parent, $repodir, $remotedir,
$seendirs, $request, $dirArgs);
}
# Announce that we are going to modify at the parent level
if ($dir eq '.' || $dir eq './')
{
$dir = '';
}
if(exists($seendirs->{$dir}))
{
return;
}
$log->debug("announcedir $dir, $repodir, $remotedir" );
my($thisRemoteDir,$thisRepoDir);
if ($dir ne "")
{
$thisRepoDir="$repodir/$dir";
if($remotedir eq ".")
{
$thisRemoteDir=$dir;
}
else
{
$thisRemoteDir="$remotedir/$dir";
}
}
else
{
$thisRepoDir=$repodir;
$thisRemoteDir=$remotedir;
}
unless ( $state->{globaloptions}{-Q} || $state->{globaloptions}{-q} )
{
print "E cvs $request: Updating $thisRemoteDir\n";
}
my ($opt_r)=$state->{opt}{r};
my $stickyInfo;
if(exists($state->{opt}{A}))
{
# $stickyInfo=undef;
}
elsif( defined($opt_r) && $opt_r ne "" )
# || ( defined($state->{opt}{D}) && $state->{opt}{D} ne "" ) # TODO
{
$stickyInfo={ 'tag' => (defined($opt_r)?$opt_r:undef) };
# TODO: Convert -D value into the form 2011.04.10.04.46.57,
# similar to an entry line's sticky date, without the D prefix.
# It sometimes (always?) arrives as something more like
# '10 Apr 2011 04:46:57 -0000'...
# $stickyInfo={ 'date' => (defined($stickyDate)?$stickyDate:undef) };
}
else
{
$stickyInfo=getDirStickyInfo($state->{prependdir} . $dir);
}
my $stickyResponse;
if(defined($stickyInfo))
{
$stickyResponse = "Set-sticky $thisRemoteDir/\n" .
"$thisRepoDir/\n" .
getStickyTagOrDate($stickyInfo) . "\n";
}
else
{
$stickyResponse = "Clear-sticky $thisRemoteDir/\n" .
"$thisRepoDir/\n";
}
unless ( $state->{globaloptions}{-n} )
{
print $stickyResponse;
print "Clear-static-directory $thisRemoteDir/\n";
print "$thisRepoDir/\n";
print $stickyResponse; # yes, twice
print "Template $thisRemoteDir/\n";
print "$thisRepoDir/\n";
print "0\n";
}
$seendirs->{$dir} = 1;
# FUTURE: This would more accurately emulate CVS by sending
# another copy of sticky after processing the files in that
# directory. Or intermediate: perhaps send all sticky's for
# $seendirs after after processing all files.
}
# update \n
# Response expected: yes. Actually do a cvs update command. This uses any
# previous Argument, Directory, Entry, or Modified requests, if they have
@ -1049,29 +1202,19 @@ sub req_update
#$log->debug("update state : " . Dumper($state));
my $last_dirname = "///";
my($repoDir);
$repoDir=$state->{CVSROOT} . "/$state->{module}/$state->{prependdir}";
my %seendirs = ();
# foreach file specified on the command line ...
foreach my $filename ( @{$state->{args}} )
foreach my $argsFilename ( @{$state->{args}} )
{
$filename = filecleanup($filename);
my $filename;
$filename = filecleanup($argsFilename);
$log->debug("Processing file $filename");
unless ( $state->{globaloptions}{-Q} || $state->{globaloptions}{-q} )
{
my $cur_dirname = dirname($filename);
if ( $cur_dirname ne $last_dirname )
{
$last_dirname = $cur_dirname;
if ( $cur_dirname eq "" )
{
$cur_dirname = ".";
}
print "E cvs update: Updating $cur_dirname\n";
}
}
# if we have a -C we should pretend we never saw modified stuff
if ( exists ( $state->{opt}{C} ) )
{
@ -1080,13 +1223,11 @@ sub req_update
$state->{entries}{$filename}{unchanged} = 1;
}
my $meta;
if ( defined($state->{opt}{r}) and $state->{opt}{r} =~ /^(1\.\d+)$/ )
{
$meta = $updater->getmeta($filename, $1);
} else {
$meta = $updater->getmeta($filename);
}
my $stickyInfo = resolveStickyInfo($filename,
$state->{opt}{r},
$state->{opt}{D},
exists($state->{opt}{A}));
my $meta = $updater->getmeta($filename, $stickyInfo);
# If -p was given, "print" the contents of the requested revision.
if ( exists ( $state->{opt}{p} ) ) {
@ -1099,6 +1240,17 @@ sub req_update
next;
}
# Directories:
prepDirForOutput(
dirname($argsFilename),
$repoDir,
".",
\%seendirs,
"update",
$state->{dirArgs} );
my $wrev = revparse($filename);
if ( ! defined $meta )
{
$meta = {
@ -1106,16 +1258,23 @@ sub req_update
revision => '0',
filehash => 'added'
};
if($wrev ne "0")
{
$meta->{filehash}='deleted';
}
}
my $oldmeta = $meta;
my $wrev = revparse($filename);
# If the working copy is an old revision, lets get that version too for comparison.
if ( defined($wrev) and $wrev ne $meta->{revision} )
my $oldWrev=$wrev;
if(defined($oldWrev))
{
$oldmeta = $updater->getmeta($filename, $wrev);
$oldWrev=~s/^-//;
if($oldWrev ne $meta->{revision})
{
$oldmeta = $updater->getmeta($filename, $oldWrev);
}
}
#$log->debug("Target revision is $meta->{revision}, current working revision is $wrev");
@ -1133,6 +1292,7 @@ sub req_update
if ( defined ( $wrev )
and defined($meta->{revision})
and $wrev eq $meta->{revision}
and $wrev ne "0"
and defined($state->{entries}{$filename}{modified_hash})
and not exists ( $state->{opt}{C} ) )
{
@ -1143,7 +1303,7 @@ sub req_update
next;
}
if ( $meta->{filehash} eq "deleted" )
if ( $meta->{filehash} eq "deleted" && $wrev ne "0" )
{
# TODO: If it has been modified in the sandbox, error out
# with the appropriate message, rather than deleting a modified
@ -1205,10 +1365,6 @@ sub req_update
$log->debug("Updating existing file 'Update-existing $dirpart'");
} else {
# instruct client we're sending a file to put in this path as a new file
print "Clear-static-directory $dirpart\n";
print $state->{CVSROOT} . "/$state->{module}/$dirpart\n";
print "Clear-sticky $dirpart\n";
print $state->{CVSROOT} . "/$state->{module}/$dirpart\n";
$log->debug("Creating new file 'Created $dirpart'");
print "Created $dirpart\n";
@ -1217,8 +1373,10 @@ sub req_update
# this is an "entries" line
my $kopts = kopts_from_path($filename,"sha1",$meta->{filehash});
$log->debug("/$filepart/$meta->{revision}//$kopts/");
print "/$filepart/$meta->{revision}//$kopts/\n";
my $entriesLine = "/$filepart/$meta->{revision}//$kopts/";
$entriesLine .= getStickyTagOrDate($stickyInfo);
$log->debug($entriesLine);
print "$entriesLine\n";
# permissions
$log->debug("SEND : u=$meta->{mode},g=$meta->{mode},o=$meta->{mode}");
@ -1266,7 +1424,9 @@ sub req_update
my $kopts = kopts_from_path("$dirpart/$filepart",
"file",$mergedFile);
$log->debug("/$filepart/$meta->{revision}//$kopts/");
print "/$filepart/$meta->{revision}//$kopts/\n";
my $entriesLine="/$filepart/$meta->{revision}//$kopts/";
$entriesLine .= getStickyTagOrDate($stickyInfo);
print "$entriesLine\n";
}
}
elsif ( $return == 1 )
@ -1282,7 +1442,9 @@ sub req_update
print $state->{CVSROOT} . "/$state->{module}/$filename\n";
my $kopts = kopts_from_path("$dirpart/$filepart",
"file",$mergedFile);
print "/$filepart/$meta->{revision}/+/$kopts/\n";
my $entriesLine = "/$filepart/$meta->{revision}/+/$kopts/";
$entriesLine .= getStickyTagOrDate($stickyInfo);
print "$entriesLine\n";
}
}
else
@ -1310,6 +1472,43 @@ sub req_update
}
# prepDirForOutput() any other existing directories unless they already
# have the right sticky tag:
unless ( $state->{globaloptions}{n} )
{
my $dir;
foreach $dir (keys(%{$state->{dirMap}}))
{
if( ! $seendirs{$dir} &&
exists($state->{dirArgs}{$dir}) )
{
my($oldTag);
$oldTag=$state->{dirMap}{$dir}{tagspec};
unless( ( exists($state->{opt}{A}) &&
defined($oldTag) ) ||
( defined($state->{opt}{r}) &&
( !defined($oldTag) ||
$state->{opt}{r} ne $oldTag ) ) )
# TODO?: OR sticky dir is different...
{
next;
}
prepDirForOutput(
$dir,
$repoDir,
".",
\%seendirs,
'update',
$state->{dirArgs} );
}
# TODO?: Consider sending a final duplicate Sticky response
# to more closely mimic real CVS.
}
}
print "ok\n";
}
@ -1342,23 +1541,11 @@ sub req_ci
my $updater = GITCVS::updater->new($state->{CVSROOT}, $state->{module}, $log);
$updater->update();
# Remember where the head was at the beginning.
my $parenthash = `git show-ref -s refs/heads/$state->{module}`;
chomp $parenthash;
if ($parenthash !~ /^[0-9a-f]{40}$/) {
print "error 1 pserver cannot find the current HEAD of module";
cleanupWorkTree();
exit;
}
setupWorkTree($parenthash);
$log->info("Lockless commit start, basing commit on '$work->{workDir}', index file is '$work->{index}'");
$log->info("Created index '$work->{index}' for head $state->{module} - exit status $?");
my @committedfiles = ();
my %oldmeta;
my $stickyInfo;
my $branchRef;
my $parenthash;
# foreach file specified on the command line ...
foreach my $filename ( @{$state->{args}} )
@ -1368,7 +1555,67 @@ sub req_ci
next unless ( exists $state->{entries}{$filename}{modified_filename} or not $state->{entries}{$filename}{unchanged} );
my $meta = $updater->getmeta($filename);
#####
# Figure out which branch and parenthash we are committing
# to, and setup worktree:
# should always come from entries:
my $fileStickyInfo = resolveStickyInfo($filename);
if( !defined($branchRef) )
{
$stickyInfo = $fileStickyInfo;
if( defined($stickyInfo) &&
( defined($stickyInfo->{date}) ||
!defined($stickyInfo->{tag}) ) )
{
print "error 1 cannot commit with sticky date for file `$filename'\n";
cleanupWorkTree();
exit;
}
$branchRef = "refs/heads/$state->{module}";
if ( defined($stickyInfo) && defined($stickyInfo->{tag}) )
{
$branchRef = "refs/heads/$stickyInfo->{tag}";
}
$parenthash = `git show-ref -s $branchRef`;
chomp $parenthash;
if ($parenthash !~ /^[0-9a-f]{40}$/)
{
if ( defined($stickyInfo) && defined($stickyInfo->{tag}) )
{
print "error 1 sticky tag `$stickyInfo->{tag}' for file `$filename' is not a branch\n";
}
else
{
print "error 1 pserver cannot find the current HEAD of module";
}
cleanupWorkTree();
exit;
}
setupWorkTree($parenthash);
$log->info("Lockless commit start, basing commit on '$work->{workDir}', index file is '$work->{index}'");
$log->info("Created index '$work->{index}' for head $state->{module} - exit status $?");
}
elsif( !refHashEqual($stickyInfo,$fileStickyInfo) )
{
#TODO: We could split the cvs commit into multiple
# git commits by distinct stickyTag values, but that
# is lowish priority.
print "error 1 Committing different files to different"
. " branches is not currently supported\n";
cleanupWorkTree();
exit;
}
#####
# Process this file:
my $meta = $updater->getmeta($filename,$stickyInfo);
$oldmeta{$filename} = $meta;
my $wrev = revparse($filename);
@ -1470,7 +1717,7 @@ sub req_ci
}
### Emulate git-receive-pack by running hooks/update
my @hook = ( $ENV{GIT_DIR}.'hooks/update', "refs/heads/$state->{module}",
my @hook = ( $ENV{GIT_DIR}.'hooks/update', $branchRef,
$parenthash, $commithash );
if( -x $hook[0] ) {
unless( system( @hook ) == 0 )
@ -1484,7 +1731,7 @@ sub req_ci
### Update the ref
if (system(qw(git update-ref -m), "cvsserver ci",
"refs/heads/$state->{module}", $commithash, $parenthash)) {
$branchRef, $commithash, $parenthash)) {
$log->warn("update-ref for $state->{module} failed.");
print "error 1 Cannot commit -- update first\n";
cleanupWorkTree();
@ -1498,7 +1745,7 @@ sub req_ci
local $SIG{PIPE} = sub { die 'pipe broke' };
print $pipe "$parenthash $commithash refs/heads/$state->{module}\n";
print $pipe "$parenthash $commithash $branchRef\n";
close $pipe || die "bad pipe: $! $?";
}
@ -1508,7 +1755,7 @@ sub req_ci
### Then hooks/post-update
$hook = $ENV{GIT_DIR}.'hooks/post-update';
if (-x $hook) {
system($hook, "refs/heads/$state->{module}");
system($hook, $branchRef);
}
# foreach file specified on the command line ...
@ -1516,7 +1763,7 @@ sub req_ci
{
$filename = filecleanup($filename);
my $meta = $updater->getmeta($filename);
my $meta = $updater->getmeta($filename,$stickyInfo);
unless (defined $meta->{revision}) {
$meta->{revision} = "1.1";
}
@ -1540,7 +1787,8 @@ sub req_ci
print "Checked-in $dirpart\n";
print "$filename\n";
my $kopts = kopts_from_path($filename,"sha1",$meta->{filehash});
print "/$filepart/$meta->{revision}//$kopts/\n";
print "/$filepart/$meta->{revision}//$kopts/" .
getStickyTagOrDate($stickyInfo) . "\n";
}
}
@ -1577,16 +1825,19 @@ sub req_status
next;
}
my $meta = $updater->getmeta($filename);
my $oldmeta = $meta;
my $wrev = revparse($filename);
my $stickyInfo = resolveStickyInfo($filename);
my $meta = $updater->getmeta($filename,$stickyInfo);
my $oldmeta = $meta;
# If the working copy is an old revision, lets get that
# version too for comparison.
if ( defined($wrev) and $wrev ne $meta->{revision} )
{
$oldmeta = $updater->getmeta($filename, $wrev);
my($rmRev)=$wrev;
$rmRev=~s/^-//;
$oldmeta = $updater->getmeta($filename, $rmRev);
}
# TODO : All possible statuses aren't yet implemented
@ -1629,6 +1880,7 @@ sub req_status
# same revision but there are local changes
if ( defined ( $wrev ) and defined($meta->{revision}) and
$wrev eq $meta->{revision} and
$wrev ne "0" and
$state->{entries}{$filename}{modified_filename} )
{
$status ||= "Locally Modified";
@ -1644,7 +1896,8 @@ sub req_status
}
if ( defined ( $state->{entries}{$filename}{revision} ) and
not defined ( $meta->{revision} ) )
( !defined($meta->{revision}) ||
$meta->{revision} eq "0" ) )
{
$status ||= "Locally Added";
}
@ -1740,98 +1993,133 @@ sub req_diff
# be providing status on ...
argsfromdir($updater);
my($foundDiff);
# foreach file specified on the command line ...
foreach my $filename ( @{$state->{args}} )
foreach my $argFilename ( @{$state->{args}} )
{
$filename = filecleanup($filename);
my($filename) = filecleanup($argFilename);
my ( $fh, $file1, $file2, $meta1, $meta2, $filediff );
my $wrev = revparse($filename);
# We need _something_ to diff against
next unless ( defined ( $wrev ) );
# Priority for revision1:
# 1. First -r (missing file: check -N)
# 2. wrev from client's Entry line
# - missing line/file: check -N
# - "0": added file not committed (empty contents for rev1)
# - Prefixed with dash (to be removed): check -N
# if we have a -r switch, use it
if ( defined ( $revision1 ) )
{
( undef, $file1 ) = tempfile( DIR => $TEMP_DIR, OPEN => 0 );
$meta1 = $updater->getmeta($filename, $revision1);
unless ( defined ( $meta1 ) and $meta1->{filehash} ne "deleted" )
}
elsif( defined($wrev) && $wrev ne "0" )
{
my($rmRev)=$wrev;
$rmRev=~s/^-//;
$meta1 = $updater->getmeta($filename, $rmRev);
}
if ( !defined($meta1) ||
$meta1->{filehash} eq "deleted" )
{
if( !exists($state->{opt}{N}) )
{
print "E File $filename at revision $revision1 doesn't exist\n";
if(!defined($revision1))
{
print "E File $filename at revision $revision1 doesn't exist\n";
}
next;
}
transmitfile($meta1->{filehash}, { targetfile => $file1 });
}
# otherwise we just use the working copy revision
else
{
( undef, $file1 ) = tempfile( DIR => $TEMP_DIR, OPEN => 0 );
$meta1 = $updater->getmeta($filename, $wrev);
transmitfile($meta1->{filehash}, { targetfile => $file1 });
elsif( !defined($meta1) )
{
$meta1 = {
name => $filename,
revision => '0',
filehash => 'deleted'
};
}
}
# Priority for revision2:
# 1. Second -r (missing file: check -N)
# 2. Modified file contents from client
# 3. wrev from client's Entry line
# - missing line/file: check -N
# - Prefixed with dash (to be removed): check -N
# if we have a second -r switch, use it too
if ( defined ( $revision2 ) )
{
( undef, $file2 ) = tempfile( DIR => $TEMP_DIR, OPEN => 0 );
$meta2 = $updater->getmeta($filename, $revision2);
unless ( defined ( $meta2 ) and $meta2->{filehash} ne "deleted" )
{
print "E File $filename at revision $revision2 doesn't exist\n";
next;
}
transmitfile($meta2->{filehash}, { targetfile => $file2 });
}
# otherwise we just use the working copy
else
elsif(defined($state->{entries}{$filename}{modified_filename}))
{
$file2 = $state->{entries}{$filename}{modified_filename};
$meta2 = {
name => $filename,
revision => '0',
filehash => 'modified'
};
}
elsif( defined($wrev) && ($wrev!~/^-/) )
{
if(!defined($revision1)) # no revision and no modifications:
{
next;
}
$meta2 = $updater->getmeta($filename, $wrev);
}
if(!defined($file2))
{
if ( !defined($meta2) ||
$meta2->{filehash} eq "deleted" )
{
if( !exists($state->{opt}{N}) )
{
if(!defined($revision2))
{
print "E File $filename at revision $revision2 doesn't exist\n";
}
next;
}
elsif( !defined($meta2) )
{
$meta2 = {
name => $filename,
revision => '0',
filehash => 'deleted'
};
}
}
}
# if we have been given -r, and we don't have a $file2 yet, lets
# get one
if ( defined ( $revision1 ) and not defined ( $file2 ) )
if( $meta1->{filehash} eq $meta2->{filehash} )
{
$log->info("unchanged $filename");
next;
}
# Retrieve revision contents:
( undef, $file1 ) = tempfile( DIR => $TEMP_DIR, OPEN => 0 );
transmitfile($meta1->{filehash}, { targetfile => $file1 });
if(!defined($file2))
{
( undef, $file2 ) = tempfile( DIR => $TEMP_DIR, OPEN => 0 );
$meta2 = $updater->getmeta($filename, $wrev);
transmitfile($meta2->{filehash}, { targetfile => $file2 });
}
# We need to have retrieved something useful
next unless ( defined ( $meta1 ) );
# Files to date if the working copy and repo copy have the same
# revision, and the working copy is unmodified
if ( not defined ( $meta2 ) and $wrev eq $meta1->{revision} and
( ( $state->{entries}{$filename}{unchanged} and
( not defined ( $state->{entries}{$filename}{conflict} ) or
$state->{entries}{$filename}{conflict} !~ /^\+=/ ) ) or
( defined($state->{entries}{$filename}{modified_hash}) and
$state->{entries}{$filename}{modified_hash} eq
$meta1->{filehash} ) ) )
{
next;
}
# Apparently we only show diffs for locally modified files
unless ( defined($meta2) or
defined ( $state->{entries}{$filename}{modified_filename} ) )
{
next;
}
print "M Index: $filename\n";
# Generate the actual diff:
print "M Index: $argFilename\n";
print "M =======" . ( "=" x 60 ) . "\n";
print "M RCS file: $state->{CVSROOT}/$state->{module}/$filename,v\n";
if ( defined ( $meta1 ) )
if ( defined ( $meta1 ) && $meta1->{revision} ne "0" )
{
print "M retrieving revision $meta1->{revision}\n"
}
if ( defined ( $meta2 ) )
if ( defined ( $meta2 ) && $meta2->{revision} ne "0" )
{
print "M retrieving revision $meta2->{revision}\n"
}
@ -1852,33 +2140,73 @@ sub req_diff
}
}
}
print "$filename\n";
print "$argFilename\n";
$log->info("Diffing $filename -r $meta1->{revision} -r " .
( $meta2->{revision} or "workingcopy" ));
( $fh, $filediff ) = tempfile ( DIR => $TEMP_DIR );
# TODO: Use --label instead of -L because -L is no longer
# documented and may go away someday. Not sure if there there are
# versions that only support -L, which would make this change risky?
# http://osdir.com/ml/bug-gnu-utils-gnu/2010-12/msg00060.html
# ("man diff" should actually document the best migration strategy,
# [current behavior, future changes, old compatibility issues
# or lack thereof, etc], not just stop mentioning the option...)
# TODO: Real CVS seems to include a date in the label, before
# the revision part, without the keyword "revision". The following
# has minimal changes compared to original versions of
# git-cvsserver.perl. (Mostly tab vs space after filename.)
my (@diffCmd) = ( 'diff' );
if ( exists($state->{opt}{N}) )
{
push @diffCmd,"-N";
}
if ( exists $state->{opt}{u} )
{
system("diff -u -L '$filename revision $meta1->{revision}'" .
" -L '$filename " .
( defined($meta2->{revision}) ?
"revision $meta2->{revision}" :
"working copy" ) .
"' $file1 $file2 > $filediff" );
} else {
system("diff $file1 $file2 > $filediff");
}
push @diffCmd,("-u","-L");
if( $meta1->{filehash} eq "deleted" )
{
push @diffCmd,"/dev/null";
} else {
push @diffCmd,("$argFilename\trevision $meta1->{revision}");
}
while ( <$fh> )
{
print "M $_";
if( defined($meta2->{filehash}) )
{
if( $meta2->{filehash} eq "deleted" )
{
push @diffCmd,("-L","/dev/null");
} else {
push @diffCmd,("-L",
"$argFilename\trevision $meta2->{revision}");
}
} else {
push @diffCmd,("-L","$argFilename\tworking copy");
}
}
close $fh;
push @diffCmd,($file1,$file2);
if(!open(DIFF,"-|",@diffCmd))
{
$log->warn("Unable to run diff: $!");
}
my($diffLine);
while(defined($diffLine=<DIFF>))
{
print "M $diffLine";
$foundDiff=1;
}
close(DIFF);
}
print "ok\n";
if($foundDiff)
{
print "error \n";
}
else
{
print "ok\n";
}
}
sub req_log
@ -2098,7 +2426,7 @@ sub argsplit
$opt = { A => 0, N => 0, P => 0, R => 0, c => 0, f => 0, l => 0, n => 0, p => 0, s => 0, r => 1, D => 1, d => 1, k => 1, j => 1, } if ( $type eq "co" );
$opt = { v => 0, l => 0, R => 0 } if ( $type eq "status" );
$opt = { A => 0, P => 0, C => 0, d => 0, f => 0, l => 0, R => 0, p => 0, k => 1, r => 1, D => 1, j => 1, I => 1, W => 1 } if ( $type eq "update" );
$opt = { l => 0, R => 0, k => 1, D => 1, D => 1, r => 2 } if ( $type eq "diff" );
$opt = { l => 0, R => 0, k => 1, D => 1, D => 1, r => 2, N => 0 } if ( $type eq "diff" );
$opt = { c => 0, R => 0, l => 0, f => 0, F => 1, m => 1, r => 1 } if ( $type eq "ci" );
$opt = { k => 1, m => 1 } if ( $type eq "add" );
$opt = { f => 0, l => 0, R => 0 } if ( $type eq "remove" );
@ -2164,62 +2492,335 @@ sub argsplit
}
}
# This method uses $state->{directory} to populate $state->{args} with a list of filenames
# Used by argsfromdir
sub expandArg
{
my ($updater,$outNameMap,$outDirMap,$path,$isDir) = @_;
my $fullPath = filecleanup($path);
# Is it a directory?
if( defined($state->{dirMap}{$fullPath}) ||
defined($state->{dirMap}{"$fullPath/"}) )
{
# It is a directory in the user's sandbox.
$isDir=1;
if(defined($state->{entries}{$fullPath}))
{
$log->fatal("Inconsistent file/dir type");
die "Inconsistent file/dir type";
}
}
elsif(defined($state->{entries}{$fullPath}))
{
# It is a file in the user's sandbox.
$isDir=0;
}
my($revDirMap,$otherRevDirMap);
if(!defined($isDir) || $isDir)
{
# Resolve version tree for sticky tag:
# (for now we only want list of files for the version, not
# particular versions of those files: assume it is a directory
# for the moment; ignore Entry's stick tag)
# Order of precedence of sticky tags:
# -A [head]
# -r /tag/
# [file entry sticky tag, but that is only relevant to files]
# [the tag specified in dir req_Sticky]
# [the tag specified in a parent dir req_Sticky]
# [head]
# Also, -r may appear twice (for diff).
#
# FUTURE: When/if -j (merges) are supported, we also
# need to add relevant files from one or two
# versions specified with -j.
if(exists($state->{opt}{A}))
{
$revDirMap=$updater->getRevisionDirMap();
}
elsif( defined($state->{opt}{r}) and
ref $state->{opt}{r} eq "ARRAY" )
{
$revDirMap=$updater->getRevisionDirMap($state->{opt}{r}[0]);
$otherRevDirMap=$updater->getRevisionDirMap($state->{opt}{r}[1]);
}
elsif(defined($state->{opt}{r}))
{
$revDirMap=$updater->getRevisionDirMap($state->{opt}{r});
}
else
{
my($sticky)=getDirStickyInfo($fullPath);
$revDirMap=$updater->getRevisionDirMap($sticky->{tag});
}
# Is it a directory?
if( defined($revDirMap->{$fullPath}) ||
defined($otherRevDirMap->{$fullPath}) )
{
$isDir=1;
}
}
# What to do with it?
if(!$isDir)
{
$outNameMap->{$fullPath}=1;
}
else
{
$outDirMap->{$fullPath}=1;
if(defined($revDirMap->{$fullPath}))
{
addDirMapFiles($updater,$outNameMap,$outDirMap,
$revDirMap->{$fullPath});
}
if( defined($otherRevDirMap) &&
defined($otherRevDirMap->{$fullPath}) )
{
addDirMapFiles($updater,$outNameMap,$outDirMap,
$otherRevDirMap->{$fullPath});
}
}
}
# Used by argsfromdir
# Add entries from dirMap to outNameMap. Also recurse into entries
# that are subdirectories.
sub addDirMapFiles
{
my($updater,$outNameMap,$outDirMap,$dirMap)=@_;
my($fullName);
foreach $fullName (keys(%$dirMap))
{
my $cleanName=$fullName;
if(defined($state->{prependdir}))
{
if(!($cleanName=~s/^\Q$state->{prependdir}\E//))
{
$log->fatal("internal error stripping prependdir");
die "internal error stripping prependdir";
}
}
if($dirMap->{$fullName} eq "F")
{
$outNameMap->{$cleanName}=1;
}
elsif($dirMap->{$fullName} eq "D")
{
if(!$state->{opt}{l})
{
expandArg($updater,$outNameMap,$outDirMap,$cleanName,1);
}
}
else
{
$log->fatal("internal error in addDirMapFiles");
die "internal error in addDirMapFiles";
}
}
}
# This method replaces $state->{args} with a directory-expanded
# list of all relevant filenames (recursively unless -d), based
# on $state->{entries}, and the "current" list of files in
# each directory. "Current" files as determined by
# either the requested (-r/-A) or "req_Sticky" version of
# that directory.
# Both the input args and the new output args are relative
# to the cvs-client's CWD, although some of the internal
# computations are relative to the top of the project.
sub argsfromdir
{
my $updater = shift;
$state->{args} = [] if ( scalar(@{$state->{args}}) == 1 and $state->{args}[0] eq "." );
# Notes about requirements for specific callers:
# update # "standard" case (entries; a single -r/-A/default; -l)
# # Special case: -d for create missing directories.
# diff # 0 or 1 -r's: "standard" case.
# # 2 -r's: We could ignore entries (just use the two -r's),
# # but it doesn't really matter.
# annotate # "standard" case
# log # Punting: log -r has a more complex non-"standard"
# # meaning, and we don't currently try to support log'ing
# # branches at all (need a lot of work to
# # support CVS-consistent branch relative version
# # numbering).
#HERE: But we still want to expand directories. Maybe we should
# essentially force "-A".
# status # "standard", except that -r/-A/default are not possible.
# # Mostly only used to expand entries only)
#
# Don't use argsfromdir at all:
# add # Explicit arguments required. Directory args imply add
# # the directory itself, not the files in it.
# co # Obtain list directly.
# remove # HERE: TEST: MAYBE client does the recursion for us,
# # since it only makes sense to remove stuff already in
# # the sandobx?
# ci # HERE: Similar to remove...
# # Don't try to implement the confusing/weird
# # ci -r bug er.."feature".
return if ( scalar ( @{$state->{args}} ) > 1 );
my @gethead = @{$updater->gethead};
# push added files
foreach my $file (keys %{$state->{entries}}) {
if ( exists $state->{entries}{$file}{revision} &&
$state->{entries}{$file}{revision} eq '0' )
{
push @gethead, { name => $file, filehash => 'added' };
}
}
if ( scalar(@{$state->{args}}) == 1 )
if(scalar(@{$state->{args}})==0)
{
my $arg = $state->{args}[0];
$arg .= $state->{prependdir} if ( defined ( $state->{prependdir} ) );
$state->{args} = [ "." ];
}
my %allArgs;
my %allDirs;
for my $file (@{$state->{args}})
{
expandArg($updater,\%allArgs,\%allDirs,$file);
}
$log->info("Only one arg specified, checking for directory expansion on '$arg'");
# Include any entries from sandbox. Generally client won't
# send entries that shouldn't be used.
foreach my $file (keys %{$state->{entries}})
{
$allArgs{remove_prependdir($file)} = 1;
}
foreach my $file ( @gethead )
$state->{dirArgs} = \%allDirs;
$state->{args} = [
sort {
# Sort priority: by directory depth, then actual file name:
my @piecesA=split('/',$a);
my @piecesB=split('/',$b);
my $count=scalar(@piecesA);
my $tmp=scalar(@piecesB);
return $count<=>$tmp if($count!=$tmp);
for($tmp=0;$tmp<$count;$tmp++)
{
if($piecesA[$tmp] ne $piecesB[$tmp])
{
return $piecesA[$tmp] cmp $piecesB[$tmp]
}
}
return 0;
} keys(%allArgs) ];
}
## look up directory sticky tag, of either fullPath or a parent:
sub getDirStickyInfo
{
my($fullPath)=@_;
$fullPath=~s%/+$%%;
while($fullPath ne "" && !defined($state->{dirMap}{"$fullPath/"}))
{
$fullPath=~s%/?[^/]*$%%;
}
if( !defined($state->{dirMap}{"$fullPath/"}) &&
( $fullPath eq "" ||
$fullPath eq "." ) )
{
return $state->{dirMap}{""}{stickyInfo};
}
else
{
return $state->{dirMap}{"$fullPath/"}{stickyInfo};
}
}
# Resolve precedence of various ways of specifying which version of
# a file you want. Returns undef (for default head), or a ref to a hash
# that contains "tag" and/or "date" keys.
sub resolveStickyInfo
{
my($filename,$stickyTag,$stickyDate,$reset) = @_;
# Order of precedence of sticky tags:
# -A [head]
# -r /tag/
# [file entry sticky tag]
# [the tag specified in dir req_Sticky]
# [the tag specified in a parent dir req_Sticky]
# [head]
my $result;
if($reset)
{
# $result=undef;
}
elsif( defined($stickyTag) && $stickyTag ne "" )
# || ( defined($stickyDate) && $stickyDate ne "" ) # TODO
{
$result={ 'tag' => (defined($stickyTag)?$stickyTag:undef) };
# TODO: Convert -D value into the form 2011.04.10.04.46.57,
# similar to an entry line's sticky date, without the D prefix.
# It sometimes (always?) arrives as something more like
# '10 Apr 2011 04:46:57 -0000'...
# $result={ 'date' => (defined($stickyDate)?$stickyDate:undef) };
}
elsif( defined($state->{entries}{$filename}) &&
defined($state->{entries}{$filename}{tag_or_date}) &&
$state->{entries}{$filename}{tag_or_date} ne "" )
{
my($tagOrDate)=$state->{entries}{$filename}{tag_or_date};
if($tagOrDate=~/^T([^ ]+)\s*$/)
{
next if ( $file->{filehash} eq "deleted" and not defined ( $state->{entries}{$file->{name}} ) );
next unless ( $file->{name} =~ /^$arg\// or $file->{name} eq $arg );
push @{$state->{args}}, $file->{name};
$result = { 'tag' => $1 };
}
shift @{$state->{args}} if ( scalar(@{$state->{args}}) > 1 );
} else {
$log->info("Only one arg specified, populating file list automatically");
$state->{args} = [];
foreach my $file ( @gethead )
elsif($tagOrDate=~/^D([0-9.]+)\s*$/)
{
next if ( $file->{filehash} eq "deleted" and not defined ( $state->{entries}{$file->{name}} ) );
next unless ( $file->{name} =~ s/^$state->{prependdir}// );
push @{$state->{args}}, $file->{name};
$result= { 'date' => $1 };
}
else
{
die "Unknown tag_or_date format\n";
}
}
else
{
$result=getDirStickyInfo($filename);
}
return $result;
}
# Convert a stickyInfo (ref to a hash) as returned by resolveStickyInfo into
# a form appropriate for the sticky tag field of an Entries
# line (field index 5, 0-based).
sub getStickyTagOrDate
{
my($stickyInfo)=@_;
my $result;
if(defined($stickyInfo) && defined($stickyInfo->{tag}))
{
$result="T$stickyInfo->{tag}";
}
# TODO: When/if we actually pick versions by {date} properly,
# also handle it here:
# "D$stickyInfo->{date}" (example: "D2011.04.13.20.37.07").
else
{
$result="";
}
return $result;
}
# This method cleans up the $state variable after a command that uses arguments has run
sub statecleanup
{
$state->{files} = [];
$state->{dirArgs} = {};
$state->{args} = [];
$state->{arguments} = [];
$state->{entries} = {};
$state->{dirMap} = {};
}
# Return working directory CVS revision "1.X" out
@ -2309,6 +2910,9 @@ sub filenamesplit
return ( $filepart, $dirpart );
}
# Cleanup various junk in filename (try to canonicalize it), and
# add prependdir to accomodate running CVS client from a
# subdirectory (so the output is relative to top directory of the project).
sub filecleanup
{
my $filename = shift;
@ -2320,11 +2924,36 @@ sub filecleanup
return undef;
}
if($filename eq ".")
{
$filename="";
}
$filename =~ s/^\.\///g;
$filename =~ s%/+%/%g;
$filename = $state->{prependdir} . $filename;
$filename =~ s%/$%%;
return $filename;
}
# Remove prependdir from the path, so that is is relative to the directory
# the CVS client was started from, rather than the top of the project.
# Essentially the inverse of filecleanup().
sub remove_prependdir
{
my($path) = @_;
if(defined($state->{prependdir}) && $state->{prependdir} ne "")
{
my($pre)=$state->{prependdir};
$pre=~s%/$%%;
if(!($path=~s%^\Q$pre\E/?%%))
{
$log->fatal("internal error missing prependdir");
die("internal error missing prependdir");
}
}
return $path;
}
sub validateGitDir
{
if( !defined($state->{CVSROOT}) )
@ -2738,6 +3367,45 @@ sub descramble
return $ret;
}
# Test if the (deep) values of two references to a hash are the same.
sub refHashEqual
{
my($v1,$v2) = @_;
my $out;
if(!defined($v1))
{
if(!defined($v2))
{
$out=1;
}
}
elsif( !defined($v2) ||
scalar(keys(%{$v1})) != scalar(keys(%{$v2})) )
{
# $out=undef;
}
else
{
$out=1;
my $key;
foreach $key (keys(%{$v1}))
{
if( !exists($v2->{$key}) ||
defined($v1->{$key}) ne defined($v2->{$key}) ||
( defined($v1->{$key}) &&
$v1->{$key} ne $v2->{$key} ) )
{
$out=undef;
last;
}
}
}
return $out;
}
package GITCVS::log;
@ -2958,6 +3626,9 @@ sub new
die "Git repo '$self->{git_path}' doesn't exist" unless ( -d $self->{git_path} );
# Stores full sha1's for various branch/tag names, abbreviations, etc:
$self->{commitRefCache} = {};
$self->{dbdriver} = $cfg->{gitcvs}{$state->{method}}{dbdriver} ||
$cfg->{gitcvs}{dbdriver} || "SQLite";
$self->{dbname} = $cfg->{gitcvs}{$state->{method}}{dbname} ||
@ -3140,6 +3811,8 @@ sub update
my $lastcommit = $self->_get_prop("last_commit");
if (defined $lastcommit && $lastcommit eq $commitsha1) { # up-to-date
# invalidate the gethead cache
$self->clearCommitRefCaches();
return 1;
}
@ -3157,45 +3830,10 @@ sub update
push @git_log_params, $self->{module};
}
# git-rev-list is the backend / plumbing version of git-log
open(GITLOG, '-|', 'git', 'rev-list', @git_log_params) or die "Cannot call git-rev-list: $!";
my @commits;
my %commit = ();
while ( <GITLOG> )
{
chomp;
if (m/^commit\s+(.*)$/) {
# on ^commit lines put the just seen commit in the stack
# and prime things for the next one
if (keys %commit) {
my %copy = %commit;
unshift @commits, \%copy;
%commit = ();
}
my @parents = split(m/\s+/, $1);
$commit{hash} = shift @parents;
$commit{parents} = \@parents;
} elsif (m/^(\w+?):\s+(.*)$/ && !exists($commit{message})) {
# on rfc822-like lines seen before we see any message,
# lowercase the entry and put it in the hash as key-value
$commit{lc($1)} = $2;
} else {
# message lines - skip initial empty line
# and trim whitespace
if (!exists($commit{message}) && m/^\s*$/) {
# define it to mark the end of headers
$commit{message} = '';
next;
}
s/^\s+//; s/\s+$//; # trim ws
$commit{message} .= $_ . "\n";
}
}
close GITLOG;
unshift @commits, \%commit if ( keys %commit );
open(my $gitLogPipe, '-|', 'git', 'rev-list', @git_log_params)
or die "Cannot call git-rev-list: $!";
my @commits=readCommits($gitLogPipe);
close $gitLogPipe;
# Now all the commits are in the @commits bucket
# ordered by time DESC. for each commit that needs processing,
@ -3294,7 +3932,7 @@ sub update
}
# convert the date to CVS-happy format
$commit->{date} = "$2 $1 $4 $3 $5" if ( $commit->{date} =~ /^\w+\s+(\w+)\s+(\d+)\s+(\d+:\d+:\d+)\s+(\d+)\s+([+-]\d+)$/ );
my $cvsDate = convertToCvsDate($commit->{date});
if ( defined ( $lastpicked ) )
{
@ -3303,7 +3941,7 @@ sub update
while ( <FILELIST> )
{
chomp;
unless ( /^:\d{6}\s+\d{3}(\d)\d{2}\s+[a-zA-Z0-9]{40}\s+([a-zA-Z0-9]{40})\s+(\w)$/o )
unless ( /^:\d{6}\s+([0-7]{6})\s+[a-f0-9]{40}\s+([a-f0-9]{40})\s+(\w)$/o )
{
die("Couldn't process git-diff-tree line : $_");
}
@ -3313,11 +3951,7 @@ sub update
# $log->debug("File mode=$mode, hash=$hash, change=$change, name=$name");
my $git_perms = "";
$git_perms .= "r" if ( $mode & 4 );
$git_perms .= "w" if ( $mode & 2 );
$git_perms .= "x" if ( $mode & 1 );
$git_perms = "rw" if ( $git_perms eq "" );
my $dbMode = convertToDbMode($mode);
if ( $change eq "D" )
{
@ -3327,11 +3961,11 @@ sub update
revision => $head->{$name}{revision} + 1,
filehash => "deleted",
commithash => $commit->{hash},
modified => $commit->{date},
modified => $cvsDate,
author => $commit->{author},
mode => $git_perms,
mode => $dbMode,
};
$self->insert_rev($name, $head->{$name}{revision}, $hash, $commit->{hash}, $commit->{date}, $commit->{author}, $git_perms);
$self->insert_rev($name, $head->{$name}{revision}, $hash, $commit->{hash}, $cvsDate, $commit->{author}, $dbMode);
}
elsif ( $change eq "M" || $change eq "T" )
{
@ -3341,11 +3975,11 @@ sub update
revision => $head->{$name}{revision} + 1,
filehash => $hash,
commithash => $commit->{hash},
modified => $commit->{date},
modified => $cvsDate,
author => $commit->{author},
mode => $git_perms,
mode => $dbMode,
};
$self->insert_rev($name, $head->{$name}{revision}, $hash, $commit->{hash}, $commit->{date}, $commit->{author}, $git_perms);
$self->insert_rev($name, $head->{$name}{revision}, $hash, $commit->{hash}, $cvsDate, $commit->{author}, $dbMode);
}
elsif ( $change eq "A" )
{
@ -3355,11 +3989,11 @@ sub update
revision => $head->{$name}{revision} ? $head->{$name}{revision}+1 : 1,
filehash => $hash,
commithash => $commit->{hash},
modified => $commit->{date},
modified => $cvsDate,
author => $commit->{author},
mode => $git_perms,
mode => $dbMode,
};
$self->insert_rev($name, $head->{$name}{revision}, $hash, $commit->{hash}, $commit->{date}, $commit->{author}, $git_perms);
$self->insert_rev($name, $head->{$name}{revision}, $hash, $commit->{hash}, $cvsDate, $commit->{author}, $dbMode);
}
else
{
@ -3382,7 +4016,7 @@ sub update
die("Couldn't process git-ls-tree line : $_");
}
my ( $git_perms, $git_type, $git_hash, $git_filename ) = ( $1, $2, $3, $4 );
my ( $mode, $git_type, $git_hash, $git_filename ) = ( $1, $2, $3, $4 );
$seen_files->{$git_filename} = 1;
@ -3392,18 +4026,10 @@ sub update
$head->{$git_filename}{mode}
);
if ( $git_perms =~ /^\d\d\d(\d)\d\d/o )
{
$git_perms = "";
$git_perms .= "r" if ( $1 & 4 );
$git_perms .= "w" if ( $1 & 2 );
$git_perms .= "x" if ( $1 & 1 );
} else {
$git_perms = "rw";
}
my $dbMode = convertToDbMode($mode);
# unless the file exists with the same hash, we need to update it ...
unless ( defined($oldhash) and $oldhash eq $git_hash and defined($oldmode) and $oldmode eq $git_perms )
unless ( defined($oldhash) and $oldhash eq $git_hash and defined($oldmode) and $oldmode eq $dbMode )
{
my $newrevision = ( $oldrevision or 0 ) + 1;
@ -3412,13 +4038,13 @@ sub update
revision => $newrevision,
filehash => $git_hash,
commithash => $commit->{hash},
modified => $commit->{date},
modified => $cvsDate,
author => $commit->{author},
mode => $git_perms,
mode => $dbMode,
};
$self->insert_rev($git_filename, $newrevision, $git_hash, $commit->{hash}, $commit->{date}, $commit->{author}, $git_perms);
$self->insert_rev($git_filename, $newrevision, $git_hash, $commit->{hash}, $cvsDate, $commit->{author}, $dbMode);
}
}
close FILELIST;
@ -3431,10 +4057,10 @@ sub update
$head->{$file}{revision}++;
$head->{$file}{filehash} = "deleted";
$head->{$file}{commithash} = $commit->{hash};
$head->{$file}{modified} = $commit->{date};
$head->{$file}{modified} = $cvsDate;
$head->{$file}{author} = $commit->{author};
$self->insert_rev($file, $head->{$file}{revision}, $head->{$file}{filehash}, $commit->{hash}, $commit->{date}, $commit->{author}, $head->{$file}{mode});
$self->insert_rev($file, $head->{$file}{revision}, $head->{$file}{filehash}, $commit->{hash}, $cvsDate, $commit->{author}, $head->{$file}{mode});
}
}
# END : "Detect deleted files"
@ -3465,13 +4091,94 @@ sub update
);
}
# invalidate the gethead cache
$self->{gethead_cache} = undef;
$self->clearCommitRefCaches();
# Ending exclusive lock here
$self->{dbh}->commit() or die "Failed to commit changes to SQLite";
}
sub readCommits
{
my $pipeHandle = shift;
my @commits;
my %commit = ();
while ( <$pipeHandle> )
{
chomp;
if (m/^commit\s+(.*)$/) {
# on ^commit lines put the just seen commit in the stack
# and prime things for the next one
if (keys %commit) {
my %copy = %commit;
unshift @commits, \%copy;
%commit = ();
}
my @parents = split(m/\s+/, $1);
$commit{hash} = shift @parents;
$commit{parents} = \@parents;
} elsif (m/^(\w+?):\s+(.*)$/ && !exists($commit{message})) {
# on rfc822-like lines seen before we see any message,
# lowercase the entry and put it in the hash as key-value
$commit{lc($1)} = $2;
} else {
# message lines - skip initial empty line
# and trim whitespace
if (!exists($commit{message}) && m/^\s*$/) {
# define it to mark the end of headers
$commit{message} = '';
next;
}
s/^\s+//; s/\s+$//; # trim ws
$commit{message} .= $_ . "\n";
}
}
unshift @commits, \%commit if ( keys %commit );
return @commits;
}
sub convertToCvsDate
{
my $date = shift;
# Convert from: "git rev-list --pretty" formatted date
# Convert to: "the format specified by RFC822 as modified by RFC1123."
# Example: 26 May 1997 13:01:40 -0400
if( $date =~ /^\w+\s+(\w+)\s+(\d+)\s+(\d+:\d+:\d+)\s+(\d+)\s+([+-]\d+)$/ )
{
$date = "$2 $1 $4 $3 $5";
}
return $date;
}
sub convertToDbMode
{
my $mode = shift;
# NOTE: The CVS protocol uses a string similar "u=rw,g=rw,o=rw",
# but the database "mode" column historically (and currently)
# only stores the "rw" (for user) part of the string.
# FUTURE: It might make more sense to persist the raw
# octal mode (or perhaps the final full CVS form) instead of
# this half-converted form, but it isn't currently worth the
# backwards compatibility headaches.
$mode=~/^\d\d(\d)\d{3}$/;
my $userBits=$1;
my $dbMode = "";
$dbMode .= "r" if ( $userBits & 4 );
$dbMode .= "w" if ( $userBits & 2 );
$dbMode .= "x" if ( $userBits & 1 );
$dbMode = "rw" if ( $dbMode eq "" );
return $dbMode;
}
sub insert_rev
{
my $self = shift;
@ -3586,6 +4293,169 @@ sub gethead
return $tree;
}
=head2 getAnyHead
Returns a reference to an array of getmeta structures, one
per file in the specified tree hash.
=cut
sub getAnyHead
{
my ($self,$hash) = @_;
if(!defined($hash))
{
return $self->gethead();
}
my @files;
{
open(my $filePipe, '-|', 'git', 'ls-tree', '-z', '-r', $hash)
or die("Cannot call git-ls-tree : $!");
local $/ = "\0";
@files=<$filePipe>;
close $filePipe;
}
my $tree=[];
my($line);
foreach $line (@files)
{
$line=~s/\0$//;
unless ( $line=~/^(\d+)\s+(\w+)\s+([a-zA-Z0-9]+)\t(.*)$/o )
{
die("Couldn't process git-ls-tree line : $_");
}
my($mode, $git_type, $git_hash, $git_filename) = ($1, $2, $3, $4);
push @$tree, $self->getMetaFromCommithash($git_filename,$hash);
}
return $tree;
}
=head2 getRevisionDirMap
A "revision dir map" contains all the plain-file filenames associated
with a particular revision (treeish), organized by directory:
$type = $out->{$dir}{$fullName}
The type of each is "F" (for ordinary file) or "D" (for directory,
for which the map $out->{$fullName} will also exist).
=cut
sub getRevisionDirMap
{
my ($self,$ver)=@_;
if(!defined($self->{revisionDirMapCache}))
{
$self->{revisionDirMapCache}={};
}
# Get file list (previously cached results are dependent on HEAD,
# but are early in each case):
my $cacheKey;
my (@fileList);
if( !defined($ver) || $ver eq "" )
{
$cacheKey="";
if( defined($self->{revisionDirMapCache}{$cacheKey}) )
{
return $self->{revisionDirMapCache}{$cacheKey};
}
my @head = @{$self->gethead()};
foreach my $file ( @head )
{
next if ( $file->{filehash} eq "deleted" );
push @fileList,$file->{name};
}
}
else
{
my ($hash)=$self->lookupCommitRef($ver);
if( !defined($hash) )
{
return undef;
}
$cacheKey=$hash;
if( defined($self->{revisionDirMapCache}{$cacheKey}) )
{
return $self->{revisionDirMapCache}{$cacheKey};
}
open(my $filePipe, '-|', 'git', 'ls-tree', '-z', '-r', $hash)
or die("Cannot call git-ls-tree : $!");
local $/ = "\0";
while ( <$filePipe> )
{
chomp;
unless ( /^(\d+)\s+(\w+)\s+([a-zA-Z0-9]+)\t(.*)$/o )
{
die("Couldn't process git-ls-tree line : $_");
}
my($mode, $git_type, $git_hash, $git_filename) = ($1, $2, $3, $4);
push @fileList, $git_filename;
}
close $filePipe;
}
# Convert to normalized form:
my %revMap;
my $file;
foreach $file (@fileList)
{
my($dir) = ($file=~m%^(?:(.*)/)?([^/]*)$%);
$dir='' if(!defined($dir));
# parent directories:
# ... create empty dir maps for parent dirs:
my($td)=$dir;
while(!defined($revMap{$td}))
{
$revMap{$td}={};
my($tp)=($td=~m%^(?:(.*)/)?([^/]*)$%);
$tp='' if(!defined($tp));
$td=$tp;
}
# ... add children to parent maps (now that they exist):
$td=$dir;
while($td ne "")
{
my($tp)=($td=~m%^(?:(.*)/)?([^/]*)$%);
$tp='' if(!defined($tp));
if(defined($revMap{$tp}{$td}))
{
if($revMap{$tp}{$td} ne 'D')
{
die "Weird file/directory inconsistency in $cacheKey";
}
last; # loop exit
}
$revMap{$tp}{$td}='D';
$td=$tp;
}
# file
$revMap{$dir}{$file}='F';
}
# Save in cache:
$self->{revisionDirMapCache}{$cacheKey}=\%revMap;
return $self->{revisionDirMapCache}{$cacheKey};
}
=head2 getlog
See also gethistorydense().
@ -3646,6 +4516,19 @@ =head2 getmeta
This function takes a filename (with path) argument and returns a hashref of
metadata for that file.
There are several ways $revision can be specified:
- A reference to hash that contains a "tag" that is the
actual revision (one of the below). TODO: Also allow it to
specify a "date" in the hash.
- undef, to refer to the latest version on the main branch.
- Full CVS client revision number (mapped to integer in DB, without the
"1." prefix),
- Complex CVS-compatible "special" revision number for
non-linear history (see comment below)
- git commit sha1 hash
- branch or tag name
=cut
sub getmeta
@ -3656,23 +4539,144 @@ sub getmeta
my $tablename_rev = $self->tablename("revision");
my $tablename_head = $self->tablename("head");
my $db_query;
if ( defined($revision) and $revision =~ /^1\.(\d+)$/ )
if ( ref($revision) eq "HASH" )
{
my ($intRev) = $1;
$db_query = $self->{dbh}->prepare_cached("SELECT * FROM $tablename_rev WHERE name=? AND revision=?",{},1);
$db_query->execute($filename, $intRev);
}
elsif ( defined($revision) and $revision =~ /^[a-zA-Z0-9]{40}$/ )
{
$db_query = $self->{dbh}->prepare_cached("SELECT * FROM $tablename_rev WHERE name=? AND commithash=?",{},1);
$db_query->execute($filename, $revision);
} else {
$db_query = $self->{dbh}->prepare_cached("SELECT * FROM $tablename_head WHERE name=?",{},1);
$db_query->execute($filename);
$revision = $revision->{tag};
}
# Overview of CVS revision numbers:
#
# General CVS numbering scheme:
# - Basic mainline branch numbers: "1.1", "1.2", "1.3", etc.
# - Result of "cvs checkin -r" (possible, but not really
# recommended): "2.1", "2.2", etc
# - Branch tag: "1.2.0.n", where "1.2" is revision it was branched
# from, "0" is a magic placeholder that identifies it as a
# branch tag instead of a version tag, and n is 2 times the
# branch number off of "1.2", starting with "2".
# - Version on a branch: "1.2.n.x", where "1.2" is branch-from, "n"
# is branch number off of "1.2" (like n above), and "x" is
# the version number on the branch.
# - Branches can branch off of branches: "1.3.2.7.4.1" (even number
# of components).
# - Odd "n"s are used by "vendor branches" that result
# from "cvs import". Vendor branches have additional
# strangeness in the sense that the main rcs "head" of the main
# branch will (temporarily until first normal commit) point
# to the version on the vendor branch, rather than the actual
# main branch. (FUTURE: This may provide an opportunity
# to use "strange" revision numbers for fast-forward-merged
# branch tip when CVS client is asking for the main branch.)
#
# git-cvsserver CVS-compatible special numbering schemes:
# - Currently git-cvsserver only tries to be identical to CVS for
# simple "1.x" numbers on the "main" branch (as identified
# by the module name that was originally cvs checkout'ed).
# - The database only stores the "x" part, for historical reasons.
# But most of the rest of the cvsserver preserves
# and thinks using the full revision number.
# - To handle non-linear history, it uses a version of the form
# "2.1.1.2000.b.b.b."..., where the 2.1.1.2000 is to help uniquely
# identify this as a special revision number, and there are
# 20 b's that together encode the sha1 git commit from which
# this version of this file originated. Each b is
# the numerical value of the corresponding byte plus
# 100.
# - "plus 100" avoids "0"s, and also reduces the
# likelyhood of a collision in the case that someone someday
# writes an import tool that tries to preserve original
# CVS revision numbers, and the original CVS data had done
# lots of branches off of branches and other strangeness to
# end up with a real version number that just happens to look
# like this special revision number form. Also, if needed
# there are several ways to extend/identify alternative encodings
# within the "2.1.1.2000" part if necessary.
# - Unlike real CVS revisions, you can't really reconstruct what
# relation a revision of this form has to other revisions.
# - FUTURE: TODO: Rework database somehow to make up and remember
# fully-CVS-compatible branches and branch version numbers.
my $meta;
if ( defined($revision) )
{
if ( $revision =~ /^1\.(\d+)$/ )
{
my ($intRev) = $1;
my $db_query;
$db_query = $self->{dbh}->prepare_cached(
"SELECT * FROM $tablename_rev WHERE name=? AND revision=?",
{},1);
$db_query->execute($filename, $intRev);
$meta = $db_query->fetchrow_hashref;
}
elsif ( $revision =~ /^2\.1\.1\.2000(\.[1-3][0-9][0-9]){20}$/ )
{
my ($commitHash)=($revision=~/^2\.1\.1\.2000(.*)$/);
$commitHash=~s/\.([0-9]+)/sprintf("%02x",$1-100)/eg;
if($commitHash=~/^[0-9a-f]{40}$/)
{
return $self->getMetaFromCommithash($filename,$commitHash);
}
# error recovery: fall back on head version below
print "E Failed to find $filename version=$revision or commit=$commitHash\n";
$log->warning("failed get $revision with commithash=$commitHash");
undef $revision;
}
elsif ( $revision =~ /^[0-9a-f]{40}$/ )
{
# Try DB first. This is mostly only useful for req_annotate(),
# which only calls this for stuff that should already be in
# the DB. It is fairly likely to be a waste of time
# in most other cases [unless the file happened to be
# modified in $revision specifically], but
# it is probably in the noise compared to how long
# getMetaFromCommithash() will take.
my $db_query;
$db_query = $self->{dbh}->prepare_cached(
"SELECT * FROM $tablename_rev WHERE name=? AND commithash=?",
{},1);
$db_query->execute($filename, $revision);
$meta = $db_query->fetchrow_hashref;
if(! $meta)
{
my($revCommit)=$self->lookupCommitRef($revision);
if($revCommit=~/^[0-9a-f]{40}$/)
{
return $self->getMetaFromCommithash($filename,$revCommit);
}
# error recovery: nothing found:
print "E Failed to find $filename version=$revision\n";
$log->warning("failed get $revision");
return $meta;
}
}
else
{
my($revCommit)=$self->lookupCommitRef($revision);
if($revCommit=~/^[0-9a-f]{40}$/)
{
return $self->getMetaFromCommithash($filename,$revCommit);
}
# error recovery: fall back on head version below
print "E Failed to find $filename version=$revision\n";
$log->warning("failed get $revision");
undef $revision; # Allow fallback
}
}
if(!defined($revision))
{
my $db_query;
$db_query = $self->{dbh}->prepare_cached(
"SELECT * FROM $tablename_head WHERE name=?",{},1);
$db_query->execute($filename);
$meta = $db_query->fetchrow_hashref;
}
my $meta = $db_query->fetchrow_hashref;
if($meta)
{
$meta->{revision} = "1.$meta->{revision}";
@ -3680,6 +4684,204 @@ sub getmeta
return $meta;
}
sub getMetaFromCommithash
{
my $self = shift;
my $filename = shift;
my $revCommit = shift;
# NOTE: This function doesn't scale well (lots of forks), especially
# if you have many files that have not been modified for many commits
# (each git-rev-parse redoes a lot of work for each file
# that theoretically could be done in parallel by smarter
# graph traversal).
#
# TODO: Possible optimization strategies:
# - Solve the issue of assigning and remembering "real" CVS
# revision numbers for branches, and ensure the
# data structure can do this efficiently. Perhaps something
# similar to "git notes", and carefully structured to take
# advantage same-sha1-is-same-contents, to roll the same
# unmodified subdirectory data onto multiple commits?
# - Write and use a C tool that is like git-blame, but
# operates on multiple files with file granularity, instead
# of one file with line granularity. Cache
# most-recently-modified in $self->{commitRefCache}{$revCommit}.
# Try to be intelligent about how many files we do with
# one fork (perhaps one directory at a time, without recursion,
# and/or include directory as one line item, recurse from here
# instead of in C tool?).
# - Perhaps we could ask the DB for (filename,fileHash),
# and just guess that it is correct (that the file hadn't
# changed between $revCommit and the found commit, then
# changed back, confusing anything trying to interpret
# history). Probably need to add another index to revisions
# DB table for this.
# - NOTE: Trying to store all (commit,file) keys in DB [to
# find "lastModfiedCommit] (instead of
# just files that changed in each commit as we do now) is
# probably not practical from a disk space perspective.
# Does the file exist in $revCommit?
# TODO: Include file hash in dirmap cache.
my($dirMap)=$self->getRevisionDirMap($revCommit);
my($dir,$file)=($filename=~m%^(?:(.*)/)?([^/]*$)%);
if(!defined($dir))
{
$dir="";
}
if( !defined($dirMap->{$dir}) ||
!defined($dirMap->{$dir}{$filename}) )
{
my($fileHash)="deleted";
my($retVal)={};
$retVal->{name}=$filename;
$retVal->{filehash}=$fileHash;
# not needed and difficult to compute:
$retVal->{revision}="0"; # $revision;
$retVal->{commithash}=$revCommit;
#$retVal->{author}=$commit->{author};
#$retVal->{modified}=convertToCvsDate($commit->{date});
#$retVal->{mode}=convertToDbMode($mode);
return $retVal;
}
my($fileHash)=safe_pipe_capture("git","rev-parse","$revCommit:$filename");
chomp $fileHash;
if(!($fileHash=~/^[0-9a-f]{40}$/))
{
die "Invalid fileHash '$fileHash' looking up"
." '$revCommit:$filename'\n";
}
# information about most recent commit to modify $filename:
open(my $gitLogPipe, '-|', 'git', 'rev-list',
'--max-count=1', '--pretty', '--parents',
$revCommit, '--', $filename)
or die "Cannot call git-rev-list: $!";
my @commits=readCommits($gitLogPipe);
close $gitLogPipe;
if(scalar(@commits)!=1)
{
die "Can't find most recent commit changing $filename\n";
}
my($commit)=$commits[0];
if( !defined($commit) || !defined($commit->{hash}) )
{
return undef;
}
# does this (commit,file) have a real assigned CVS revision number?
my $tablename_rev = $self->tablename("revision");
my $db_query;
$db_query = $self->{dbh}->prepare_cached(
"SELECT * FROM $tablename_rev WHERE name=? AND commithash=?",
{},1);
$db_query->execute($filename, $commit->{hash});
my($meta)=$db_query->fetchrow_hashref;
if($meta)
{
$meta->{revision} = "1.$meta->{revision}";
return $meta;
}
# fall back on special revision number
my($revision)=$commit->{hash};
$revision=~s/(..)/'.' . (hex($1)+100)/eg;
$revision="2.1.1.2000$revision";
# meta data about $filename:
open(my $filePipe, '-|', 'git', 'ls-tree', '-z',
$commit->{hash}, '--', $filename)
or die("Cannot call git-ls-tree : $!");
local $/ = "\0";
my $line;
$line=<$filePipe>;
if(defined(<$filePipe>))
{
die "Expected only a single file for git-ls-tree $filename\n";
}
close $filePipe;
chomp $line;
unless ( $line=~m/^(\d+)\s+(\w+)\s+([a-zA-Z0-9]+)\t(.*)$/o )
{
die("Couldn't process git-ls-tree line : $line\n");
}
my ( $mode, $git_type, $git_hash, $git_filename ) = ( $1, $2, $3, $4 );
# save result:
my($retVal)={};
$retVal->{name}=$filename;
$retVal->{revision}=$revision;
$retVal->{filehash}=$fileHash;
$retVal->{commithash}=$revCommit;
$retVal->{author}=$commit->{author};
$retVal->{modified}=convertToCvsDate($commit->{date});
$retVal->{mode}=convertToDbMode($mode);
return $retVal;
}
=head2 lookupCommitRef
Convert tag/branch/abbreviation/etc into a commit sha1 hash. Caches
the result so looking it up again is fast.
=cut
sub lookupCommitRef
{
my $self = shift;
my $ref = shift;
my $commitHash = $self->{commitRefCache}{$ref};
if(defined($commitHash))
{
return $commitHash;
}
$commitHash=safe_pipe_capture("git","rev-parse","--verify","--quiet",
$self->unescapeRefName($ref));
$commitHash=~s/\s*$//;
if(!($commitHash=~/^[0-9a-f]{40}$/))
{
$commitHash=undef;
}
if( defined($commitHash) )
{
my $type=safe_pipe_capture("git","cat-file","-t",$commitHash);
if( ! ($type=~/^commit\s*$/ ) )
{
$commitHash=undef;
}
}
if(defined($commitHash))
{
$self->{commitRefCache}{$ref}=$commitHash;
}
return $commitHash;
}
=head2 clearCommitRefCaches
Clears cached commit cache (sha1's for various tags/abbeviations/etc),
and related caches.
=cut
sub clearCommitRefCaches
{
my $self = shift;
$self->{commitRefCache} = {};
$self->{revisionDirMapCache} = undef;
$self->{gethead_cache} = undef;
}
=head2 commitmessage
this function takes a commithash and returns the commit message for that commit
@ -3745,6 +4947,97 @@ sub gethistorydense
return $result;
}
=head2 escapeRefName
Apply an escape mechanism to compensate for characters that
git ref names can have that CVS tags can not.
=cut
sub escapeRefName
{
my($self,$refName)=@_;
# CVS officially only allows [-_A-Za-z0-9] in tag names (or in
# many contexts it can also be a CVS revision number).
#
# Git tags commonly use '/' and '.' as well, but also handle
# anything else just in case:
#
# = "_-s-" For '/'.
# = "_-p-" For '.'.
# = "_-u-" For underscore, in case someone wants a literal "_-" in
# a tag name.
# = "_-xx-" Where "xx" is the hexadecimal representation of the
# desired ASCII character byte. (for anything else)
if(! $refName=~/^[1-9][0-9]*(\.[1-9][0-9]*)*$/)
{
$refName=~s/_-/_-u--/g;
$refName=~s/\./_-p-/g;
$refName=~s%/%_-s-%g;
$refName=~s/[^-_a-zA-Z0-9]/sprintf("_-%02x-",$1)/eg;
}
}
=head2 unescapeRefName
Undo an escape mechanism to compensate for characters that
git ref names can have that CVS tags can not.
=cut
sub unescapeRefName
{
my($self,$refName)=@_;
# see escapeRefName() for description of escape mechanism.
$refName=~s/_-([spu]|[0-9a-f][0-9a-f])-/unescapeRefNameChar($1)/eg;
# allowed tag names
# TODO: Perhaps use git check-ref-format, with an in-process cache of
# validated names?
if( !( $refName=~m%^[^-][-a-zA-Z0-9_/.]*$% ) ||
( $refName=~m%[/.]$% ) ||
( $refName=~/\.lock$/ ) ||
( $refName=~m%\.\.|/\.|[[\\:?*~]|\@\{% ) ) # matching }
{
# Error:
$log->warn("illegal refName: $refName");
$refName=undef;
}
return $refName;
}
sub unescapeRefNameChar
{
my($char)=@_;
if($char eq "s")
{
$char="/";
}
elsif($char eq "p")
{
$char=".";
}
elsif($char eq "u")
{
$char="_";
}
elsif($char=~/^[0-9a-f][0-9a-f]$/)
{
$char=chr(hex($char));
}
else
{
# Error case: Maybe it has come straight from user, and
# wasn't supposed to be escaped? Restore it the way we got it:
$char="_-$char-";
}
return $char;
}
=head2 in_array()
from Array::PAT - mimics the in_array() function

551
t/t9402-git-cvsserver-refs.sh Executable file
View File

@ -0,0 +1,551 @@
#!/bin/sh
test_description='git-cvsserver and git refspecs
tests ability for git-cvsserver to switch between and compare
tags, branches and other git refspecs'
. ./test-lib.sh
#########
check_start_tree() {
rm -f "$WORKDIR/list.expected"
echo "start $1" >>"${WORKDIR}/check.log"
}
check_file() {
sandbox="$1"
file="$2"
ver="$3"
GIT_DIR=$SERVERDIR git show "${ver}:${file}" \
>"$WORKDIR/check.got" 2>"$WORKDIR/check.stderr"
test_cmp "$WORKDIR/check.got" "$sandbox/$file"
stat=$?
echo "check_file $sandbox $file $ver : $stat" >>"$WORKDIR/check.log"
echo "$file" >>"$WORKDIR/list.expected"
return $stat
}
check_end_tree() {
sandbox="$1" &&
find "$sandbox" -name CVS -prune -o -type f -print >"$WORKDIR/list.actual" &&
sort <"$WORKDIR/list.expected" >expected &&
sort <"$WORKDIR/list.actual" | sed -e "s%cvswork/%%" >actual &&
test_cmp expected actual &&
rm expected actual
}
check_end_full_tree() {
sandbox="$1" &&
sort <"$WORKDIR/list.expected" >expected &&
find "$sandbox" -name CVS -prune -o -type f -print |
sed -e "s%$sandbox/%%" | sort >act1 &&
test_cmp expected act1 &&
git ls-tree --name-only -r "$2" | sort >act2 &&
test_cmp expected act2 &&
rm expected act1 act2
}
#########
check_diff() {
diffFile="$1"
vOld="$2"
vNew="$3"
rm -rf diffSandbox
git clone -q -n . diffSandbox &&
(
cd diffSandbox &&
git checkout "$vOld" &&
git apply -p0 --index <"../$diffFile" &&
git diff --exit-code "$vNew"
) >check_diff_apply.out 2>&1
}
#########
cvs >/dev/null 2>&1
if test $? -ne 1
then
skip_all='skipping git-cvsserver tests, cvs not found'
test_done
fi
if ! test_have_prereq PERL
then
skip_all='skipping git-cvsserver tests, perl not available'
test_done
fi
"$PERL_PATH" -e 'use DBI; use DBD::SQLite' >/dev/null 2>&1 || {
skip_all='skipping git-cvsserver tests, Perl SQLite interface unavailable'
test_done
}
unset GIT_DIR GIT_CONFIG
WORKDIR=$(pwd)
SERVERDIR=$(pwd)/gitcvs.git
git_config="$SERVERDIR/config"
CVSROOT=":fork:$SERVERDIR"
CVSWORK="$(pwd)/cvswork"
CVS_SERVER=git-cvsserver
export CVSROOT CVS_SERVER
rm -rf "$CVSWORK" "$SERVERDIR"
test_expect_success 'setup v1, b1' '
echo "Simple text file" >textfile.c &&
echo "t2" >t2 &&
mkdir adir &&
echo "adir/afile line1" >adir/afile &&
echo "adir/afile line2" >>adir/afile &&
echo "adir/afile line3" >>adir/afile &&
echo "adir/afile line4" >>adir/afile &&
echo "adir/a2file" >>adir/a2file &&
mkdir adir/bdir &&
echo "adir/bdir/bfile line 1" >adir/bdir/bfile &&
echo "adir/bdir/bfile line 2" >>adir/bdir/bfile &&
echo "adir/bdir/b2file" >adir/bdir/b2file &&
git add textfile.c t2 adir &&
git commit -q -m "First Commit (v1)" &&
git tag v1 &&
git branch b1 &&
git clone -q --bare "$WORKDIR/.git" "$SERVERDIR" >/dev/null 2>&1 &&
GIT_DIR="$SERVERDIR" git config --bool gitcvs.enabled true &&
GIT_DIR="$SERVERDIR" git config gitcvs.logfile "$SERVERDIR/gitcvs.log"
'
rm -rf cvswork
test_expect_success 'cvs co v1' '
cvs -f -Q co -r v1 -d cvswork master >cvs.log 2>&1 &&
check_start_tree cvswork &&
check_file cvswork textfile.c v1 &&
check_file cvswork t2 v1 &&
check_file cvswork adir/afile v1 &&
check_file cvswork adir/a2file v1 &&
check_file cvswork adir/bdir/bfile v1 &&
check_file cvswork adir/bdir/b2file v1 &&
check_end_tree cvswork
'
rm -rf cvswork
test_expect_success 'cvs co b1' '
cvs -f co -r b1 -d cvswork master >cvs.log 2>&1 &&
check_start_tree cvswork &&
check_file cvswork textfile.c v1 &&
check_file cvswork t2 v1 &&
check_file cvswork adir/afile v1 &&
check_file cvswork adir/a2file v1 &&
check_file cvswork adir/bdir/bfile v1 &&
check_file cvswork adir/bdir/b2file v1 &&
check_end_tree cvswork
'
test_expect_success 'cvs co b1 [cvswork3]' '
cvs -f co -r b1 -d cvswork3 master >cvs.log 2>&1 &&
check_start_tree cvswork3 &&
check_file cvswork3 textfile.c v1 &&
check_file cvswork3 t2 v1 &&
check_file cvswork3 adir/afile v1 &&
check_file cvswork3 adir/a2file v1 &&
check_file cvswork3 adir/bdir/bfile v1 &&
check_file cvswork3 adir/bdir/b2file v1 &&
check_end_full_tree cvswork3 v1
'
test_expect_success 'edit cvswork3 and save diff' '
(
cd cvswork3 &&
sed -e "s/line1/line1 - data/" adir/afile >adir/afileNEW &&
mv -f adir/afileNEW adir/afile &&
echo "afile5" >adir/afile5 &&
rm t2 &&
cvs -f add adir/afile5 &&
cvs -f rm t2 &&
! cvs -f diff -N -u >"$WORKDIR/cvswork3edit.diff"
)
'
test_expect_success 'setup v1.2 on b1' '
git checkout b1 &&
echo "new v1.2" >t3 &&
rm t2 &&
sed -e "s/line3/line3 - more data/" adir/afile >adir/afileNEW &&
mv -f adir/afileNEW adir/afile &&
rm adir/a2file &&
echo "a3file" >>adir/a3file &&
echo "bfile line 3" >>adir/bdir/bfile &&
rm adir/bdir/b2file &&
echo "b3file" >adir/bdir/b3file &&
mkdir cdir &&
echo "cdir/cfile" >cdir/cfile &&
git add -A cdir adir t3 t2 &&
git commit -q -m 'v1.2' &&
git tag v1.2 &&
git push --tags gitcvs.git b1:b1
'
test_expect_success 'cvs -f up (on b1 adir)' '
( cd cvswork/adir && cvs -f up -d ) >cvs.log 2>&1 &&
check_start_tree cvswork &&
check_file cvswork textfile.c v1 &&
check_file cvswork t2 v1 &&
check_file cvswork adir/afile v1.2 &&
check_file cvswork adir/a3file v1.2 &&
check_file cvswork adir/bdir/bfile v1.2 &&
check_file cvswork adir/bdir/b3file v1.2 &&
check_end_tree cvswork
'
test_expect_success 'cvs up (on b1 /)' '
( cd cvswork && cvs -f up -d ) >cvs.log 2>&1 &&
check_start_tree cvswork &&
check_file cvswork textfile.c v1.2 &&
check_file cvswork t3 v1.2 &&
check_file cvswork adir/afile v1.2 &&
check_file cvswork adir/a3file v1.2 &&
check_file cvswork adir/bdir/bfile v1.2 &&
check_file cvswork adir/bdir/b3file v1.2 &&
check_file cvswork cdir/cfile v1.2 &&
check_end_tree cvswork
'
# Make sure "CVS/Tag" files didn't get messed up:
test_expect_success 'cvs up (on b1 /) (again; check CVS/Tag files)' '
( cd cvswork && cvs -f up -d ) >cvs.log 2>&1 &&
check_start_tree cvswork &&
check_file cvswork textfile.c v1.2 &&
check_file cvswork t3 v1.2 &&
check_file cvswork adir/afile v1.2 &&
check_file cvswork adir/a3file v1.2 &&
check_file cvswork adir/bdir/bfile v1.2 &&
check_file cvswork adir/bdir/b3file v1.2 &&
check_file cvswork cdir/cfile v1.2 &&
check_end_tree cvswork
'
# update to another version:
test_expect_success 'cvs up -r v1' '
( cd cvswork && cvs -f up -r v1 ) >cvs.log 2>&1 &&
check_start_tree cvswork &&
check_file cvswork textfile.c v1 &&
check_file cvswork t2 v1 &&
check_file cvswork adir/afile v1 &&
check_file cvswork adir/a2file v1 &&
check_file cvswork adir/bdir/bfile v1 &&
check_file cvswork adir/bdir/b2file v1 &&
check_end_tree cvswork
'
test_expect_success 'cvs up' '
( cd cvswork && cvs -f up ) >cvs.log 2>&1 &&
check_start_tree cvswork &&
check_file cvswork textfile.c v1 &&
check_file cvswork t2 v1 &&
check_file cvswork adir/afile v1 &&
check_file cvswork adir/a2file v1 &&
check_file cvswork adir/bdir/bfile v1 &&
check_file cvswork adir/bdir/b2file v1 &&
check_end_tree cvswork
'
test_expect_success 'cvs up (again; check CVS/Tag files)' '
( cd cvswork && cvs -f up -d ) >cvs.log 2>&1 &&
check_start_tree cvswork &&
check_file cvswork textfile.c v1 &&
check_file cvswork t2 v1 &&
check_file cvswork adir/afile v1 &&
check_file cvswork adir/a2file v1 &&
check_file cvswork adir/bdir/bfile v1 &&
check_file cvswork adir/bdir/b2file v1 &&
check_end_tree cvswork
'
test_expect_success 'setup simple b2' '
git branch b2 v1 &&
git push --tags gitcvs.git b2:b2
'
test_expect_success 'cvs co b2 [into cvswork2]' '
cvs -f co -r b2 -d cvswork2 master >cvs.log 2>&1 &&
check_start_tree cvswork &&
check_file cvswork textfile.c v1 &&
check_file cvswork t2 v1 &&
check_file cvswork adir/afile v1 &&
check_file cvswork adir/a2file v1 &&
check_file cvswork adir/bdir/bfile v1 &&
check_file cvswork adir/bdir/b2file v1 &&
check_end_tree cvswork
'
test_expect_success 'root dir edit [cvswork2]' '
(
cd cvswork2 && echo "Line 2" >>textfile.c &&
! cvs -f diff -u >"$WORKDIR/cvsEdit1.diff" &&
cvs -f commit -m "edit textfile.c" textfile.c
) >cvsEdit1.log 2>&1
'
test_expect_success 'root dir rm file [cvswork2]' '
(
cd cvswork2 &&
cvs -f rm -f t2 &&
cvs -f diff -u >../cvsEdit2-empty.diff &&
! cvs -f diff -N -u >"$WORKDIR/cvsEdit2-N.diff" &&
cvs -f commit -m "rm t2"
) >cvsEdit2.log 2>&1
'
test_expect_success 'subdir edit/add/rm files [cvswork2]' '
(
cd cvswork2 &&
sed -e "s/line 1/line 1 (v2)/" adir/bdir/bfile >adir/bdir/bfileNEW &&
mv -f adir/bdir/bfileNEW adir/bdir/bfile &&
rm adir/bdir/b2file &&
cd adir &&
cvs -f rm bdir/b2file &&
echo "4th file" >bdir/b4file &&
cvs -f add bdir/b4file &&
! cvs -f diff -N -u >"$WORKDIR/cvsEdit3.diff" &&
git fetch gitcvs.git b2:b2 &&
(
cd .. &&
! cvs -f diff -u -N -r v1.2 >"$WORKDIR/cvsEdit3-v1.2.diff" &&
! cvs -f diff -u -N -r v1.2 -r v1 >"$WORKDIR/cvsEdit3-v1.2-v1.diff"
) &&
cvs -f commit -m "various add/rm/edit"
) >cvs.log 2>&1
'
test_expect_success 'validate result of edits [cvswork2]' '
git fetch gitcvs.git b2:b2 &&
git tag v2 b2 &&
git push --tags gitcvs.git b2:b2 &&
check_start_tree cvswork2 &&
check_file cvswork2 textfile.c v2 &&
check_file cvswork2 adir/afile v2 &&
check_file cvswork2 adir/a2file v2 &&
check_file cvswork2 adir/bdir/bfile v2 &&
check_file cvswork2 adir/bdir/b4file v2 &&
check_end_full_tree cvswork2 v2
'
test_expect_success 'validate basic diffs saved during above cvswork2 edits' '
test $(grep Index: cvsEdit1.diff | wc -l) = 1 &&
test ! -s cvsEdit2-empty.diff &&
test $(grep Index: cvsEdit2-N.diff | wc -l) = 1 &&
test $(grep Index: cvsEdit3.diff | wc -l) = 3 &&
rm -rf diffSandbox &&
git clone -q -n . diffSandbox &&
(
cd diffSandbox &&
git checkout v1 &&
git apply -p0 --index <"$WORKDIR/cvsEdit1.diff" &&
git apply -p0 --index <"$WORKDIR/cvsEdit2-N.diff" &&
git apply -p0 --directory=adir --index <"$WORKDIR/cvsEdit3.diff" &&
git diff --exit-code v2
) >"check_diff_apply.out" 2>&1
'
test_expect_success 'validate v1.2 diff saved during last cvswork2 edit' '
test $(grep Index: cvsEdit3-v1.2.diff | wc -l) = 9 &&
check_diff cvsEdit3-v1.2.diff v1.2 v2
'
test_expect_success 'validate v1.2 v1 diff saved during last cvswork2 edit' '
test $(grep Index: cvsEdit3-v1.2-v1.diff | wc -l) = 9 &&
check_diff cvsEdit3-v1.2-v1.diff v1.2 v1
'
test_expect_success 'cvs up [cvswork2]' '
( cd cvswork2 && cvs -f up ) >cvs.log 2>&1 &&
check_start_tree cvswork2 &&
check_file cvswork2 textfile.c v2 &&
check_file cvswork2 adir/afile v2 &&
check_file cvswork2 adir/a2file v2 &&
check_file cvswork2 adir/bdir/bfile v2 &&
check_file cvswork2 adir/bdir/b4file v2 &&
check_end_full_tree cvswork2 v2
'
test_expect_success 'cvs up -r b2 [back to cvswork]' '
( cd cvswork && cvs -f up -r b2 ) >cvs.log 2>&1 &&
check_start_tree cvswork &&
check_file cvswork textfile.c v2 &&
check_file cvswork adir/afile v2 &&
check_file cvswork adir/a2file v2 &&
check_file cvswork adir/bdir/bfile v2 &&
check_file cvswork adir/bdir/b4file v2 &&
check_end_full_tree cvswork v2
'
test_expect_success 'cvs up -r b1' '
( cd cvswork && cvs -f up -r b1 ) >cvs.log 2>&1 &&
check_start_tree cvswork &&
check_file cvswork textfile.c v1.2 &&
check_file cvswork t3 v1.2 &&
check_file cvswork adir/afile v1.2 &&
check_file cvswork adir/a3file v1.2 &&
check_file cvswork adir/bdir/bfile v1.2 &&
check_file cvswork adir/bdir/b3file v1.2 &&
check_file cvswork cdir/cfile v1.2 &&
check_end_full_tree cvswork v1.2
'
test_expect_success 'cvs up -A' '
( cd cvswork && cvs -f up -A ) >cvs.log 2>&1 &&
check_start_tree cvswork &&
check_file cvswork textfile.c v1 &&
check_file cvswork t2 v1 &&
check_file cvswork adir/afile v1 &&
check_file cvswork adir/a2file v1 &&
check_file cvswork adir/bdir/bfile v1 &&
check_file cvswork adir/bdir/b2file v1 &&
check_end_full_tree cvswork v1
'
test_expect_success 'cvs up (check CVS/Tag files)' '
( cd cvswork && cvs -f up ) >cvs.log 2>&1 &&
check_start_tree cvswork &&
check_file cvswork textfile.c v1 &&
check_file cvswork t2 v1 &&
check_file cvswork adir/afile v1 &&
check_file cvswork adir/a2file v1 &&
check_file cvswork adir/bdir/bfile v1 &&
check_file cvswork adir/bdir/b2file v1 &&
check_end_full_tree cvswork v1
'
# This is not really legal CVS, but it seems to work anyway:
test_expect_success 'cvs up -r heads/b1' '
( cd cvswork && cvs -f up -r heads/b1 ) >cvs.log 2>&1 &&
check_start_tree cvswork &&
check_file cvswork textfile.c v1.2 &&
check_file cvswork t3 v1.2 &&
check_file cvswork adir/afile v1.2 &&
check_file cvswork adir/a3file v1.2 &&
check_file cvswork adir/bdir/bfile v1.2 &&
check_file cvswork adir/bdir/b3file v1.2 &&
check_file cvswork cdir/cfile v1.2 &&
check_end_full_tree cvswork v1.2
'
# But this should work even if CVS client checks -r more carefully:
test_expect_success 'cvs up -r heads_-s-b2 (cvsserver escape mechanism)' '
( cd cvswork && cvs -f up -r heads_-s-b2 ) >cvs.log 2>&1 &&
check_start_tree cvswork &&
check_file cvswork textfile.c v2 &&
check_file cvswork adir/afile v2 &&
check_file cvswork adir/a2file v2 &&
check_file cvswork adir/bdir/bfile v2 &&
check_file cvswork adir/bdir/b4file v2 &&
check_end_full_tree cvswork v2
'
v1hash=$(git rev-parse v1)
test_expect_success 'cvs up -r $(git rev-parse v1)' '
test -n "$v1hash" &&
( cd cvswork && cvs -f up -r "$v1hash" ) >cvs.log 2>&1 &&
check_start_tree cvswork &&
check_file cvswork textfile.c v1 &&
check_file cvswork t2 v1 &&
check_file cvswork adir/afile v1 &&
check_file cvswork adir/a2file v1 &&
check_file cvswork adir/bdir/bfile v1 &&
check_file cvswork adir/bdir/b2file v1 &&
check_end_full_tree cvswork v1
'
test_expect_success 'cvs diff -r v1 -u' '
( cd cvswork && cvs -f diff -r v1 -u ) >cvsDiff.out 2>cvs.log &&
test ! -s cvsDiff.out &&
test ! -s cvs.log
'
test_expect_success 'cvs diff -N -r v2 -u' '
( cd cvswork && ! cvs -f diff -N -r v2 -u ) >cvsDiff.out 2>cvs.log &&
test ! -s cvs.log &&
test -s cvsDiff.out &&
check_diff cvsDiff.out v2 v1 >check_diff.out 2>&1
'
test_expect_success 'cvs diff -N -r v2 -r v1.2' '
( cd cvswork && ! cvs -f diff -N -r v2 -r v1.2 -u ) >cvsDiff.out 2>cvs.log &&
test ! -s cvs.log &&
test -s cvsDiff.out &&
check_diff cvsDiff.out v2 v1.2 >check_diff.out 2>&1
'
test_expect_success 'apply early [cvswork3] diff to b3' '
git clone -q . gitwork3 &&
(
cd gitwork3 &&
git checkout -b b3 v1 &&
git apply -p0 --index <"$WORKDIR/cvswork3edit.diff" &&
git commit -m "cvswork3 edits applied"
) &&
git fetch gitwork3 b3:b3 &&
git tag v3 b3
'
test_expect_success 'check [cvswork3] diff' '
( cd cvswork3 && ! cvs -f diff -N -u ) >"$WORKDIR/cvsDiff.out" 2>cvs.log &&
test ! -s cvs.log &&
test -s cvsDiff.out &&
test $(grep Index: cvsDiff.out | wc -l) = 3 &&
test_cmp cvsDiff.out cvswork3edit.diff &&
check_diff cvsDiff.out v1 v3 >check_diff.out 2>&1
'
test_expect_success 'merge early [cvswork3] b3 with b1' '
( cd gitwork3 && git merge "message" HEAD b1 ) &&
git fetch gitwork3 b3:b3 &&
git tag v3merged b3 &&
git push --tags gitcvs.git b3:b3
'
# This test would fail if cvsserver properly created a ".#afile"* file
# for the merge.
# TODO: Validate that the .# file was saved properly, and then
# delete/ignore it when checking the tree.
test_expect_success 'cvs up dirty [cvswork3]' '
(
cd cvswork3 &&
cvs -f up &&
! cvs -f diff -N -u >"$WORKDIR/cvsDiff.out"
) >cvs.log 2>&1 &&
test -s cvsDiff.out &&
test $(grep Index: cvsDiff.out | wc -l) = 2 &&
check_start_tree cvswork3 &&
check_file cvswork3 textfile.c v3merged &&
check_file cvswork3 t3 v3merged &&
check_file cvswork3 adir/afile v3merged &&
check_file cvswork3 adir/a3file v3merged &&
check_file cvswork3 adir/afile5 v3merged &&
check_file cvswork3 adir/bdir/bfile v3merged &&
check_file cvswork3 adir/bdir/b3file v3merged &&
check_file cvswork3 cdir/cfile v3merged &&
check_end_full_tree cvswork3 v3merged
'
# TODO: test cvs status
test_expect_success 'cvs commit [cvswork3]' '
(
cd cvswork3 &&
cvs -f commit -m "dirty sandbox after auto-merge"
) >cvs.log 2>&1 &&
check_start_tree cvswork3 &&
check_file cvswork3 textfile.c v3merged &&
check_file cvswork3 t3 v3merged &&
check_file cvswork3 adir/afile v3merged &&
check_file cvswork3 adir/a3file v3merged &&
check_file cvswork3 adir/afile5 v3merged &&
check_file cvswork3 adir/bdir/bfile v3merged &&
check_file cvswork3 adir/bdir/b3file v3merged &&
check_file cvswork3 cdir/cfile v3merged &&
check_end_full_tree cvswork3 v3merged &&
git fetch gitcvs.git b3:b4 &&
git tag v4.1 b4 &&
git diff --exit-code v4.1 v3merged >check_diff_apply.out 2>&1
'
test_done