#!/usr/bin/perl -w use strict; use warnings; use Cwd; use File::Copy; use File::Basename; =pod =head1 NAME commit-cvs-copy - Client script to copy files in CVS while preserving CVS logs and blame. =head1 SYNOPSIS B I I =head1 DESCRIPTION C allows files to be copied from one location in CVS to another, preserving the CVS log/blame information from the original location. It takes as input a I file, of the following format: mozilla/original/location.file mozilla/target/location.file mozilla/original/location.file2 mozilla/target/location.file2 ... C does not modify or use the current working directory; all operations are performed in a temporary directory. =head1 REQUIREMENTS C is has been tested on MacOS and Linux on a limited number of input files. It is recommended to test this script using a temporary repository before using in production environments, e.g. an rsynced copy of the Mozilla CVS repository. C is unlikely to work using ActiveState perl, and is untested with cygwin or MSYS perl. =head1 LICENSE The MIT License Copyright (c) 2006 The Mozilla Foundation > Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. =head1 AUTHOR Benjamin Smedberg =head1 MAINTENANCE Maintenance releases of C will be available at L. =cut sub DoCopy { my ($source, $dest) = @_; copy($source, $dest) || die "Couldn't copy from '$source' to '$dest'."; } sub Confirm { my $message = $_[0]; print "$message "; my $confirm = ; if ($confirm !~ /^y/i) { die "User abort."; } } sub Call { my $command = $_[0]; system("$command >/dev/null 2>&1") == 0 || die "Command failed: '$command'; exit code $?"; } sub GetCVSLog { my ($cvsroot, $location) = @_; my $command = "cvs -d \"$cvsroot\" log -b \"$location\""; open OUTPUT, "-|", $command || die "Command failed: '$command'; exit code $?"; my @cvsversions; my $curversion = ""; my $date; my $committer; my $comment; while (defined(my $line = )) { if ($line =~ /^revision ([\d\.]+)$/) { if ($curversion && $curversion ne "dead") { unshift @cvsversions, { "version" => $curversion, "comment" => "$comment\n". "Original committer: $committer\n". "Original revision: $curversion\n". "Original date: $date\n" }; } $curversion = $1; my $dateline = ; $dateline =~ m~^date: (\d\d\d\d/\d\d/\d\d \d\d:\d\d:\d\d); author: ([^;]+); state: (Exp|dead)~ || die ("Couldn't parse date line: '$dateline'"); $date = $1; $committer = $2; $curversion = "dead" if $3 eq "dead"; $comment = ""; } else { if ($line !~ /^[-=]+$/ && $line !~ /^branches: /) { $comment .= $line; } } } $curversion or die "Couldn't parse cvslog output."; if ($curversion ne "dead") { unshift @cvsversions, { "version" => $curversion, "comment" => "$comment\n". "This file was copied in CVS from the following location:\n". "$location\n". "Original committer: $committer\n". "Original revision: $curversion\n". "Original date: $date\n" }; } close(OUTPUT) || die ("Couldn't read from command."); return \@cvsversions; } if (scalar(@ARGV) != 2) { die "Usage: commit-cvs-copy foo.cvsmoves CVSROOT"; } my ($cvscopies, $cvsroot) = @ARGV; open(CVSMOVES, "<", $cvscopies) || die("Couldn't open '$cvscopies' for reading."); my @copylines = ; close(CVSMOVES) || die "Couldn't close '$cvscopies'."; my %copies; print "Performing the following copies:\n"; for my $line (@copylines) { next if ($line =~ /^\#/ || $line =~ /^\s*$/); if ($line !~ /^(\S+)\s+(\S+)$/) { die "Couldn't parse line:\n$line"; } print "$1 =>\t$2\n"; $copies{$1} = { "destination" => $2 }; } Confirm("Is this list of copies correct?"); # TODO: use File::Temp my $tmpdir = `mktemp -d -t cvscopy`; chomp $tmpdir; ($tmpdir ne "" && -d $tmpdir) || die "Couldn't create temporary directory."; print "Temporary directory: '$tmpdir'\n"; my $saveddir = cwd(); chdir $tmpdir; my %dests; for my $out (values(%copies)) { my $dest = $out->{"destination"}; $dests{dirname($dest)} = 1; } for my $dest (keys(%dests)) { print "Checking out '$dest'\n"; Call("cvs -d \"$cvsroot\" co -l \"$dest\""); } for my $out (values(%copies)) { my $dest = $out->{"destination"}; print "Checking for '$dest'\n"; if (-e $dest) { Confirm("Destination '$dest' exists.\nType \"y\" to replace it."); } else { Call("touch $dest"); my $dir = dirname($dest); my $file = basename($dest); Call("cd \"$dir\" && cvs -d \"$cvsroot\" add \"$file\""); } } for my $source (keys(%copies)) { Call("cvs -d \"$cvsroot\" co \"$source\""); my $versions = GetCVSLog($cvsroot, $source); $copies{$source}->{"cvsversions"} = $versions; print "File: $source\n"; for my $i (@$versions) { my $version = $i->{"version"}; Call("cvs -d \"$cvsroot\" co -r \"$version\" \"$source\""); DoCopy($source, "${source}__version${version}"); } } print "Beginning commit... please don't cancel in the middle unless you reall mean it!\n"; for my $source (keys(%copies)) { print "$source\n"; my $dest = $copies{$source}->{"destination"}; my $versions = $copies{$source}->{"cvsversions"}; for my $i (@$versions) { my $version = $i->{"version"}; print " $version\n"; DoCopy("${source}__version${version}", $dest); my $comment = $i->{"comment"}; open(COMMENT, ">", "$tmpdir/comment") || die("Couldn't open comment file."); print COMMENT $comment; close COMMENT; Call("cvs -d \"$cvsroot\" commit -F \"$tmpdir/comment\" \"$dest\""); } } chdir $saveddir; # TODO: use rmtree() system "rm -rf $tmpdir";