#! /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() { 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 () { 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 () { 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 () { 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: $!"); =~ m!:([^:]+?)\n?$!; $cvs_root=$1; close(ROOT); # Lookup the path in CVS/Repository. open (REPOSITORY, "< CVS/Repository") || error ("couldn't open CVS/Repository: $!"); =~ m|(\S+)|; $path = $1; close(REPOSITORY); } open(CVSADM, "cvsu --ignore --types AMROG |") || error ("Cannot read output of cvsu: $!"); while () { 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 ();