#!/usr/bin/perl -w # $Date: 2009/11/08 14:29:16 $ # simple script to make real local commits in a format (mainly) compatible to # the old cvsq, so that a cvsq upload will correctly upload these things. # # see cvsq(1) and cvsq-files(1) for a documentation # # If you received this file without a man page, see # http://www.linta.de/~aehlig/cvsq/ use strict; use Cwd; use Getopt::Long qw(:config require_order); use File::Temp; my $cvs = "cvs"; my $CVSdir = "CVS"; my $command = "ci"; # The default for this script... my $cleancommand = $ENV{'CVSQ_MAKE_CLEAN'}; my $home=$ENV{'HOME'}; defined $home && -d $home or die "Invalid home directory.\n"; my $pwd = getcwd(); my $date = `date`; chomp($date); my $host = `hostname`; chomp($host); ### The special call with file environentvariable CVSQ_ACTION being hardlink if (defined $ENV{'CVSQ_ACTION'} && $ENV{'CVSQ_ACTION'} eq "hardlink") { @ARGV == 4 && $ARGV[0] eq "hardlink" or die "The hardlink call is $0 hardlink .\n"; say_hardlink($ARGV[1],$ARGV[2],$ARGV[3] . "-late"); exit 0; } ## Ensure, all the needed directories are present. foreach my $dir (qw! .cvsq .cvsq/todo .cvsq/data .cvsq/tmp .cvsq/config .cvsq/dirnames .cvsq/todo-pre .cvsq/todo-post .cvsq/description !) { -d "$home/$dir" and next; mkdir "$home/$dir", 0700 or die "Unable to create directory `$home/$dir' ($!)\n"; } ### The main entrypoint my $force = 0; GetOptions( 'force' => sub {$force =1}, 'svn' => sub {$cvs = "svn"; $CVSdir=".svn"}, 'noclean|nc' => sub {$cleancommand = undef}, 'clean=s' => \$cleancommand ); # After the internal options are removed something that we know should remain... if (@ARGV != 0) { for ($ARGV[0]) { (/^ci$/ || /^commit$/) && do { @ARGV == 1 or die "Paramters given for a commit; only full ci supported.\n"; last;}; /^dir$/ && do { $command = "dir"; shift; @ARGV == 1 or die "adding directories only strictly one at at time!.\n"; last; }; /^add$/ && do { $command = "add"; shift; last; }; /^ambient$/ && do { $command = "ambient"; shift; last; }; /^ambientpost$/ && do { $command = "ambientpost"; shift; last; }; (/^up$/ || /^upload$/) && do { $command = "up"; @ARGV == 1 or die "The upload command does not support any parameters.\n"; last;}; (/^pre$/ || /^do$/ || /^immediate$/) && do { $command = "pre"; shift; last;}; /^queue$/ && do { print_queue(); exit 0;}; /^desc$/ && do { print_desc(); exit 0;}; /^diff$/ && do { print_diff($ARGV[1]); exit 0;}; die "cvsq command $_ not supported. Did you mean \"cvsq do $_ ...\"?\n"; } } ## Parse the file with the command prefixes my $prefix =""; if (-f "$home/.cvsq/config/prefix" && -f "./CVS/Root") { open(ROOT, "< ./CVS/Root") or die "Couldn't read CVS/Root ($!)\n"; my $cvsroot = do {local $/; }; close(ROOT) or die "Closing CVS/Root failed ($!)\n"; open(PREFIX, "< $home/.cvsq/config/prefix") or die "failed opening $home/.cvsq/config/prefix ($!).\n"; while() { /^(\S+)\s(.*)$/ or next; if (index($cvsroot,$1) == 0) { $prefix = $2; } } close(PREFIX) or die "failed closing $home/.cvsq/config/prefix ($!).\n"; } ## Sanity check whether we're really in a working directory ($command eq "up") or ($command eq "ambient") or ($command eq "ambientpost") or (-d "./$CVSdir") or $force or die "No directory $CVSdir found. Is this really a $cvs working directory? (Use -force carry out the action anyway)\n"; ###################################### ### The command PRE is immediately ### ###################################### if ($command eq "pre") { my ($tmp,$tmpname) = File::Temp::tempfile('pre-XXXXXX', DIR => "$home/.cvsq/tmp", UNLINK => 1); print $tmp "$prefix $cvs"; foreach(@ARGV) { print $tmp " \Q$_\E"; } print $tmp "\nexit \$?\n"; system "sh", $tmpname; exit $?; } ############################################################## ## For an adddir we have to make a fake CVS directory ## ## this must be done BEFORE allocating a place in the queue ## ############################################################## if ($command eq "dir") { my $dir = $ARGV[0]; (-d $dir) or die "$dir is not a directory\n"; (-e "$dir/$CVSdir") and die "$dir/$CVSdir exists. Maybe this directory is already (queued) to be added?\n"; mkdir "$dir/$CVSdir"; } ################################### ### QUEUED COMMANDS FOLLOW HERE ### ################################### ## Find name in the .cvsq queue opendir my $queue, "$home/.cvsq/todo" or die "Unable to open todo directory `$home/.cvsq/todo' ($!)\n"; my $slot = 1; while (defined( my $spooled = readdir $queue )) { $spooled =~ /^\d+$/ or next; $spooled >= $slot and $slot = $spooled + 1; } closedir $queue; ############################################################################### ###### For uploads slot number 0 severes as lock. This has the advantage ###### ###### to also block old cvsq upload scripts. ###### ############################################################################### # make sure we have the slot. And block everything from beeing commited # note that there is a race condition! # First create a temporary file, with contents being "exit -1" my ($tmp,$tmpname) = File::Temp::tempfile('fail-XXXXXX', DIR => "$home/.cvsq/tmp"); print $tmp "exit -1\n"; close $tmp or die "Couldn't close tmp file ($!).\n"; # we assume that link is atomic and in deed will fail, if the file already exists if ($command eq "up") { symlink $tmpname, "$home/.cvsq/todo/0" or die "Couldn't $home/.cvsq due to a seeming race condition ($!).\n"; } else { symlink $tmpname, "$home/.cvsq/todo/$slot" or die "Couldn't allocate a new slot due to a seeming race condition ($!).\n"; } ################################################################################ ### Now the UPLOAD command which is IMMEDIATE ### ################################################################################ if ($command eq "up") { print "\n\nStarting pre upload...\n\n"; opendir my $queue, "$home/.cvsq/todo-pre" or die "Unable to open pre todo directory `$home/.cvsq/todo-pre' ($!)\n"; my @TASKS = readdir $queue; close $queue; @TASKS = grep(/^[0-9]+$/,@TASKS); @TASKS = sort {$a <=> $b} (@TASKS); foreach (@TASKS) { $_ =~ /^[0-9]+$/ or next; $_ == "0" and next; print "$_..."; system "/bin/sh", "$home/.cvsq/todo-pre/$_"; printf " [return value %s]\n", $? >> 8; $? == 0 or die "Call failed. Please fix problems manually! Exiting. Leaving lock to prevent further unwanted commits. Remove $home/.cvsq/todo/0 and $tmpname after fixing problems with the commit scheduled as $home/.cvsq/todo-pre/$_\n"; unlink "$home/.cvsq/todo-pre/$_" or die "Couldn't unlink $home/.cvsq/todo-pre/$_ Exiting. Leaving lock to prevent double-commits. Remove $home/.cvsq/todo/0 and $tmpname after removing the above file.\n"; } print "\n\nStarting main upload...\n\n"; opendir $queue, "$home/.cvsq/todo" or die "Unable to open todo directory `$home/.cvsq/todo' ($!)\n"; @TASKS = readdir $queue; close $queue; @TASKS = grep(/^[0-9]+$/,@TASKS); @TASKS = sort {$a <=> $b} (@TASKS); foreach (@TASKS) { $_ =~ /^[0-9]+$/ or next; $_ == "0" and next; print "$_...\n"; system "/bin/sh", "$home/.cvsq/todo/$_"; printf "\n[return value %s]\n\n\n", $? >> 8; $? == 0 or die "Call failed. Please fix problems manually! Exiting. Leaving lock to prevent further unwanted commits. Remove $home/.cvsq/todo/0 and $tmpname after fixing problems with the commit scheduled as $home/.cvsq/todo/$_\n"; unlink "$home/.cvsq/todo/$_" or die "Couldn't unlink $home/.cvsq/todo/$_ Exiting. Leaving lock to prevent double-commits. Remove $home/.cvsq/todo/0 and $tmpname after removing the above file.\n"; } print "\n\nStarting post upload...\n\n"; opendir $queue, "$home/.cvsq/todo-post" or die "Unable to open pre todo directory `$home/.cvsq/todo-post' ($!)\n"; @TASKS = readdir $queue; close $queue; @TASKS = grep(/^[0-9]+$/,@TASKS); @TASKS = sort {$a <=> $b} (@TASKS); foreach (@TASKS) { $_ =~ /^[0-9]+$/ or next; $_ == "0" and next; print "$_..."; system "/bin/sh", "$home/.cvsq/todo-post/$_"; printf " [return value %s]\n", $? >> 8; $? == 0 or die "Call failed. Please fix problems manually! Exiting. Leaving lock to prevent further unwanted commits. Remove $home/.cvsq/todo/0 and $tmpname after fixing problems with the commit scheduled as $home/.cvsq/todo-post/$_\n"; unlink "$home/.cvsq/todo-post/$_" or die "Couldn't unlink $home/.cvsq/todo-pre/$_ Exiting. Leaving lock to prevent double-commits. Remove $home/.cvsq/todo/0 and $tmpname after removing the above file.\n"; } print "\n\nUpload finished.\n"; unlink "$home/.cvsq/todo/0" or die "Couldn't remove lock ($!).\n"; unlink $tmpname or print "Warning: couldn't remove temporary file $tmpname ($!).\n"; exit 0; } ########################################################################### ## For a commit we actually have to copy files and ask for a log message ## ########################################################################### my $message; if ($command eq "ci") { # make a safe copy of the data system "cp", "-r", "-p", $pwd, "$home/.cvsq/data/$slot" and die "Copying failed ($!).\n"; # honor the CVSQ_MAKE_CLEAN option defined $cleancommand and system("cd \Q$home/.cvsq/data/$slot\E; $cleancommand") and print "Warning, the cleaning failed.\n"; # Replace all subdirs by symbolic links to the original CVS subdir my @cvsDirs = `cd \Q$home/.cvsq/data/$slot\E; find . -name $CVSdir`; foreach(@cvsDirs) { chomp; s/^\.\///; system "rm", "-rf", "$home/.cvsq/data/$slot/$_" and die "Couldn't remove $home/.cvsq/data/$slot/$_ ($!)\n"; system "ln", "-s", "$pwd/$_", "$home/.cvsq/data/$slot/$_" and die "Couldn't link $pwd/$_ to $home/.cvsq/data/$slot/$_ ($!)\n"; } add_hardlinks(); open(DESC,"> $home/.cvsq/description/$slot") or die "Couldn't open $home/.cvsq/description/$slot ($!)\n"; print DESC "ci: $pwd"; close(DESC) or die "Closing $home/.cvsq/description/$_ failed ($!)\n"; # Ask for a log message. First find out, what the corresponding editor is my $editor = $ENV{'CVSQEDITOR'} || $ENV{'CVSEDITOR'} || $ENV{'VISUAL'} || $ENV{'EDITOR'} || '/bin/ed'; open (TMPFILE, "> $home/.cvsq/tmp/$slot.txt") or die "Couldn't generate temporary file ($!).\n"; print TMPFILE <<"EOI"; cvsq: ------------------------------------------------------------ cvsq: Please enter log message for local commit of cvsq: directory $pwd cvsq: cvsq: Lines starting with cvsq: are removed automatically cvsq: ------------------------------------------------------------ EOI close(TMPFILE) or die "Closing tmp-file failed ($!)\n"; system $editor, "$home/.cvsq/tmp/$slot.txt" and print "Warning: editor session failed ($?)\n"; open(TMPFILE, "< $home/.cvsq/tmp/$slot.txt") or die "Couldn't read log message ($!)\n"; $message = do {local $/; }; close(TMPFILE) or die "Closing tmp-file failed ($!)\n"; $message =~ s/(^|\n)cvsq:.*//g; $message .= "\n\n[commit scheduled on $host via $0 at $date]\n"; $message =~ s/\'/\'\\\'\'/g; } ########################################################### ## Most other commands just register their description... # ########################################################### ## add ## if ($command eq "add") { open(DESC,"> $home/.cvsq/description/$slot") or die "Couldn't open $home/.cvsq/description/$slot ($!)\n"; print DESC "add ($pwd): @ARGV"; close(DESC) or die "Closing $home/.cvsq/description/$_ failed ($!)\n"; } ## ambient ## if ($command eq "ambient") { open(DESC,"> $home/.cvsq/description/$slot") or die "Couldn't open $home/.cvsq/description/$slot ($!)\n"; print DESC "ambient ($pwd): @ARGV"; close(DESC) or die "Closing $home/.cvsq/description/$_ failed ($!)\n"; } ## ambientpost ## if ($command eq "ambientpost") { open(DESC,"> $home/.cvsq/description/$slot") or die "Couldn't open $home/.cvsq/description/$slot ($!)\n"; print DESC "ambientpost ($pwd): @ARGV"; close(DESC) or die "Closing $home/.cvsq/description/$_ failed ($!)\n"; } ## dir ## if ($command eq "dir") { open(DESC,"> $home/.cvsq/description/$slot") or die "Couldn't open $home/.cvsq/description/$slot ($!)\n"; print DESC "dir ($pwd): $ARGV[0]"; close(DESC) or die "Closing $home/.cvsq/description/$_ failed ($!)\n"; } ###################################### ## generate pre commmit Skript file ## ###################################### # Write the at-upload script. The file conventions of cvq say, it has to be # a script that can be executed via /bin/sh # For the case that we die during it's generation, we first put it at some (well defined) # temporary place. if ($command eq "ci") { symlink $tmpname, "$home/.cvsq/todo-pre/$slot" or die "Couldn't allocate a slot for pre commit ($!).\n"; open(ATUPLOAD, "> $home/.cvsq/tmp/$slot-pre-script") or die "Could not generate script file ($!)\n"; print ATUPLOAD <<"EOI"; CVSQ_ACTION="hardlink" $0 hardlink \Q$pwd\E \Q$home/.cvsq/data/$slot\E $slot exit \$? EOI close(ATUPLOAD) or die "Could not close script file ($!)\n"; # By a rename put the actual script over the place where we have our current "exit -1"; rename "$home/.cvsq/tmp/$slot-pre-script", "$home/.cvsq/todo-pre/$slot" or die "Rename failed ($!).\n"; } ####################################### ## generate post commmit Skript file ## ####################################### # Write the at-upload script. The file conventions of cvq say, it has to be # a script that can be executed via /bin/sh # For the case that we die during it's generation, we first put it at some (well defined) # temporary place. ## ci ## if ($command eq "ci") { symlink $tmpname, "$home/.cvsq/todo-post/$slot" or die "Couldn't allocate a slot for post commit ($!).\n"; open(ATUPLOAD, "> $home/.cvsq/tmp/$slot-post-script") or die "Could not generate script file ($!)\n"; print ATUPLOAD <<"EOI"; rm -rf \Q$home/.cvsq/data/$slot\E && rm -rf \Q$home/.cvsq/dirnames/$slot\E && rm -rf \Q$home/.cvsq/description/$slot\E exit \$? EOI close(ATUPLOAD) or die "Could not close script file ($!)\n"; # By a rename put the actual script over the place where we have our current "exit -1"; rename "$home/.cvsq/tmp/$slot-post-script", "$home/.cvsq/todo-post/$slot" or die "Rename failed ($!).\n"; } ### ambientpost actually have to do some work in the post commit phase if ($command eq "ambientpost") { symlink $tmpname, "$home/.cvsq/todo-post/$slot" or die "Couldn't allocate a slot for post commit ($!).\n"; open(ATUPLOAD, "> $home/.cvsq/tmp/$slot-post-script") or die "Could not generate script file ($!)\n"; print ATUPLOAD "cd \Q$pwd\E\n"; foreach(@ARGV) { print ATUPLOAD " \Q$_\E"; } print ATUPLOAD " && rm -rf \Q$home/.cvsq/description/$slot\E\n"; print ATUPLOAD "exit \$?"; close(ATUPLOAD) or die "Could not close script file ($!)\n"; # By a rename put the actual script over the place where we have our current "exit -1"; rename "$home/.cvsq/tmp/$slot-post-script", "$home/.cvsq/todo-post/$slot" or die "Rename failed ($!).\n"; } ### clean up the description files (most commands, except ci) if (($command eq "ambient") or ($command eq "add") or ($command eq "dir")) { symlink $tmpname, "$home/.cvsq/todo-post/$slot" or die "Couldn't allocate a slot for post commit ($!).\n"; open(ATUPLOAD, "> $home/.cvsq/tmp/$slot-post-script") or die "Could not generate script file ($!)\n"; print ATUPLOAD <<"EOI"; rm -rf \Q$home/.cvsq/description/$slot\E exit \$? EOI close(ATUPLOAD) or die "Could not close script file ($!)\n"; # By a rename put the actual script over the place where we have our current "exit -1"; rename "$home/.cvsq/tmp/$slot-post-script", "$home/.cvsq/todo-post/$slot" or die "Rename failed ($!).\n"; } ########################## ## generate Skript file ## ########################## # Write the at-upload script. The file conventions of cvq say, it has to be # a script that can be executed via /bin/sh # For the case that we die during it's generation, we first put it at some (well defined) # temporary place. open(ATUPLOAD, "> $home/.cvsq/tmp/$slot-script") or die "Could not generate script file ($!)\n"; ### ci ### if ($command eq "ci") { print ATUPLOAD <<"EOI"; cd \Q$home/.cvsq/data/$slot\E test -f \Q$home/.cvsq/todo/$slot-hardlink\E && /bin/sh \Q$home/.cvsq/todo/$slot-hardlink\E && rm -f \Q$home/.cvsq/todo/$slot-hardlink\E $prefix $cvs ci -m '$message' && /bin/sh \Q$home/.cvsq/todo/$slot-late-hardlink\E && rm -f \Q$home/.cvsq/todo/$slot-late-hardlink\E exit \$? EOI unlink "$home/.cvsq/tmp/$slot.txt" or die "Unable to remove message file ($!).\n"; } ## add ## if ($command eq "add") { print ATUPLOAD "cd \Q$pwd\E\n"; print ATUPLOAD "$prefix $cvs add"; foreach(@ARGV) { print ATUPLOAD " \Q$_\E"; } print ATUPLOAD "\nexit \$?\n"; } ## dir ## if ($command eq "dir") { print ATUPLOAD "cd \Q$pwd\E\n"; print ATUPLOAD "rm -rf \Q$pwd/$ARGV[0]/$CVSdir\E\n"; print ATUPLOAD "$prefix $cvs add \Q$ARGV[0]\E\n"; print ATUPLOAD "\nexit \$?\n"; } ## ambient ## if ($command eq "ambient") { print ATUPLOAD "cd \Q$pwd\E\n"; foreach(@ARGV) { print ATUPLOAD " \Q$_\E"; } print ATUPLOAD "\nexit \$?\n"; } ## ambientpost ## if ($command eq "ambientpost") { print ATUPLOAD "exit 0\n"; } close(ATUPLOAD) or die "Could not close script file ($!)\n"; ######################### ## activate the script ## ######################### # By a rename put the actual script over the place where we have our current "exit -1"; rename "$home/.cvsq/tmp/$slot-script", "$home/.cvsq/todo/$slot" or die "Rename failed ($!).\n"; # unlink $tmpname or print "Warning: couldn't clean up the lock skript $tmpname ($!).\n"; ################################################################################## ## Write some happy welcome messages to the user, reminding what has happend... ## ################################################################################## ## ci ## if ($command eq "ci") { print <<"EOI"; Locally committed the current subdirectory. It is scheduled as slot number $slot. Use "$0 upload" to enter the changes into the real cvs repository. A safe copy can be found at $home/.cvsq/data/$slot. EOI } ## add ## if ($command eq "add") { print <<"EOI"; Scheduled the addition of the following files as slot number $slot. @ARGV Use "$0 ci" to also schedule the commit. EOI } ## dir ## if ($command eq "dir") { print <<"EOI"; Scheduled the addition directory $ARGV[0] as slot number $slot. Use "$0 ci" to also schedule the commit! EOI } ## ambient ## if ($command eq "ambient") { print <<"EOI"; Scheduled ambient command '@ARGV' as slot number $slot. This command will be executed in directory $pwd at the time of upload. EOI } ## ambientpost ## if ($command eq "ambientpost") { print <<"EOI"; Scheduled ambient post command '@ARGV' as slot number $slot. This command will be executed in directory $pwd at the time of the post upload. EOI } ############################################################################################### ############### Subroutines ################################################################### ############################################################################################### sub say_hardlink { my $source = shift; my $target = shift; my $hardslot = shift; $source eq $target and die "cannot hardlink $source into it self.\n"; my @files = `cd $source; find . -print`; open(ATHARDLINK,">> $home/.cvsq/todo/$hardslot-hardlink") or die "Couldn't open hardlink file $home/.cvsq/todo/$slot-hardlink ($!)\n"; foreach(@files) { chomp; /\/CVS\// and next; /\/\.svn\// and next; s/^\.\///; -f "$source/$_" && (! -l "$source/$_") or next; -f "$target/$_" && (! -l "$target/$_") or next; system "cmp", "-s", "$source/$_", "$target/$_"; ($? >> 8) <= 1 or die "Call to cmp failded ($?).\n"; if (($? >> 8) == 0) { print ATHARDLINK "rm -f \Q$source/$_\E\n"; print ATHARDLINK "cp -p \Q$target/$_\E \Q$source/$_\E\n"; } } close(ATHARDLINK) or die "Couldn't close hardlink file $hardslot"; } sub add_hardlinks { opendir my $dirnames, "$home/.cvsq/dirnames" or die "Unable to open directory `$home/.cvsq/dirnames' ($!)\n"; my @DIRS = readdir $dirnames; close $dirnames; @DIRS = grep(/^[0-9]+$/,@DIRS); @DIRS = sort {$b <=> $a} (@DIRS); my $name; foreach(@DIRS) { open(NAME, "< $home/.cvsq/dirnames/$_") or die "Couldn't read $home/.cvsq/dirnames/$_ ($!)\n"; $name = do {local $/; }; close(NAME) or die "Closing $home/.cvsq/dirnames/$_ failed ($!)\n"; if ($name eq $pwd) { say_hardlink("$home/.cvsq/data/$slot","$home/.cvsq/data/$_",$slot); last; } } open(NAME,"> $home/.cvsq/dirnames/$slot") or die "Couldn't open $home/.cvsq/dirnames/$slot ($!)\n"; print NAME $pwd; close(NAME) or die "Closing $home/.cvsq/dirnames/$_ failed ($!)\n"; } sub print_queue { opendir my $dirnames, "$home/.cvsq/dirnames" or die "Unable to open directory `$home/.cvsq/dirnames' ($!)\n"; my @DIRS = readdir $dirnames; close $dirnames; @DIRS = grep(/^[0-9]+$/,@DIRS); @DIRS = sort {$a <=> $b} (@DIRS); my $name; print " [nr] Scheduled dir:\n"; foreach(@DIRS) { printf "%4s %s\n", $_ , `cat $home/.cvsq/dirnames/$_`; } print "[end]\n\n"; } sub print_desc { opendir my $desc, "$home/.cvsq/description" or die "Unable to open directory `$home/.cvsq/description' ($!)\n"; my @DESCS = readdir $desc; close $desc; @DESCS = grep(/^[0-9]+$/,@DESCS); @DESCS = sort {$a <=> $b} (@DESCS); my $name; print " [nr] description:\n"; foreach(@DESCS) { printf "%4s %s\n", $_ , `cat $home/.cvsq/description/$_`; } print "[end]\n\n"; } sub print_diff { my $filename = shift; defined($filename) or $filename="."; opendir my $dirnames, "$home/.cvsq/dirnames" or die "Unable to open directory `$home/.cvsq/dirnames' ($!)\n"; my @DIRS = readdir $dirnames; close $dirnames; @DIRS = grep(/^[0-9]+$/,@DIRS); @DIRS = sort {$b <=> $a} (@DIRS); foreach(@DIRS) { open(NAME, "< $home/.cvsq/dirnames/$_") or die "Couldn't read $home/.cvsq/dirnames/$_ ($!)\n"; my $name = do {local $/; }; close(NAME) or die "Closing $home/.cvsq/dirnames/$_ failed ($!)\n"; if ($name eq $pwd) { printf "diff -r %s %s\n", "$home/.cvsq/data/$_/$filename", $filename; system "diff", "-r", "$home/.cvsq/data/$_/$filename", $filename; printf "[return value %d]\n", $? >> 8; last; } } }