#! /usr/bin/env python """ svncleancopy.py [--modified] Make a clean copy of an existing working copy. Files not checked in from the source working copy are not copied over. Modified and added files are only copied with '--modified'. If the source working copy was too heavily modified (e.g. missing directories) you will need to issue 'svn up' or 'svn switch' before the new working copy is complete. Note that if the two working copies are on the same filesystem, hard links are used to reduce disk usage (in a way that should be fully transparent: only between the two copies of the '.svn/text-base' administrative area, not between the files outside '.svn'.). """ import os, sys, stat, re r_status_unmodified = re.compile(r"\s+\d+\s+\d+\s+\S+\s+(\S+)$") r_status_modified = re.compile(r"M\s+\d+\s+\d+\s+\S+\s+(\S+)$") r_status_added = re.compile(r"A\s+\d+\s+\?\s+\?\s+(\S+)$") r_status_external = re.compile(r"X\s+(\S+)$") def quote(s): return "'%s'" % s.replace("'", "'\\''") def svnwccpl(srcdir, dstdir, cache, include_modified=False): srcsvn = os.path.join(srcdir, '.svn') if not cache.isdir(srcsvn): print >> sys.stderr, "(skipping missing %r)" % (srcdir,) return 1 names = dict.fromkeys(cache.listdir(srcdir)) g = cache.do("svn status -N -v %s" % quote(srcdir)) unmodified_or_external = [] for line in g: line = line.rstrip('\n') match = (r_status_unmodified.match(line) or r_status_external.match(line)) if include_modified and not match: match = (r_status_modified.match(line) or r_status_added.match(line)) if match: fullpath = match.group(1) assert fullpath.startswith(srcdir) name = fullpath[len(srcdir):].lstrip('/') unmodified_or_external.append(name) dstsvn = os.path.join(dstdir, '.svn') cache.mkdir(dstdir) cache.mkdir(dstsvn) copy_dotsvn(srcsvn, dstsvn, cache) for name in unmodified_or_external: if name in names: src = os.path.join(srcdir, name) dst = os.path.join(dstdir, name) st = cache.lstat(src) if stat.S_ISREG(st.st_mode): _copyfile(src, dst, cache) elif stat.S_ISDIR(st.st_mode): svnwccpl(src, dst, cache, include_modified) else: continue # preserve permissions cache.chmod(dst, stat.S_IMODE(st.st_mode)) return 0 def _copyfile(src, dst, cache): cache.write(dst, cache.read(src)) def copy_dotsvn(srcsvn, dstsvn, cache, top=True, linkok=False): for name in cache.listdir(srcsvn): src = os.path.join(srcsvn, name) dst = os.path.join(dstsvn, name) st = cache.lstat(src) if stat.S_ISREG(st.st_mode): if linkok and name.endswith('.svn-base'): try: cache.link(src, dst) continue except OSError: pass _copyfile(src, dst, cache) elif stat.S_ISDIR(st.st_mode): cache.mkdir(dst) copy_dotsvn(src, dst, cache, top = False, linkok = (top and name == 'text-base')) else: raise OSError("Cannot handle special file %r" % (src,)) # preserve permissions cache.chmod(dst, stat.S_IMODE(st.st_mode)) class Cache(object): def __init__(self): self.writing = False self.cache_listdir = {} self.cache_stat = {} self.cache_lstat = {} self.cache_popen = {} self.cache_read = {} def isdir(self, path): try: st = self.stat(path) except OSError: return False return stat.S_ISDIR(st.st_mode) def listdir(self, path): try: return self.cache_listdir[path] except KeyError: dir = os.listdir(path) self.cache_listdir[path] = dir return dir def stat(self, path): try: return self.cache_stat[path] except KeyError: st = os.stat(path) self.cache_stat[path] = st return st def lstat(self, path): try: return self.cache_lstat[path] except KeyError: st = os.lstat(path) self.cache_lstat[path] = st return st def do(self, cmd): try: return self.cache_popen[cmd] except KeyError: g = os.popen(cmd) lines = g.readlines() g.close() self.cache_popen[cmd] = lines return lines def mkdir(self, path): if self.writing: os.mkdir(path) def chmod(self, path, mod): if self.writing: os.chmod(path, mod) def read(self, path): try: return self.cache_read[path] except KeyError: f = open(path, 'rb') data = f.read() f.close() self.cache_read[path] = data return data def write(self, path, data): if self.writing: g = open(path, 'wb') g.write(data) g.close() def link(self, src, dst): if self.writing: os.link(src, dst) if __name__ == '__main__': include_modified = len(sys.argv) > 1 and sys.argv[1] == '--modified' if include_modified: del sys.argv[1] if len(sys.argv) != 3: print __doc__ sys.exit(2) srcdir, dstdir = sys.argv[1:] srcdir = srcdir.rstrip('/') dstdir = dstdir.rstrip('/') if os.path.exists(dstdir): print >> sys.stderr, "Cannot create directory %r: File exists" % ( dstdir,) sys.exit(1) if not os.path.isdir(os.path.dirname(dstdir) or os.curdir): print >> sys.stderr, ("Cannot create directory %r: " "Parent dir does not exists" % (dstdir,)) sys.exit(1) # print >> sys.stderr, "Reading %r..." % (srcdir,), cache = Cache() res = svnwccpl(srcdir, dstdir, cache, include_modified) if res != 0: print >> sys.stderr, "No source found" sys.exit(res) print >> sys.stderr, "OK" # print >> sys.stderr, "Writing %r..." % (dstdir,), cache.writing = True res = svnwccpl(srcdir, dstdir, cache, include_modified) assert res == 0 print >> sys.stderr, "OK" # ## print >> sys.stderr, "Restoring any modified file...", ## g = os.popen("svn revert -R %s" % quote(dstdir)) ## firstline = True ## for line in g: ## if firstline: ## print >> sys.stderr ## firstline = False ## sys.stderr.write(' ' + line) ## g.close() ## if firstline: ## print >> sys.stderr, "OK"