#!/usr/bin/perl # Author: Gregory A. Marton # Copyright: 2001-2002 Gregory Marton # Distribution: GNU lesser general public license # http://www.gnu.org/copyleft/lesser.html # You may protest that this isn't a library, but # any perl program with subroutines is, so interpret # it that way. # Please report bugs to: 0m3lb3q001@sneakemail.com #< preliminaries sub get_CVS_VERSION { return sprintf("%d.%02d", ($_[0] =~ /\s(\d+)\.(\d+)/)); } my $VERSION=&get_CVS_VERSION(q$Id: vc.pl 536 2006-11-22 03:12:22Z gremio $); use strict; use File::Copy; use File::Path; use Cwd; my $Verbose=1; my $Me = $0; $Me =~ s:.*/::; my @PASS_ARGV=(); # args before cmd: cvs >>-d /root<< co -P project my @FLAGS=(); # args modifying cmd: cvs -d /root co >>-P<< project my @FILES=(); # args to cmd: cvs -d /root co -P >>project<< #> my $CVS="cvs -q -z5"; #< check VC type sub vc_type { my $vc_type = ""; if (-d "CVS") { if (-d "RCS") { print STDERR qq(This directory is under BOTH cvs and rcs.\n); print STDERR qq(This is either something rather wrong, or just\n); print STDERR qq( very poor version control design.\n); print STDERR qq(Please deal with it yourself); print STDERR qq( or ask Gremio) if defined $ENV{USER} and $ENV{USER} ne "gremio"; die ".\n"; } $vc_type="CVS"; } elsif (grep(/^-d$/, @PASS_ARGV)) { foreach my $a (@PASS_ARGV) { if ($a =~ /^-d$/) { $vc_type = "CVSd"; } elsif ($vc_type) { $ENV{CVSROOT} = $a; last; } } } elsif (-d "RCS") { $vc_type="RCS"; } elsif (-d "SCCS") { die "The Source Code Control System (SCCS) is not yet supported.\n"; } elsif (-e <*,v>) { warn "I see some ,v files around. You should put them in an RCS directory.\n"; $vc_type="RCS,v"; } elsif (not @_) { die "no source control here. (try '$Me import')\n"; } if (defined $ENV{CVSROOT} and $vc_type eq "RCS") { #warn "Using RCS. Consider '$Me rcs2cvs' to put it under CVS.\n"; } return $vc_type; } #> #< synonym("syn1","syn2",...,"synN") returns 1 if $_ in synonyms. sub synonym { my ($command, @actions) = @_; while (@actions) { my $keys = shift @actions; my $action = shift @actions; foreach my $cmd (@$keys) { if ($cmd eq $command) { &$action(); exit; } } } } #> #< cvs and rcs primitives sub cvs { my $command = "$CVS ". join(" ", @PASS_ARGV) ." ". join(" ", @_); print "> $command\n" if $ENV{DEBUG}; system($command); } sub exec_cvs { my $command = "$CVS ". join(" ", @PASS_ARGV) ." ". join(" ", @_); print "> $command\n" if $ENV{DEBUG}; exec $command; } sub rcs { my ($nothing)=grep(/^-n$/, @PASS_ARGV); my $command = "rcs ". join(" ", grep(!/^-n$/, @PASS_ARGV)) ." ". join(" ", @_); print "> $command\n" if $ENV{DEBUG} or $nothing; qx($command) unless $nothing; } sub co { my ($nothing)=grep(/^-n$/, @PASS_ARGV); my $command = "co ". join(" ", grep(!/^-n$/, @PASS_ARGV)) ." ". join(" ", @_); print "> $command\n" if $ENV{DEBUG} or $nothing; qx($command) unless $nothing; } sub ci { my ($nothing)=grep(/^-n$/, @PASS_ARGV); my $command = "ci ". join(" ", grep(!/^-n$/, @PASS_ARGV)) ." ". join(" ", @_); print "> $command\n" if $ENV{DEBUG} or $nothing; qx($command) unless $nothing; } sub rlog { my ($nothing)=grep(/^-n$/, @PASS_ARGV); my $command = "rlog ". join(" ", grep(!/^-n$/, @PASS_ARGV)) ." ". join(" ", @_); print "> $command\n" if $ENV{DEBUG}; qx($command); } sub rcsdiff { my ($nothing)=grep(/^-n$/, @PASS_ARGV); my $command = "rcsdiff -q ". join(" ", grep(!/^-n$/, @PASS_ARGV)) ." ". join(" ", @_); print "> $command\n" if $ENV{DEBUG}; qx($command); } sub diff { my ($nothing)=grep(/^-n$/, @PASS_ARGV); my $command = "diff ". join(" ", grep(!/^-n$/, @PASS_ARGV)) ." ". join(" ", @_); print "> $command\n" if $ENV{DEBUG}; qx($command); } #> #< usage sub usage { my $message = shift; $message ||= ""; print STDERR $message . "\n"; print STDERR qq(usage: vc [cvs-options] command [command-options] [command-arguments] commands are: help - get more extensive help on all commands version - show version information about $Me import, checkout, update, add, commit, remove, as in CVS rename - rename a file under version control edit - notify version control that you will make changes unedit - destroy changes and notify that you are done making changes cu - commit changes and notify that you are done making changes diff, log, as in CVS message - set the log message for a particular version of a file rcs2cvs - convert an RCS repository to a CVS repository repository - change the CVS repository of the current directory root - change the CVS root of the current directory ); print $message . "\n"; exit 1; } #> #< rcsables sub rcsables { my @files = @_; my $warning = 1; my %rcsables = (); my %vcfiles = vcfiles(cwd()); # vcfiles point at their ,v file, directories though point at themselves: if (not scalar @files) { @files = keys %vcfiles; } foreach (@files) { if ($_ eq $vcfiles{$_}) { if ($warning) { warn "Warning: subdirectories ($_) are not explored under RCS.\n"; $warning=0; } next; } $rcsables{$_} = $vcfiles{$_}; } return %rcsables; } #> #< vcfiles sub vcfiles { my $directory = shift; my $backdir = cwd(); chdir $directory; my %ignore = ( "CVS" => 1, "RCS" => 1, "." => 1, ".." => 1 ); my @ignorepttns = (); if (-f ".cvsignore") { if (open(IGN, ".cvsignore")) { while () { chomp; if (/[\?\*\#]/) { $_ = quotemeta $_; #so `.*' will turn first into `\.\*' s/\\\*/.*/g; #then we fix the * s/\\\?/./g; #or the ? s/(? #< parseflags(pattern, \@args) -- return %getopts on @args, moving flags from @args to @FLAGS sub parseflags { my ($pattern, $args) = @_; my @hold_argv = @ARGV; @ARGV = @$args; my %opts; use Getopt::Std; getopts($pattern, \%opts); @FLAGS = (); my $i = 0; foreach my $arg (@$args) { if ($arg eq $ARGV[$i]) { $i++; } else { push @FLAGS, $arg; } } @$args = @ARGV; @ARGV = @hold_argv; return %opts; } #> #< Command Line Interpreter sub cli { my $command = ""; # command-line processing: generates @PASS_ARGV, $command (into $_), #< and @FILES which includes options after the command. warn "ENV{DEBUG} is true.\n" if $ENV{DEBUG}; while ($_ = shift @ARGV) { if (not $command) { if (/^-/) { push @PASS_ARGV, $_; die "$Me: option requires an argument -- ". $PASS_ARGV[$#PASS_ARGV] ."\n" if /^-[bdez]/ and not @ARGV; push @PASS_ARGV, shift @ARGV if /^-[bdez]/; } else { $command = $_; } } else { push @FILES, $_ } } if ($ENV{DEBUG}) { use Data::Dumper; print "command=$command PASS_ARGV=".Dumper(@PASS_ARGV)."\n"; } $_ = $command; #> usage() unless $command; # Standard shared (between CVS and RCS) commands: synonym($command, #< add ["add", "ad", "new"] => sub { if (vc_type() eq "CVS") { cvs_add(@FILES); } elsif (vc_type() eq "CVSd") { die "No repository here. Try `$Me import`\n"; } else { unless (@FILES) { die "please specify files to add.\n"; } rcs_add(@FILES); } }, #> #< admin ["admin","adm","rcs"] => sub { if (vc_type() =~ /^CVS/) { exec_cvs($command, @FILES); } else { unless (@FILES) { die "please specify files to admin!\n"; } rcs(@FILES); } }, #> #< checkout ["co","checkout","get"] => sub { my $type = vc_type(1); if ((not $type and defined $ENV{CVSROOT}) or $type =~ /^CVS/) { exec_cvs("co -P", @FILES); } elsif (not $type) { die "cannot find any source control.\n"; } else { unless (@FILES) { die "please specify files to $command.\n"; } co("-l", @FILES); } }, #> #< commit ["commit", "checkin", "check", "ci"] => sub { if (vc_type() =~ /^CVS/) { exec_cvs($command, @FILES); } else { if (not @FILES) { @FILES = rcs_update(); # Note: rcs_update called in list context collects # modified files instead of printing the report. } if (not @FILES) { die "nothing to check in.\n"; } my @message; if ($FILES[0] eq "-m") { shift @FILES; push @message, qq(-m") . (shift @FILES) . qq("); } my %rcsables = rcsables(@FILES); @FILES = keys %rcsables; ci(@message, "-l", @FILES); } }, #> #< diff ["diff","di","dif","rcsdiff"] => sub { if (vc_type() =~ /^CVS/) { exec_cvs("diff", @FILES); } else { if (not @FILES) { @FILES = rcs_update(); # Note: rcs_update called in list context collects # modified files instead of printing the report. } print rcsdiff(@FILES); } }, #> #< edit ["edit", "lock"] => sub { if (vc_type() =~ /^CVS/) { cvs("update ", @FILES); exec_cvs("edit", @FILES); } else { unless (@FILES) { die "please specify files to $command.\n"; } # This is just not effective at overriding other people's perms: # rcs("-l", @FILES); # foreach(@FILES) { # qx(chmod +w $_) if -e $_ and not -w _; # } # but this should be: foreach my $file (@FILES) { if (rcsdiff("--brief", "-q", $file)) { warn "$file has been modified.\n Trying to get a lock anyway -- permissions may break.\n"; rcs("-l", $file); } else { co("-l", $file); } } } }, #> #< import ["import","im","imp"] => sub { if (defined $ENV{CVSROOT}) { if (not @FILES) { my $curdir = cwd(); $curdir =~ s:.*/::; @FILES = ($curdir); } if (1 == scalar @FILES) { push @FILES, $ENV{USER}, "START"; } else { usage("import takes at most one argument, the repository name."); } exec_cvs($command, @FILES); } else { mkpath([cwd()."/RCS"], 1, 0777); rcs_add(); } }, #> #< log ["log","lo","rlog"] => sub { if (vc_type() =~ /^CVS/) { exec_cvs($command, @FILES); } else { unless (@FILES) { die "please specify files to see the log for.\n"; } print rlog(@FILES); } }, #> #< rdiff ["rdiff","patch","pa"] => sub { if (vc_type() =~ /^CVS/) { exec_cvs($command, @FILES); } else { unless (@FILES) { die "please specify file to rdiff.\n"; } rcsdiff("-q", "-c", @FILES); } }, #> #< remove ["remove","rm","delete"] => sub { if (vc_type() =~ /^CVS/) { cvs_remove(@FILES); } else { unless (@FILES) { die "please specify files to $command.\n"; } rcs_remove(@FILES); } }, #> #< tag ["tag", "ta", "freeze"] => sub { if (vc_type() =~ /^CVS/) { exec_cvs($command, @FILES); } else { unless (1 < @FILES) { if (not @FILES) { die "usage: $Me tag [-r rev] name files\n"; } else { die "please specify files to tag\n"; } } my $name = shift @FILES; my $rev=""; if ($FILES[0] =~ /^-r(.*)/) { if ($1) { $rev = $1; } else { $rev = $FILES[1]; shift @FILES; } shift @FILES; } unless (@FILES) { my %rcsables = rcsables(); @FILES = keys %rcsables; } unless (@FILES) { die "nothing to tag.\n"; } rcs("-N$name:$rev", @FILES); } }, #> #< unedit ["unedit", "unlock"] => sub { if (vc_type() =~ /^CVS/) { exec_cvs("unedit", @FILES); } else { my %opts = parseflags("r:", \@FILES); unless (@FILES) { die "please specify files to $command.\n"; } my $u = "-u"; if ($opts{r}) { $u .= $opts{r}; } rcs($u, @FILES); foreach(@FILES) { qx(chmod -w $_) if -w $_; } if ($opts{r}) { unshift @FILES, "-r$opts{r}"; } co(@FILES); } }, #> #< update ["update", "upd", "up"] => sub { if (vc_type() =~ /^CVS/) { exec_cvs($command, @FILES); } else { rcs_update(@FILES); return; } }, #> #< status ["status", "stat", "st"] => sub { if (vc_type() =~ /^CVS/) { exec_cvs($command, @FILES); } else { rcs_status(@FILES); } }, #> ); # New by Gremio: synonym($command, #TODO: rename should only re-add those files that were originally # in the repository for the new location! #< rename ["rename","ren","mv"] => sub { if (vc_type() =~ /^CVS/) { cvs_rename(@FILES); } else { rcs_rename(@FILES); } }, #> #TODO: message should prepend instead of replacing! #< message ["message","msg"] => sub { my $r = shift @FILES; if ($r =~ /^-r(.+)/) { $r = $1; } elsif ($r eq "-r") { $r = shift @FILES; } else { unshift @FILES, $r; $r = ""; } my $m = shift @FILES; if (-e $m or not @FILES) { unshift @FILES, $m; print qq(please enter a comment: (end with ^D or a . on a line by itself)\n); $m =""; while () { last if /^\.$/; $m.=$_; } $m =~ s/\n/\\\n/gs; $m =~ s/\"/\\\"/gs; $m =~ s/[^\s\x20-\x7E]/?/gs; } if (vc_type() =~ /^CVS/) { if ($r) { exec_cvs(qq(admin -m$r:"$m" ), @FILES); } else { exec_cvs(qq(admin -t-"$m" ), @FILES); } } else { if ($r) { rcs(qq( -m$r:"$m" ), @FILES); } else { rcs(qq( -t-"$m" ), @FILES); } } }, #> #TODO: when readding previously locked files, just overwrite! #< rcs2cvs # Syntax vc [-d cvsroot] rcs2cvs (or convert) repository_name # One of $CVSROOT or -d must be specified. ["rcs2cvs","convert"] => sub { if (vc_type() !~ /RCS/) { die "this directory is not under RCS control.\n"; } #< check the CVSROOT unless ($ENV{CVSROOT}) { unless (@PASS_ARGV == 2 and $PASS_ARGV[0] eq "-d") { print STDERR "you must either set your CVSROOT environment variable or\n"; die "specify it on the command line: $Me -d $command \n"; } $ENV{CVSROOT} = $PASS_ARGV[1]; } #> unless (1 == scalar @FILES) { die "usage: $Me [ -d ] $command \n"; } rcs2cvs($ENV{CVSROOT}, $FILES[0]); return; }, #> #< version ["version"] => sub { print "$Me - a version control wrapper for CVS and RCS so far.\n\n"; print "$Me version: $VERSION\n"; print "version control detected here: " . (vc_type(1) or "none.") . "\n"; print "more help available soon. \n"; }, #> #< help ["help","h"] => sub { exec "perldoc $0"; }, #> #< set repository ["repository","rep"] => sub { cvs_change_repository(shift @FILES); }, #> #< set root ["root"] => sub { cvs_change_root(shift @FILES); }, #> #< fix dying gasps ["gasps"] => sub { cvs_fix_dying_gasps(); }, #> #< cu ["cu"] => sub { if (vc_type() =~ /^CVS/) { cvs("commit ", @FILES); exec_cvs("unedit", @FILES); } else { if (not @FILES) { @FILES = rcs_update(); # Note: rcs_update called in list context collects # modified files instead of printing the report. } if (not @FILES) { die "nothing to check in.\n"; } my @message; if ($FILES[0] eq "-m") { shift @FILES; push @message, qq(-m") . (shift @FILES) . qq("); } my %rcsables = rcsables(@FILES); @FILES = keys %rcsables; ci(@message, "-u", @FILES); } }, #> ); #< CVS only: annotate, editors, export, history, some new stuff if (vc_type() !~ /CVS/) { synonym($command, ["annotate", "ann", "editors", "export","exp","ex", "history","hi","his", "login", "logon", "lgn", "release", "rel", "re", "rtag", "rt", "rfreeze", "watch", "watchers", "repository","rep", "root", "gasps"] => sub { die "$command is not implemented for RCS. Please bug Gremio.\n"; }); die "unrecognized command `$command'.\n"; } else { exec_cvs($command, @FILES); } #> } #> cli(); #< traverse sub traverse { my ($directory, $func, @rest) = @_; my $n = 0; my %vcfiles = vcfiles($directory); foreach my $name (keys %vcfiles) { $name = qq($directory/$name); if (&{$func}($name, @rest) and not -l $name and -d _) { $n += traverse($name, $func, @rest); } } return $n; } sub traverseall { my ($directory, $func, @rest) = @_; my $n = 0; opendir (DIR, $directory) or die "cannot readdir $directory: $!\n"; foreach (readdir DIR) { next if /^\.\.?$/; #skip special directories. my $name = qq($directory/$_); if (&{$func}($name, @rest) and not -l $name and -d _) { $n += traverseall($name, $func, @rest); } } return $n; } #> #< get_Root sub get_Root { my $Root=""; open(ROOT, ") { $Root=$_; } close ROOT; chomp $Root; return $Root; } #> #< get_Rep sub get_Rep { my $Rep=""; open(REP, ") { $Rep=$_; } close REP; chomp $Rep; return $Rep; } #> #< rcs add sub rcs_add { my @files = @_; my $message; if (@files and $files[0] eq "-m") { shift @files; $message = shift @files; } my %files = rcsables(@files); my $attic="RCS/Attic"; foreach my $file (keys %files) { my $vfile = $files{$file}; if ($vfile eq 1) { if (defined $message) { rcs(qq(-t-"$message"), "-i", $file); } else { rcs("-i", $file); } ci("-u", $file); } elsif ($vfile =~ m:^RCS/Attic/:) { move($vfile, "RCS/"); warn "$file restored from previous delete.\n"; my @message = (defined $message)?(qq(-m"$message")):(); ci(@message, "-u", $file) if -e $file; } else { warn "$file is already under version control.\n"; } } } #> #< cvs add sub cvs_add { my %opts = parseflags("k:m:", \@_); foreach my $dir (@_) { if (-d $dir) { # do my own traversal: can't use traverse, because I want those # things that have ?'s, not just all files. I'm certainly not # going to do .cvsignore's and stuff to figure it out! my @dirs = ($dir); my @files; traverse($dir, sub { my $name=shift; if (-d $name) { return 0 if $name =~ m:/CVS$:; if (-d "$name/CVS") { print STDERR "$name already has version control.\n"; return 0; } else { push @dirs, $name; } } else { push @files, $name; } return 1; }); cvs("add", @FLAGS, @dirs) if @dirs; cvs("add", @FLAGS, @files) if @files; } else { cvs("add", @FLAGS, $dir); } } } #> #< rcs remove (list of files) sub rcs_remove { my @files = @_; if (-d "RCS") { my $rdir=cwd()."/RCS"; my $attic=$rdir."/Attic"; mkpath([$attic], 0, 0777); foreach my $file (@files) { if (-e $file) { die "$file exists. Please remove it first.\n"; } next unless -e "$rdir/$file,v"; move("$rdir/$file,v",$attic); } } else { die "I can't safely do this without an CVS or an RCS directory.\nRemove the ,v file yourself.\n"; } } #> #< cvs remove (file or directory) sub cvs_remove { my %opts = parseflags("k:flR", \@_); foreach my $dir (@_) { my $unique_suffix = ".-".$$."_"; if (-d $dir and not $opts{f}) { traverse($dir, sub { my $name = shift; # warn "examining [$name] from ". cwd() ."\n"; return 0 if $name =~ m:/CVS$:; return 1 if -d $name; move($name, $name.$unique_suffix); # file needs to be "gone" before removal. return 1; }); cvs("remove", @FLAGS, $dir) and print "use '$Me commit' to remove $dir permanently.\n"; traverse($dir, sub { my $name = shift; # warn "examining [$name] from ". cwd() ."\n"; if ($name =~ m:/CVS$:) { # system("/bin/rm -fr $name") and warn "cannot remove $name: $!\n"; # cannot commit if CVS dirs are removed!!! return 0; } return 1 if -d $name; my $oldname = $name; $oldname =~ s/$unique_suffix$//; move($name, $oldname); return 1; }); print STDERR "you must commit, and remove all local CVS directories before readding it.\n"; } else { cvs("remove", @FLAGS, $dir) and print "use '$Me commit' to remove $dir permanently.\n"; } } #< This way of doing things is efficient and convenient, but does not leave # What CVS defines as a repository. # I want to try to be not only nice to CVS, but also history-preserving, so # automagically hacking the repository for this sort of thing is out. # # my $Root=get_Root(); # my $Rep = get_Rep(); # my $ent = qq($Root/$Rep/$dir); # if ($Root =~ /^:ext:/ and -d $dir) { # die "cannot remove directories on remote servers.\nYou can remove individual files though. Yeah, I know that sucks. Blame CVS.\n"; # } elsif ( -d $ent ) { # if ( -e $dir ) { # die "$dir exists; remove it first.\n"; # } # mkpath([qq($Root/$Rep/Attic)], 1, 0777); # move($ent, qq($Root/$Rep/Attic)); # } elsif (-e $ent) { # exec_cvs("remove $dir"); # } else { # print "nothing known about $dir.\n"; #> } } #> #< cvs change repository sub cvs_change_repository { my $newrep=shift; print STDERR "repository $newrep\n" if $ENV{DEBUG}; traverseall(".", sub { my $name = shift; return 1 unless $name =~ m:/CVS/Repository$:; chomp $name; my $new = $name; $new =~ s|^./|$newrep/|; $new =~ s|/CVS/Repository$||m; warn "writing $new to $name\n"; open(REP, ">" . $name) or die "couldn't open $name: $!\n"; print REP $new ."\n"; close REP; return 1; }); } #> #< cvs change root sub cvs_change_root { my $newroot = shift; my @dirq = ("."); while (@dirq) { my $d = shift @dirq; local *DIR; opendir(*DIR, $d) or die "cannot read [$d]: $!\n"; my $root = qq($d/CVS/Root); if (-e $root) { die "cannot write $d/CVS/Root" unless -w _; open(ROOT, ">$root") or die "cannot write to $root: $!\n"; print ROOT $newroot ."\n"; close ROOT; } else { warn "$d/ does not have a CVS/Root file.\n"; next; } foreach (readdir *DIR) { next if /^(\.\.?|CVS|RCS)$/; #skip special directories. my $dd = qq($d/$_); push @dirq, $dd if not -l $dd and -d _; } } } #> #< cvs fix dying gasps $|=1; #autoflush status. sub cvs_fix_dying_gasps { my $directory=shift; return 0 if $directory eq "CVS" or not -d $directory; my $indent=""; print $indent . $directory ."... " if $Verbose; my $cvsdir = $directory . "/CVS"; my $Entries = $cvsdir . "/Entries"; my $Log = $cvsdir . "/Entries.Log"; #< check existence, permissions if (not -d $directory) { die qq([$directory] is not a directory.\n); } if (not -d $cvsdir) { die qq([$directory] is not under CVS control.\n); } if (not -r _ or not -w _ or not -x _) { die qq([$cvsdir] is inaccessible (must be rwx).\n); } if (not -e $Entries or not -f _ or not -r _ or not -w _) { die qq([$Entries] is inaccessible (must be rw and a plain file).\n); } #> #< traverse directories below opendir(DIR, $directory) or die "cannot read [$directory]: $!\n"; my $fixed = 0; my $first = 1; foreach (readdir DIR) { next if /^\.\.?$/; #skip special directories. next if /^CVS$/; #skip cvs directory. next if -l qq($directory/$_) or not -d _; #skip links and files. if ($first) { print qq(\n) if $Verbose; $indent .= " "; $first = 0; } fix_dying_gasps(qq($directory/$_)); $fixed++; } if ($fixed) { chop $indent; chop $indent; print $indent . $directory . " " if $Verbose; } else { print qq(is okay.\n) if $Verbose; return; } #> #< check the Entries.Log file if (not -e $Log) { die qq([$Log] is not there. Were there dying gasps?\n); } if (not -r _ or not -f _ or not -w _) { die qq([$Log] is inaccessible. (must be rw)\n); } #> #< fix the Entries file # read the entries file, my $entries_contents; open (ENT, "<".$Entries) or die "cannot open [$Entries]: $!\n"; $entries_contents = join("", ); close ENT; $entries_contents =~ s/D\n$//s; #remove trailing D. # read the directory entries from log. my $log_contents; open (LOG, "<".$Log) or die "cannot open [$Log]: $!\n"; $log_contents = join("", ); close LOG; # remove leading A's. $log_contents =~ s/^A //mg; # write good Entries file. open (ENT, ">".$Entries) or die "cannot write [$Entries]: $!\n"; print ENT $entries_contents, $log_contents; close ENT; # remove the now junk log file. unlink $Log or die "cannot unlink [$Log]: $!\n"; print qq(is fixed.\n) if $Verbose; #> } #> #< rcs rename sub rcs_rename { my ($was, $willbe) = @_; if (-e $willbe or -e "$willbe,v" or -e "RCS/$willbe,v") { die "$willbe already exists.\n"; } else { move($was,$willbe); rcs_remove($was); rcs_add($willbe); } } #> #< cvs rename sub cvs_rename { my ($was, $willbe) = @_; if (-e $willbe) { die "$willbe already exists.\n"; } elsif (not -d $was) { move($was,$willbe); cvs_remove($was); cvs_add($willbe); } else { mkpath([$willbe], 0, 0777); my $update = cvs("update", $was); my %question = (); foreach (split /\n/, $update) { if (/^\?\s+(.*)$/) { $question{$1}=1; } } traverse($was, sub { my $name = shift; # warn "examining [$name] from ". cwd() ."\n"; return 0 if $name =~ m:/CVS$:; my $new = $name; $new =~ s/^$was/$willbe/; if (-d $name) { mkpath([$new], 0, 0777); return 1; } elsif ($question{$name}) { # do nothing for now. move later. } else { move($name, $new); # file needs to be "gone" before removal. return 1; } }); cvs("remove", $was); cvs_add($willbe); traverse($was, sub { my $name = shift; # warn "examining [$name] from ". cwd() ."\n"; return 0 if $name =~ m:/CVS$:; my $new = $name; $new =~ s/^$was/$willbe/; if (-d $name) { return 1; } elsif ($question{$name}) { move($name, $new); return 1; } else { die("[$name]: this should no longer be here."); } }); #cvs("ci", $was, $willbe); print STDERR "you may now remove $was.\n"; } #< Do not hack the repository this way. It's not history-preserving: # my $Root = get_Root(); # my $Rep = get_Rep(); # if ($Root =~ /^:ext:/) { # die "cannot rename things on remote servers.\n"; # } # if (-d "$Root/$Rep/$was") { # move("$Root/$Rep/$was", "$Root/$Rep/$willbe"); # } else { # move("$Root/$Rep/$was,v", "$Root/$Rep/$willbe,v"); # } # cvs("-Q update -d $willbe"); #> remove(\1, $was); } #> #< rcs update sub rcs_update { my %files = rcsables(@_); my @result = (); my $collectmode = wantarray; my $cwd = cwd(); foreach my $file (keys %files) { my $vfile = $files{$file}; if (-e $file) { if (-e $vfile) { if ($vfile =~ m:/Attic/:) { print "d $file\n" unless $collectmode; } elsif (rcsdiff("--brief", "-q", $file)) { if ($collectmode) { push @result, $file; } else { print "m $file\n"; } } elsif (rlog("-L", "-R", "-l", $file)) { if ($collectmode) { push @result, $file; } else { print "l $file\n"; } } else { # do nothing. The file is up to date. } } else { print "? $file\n" unless $collectmode; } } elsif (-e $vfile and $vfile !~ m:/Attic/:) { # the sandbox copy was *ahem* lost. Let's do like CVS and update! unless ($collectmode) { co("-q", "-u", $file); print "u $file\n" if -e $file or grep(/^-n$/, @PASS_ARGV); } } } return @result; } #> #< rcs status sub rcs_status { my @files = @_; my $alltags=""; foreach my $file (@files){ if($file eq "-v") { $alltags = 1; next; } my $rlogoutput = rlog($file); if ($rlogoutput){ print(("=" x 67) . "\n"); #finding the appropriate fields #< find repository filename my ($match) = ($rlogoutput =~ /^RCS file: (.+)$/m); my $rcsfile; if ((my $pwd = cwd()) eq "/") { $rcsfile = $match; } else { $rcsfile = $pwd . "/" . $match; } $rcsfile =~ s:/./:/:g; #> #< show working filename $rlogoutput =~ /^Working file: (.*\/)?([^\/]+)\shead:/m; my $workingfile = $2; print "File : $workingfile\tStatus: "; #> #look through all the named(tagged) revs and call rcsdiff #with the working file. If one rev matches, return the #corresponding tag, otherwise return the tag of the head #rev. If two or more rev matches, return the lastest tag (not latest rev) #(This may happen, for example, if we checkout an #older revision with a "D" tag and check it in with a "P" #tag) $rlogoutput =~ /^symbolic names:$/m; $' =~ /^keyword/m; my $tagpart = $`; my @lines = split(/$/m,$tagpart); my $realtag = ""; my $realrev = ""; my $modified =""; # check each tagged revision to see if it matches the current. foreach my $line (@lines){ if($line =~ /\S/) { $line =~ /^\s+(\S+): (\S+)$/m; my $tag = $1; my $rev = $2; if (not ($realrev or (rcsdiff("--brief", "-q", "-r$rev" , $file)))) { $realrev = $rev; $realtag = $tag; $modified = ""; } } } #< if this is not a tagged revision, get the info w.r.t. HEAD if (not $realrev) { $rlogoutput =~ /^head: (\S+)\s*$/m; $realrev = $1; $modified = rcsdiff("--brief", "-q" , $file); if($tagpart =~ /\s+(\S+): $realrev\s/){ $realtag = $1;} } #> #< find the date $rlogoutput = rlog("-r$realrev",$file); $rlogoutput =~ /---------------------/; $' =~ /date: ([^;]+);/; my $date = $1; #> #< show the modified bit: if($modified) { print "Locally Modified\n\n"; } else { print "Up-to-date\n\n"; } #> # Working and repository revisions are just the same: print " Working revision:\t$realrev\t$date\n"; print " Repository revision:\t$realrev\t$rcsfile\n"; #< "Sticky" info # There's no concept of 'stickyness' in RCS, but we # can sort of say that something was checked out with # a given tag if it is an unmodified version of that # tag. This won't make a difference for branching or # anything, that still has to be manual, but it might # be useful. For one of the matches as determined # above, print the tag and date. Options don't make # sense. print " Sticky Tag:\t\t"; if ($realtag) { print "$realtag (revision:$realrev)\n"; print " Sticky Date:\t\t$date\n"; } else { print "(none)\n"; print " Sticky Date:\t\t(none)\n"; } print " Sticky Options:\t(none)\n\n"; #> #< -v option on the command line. if ($alltags) { print " Existing Tags:\n"; foreach my $line (@lines){ if ($line =~ /\S/) { $line =~ /^\s+(\S+): (\S+)$/m; my $tag = $1; my $rev = $2; printf("\t%-25s\t(revision: %s)\n", $tag,$rev); } else { my $nl = @lines; if($nl == 1) { print "\tNo Tags Exist\n"; } } } } #> } else { # do nothing: rcs already failed and hopefully gave a decent # message. } } } #> #< rcs2cvs # With thanks to rcs2cvs by Chuck Moss, mossc@mossc.com # whose ksh script 'rcs2cvs' this is almost a translation of. sub rcs2cvs { my ($root, $rep) = @_; #< check the root if ($root =~ /^:ext:/) { die "this conversion requires direct changes to the repository that cannot be done to a remote CVSROOT. Sorry.\n"; } if (! -d $root) { die "[$root] is not a directory.\n"; } if (! -w _) { die "the CVSROOT is not writeable.\n"; } #> my $path = ""; my $target = qq($root/$rep); if (-d $target) { print "That repository already exists. Add these files? [n] "; $_ = ; die "files not added.\n" unless /^y/i; if (not -w _) { die "The repository is not writeable.\n"; } } $|=1; my @stack; push @stack, "."; while (my $dir = pop @stack) { next unless -d $dir."/RCS"; #< copy each RCS'ed file opendir(RCS, "$dir/RCS") or die "cannot open $dir/RCS: $!\n"; mkpath(["$target/$dir"], \1, 0777); foreach my $file (readdir RCS) { next if $file =~ /^\.\.?$/; my $basename = $file; $basename =~ s/,v$//; print "$dir/$basename ...\n"; # check if the file is locked: if (rlog("-L -R $dir/$basename")) { warn "\n WARNING: $dir/$basename is locked!\n\n"; } # check that we're not overwriting a file: if (-e "$target/$dir/$file") { die " ERROR: the file $target/$dir/$file already exists!\n"; } # copy it! copy("$dir/RCS/$file", "$target/$dir/$file"); # preserve execute permissions: if (-x "$dir/RCS/$file") { chmod(0775, "$target/$dir/$file"); } } close RCS; #> #< recurse into subdirectories opendir(DIR, $dir) or die "cannot open $dir: $!\n"; foreach my $file (readdir DIR) { next if $file =~ /^\.\.?$/; next unless -d "$dir/$file" and -r _ and -x _; push @stack, "$dir/$file"; } #> } } #> 1; __END__ =pod =head1 NAME vc - a Version Control wrapper for CVS and RCS. =head1 SYNOPSYS vc [cvs-options] B [command-options] [command-arguments] =head1 NOTE This documentation supplements, but does not replace, the documentation for CVS and the various RCS tools. There are several new commands and commands whose behavior is somewhat modified; These are described in most detail. =head1 DESCRIPTION The Concurrent Versions System (CVS) is a version control system built on the Revision Control System (RCS) which can handle entire directory trees of files rather than entries just within a directory. While RCS stores its revision data files in the directory under version control, CVS has an abstract location for these, called the CVSROOT. One uses RCS "in place", or in just one location, whereas under CVS one checks out a B to make changes in, then checks those changes in to the B when satisfied with them. This wrapper script, VC, aims to combine the interfaces somewhat, and to "fix" behaviors in each that the author deemed "broken". In general, commands that are meaningful to both RCS and CVS should work similarly under each. The point was not to hide CVS and RCS from the user, but to cut down on the amount of discontinuity and frustration when working with either one or switching between them. Please read the CVS manpage and the various RCS manpages for more in-depth documentation on most commands. =head1 ESSENTIAL COMMANDS This section details commands. Your intuitions from CVS should work and are relied upon in these descriptions. Emphasis is on differences between CVS and RCS behaviors for the most common syntax of commands, and the most common usages. =over =item about VC =item managing the SANDBOX (current directory) =item managing FILES =item REVISIONS of a file =back =head2 about VC =over =item B Display version information about vc. =item B Display this man page. Synonyms: h =back =head2 managing the SANDBOX (current directory) import update rcs2cvs repository root gasps =over =item B [optional for CVS: name ] Initiate version control in the current directory, initially adding almost* all contents. Synonyms: imp im Uses CVS if the $CVSROOT environment variable is defined, otherwise RCS. When using CVS, you can optionally specify the repository name. The current directory name is used by default. The vendor and release tags will be set to the $USER environment variable and "START" for CVS. Note when importing into CVS that the current directory does NOT become an active sandbox, unlike importing into RCS. For CVS you must check out a new sandbox after import and may remove the original directory. if .cvsignore is present, filenames in it, one per line, will be ignored during import. * for RCS, emacs ~ files, .\# files and \#...\# files are ignored. for CVS, these are expected to be in the cvsroot/cvsignore file. Symbolic links are not followed. =item B filenames Bring the working directory up to date with changes from the repository. Under RCS, this correctly displays CVS-style status for files in the current directory, paying attention to .cvsignore files, and resurrecting files which have been 'lost'. if .cvsignore is present, filenames in it, one per line, will be ignored during update. B: The ideal situation is that in which B says nothing at all. Synonyms: upd up =item B [-d CVSROOT] B new_repository_name Convert the current RCS-controlled directory to CVS control NOTE: this involves 'hacking the repository' so you must be on the same filesystem as the CVSROOT. =item B new_name Set the repository name for the current sandbox to be new_name. This is rarely useful, but good to have. Synonyms: rep =item B new_CVSROOT Set the CVSROOT associated with the current sandbox to new_CVSROOT. This is most useful when relocating the CVSROOT. =item B Occasionally cvs will fail slightly during remote checkout, complaining about 'unexpected dying gasps from '. This will try to fix the resulting sandbox. =back =head2 managing FILES add checkout remove rename =over =item B [filenames] Put a file under version control for the first time. Synonyms: ad new For CVS, you need to be in a sandbox for an existing repository. For RCS, there needs to be an RCS directory already. (import will create one) if .cvsignore is present, filenames in it, one per line, will be ignored during adding of entire directories (as with import). =item B [filenames] check out a file or repository from version control. Synonyms: co get CVS and RCS mean slightly different things by checkout. Under RCS you get a fresh copy of a file. Under CVS you get an entire fresh sandbox. =item B filenames Remove files and directories from the source repository. In CVS, does not take effect until a commit on the same files. In RCS, puts files in the Attic -- a deviation from standard RCS. Synonyms: rm delete =item B old_name new_name Renames old_name to new_name in the repository and in the current directory. This is history preserving in that repository files are removed and then readded. Synonyms: ren mv =back =head2 REVISIONS of a file commit cu tag log message diff edit unedit rdiff =over =item B [filenames] "publish" changes to files under version control. Synonyms: checkin check ci if .cvsignore is present, filenames in it, one per line, will be ignored during checkin. B: this keeps a lock! See also the new command B. =item B filenames Check in and Unlock filenames. This is most useful in RCS, or when you regularly B before modifying files. It checks in and lets go in one step. if .cvsignore is present, filenames in it, one per line, will be ignored during checkin. Other Mnemonic: "see you!" :-) =item B [-r rev] filenames Specify a symbolic tag for files in the repository. By default this tags the latest revision. Setting a tag to a name already tagged for that file clobbers the old tag, marking the new revision with it (i.e. B style) Synonyms: ta freeze =item B [-r rev] filenames Display log information. If B<-r> revision is specified, then display only the message for that revision. Does not affect repository or working directory. Synonyms: lo rlog =item B [-r rev] filename Change the log message for a revision, by default the latest, of a file WARNING: this replaces the current log message! Do not destroy information! Synonyms: msg =item B [-r rev] [-r rev] [filenames] see differences between revisions, between the current revision and a particular one, or between the current and latest versions (depending on how many B<-r>s you specify). Synonyms: di dif rcsdiff =item B [filenames] Update files and lock them for editing. Synonyms: edit lock Under CVS does B THEN B. Under RCS does B. =item B filenames Revert filenames to those last checked into the repository, and unlock. This is usually done with either B or by removing the file from the sandbox and updating. Synonyms: unlock =item B [-r rev] [-r rev] filenames Prepare a collection of diffs as a patch file between two releases in the repository. Does not affect repository or working directory. Synonyms: patch pa =back =head1 BUGS Lots of bugs have been reported, and they are all listed here. Mostly I have not yet had time to fix them, or am not sure how. =over =item help is annoying in emacs. the dumb terminal emacs forces on people makes the "less" in the help mode not page right - requires return after the space. ugh. Not sure how to fix it. OTOH this is a command-line tool. Emacs has its own version control stuff... =item feature: vc up should tell you about new nonempty directories to -d I can add this when CVSROOT is not :ext: at any rate. =item vc rename should preserve sticky tags like -kb, yes, but also like branchpoints? renaming under cvs is best done by removing the file, then adding it under a new name. This preserves the property of being able to go back in time and see what you actually saw, and is thus better than hacking the repository even though you can no longer vc log to see older changes. Should rename preserve what branch the file lives on? probably. Should it preserve a sticky version number? probably not. =item vc add should detect binary files and add -kb automatically yes, it should. soon. =item vc message under emacs loses line breaks okay, there is no equivalent to vc message in emacs already, so I suppose this is a problem. I think the deal is that I have to pass the text to rcs, and I either do it wrong or there is no good way to do it. must explore. =item vc import vs. add vc add on something that needs to be imported should be smarter than cvs and actually ask if you meant to import a new thing rather than just saying you used the wrong command. There is basically no useful purpose for import as far as I can tell that would not be as well served by the "add" keyword. =item add ability to grep the underlying rcsfiles one of the "advantages" of rcs over cvs is apparently that if you want to find when you did something to what file, you can simply grep -r in the general vicinity of the change and get something reasonable. There is no good reason I should not support that if CVSROOT is not :ext:*. =item vc rename directories does not let you check in the removed files in those directories. oops. yep, that is true. workaround: vc up -d removed-directory, vc ci removed-directory, rm removed-directory. I will fix this soon. =back =head1 Author Gregory A. Marton L =begin html =head1 Download This program is distributed under the GNU General Public License: L The source: L =end html =cut ;