#!/usr/bin/env perl -w
use strict 'subs';
use Getopt::Std;

($prog = $0) =~ s|.*/||;
$usage = <<END;
usage:
  $prog [-vn][-r repository] oldname newname [oldname newname ...]
or
  $prog [-vn] -x.old.new [except ...]
END

# $Header: /u/s/o/solomon/util/RCS/cvs_rename.pl,v 1.9 1997/11/29 12:27:52 solomon Exp solomon $

=head1 NAME

cvs_rename - program to rename files that are under control of CVS

=head1 SYNOPSIS

cvs_rename I<[-n][-v][-r repository]> old-name new-name [old-name new-name ...]
cvs_rename I<[-vn] -x.old.new> [except ...]

=head1 DESCRIPTION

Renaming a file that is under the control of CVS is not so simple as

	mv old-name new-name

CVS should have a command to do that, but it doesn't (at least as of release
1.9). Instead, it suggests no fewer than three different ways of accomplishing
the task in the online documenation (topic "Moving files" in the info doc).
The first and simplest is to I<cvs remove> the old version and I<cvs add> the
second.  Unfortunately, it leaves no record of the connection between the two,
and I<cvs log> doesn't do the right thing.  The second way is simply to rename
the history file ($CVSROOT/whatever/old-name,v), but that has problems as well
(see the CVS info for details).  The third is a variant of that that leaves
old-name,v intact.  That's what this program does.

The first version of the call simply renames I<old-name> in the current
directory to I<new-name>.
The second version renames I<foo.old> to I<foo.new> for each file I<foo.old>
in the current directory.
Any remaining arguments are names of files that should I<not> be renamed.
It either case, it is a fatal error if a desination file
exists either in the current directory or in the repository.

=head1 OPTIONS

=over 4

=item [-n]

Don't actually do it, just indicate what would happen if you did.
(Like I<make -n>).

=item [-v]

Verbose output about what's happening.

=item [-m]

Additional text to prepend for the rename commit message.

=back

=head1 AUTHOR

Marvin Solomon (solomn@cs.wisc.edu), June 1997

=cut


# Argv processing

die $usage unless
	getopts("vnx:r:m:");

# -v: verbose
$opt_v = 0 unless defined $opt_v;
# -n: don't do it (like make -n)
$opt_n = 0 unless defined $opt_n;
# -m: additional commit message to prepend 
$opt_m = "" unless defined $opt_m;
# -r: directory containing the repository
chomp($opt_r = `cat CVS/Repository`) unless $opt_r;

# Find out where we are
die "cannot find repository; giving up"
  	unless $opt_r;

# Actually do the renames

if ($opt_x) {
	die "bad -x option\n"
		unless ($oldext,$newext) = $opt_x =~ /^\.([^.]+)\.([^.]+)$/;
	foreach $file (@ARGV) { $except{$file} = 1; }
	die "opendir" unless opendir(DIR,'.');
	@files = grep { /\.$oldext$/ && !$except{$_} } readdir(DIR);
	closedir DIR;
	unless(@files) {
		print STDERR "nothing to be renamed\n";
		exit 0;
	}

	# Make sure everything will go without a hitch before doing anything
	foreach $old (@files) {
		$new = $old;
		$new =~ s/$oldext$/$newext/;
		die "$new already exists\n" if -e "$new";
		die "$opt_r/$old,v: $!\n" unless -e "$opt_r/$old,v";
		die "$opt_r/$new,v already exists\n" if -e "$opt_r/$new,v";
		$tags{$old} = check_status($old);
	}

	# Ok, let 'er rip
	$commit = '';
	foreach $old (@files) {
		$new = $old;
		$new =~ s/$oldext$/$newext/;
		do_rename($old,$new,$tags{$old});
		$commit .= " $old";
	}
	$msg = "$opt_m";
	$msg .= "Renamed from *.$oldext to *.$newext";
	do_sys("cvs commit -f -m \"$msg\" $commit");
}
else { # not opt_x
	die "wrong number of arguments\n$usage"
		unless (@ARGV > 0) && ((@ARGV % 2) == 0);

	# Make sure everything will go without a hitch before doing anything
	@args = @ARGV;
	while ($old = shift()) {
		$new = shift();
		die "$new already exists\n" if -e "$new";
		die "$opt_r/$old,v: $!\n" unless -e "$opt_r/$old,v";
		die "$opt_r/$new,v already exists\n" if -e "$opt_r/$new,v";
		$tags{$old} = check_status($old);
	}
	@ARGV = @args;

	# Ok, let 'er rip
	$commit = '';
	$msg = '';
	while ($old = shift()) {
		$new = shift();
		do_rename($old,$new,$tags{$old});
		if ($msg) {
			$msg .= " and from $old to $new";
		}
		else {
			$msg = "$opt_m";
			$msg .= "Renamed from $old to $new";
		}
		$commit .= " $old";
	}
	do_sys("cvs commit -f -m \"$msg\" $commit");
}
exit;

# Make sure file to be renamed is up-to-date with respect to the repository
# Return list of tags from the cvs status command.
sub check_status {
	local($old) = @_;
	local($status, $info);

	die "cvs status: $!\n"
		unless $info = `cvs status -v $old`;
	die "cannot parse cvs status"
		unless $info =~ /^\=+\nFile.*Status: (.*)/;
	$status = $1;
	die "cannot rename; file $old is $status, not Up-to-date\n"
		unless $status eq 'Up-to-date';

	# Generate a list of tags in the old version
	die "cannot parse cvs status"
		unless $info =~ s/.*Existing Tags:\n//s;
	return $info;
}

sub do_rename {
	local ($old, $new, $info) = @_;

	# 1. Copy the file in the repository
	do_sys("cp $opt_r/$old,v $opt_r/$new,v");
	do_sys("rm $old");

	# 2. Make the version under the old name obsolete.
	do_sys("cvs remove $old");

	# 3. Get the latest version under the new name.
	do_sys("cvs update $new");

	# 4. Remove all tags from the new version
	unless ($info =~ /No Tags/) {
		while ($info =~ s/\s*(\S+)\s*\((\w+):.*\n//) {
		    $tag = $1;    
		    $branch = $2;
		    $arg = '-d';
		    if( $branch =~ /branch/ ) {
			$arg .= 'B';
		    }  
		    do_sys("cvs tag $arg $tag $new");
		}
	}
} # do_rename

sub do_sys {
	local ($cmd) = @_;
	print STDERR "+ $cmd\n" if $opt_v;
	return if $opt_n;
	system($cmd);
	die "$cmd: $!" if $?;
}
