#! /usr/bin/env python """# ____________________________________________________________ # # usage: atomicmoves.py # ____________________________________________________________ # # This is an example of a command-file. It is basically a sequence of # 'cp' and 'rm' commands, which are atomically committed in a single new # revision. Unlike the 'svn' client, it allows 'rm' to be followed by # a 'cp' that immediately replaces the entry with a new one. # Specify the root -- which must be a svn:// or svn+ssh:// url -- and # its expected head revision. root svn+ssh://snake/home/arigo/SvnRootTest/my/tmp 86 # Paste a subdir of the trunk to branch rm dir branch/nbd/md5sum cp dir hacks/nbd/md5sum branch/nbd/md5sum # Restore the previous version of a file rm file hacks/quantum.py cp file hacks/quantum.py@85 hacks/quantum.py """ import sys, posixpath, os import pysvn.ra, pysvn.delta def main(f): def error(msg, *args): raise ValueError("line %d: %s" % (linenum, msg % args)) def cmdargs(input, cmd, nbargs): assert input[0] == cmd if len(input) != nbargs+1: error("%r requires exactly %d argument%s", cmd, nbargs, "s"[:nbargs>1]) return input[1:] lines = [] roots = [] linenum = 0 for line in f: linenum += 1 line = line.strip() if line.startswith('#'): continue input = line.split() if not input: continue if input[0] == 'root': root, rootrev = cmdargs(input, 'root', 2) if rootrev != 'HEAD': rootrev = int(rootrev) roots.append((root, rootrev)) continue elif input[0] == 'rm': kind, fn2 = cmdargs(input, 'rm', 2) fn1 = None elif input[0] == 'cp': kind, fn1, fn2 = cmdargs(input, 'cp', 3) else: error("unknown command %r", input[0]) if kind not in ('file', 'dir'): error("kind must be file or dir") if (fn1 and fn1.startswith('/')) or fn2.startswith('/'): error("absolute path not allowed") if '@' in fn2: error("cannot specify revision here") lines.append((kind, fn1, fn2)) if not roots: raise EOFError("no 'root' specified") root, rootrev = roots[0] if roots != [(root, rootrev)]*len(roots): raise ValueError("multiple 'root' not allowed") if not lines: a = raw_input("Nothing to do! Are you sure you want to continue? ") if not a.upper().startswith('Y'): raise EOFError("nothing to do") if rootrev == 'HEAD': repo = pysvn.ra.connect(root) rootrev = repo.get_latest_rev() else: repo = None deltas = [] SEP = '-- this line, and those below, ignored --' loglines = ['', SEP, ''] for kind, fn1, fn2 in lines: if fn1 is None: # remove d = pysvn.delta.Delta(fn2, kind, rootrev, None) loglines.append('D %s' % fn2) else: # copyfrom if '@' in fn1: fn1, rev1 = fn1.split('@') rev1 = int(rev1) else: rev1 = rootrev d = pysvn.delta.Delta(fn2, kind, None, 'HEAD') d.copyfrom = (posixpath.join(root, fn1), rev1) loglines.append('A %s (from %s, r%d)' % ((fn2,)+d.copyfrom)) deltas.append(d) loglines.append('') logfn = 'atomicmoves-commit.tmp' n = 1 while os.path.exists(logfn): n += 1 logfn = 'atomicmoves-commit-%d.tmp' % n f = open(logfn, 'w') f.write('\n'.join(loglines)) f.close() editor = os.environ.get('SVN_EDITOR') if not editor: editor = os.environ.get('EDITOR', 'vi') os.system('%s %s' % (editor, logfn)) loglines = [] f = open(logfn, 'r') for line in f: if line.strip() == SEP: break loglines.append(line) f.close() os.unlink(logfn) log = ''.join(loglines) if not log.strip(): raise ValueError("no check-in message") if repo is None: repo = pysvn.ra.connect(root) newrev, date = repo.commit(log, deltas, {'': rootrev}) print '%s: committed revision %d.' % (sys.argv[0], newrev) if __name__ == '__main__': if len(sys.argv) != 2: print >> sys.stderr, __doc__ sys.exit(2) main(open(sys.argv[1], 'r'))