#! /usr/bin/env python import os, sys, stat, md5, posixpath, fnmatch import pysvn.ra from pysvn.delta import Delta # ____________________________________________________________ REPO_URL = 'svn:///home/arigo/svn/arigo/hack/svnutil/repo-sradmin/root' TRACKPATHS = ['/etc', '/usr/share/X11/xkb'] IGNORES = ['*~'] def get_repo_connx(): repo = pysvn.ra.connect(REPO_URL) return repo, repo.get_latest_rev() class NoPermission(OSError): pass def warn(e): if not QUIET and not isinstance(e, NoPermission): print >> sys.stderr, '[warning] %s: %s' % (e.__class__.__name__, e) def quote(s): return "'%s'" % (s.replace("'", "'\\''"),) def uidname(uid, _cache={}): try: return _cache[uid] except KeyError: import pwd try: name = pwd.getpwuid(uid)[0] except KeyError: name = str(uid) assert name.isalnum(), (uid, name) _cache[uid] = name return name def gidname(gid, _cache={}): try: return _cache[gid] except KeyError: import grp try: name = grp.getgrgid(gid)[0] except KeyError: name = str(gid) assert name.isalnum(), (gid, name) _cache[gid] = name return name def reprmode(st_mode): if stat.S_ISREG(st_mode): ckind = '-' elif stat.S_ISDIR(st_mode): ckind = 'd' elif stat.S_ISLNK(st_mode): ckind = 'l' else: ckind = '!' letters = [ckind] for shift in [6, 3, 0]: mode = st_mode >> shift if mode & 4: letters.append('r') else: letters.append('-') if mode & 2: letters.append('w') else: letters.append('-') if mode & 1: letters.append('x') else: letters.append('-') return ''.join(letters) # ____________________________________________________________ class FSNode: def __init__(self, fspath): self.fspath = fspath self.st = os.lstat(fspath) if stat.S_ISREG(self.st.st_mode): self.kind = 'file' elif stat.S_ISDIR(self.st.st_mode): self.kind = 'dir' elif stat.S_ISLNK(self.st.st_mode): self.kind = 'symlink' else: raise OSError("unsupported file kind") def listdir(self): assert self.kind == 'dir' result = {} if not os.access(self.fspath, os.R_OK): return result try: names = os.listdir(self.fspath) except (IOError, OSError), e: warn(e) else: for name in names: try: result[name] = FSNode(os.path.join(self.fspath, name)) except (IOError, OSError), e: warn(e) return result def getdatablocks(self): if self.kind == 'file': if not os.access(self.fspath, os.R_OK): raise NoPermission(self.fspath) f = open(self.fspath, 'rb') while 1: data = f.read(32768) if not data: break yield data f.close() elif self.kind == 'symlink': yield 'link ' + os.readlink(self.fspath) else: raise Exception('wrong kind') def getchecksum(self): m = md5.md5() try: for block in self.getdatablocks(): m.update(block) except (IOError, OSError), e: warn(e) return 'unreadable' return m.hexdigest() def getmetadata(self): st = self.st return '%s %s %s' % (reprmode(st.st_mode), uidname(st.st_uid), gidname(st.st_gid)) def getrakind(self): if self.kind == 'dir': return 'dir' else: return 'file' def make_commit_delta(self, oldrev): delta = Delta(self.fspath.strip('/'), self.getrakind(), oldrev, 'HEAD') if oldrev is None: if self.kind == 'symlink': delta.propdelta['svn:special'] = '*' self.delta_update_metadata(delta) if self.kind != 'dir': self.delta_update_content(delta, basetext='') return delta def delta_update_metadata(self, delta): delta.propdelta['st'] = self.getmetadata() def delta_update_content(self, delta, basetext): try: newtext = ''.join(self.getdatablocks()) except (IOError, OSError), e: warn(e) delta.propdelta['unreadable'] = '*' else: delta.makedelta(basetext, newtext) class RANode: def __init__(self, connx, rapath, rakind='dir'): self.connx = connx self.rapath = rapath ra, rev = connx if rakind == 'dir': self.kind = 'dir' _, self.props, self.entries = ra.get_dir(self.rapath, rev, want_props=True, want_contents=True) else: self.checksum, _, self.props, _ = ra.get_file(self.rapath, rev, want_props=True, want_contents=False) if self.props.get('svn:special'): self.kind = 'symlink' else: self.kind = 'file' def listdir(self): assert self.kind == 'dir' result = {} for name, statdict in self.entries.items(): result[name] = RANode(self.connx, posixpath.join(self.rapath, name), rakind = statdict['svn:entry:kind']) return result def getdatablocks(self): ra, rev = self.connx _, _, _, data = ra.get_file(self.rapath, rev, want_props=False, want_contents=True) return [data] def getchecksum(self): assert self.kind != 'dir' if self.props.get('unreadable'): return 'unreadable' else: return self.checksum def getmetadata(self): return self.props.get('st', '') def getignores(self): lines = self.props.get('svn:ignore', '').splitlines() return [line for line in lines if line] def getrev(self): ra, rev = self.connx return rev def enum_status(path, wc, rn, parentignores=[]): c_content = ' ' c_meta = ' ' if wc is None: c_content = '!' elif rn is None: for pattern in IGNORES + parentignores: if fnmatch.fnmatch(os.path.basename(path), pattern): return c_content = '?' elif wc.kind != rn.kind: c_content = '~' else: if wc.kind == 'dir': lst1 = wc.listdir() lst2 = rn.listdir() names = lst1.copy() names.update(lst2) names = names.keys() names.sort() ignores = rn.getignores() for name in names: for item in enum_status(os.path.join(path, name), lst1.get(name), lst2.get(name), ignores): yield item else: if wc.getchecksum() != rn.getchecksum(): c_content = 'M' if wc.getmetadata() != rn.getmetadata(): c_meta = 'M' if c_content != ' ' or c_meta != ' ': yield (c_content, c_meta, path, wc, rn) def commit_from_status(callback, logmsg, verbose=False, paths=()): connx = get_repo_connx() deltas = [] for trackpath in paths or TRACKPATHS: wc = FSNode(trackpath) rn = RANode(connx, trackpath.strip('/')) for item in enum_status(trackpath, wc, rn): changes = False for delta in callback(*item): #print delta deltas.append(delta) changes = True if verbose: c_content, c_meta, path, _, _ = item if c_content != ' ' or c_meta != ' ' or changes: if changes: color = color_bold else: color = '' print '%s%c%c\t%s%s' % (color, c_content, c_meta, path, color_stop) commit(connx, deltas, logmsg) color_bold = '\033[1m' color_red = '\033[31m\033[1m' color_stop = '\033[0m' # ____________________________________________________________ def commit(connx, deltas, logmsg): if deltas: answer = raw_input('sradmin: commit %d changes? ' % (len(deltas),)) if not answer.lower().startswith('y'): return ra, rev = connx newrev, _ = ra.commit(LOGMSG or logmsg, deltas, {'': rev}) print 'sradmin: committed revision %d.' % (newrev,) def cmd_status(*paths): connx = get_repo_connx() for trackpath in paths or TRACKPATHS: wc = FSNode(trackpath) rn = RANode(connx, trackpath.strip('/')) for c_content, c_meta, path, _, _ in enum_status(trackpath, wc, rn): print '%c%c\t%s' % (c_content, c_meta, path) cmd_st = cmd_status def cmd_ignore(*paths): for path1 in paths: for trackpath in TRACKPATHS: if os.path.dirname(path1).startswith(trackpath): break else: raise Exception("path not tracked: %r" % (path1,)) connx = get_repo_connx() ra, rev = connx deltas = [] for path1 in paths: dir1 = os.path.dirname(path1) name1 = os.path.basename(path1) rn = RANode(connx, dir1.strip('/')) if name1 not in rn.getignores(): svnignore = rn.props.get('svn:ignore', '') if not svnignore.endswith('\n'): svnignore += '\n' svnignore += name1 svnignore += '\n' delta = Delta(rn.rapath, 'dir', rev, 'HEAD') delta.propdelta['svn:ignore'] = svnignore #print delta deltas.append(delta) kind = ra.check_path(path1.strip('/')) if kind: delta = Delta(path1.strip('/'), kind, rev, None) # svn rm deltas.append(delta) commit(connx, deltas, "cmd_ignore") def _addall(c_content, c_meta, path, wc, rn): if c_content == '?': yield wc.make_commit_delta(oldrev=None) def cmd_addall(): commit_from_status(_addall, "cmd_addall", verbose=True) def _rmall(c_content, c_meta, path, wc, rn): if c_content == '!': ra, oldrev = rn.connx yield Delta(path.strip('/'), rn.kind, oldrev, None) def cmd_rmall(): commit_from_status(_rmall, "cmd_rmall", verbose=True) def _commit(c_content, c_meta, path, wc, rn): if c_content == 'M' or c_meta == 'M': delta = Delta(path.strip('/'), wc.getrakind(), rn.getrev(), 'HEAD') if c_meta == 'M': wc.delta_update_metadata(delta) if c_content == 'M': basetext = ''.join(rn.getdatablocks()) wc.delta_update_content(delta, basetext) del basetext yield delta def cmd_commit(*paths): commit_from_status(_commit, "cmd_commit", verbose=True, paths=paths) def _commitall(c_content, c_meta, path, wc, rn): for op in [_addall, _rmall, _commit]: for delta in op(c_content, c_meta, path, wc, rn): yield delta def cmd_commitall(): commit_from_status(_commitall, "cmd_commitall", verbose=True) def cmd_create(): connx = get_repo_connx() ra, rev = connx deltas = [] for trackpath in TRACKPATHS: check = '' for component in trackpath.split('/'): if component: check = posixpath.join(check, component) if ra.check_path(check) != 'dir': delta = Delta(check, 'dir', None, 'HEAD') deltas.append(delta) print 'A %s' % (check,) commit(connx, deltas, "cmd_create") def cmd_diff(*paths): seen = [] def diff(c_content, c_meta, path, wc, rn): if c_content != ' ' or c_meta != ' ': if not seen: print '='*79 print 'Index:', path print if c_meta != ' ': print 'PERMISSIONS CHANGED' print '-\t' + rn.getmetadata() print '+\t' + wc.getmetadata() print if c_content == ' ': pass elif c_content == '!': print 'REMOVED' print elif c_content == '?': print 'ADDED' print elif c_content == 'M': if not QUIET: tmpfile = '/tmp/sradmin-repository' f = open(tmpfile, 'w') f.writelines(rn.getdatablocks()) f.close() sys.stdout.flush() os.system("diff -u %s %s" % (quote(tmpfile), quote(wc.fspath))) os.unlink(tmpfile) else: assert False, repr(c_content) print '='*79 seen.append(path) return [] commit_from_status(diff, None, paths=paths) QUIET = False LOGMSG = None if __name__ == '__main__': while sys.argv[1].startswith('-'): opt = sys.argv.pop(1) if opt == '-q': QUIET = True elif opt == '-m': LOGMSG = sys.argv.pop(1) else: raise Exception("unknown option: %r" % (opt,)) cmd = sys.argv[1] globals()['cmd_' + cmd](*sys.argv[2:])