#
# Requires Python 2.5 or newer.
#
-import sys, os, cStringIO, subprocess
-import time, calendar, re, signal, copy, shlex
-import email.utils
+"""
+Usage: rcs-fast-import [-v] [-u] [-l] [-d] [-n] [-V]
+
+Break the content of a git-fast-import stream on stdin into individual RCS
+master files in RCS subdirectories of the corresponding directory structure.
+
+With the -v option, emit a twirling-baton prompt during stream read, and
+progress messages during RCS writes.
+
+With the -l option, check out and lock all resulting files at their latest
+revisions; with the -u option, check out but do not lock.
+
+The -V option displays the program version number and exits. -? displays
+this help and exits.
+
+The -d option enables delete operations in the the input
+stream. Normally, delete operations in the input stream are ignored so
+as to maximize access to the unpacked history.
+
+The -n option sets dry-run mode; parse the import stream, then go through
+all logic other than actually creating files. Useful for debugging in
+combination with -v.
+
+R file operations rename corresponding RCS master files; C file operations
+copy them.
+
+If D, R, or C operations are present, the conversion will not round-trip
+properly (that is, rcs-fast-export | rcs-fast-import won't be an exact
+copy) with differences near deleted, renamed, and copied files.
+
+"""
+
+import sys, os, getopt, re, cStringIO, subprocess, shutil
+import time, calendar, copy, shlex, email.utils
version="0.1"
+#
+# Generic import-stream machinery swiped from reposurgeon begins here.
+#
+
verbose = 0
DEBUG_SHUFFLE = 2 # Debug directory handling
DEBUG_DELETE = 2 # Debug canonicalization after deletes
def __init__(self):
self.ref = None
self.committish = None
- def __str__(self):
- st = "reset %s\n" % self.ref
- if self.committish:
- st += "from %s\n\n" % self.committish
- return st
class FileOp:
"Represent a primitive operation on a file."
self.op = "deleteall"
else:
raise FatalException("unexpected fileop %s while parsing" % opline)
- def paths(self):
- "return the set of all paths touched by this file op."
- if self.op in ("M", "D"):
- return set([self.path])
- elif self.op in ("R", "C"):
- return set([self.source, self.target])
- def relevant(self, other):
- "Do two fileops touch the same file(s)?"
- if self.op == "deleteall" or other.op == "deleteall":
- return True
- else:
- return self.paths() & other.paths()
- return st
class Commit:
"Generic commit object."
def parents(self):
"Get a list of this commit's parents."
return filter(lambda e:e.mark in self.parent_marks,self.repo.commits())
- def cliques(self):
- "Return a dictionary mapping filenames to associated M cliques."
- cliques = {}
- for (i, fileop) in enumerate(self.fileops):
- if fileop.op == "M":
- if fileop.path not in cliques:
- cliques[fileop.path] = []
- cliques[fileop.path].append(i)
- return cliques
class Passthrough:
"Represents a passthrough line."
def announce(msg):
print "rcs-fast-import:", msg
+def master(filename):
+ "Return the master file corresponding to a specified filename."
+ return "RCS/%s,v" % filename
+
def do_or_die(cmd, legend=""):
"Either execute a command or raise a fatal exception."
if legend:
except OSError, e:
raise FatalException("execution of %s%s failed: %s" % (cmd, legend, e))
+#
+# All RCS-specific code is beyond this point.
+# It wouldn't be difficult to adapt this for SCCS, if there were any need.
+#
+
+def modify_file(path, mode, copy):
+ "Perform RCS file modifications,"
+ rcsdir = os.path.join(os.path.dirname(path), "RCS")
+ if not os.path.exists(rcsdir):
+ try:
+ os.mkdir(rcsdir)
+ except OSError:
+ raise FatalException("can't create %s." % rcsdir)
+ # FIXME
+
if __name__ == '__main__':
+ (options, arguments) = getopt.getopt(sys.argv[1:], "dlnuvV?")
+ do_deletes = do_locked_checkouts = do_unlocked_checkouts = False
+ dryrun = False
+ for (switch, val) in options:
+ if (switch == '-?'):
+ print __doc__
+ raise SystemExit, 0
+ elif (switch == '-d'):
+ do_deletes = True
+ elif (switch == '-l'):
+ do_locked_checkouts = True
+ elif (switch == '-n'):
+ dryrun = True
+ elif (switch == 'u'):
+ do_unlocked_checkouts = True
+ elif (switch == '-v'):
+ verbose += 1
+ elif (switch == '-V'):
+ print "rcs-fast-import version %s" % version
+
repo = Repository()
repo.fast_import(sys.stdin)
+ try:
+ for (i, event) in enumerate(repo.events):
+ if isinstance(event, Blob) or isinstance(event, Reset) or isinstance(event, Passthrough):
+ continue
+ elif isinstance(event, Tag):
+ # Tagger and comment info is lost.
+ if verbose:
+ announce("Tagging %s with %s" % (event.committish, event.name))
+ elif isinstance(event, Commit):
+ for op in event.fileops:
+ if op.op == "M":
+ if verbose:
+ announce("%5d M %s" % (i+1, op.path))
+ if not dryrun:
+ modify_file(op.path, op.mode, op.copyname)
+ elif op.op in ("D", "deleteall"):
+ masterfile = master(op.path)
+ # If we delete masters, we lose ability to
+ # retrieve old versions.
+ if do_deletes:
+ if verbose:
+ announce("%5d D %s" % (i+1, op.path))
+ if not dryrun:
+ os.remove(masterfile)
+ elif verbose:
+ announce("%5d D %s ignored" % (i+1, masterfile))
+ elif op.op == "R":
+ # Dodgy, but since user is more likely to be
+ # interested in later versions this is better
+ # than doing nothing.
+ if verbose:
+ announce("%5d R '%s' '%s'" % (i+1, op.source, op.target))
+ if not dryrun:
+ os.rename(master(op.source), master(op.target))
+ elif op.op == "C":
+ # The copy will get the original's history.
+ # There's a case that what we ought to do instead
+ # is check out the tip rev and use it to initialize
+ # a new master.
+ if verbose:
+ announce("%d C '%s' '%s'" % (i+1, op.source, op.target))
+ if not dryrun:
+ shutil.copy(master(op.source), master(op.target))
+ else:
+ raise FatalException("unknown op type %s" % op.op)
+ if do_locked_checkouts or do_unlocked_checkouts:
+ # FIXME
+ pass
+ raise SystemExit, 0
+ except FatalException, e:
+ complain(e.msg)
+ repo.cleanup()
+ raise SystemExit, 1
+ except OSError, e:
+ complain(e)
+ repo.cleanup()
+ raise SystemExit, 1
+
# end