#! /usr/bin/perl -w
# This script simulates some cvs commands even for readonly or diconnected
# CVS repositories.
require 5.004;
use Time::Local;
use Getopt::Long;
use strict;
use vars qw($force_mode $entries_tmp $cvs_root $path);
# Print message and exit (like "die", but without raising an exception).
# Newline is added at the end.
sub error ($)
{
print STDERR "cvsdo: ERROR: " . shift(@_) . "\n";
cleanup ();
exit 1;
}
# Print a warning message.
# Newline is added at the end.
sub warning ($)
{
print STDERR "cvsdo: WARNING: " . shift(@_) . "\n";
}
# Print message and force UNIX-style newline. Useful for diffs.
sub unix_print ($)
{
my $msg = shift(@_);
chomp $msg;
print $msg . "\012";
}
# Process a single file (add and remove commands).
# Arguments: file, command.
sub process_file ($$)
{
my $short_file;
my $entries;
my $file_exists = 0;
my $file_listed = 0;
my $file = shift (@_);
my $command = shift (@_);
my $cmd_add = ($command eq 'add');
my $cmd_remove = ($command eq 'remove');
if (-e $file) {
unless (-f $file) {
error ("File $file is not a plain file");
}
$file_exists = 1;
}
if ( $cmd_add && ! $file_exists && ! $force_mode ) {
error ("File $file doesn't exist");
} elsif ( $cmd_remove && $file_exists && ! $force_mode ) {
error ("Won't remove existing file $file");
}
$entries = $file;
$entries =~ s{^(([^ ]+/)?)([^/ ]+)$}{${1}CVS/Entries};
$short_file = $3;
unless ($entries) {
error("Wrong filename $file");
}
$entries_tmp = $entries . ".tmp";
open(NEW_ENTRIES, "> $entries_tmp") ||
error("Cannot open $entries_tmp for writing");
open(ENTRIES, "< $entries") ||
error("Cannot open $entries for reading");
while(<ENTRIES>) {
if ( m{^(/([^/]+)/)(-?)([^/]+)/[^/]*/[^/]*/(.*$)} && $2 eq $short_file ) {
$file_listed = 1;
last if $cmd_add;
unless ( $4 eq '0' ) {
if ( $3 eq '-' ) {
error("File $file is already removed");
} else {
print NEW_ENTRIES "$1-$3$4/dummy timestamp//$5\n";
}
}
} else {
print NEW_ENTRIES $_;
}
}
if ( $cmd_add && $file_listed ) {
error("File $file is already listed in $entries");
}
if ( $cmd_remove && ! $file_listed ) {
error("File $file is not listed in $entries");
}
if ( $cmd_add ) {
print NEW_ENTRIES "/$short_file/0/dummy timestamp//\n";
}
close (ENTRIES);
close (NEW_ENTRIES);
rename $entries_tmp, $entries ||
error ("Cannot rename $entries_tmp to $entries");
$cmd_remove && $file_exists &&
( unlink $file || error ("Cannot delete file $file") );
}
# Handle added files (diff).
sub handle_added ($)
{
my $file = shift(@_);
my $nullfile;
if ($^O =~ m!win|os/2!i) {
$nullfile = 'nul';
} else {
$nullfile = '/dev/null';
}
my $diff_opts = $ENV{'DIFFCMD'}||'-u';
open(DIFFOUT, "diff $diff_opts -L $nullfile -L $file $nullfile $file |") ||
error ("Cannot read output of diff: $!");
unix_print ("Index: $file");
while (<DIFFOUT>) {
unix_print ($_);
}
}
# Handle removed files (diff).
sub handle_removed ($)
{
my $file = shift(@_);
# FIXME: scan for backup copies, as in handle_modified()
# Any ideas about how to make `patch' erase that file?
unix_print ("File $file should be removed!\n");
}
# Handle modified files (diff)
sub handle_modified ($)
{
my $file = shift(@_);
# split into directory and file name
$file =~ m{^((.*/)?)([^/]+)};
my $short_file = $3;
my $dir = $1;
my %months = (
"Jan" => 0,
"Feb" => 1,
"Mar" => 2,
"Apr" => 3,
"May" => 4,
"Jun" => 5,
"Jul" => 6,
"Aug" => 7,
"Sep" => 8,
"Oct" => 9,
"Nov" => 10,
"Dec" => 11
);
# Lookup the original timestamp in CVS/Entries.
open (ENTRIES, "< ${dir}CVS/Entries")
|| error ("couldn't open ${dir}CVS/Entries: $!");
my $cvs_rev;
my $date_str;
while (<ENTRIES>) {
if ( m{^/$short_file/([^/]*)/([^/]+)/} ) {
$cvs_rev = $1;
$date_str = $2;
last;
}
}
unless (defined $date_str) {
error ("$file is not listed in ${dir}CVS/Entries");
}
close (ENTRIES);
$date_str = 'Unk Jan 01 01:01:01 1970' if $date_str =~ /^(?:Result of merge|dummy timestamp)/;
unless ($date_str =~ m{^(...) (...) (..) (..):(..):(..) (....)$}) {
error ("Invalid timestamp for $file: $date_str");
}
my $basetime = timegm($6, $5, $4, $3, $months{$2}, $7 - 1900);
# Scan the directory for similar files.
my $backup_file;
opendir (DIR, $dir eq "" ? "." : $dir) ||
error ("Cannot open directory $dir: $!");
foreach (readdir (DIR)) {
m{$short_file} || next;;
my $candidate = $dir . $_;
stat ($candidate) || next;
if ($basetime == (stat _) [9]) {
$backup_file = $candidate;
last;
}
}
closedir (DIR);
unless (defined $backup_file) {
my $cvs_rev2 = $cvs_rev; $cvs_rev2 =~ tr/\./_/;
if ($file eq $short_file) {
$backup_file = ".#$file.$cvs_rev2";
} else {
$backup_file = "$dir.#$short_file.$cvs_rev2";
}
if (-f $backup_file) {
warning ("Using $backup_file for $file");
} else {
warning ("Backup file for $file not found; retrieving from cvs as $backup_file");
`cvs up -p -r $cvs_rev $file 2>&1 > $backup_file`;
}
}
my $diff_opts = $ENV{'DIFFCMD'}||'-u';
if ($short_file eq "ChangeLog") {
$diff_opts = "-u1";
}
open(DIFFOUT,
"diff $diff_opts -L $file -L $file $backup_file $file |") ||
error ("Cannot read output of diff: $!");
unix_print ("Index: $file");
unix_print ("===================================================================");
my $cvspath = "$cvs_root/$path";
$cvspath = $path if $cvs_root eq $path;
unix_print ("RCS file: $cvspath/$file,v");
unix_print ("retrieving revision $cvs_rev");
unix_print ("diff $diff_opts -r$cvs_rev $file");
while (<DIFFOUT>) {
unix_print ($_);
}
}
# Handle `diff' command.
sub handle_diff ()
{
my %file_list;
if ($#ARGV >= 0)
{
usage () if $ARGV[0] eq '--help';
$file_list{$_}=1 foreach (@ARGV);
}
if (-d 'CVS')
{
# Lookup the cvsroot in CVS/Root.
open (ROOT, "< CVS/Root")
|| error ("couldn't open CVS/Root: $!");
<ROOT> =~ m!:([^:]+?)\n?$!; $cvs_root=$1;
close(ROOT);
# Lookup the path in CVS/Repository.
open (REPOSITORY, "< CVS/Repository")
|| error ("couldn't open CVS/Repository: $!");
<REPOSITORY> =~ m|(\S+)|; $path = $1;
close(REPOSITORY);
}
open(CVSADM, "cvsu --ignore --types AMROG |") ||
error ("Cannot read output of cvsu: $!");
while (<CVSADM>) {
chomp;
if ($_ !~ m{^([AMROG]) (.*)$}) {
error ("Unrecognized output from cvsu");
}
my $type = $1;
my $file = $2;
if ($#ARGV >= 0) {
next unless $file_list{$file};
delete $file_list{$file};
}
if ($type eq "A") {
handle_added ($file);
}
elsif ($type eq "R") {
handle_removed ($file);
}
else {
handle_modified ($file);
}
}
}
# Print usage information and exit.
sub usage ()
{
print "Usage: cvsdo COMMAND FLAGS FILES\n" .
"Simulate cvs commands without accessing the CVS server\n" .
"Commands supported:\n" .
" add Add a new file\n" .
" -f | --force Don't check whether the file exists\n" .
" remove Remove a file\n" .
" -f | --force Delete existing files\n" .
" diff Create a diff\n";
exit 1;
}
# Print version information and exit.
sub version ()
{
print "cvsdo - CVS Disconnected Operation, version -VERSION-\n";
exit 0;
}
# Remove temporary files.
sub cleanup ()
{
(defined $entries_tmp) && (-e $entries_tmp) &&
( unlink $entries_tmp ||
error ("Cannot delete file $entries_tmp") );
}
# Parse command line.
sub Main ()
{
$force_mode = 0; # Forced operation
my $want_help = 0; # Print help and exit
my $want_ver = 0; # Print version and exit
my %options = (
"force" => \$force_mode,
"help" => \$want_help,
"version" => \$want_ver
);
GetOptions(%options);
usage() if $want_help;
version() if $want_ver;
my $command = shift (@ARGV);
if ( $want_ver || !$command || ($command !~ /(add|remove|diff)/) ) {
usage();
}
if ($command =~ /diff/) {
handle_diff ();
} else {
if ( $#ARGV < 0 ) {
error ("No files specified");
}
foreach (@ARGV) {
process_file ($_, $command);
}
}
cleanup();
}
Main ();