from __future__ import with_statement import os, sys, subprocess from cStringIO import StringIO import codecs import pysvn import bazsvn from bazsvn import hook from bazbase import NoResultFound from bazbase.flavors import FLAVORS, str_to_bool from bazbase import db, custom, model from . import share, format class InvalidCommit(Exception): pass class format_handler(object): def __init__(self, cat, cwd): self.cat = cat self.cwd = cwd def start_element(self,e,parent=None): if parent is not None: pare = model.Element.get(parent) try: elm = model.Element.get(e) except NoResultFound: model.Element(e,pare) else: elm.parent = pare self.present_props = set() def end_element(self, e): """Remove any props that weren't present.""" elm = model.Element.get(e) for pname in elm: if pname not in self.present_props: del elm[pname] def prop(self, e, p, contents): self.present_props.add(p) prop = model.Prop.get(p) model.Element.get(e).set_value_for_prop(prop, contents) def get_include(self, path): try: return self.cat(os.path.join(self.cwd, path)) except pysvn.ClientError: # This is generally a File Not Found return None class prop_handler(object): def start_element(self,p,parent=None): assert parent is None model.Prop.set_or_create(p) def end_element(self, p): pass def prop(self,p,a,contents): assert a in ('flavor', 'default', 'visible', 'comment') if a == 'visible': val = str_to_bool(contents) elif a == 'default': val = contents else: val = unicode(contents, 'utf-8') setattr(model.Prop.get(p), a, val) def make_svnlook_cat(repos): def cat(path): cmd = ['svnlook', 'cat', repos, path] p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) out, err = p.communicate() if p.returncode != 0: raise EnvironmentError(p.returncode, err) return out return cat def make_svnlook_tree(repos, transid=None): def tree(path, non_recursive=False): cmd = ['svnlook', 'tree', repos] if transid is not None: cmd += ['-t', transid] cmd += [path, '--full-paths'] if non_recursive: cmd.append('--non-recursive') p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) out, err = p.communicate() if p.returncode != 0: raise EnvironmentError(p.returncode, err) return (unicode(p, 'utf-8') for p in out.split()) return tree def prefixes(path): while '/' in path: path, chopped = path.rsplit('/', 1) print >>sys.stderr, path yield path def precommit(repos, transid): print >>sys.stderr,"Starting..." trans = pysvn.Transaction(repos, transid) username = trans.revpropget('svn:author') log_message = trans.revpropget('svn:log') if log_message.startswith('Web Edit'): # It was caused by our baz hook, so don't send it back return apply_changes(trans.changed(), username, cat=lambda f: trans.cat(f), tree=make_svnlook_tree(repos, transid)) def regen_database(): from . import custom as bazsvn_custom with db.begin_transaction(): model.clear() tree = make_svnlook_tree(bazsvn_custom.REPOSITORY) #all = tree('') #changed = dict((f, ('A', # pysvn.node_kind.dir if f.endswith('/') # else pysvn.node_kind.file, # True, True)) for f in all) add_dir = ('A', pysvn.node_kind.dir, True, True) changed = {u'props': add_dir, u'Object': add_dir} apply_changes(changed, 'regen_database', cat=make_svnlook_cat(bazsvn_custom.REPOSITORY), tree=tree) def apply_changes(orig_changes, username, cat, tree): bazsvn.custom.get_username = lambda: username share.make_svndriven() if hook == custom.version_control_hook: custom.version_control_hook = None # delfiles is a list in reverse depth-first order; it's deleted in order delfiles = [] needskids = set() needsnokids = set() with db.begin_transaction(): changes = dict(orig_changes) # Recurse down added directories for fil in orig_changes: action,kind,text_mod,prop_mod = changes[fil] if kind == pysvn.node_kind.dir: if action == 'A': for cont in tree(fil): if cont.endswith('/'): cont = cont[:-1] ckind = pysvn.node_kind.dir else: ckind = pysvn.node_kind.file changes[cont] = (action, ckind, True, False) elif action == 'D': # Directory deletes are special delparent = fil.rsplit('/', 1)[-1] try: e = model.Element.get(delparent) except NoResultFound: pass else: parent_path = "%s/%s.yaml" % (fil, delparent) if hook._elementpath(e) == parent_path: # We deleted a whole tree. for descendant in e.withDescendants(): changes[hook._elementpath(descendant)] = ( action, pysvn.node_kind.file, True, False) elif kind == pysvn.node_kind.file and not fil.endswith('.yaml'): # Could be a changing include... for neighbor in tree(fil.rsplit('/', 1)[0], non_recursive=True): if neighbor.endswith('.yaml') and neighbor not in changes: changes[neighbor] = ('R', pysvn.node_kind.file, False, False) keys = changes.keys() print >>sys.stderr,"Keys: ",keys # We want props, then fewer path segments first, then defs, then alphabetical keys.sort() keys.sort(key=lambda s:s.count('/') < 1 or s.split('/')[-1] != s.split('/')[-2]+'.yaml') keys.sort(key=lambda s:s.count('/')) keys.sort(key=lambda s:not s.startswith('props/')) # Precalculate old paths, because after we've made chances some # of these could change. oldpaths = {} for ename in (k.rsplit('/', 1)[-1].rsplit('.yaml', 1)[0] for k in keys if (k.endswith('.yaml') and not k.startswith('props/'))): try: oldpaths[ename] = hook._elementpath(model.Element.get(ename)) except NoResultFound: pass while len(keys) > 0: fil = keys.pop(0) action,kind,text_mod,prop_mod = changes[fil] print >>sys.stderr, "Considering %s %s... (%s)" % ( action, fil, kind) path = fil.split('/') is_def = len(path) >= 2 and path[-1] == path[-2]+'.yaml' if len(path) <= 0: assert action in ('A','R') continue if (path[-1].rsplit('.', 1)[-1] != 'yaml' or len(path) <= 1 or path[0] not in ('Object', 'props')): continue assert kind == pysvn.node_kind.file if (path[0] == 'props'): if len(path) != 2: # prop files only matter in props/ continue pname = path[-1][:-5] cls = model.Prop else: ename = path[-1].rsplit('.',1)[0] cls = model.Element if action == 'D': if cls == model.Prop: db.delprop(model.Prop.get(pname)) else: if len(path) >= 2 and not is_def: parent = model.Element.get(path[-2]) elif len(path) >= 3 and is_def: parent = model.Element.get(path[-3]) else: parent = None if parent is not None and parent not in needsnokids: needskids.add(parent) delfiles.insert(0, fil) else: assert action in ('A','R'),action if action == 'A' and cls == model.Element: if not is_def: assert len(path) > 1 parname = path[-2] else: assert path[-2] == ename if len(path) >= 3: parname = path[-3] else: assert ename == u'Object' parname = None if parname is not None: try: parent = model.Element.get(parname) except NoResultFound: raise InvalidCommit( "Parent '%s' for element '%s' undefined!" % (parname, ename)) else: parent = None try: e = model.Element.get(ename) except NoResultFound: e = model.Element(ename,parent) else: # Make sure our old loc is deleted oldpath = oldpaths[ename] if oldpath in delfiles: delfiles.remove(oldpath) elif (oldpath in keys and changes[oldpath][0] == 'D'): keys.remove(oldpath) elif fil != oldpath: # If you're moving something to where it should # have been all along, you're fixing something, # so be quiet. Otherwise, error. raise InvalidCommit("Path '%s' added, but the exising location for this element, '%s', was not removed!" % (fil,oldpath)) e.parent = parent if not is_def: needsnokids.add(e) if e in needskids: needskids.remove(e) if (is_def and len(path) != 2): needskids.add(e) if e in needsnokids: needsnokids.remove(e) catio = codecs.getreader('utf-8')(StringIO(cat(fil))) # Now do the appropriate mods if cls == model.Prop: format.parse(fil,prop_handler(),inf=catio) else: format.parse(fil, format_handler(cat, os.path.dirname(fil)), inf=catio) # Do cleanup for f in delfiles: ename = f.rsplit('/',1)[-1].rsplit('.',1)[0] print >>sys.stderr, "Deleting element %s." % ename e = model.Element.get(ename) db.edelete(e) if e in needskids: needskids.remove(e) if e in needsnokids: needsnokids.remove(e) for nk in needskids: if nk.countDescendants() <= 0: raise InvalidCommit("If %s has no kids, it needs to be in the form %s.yaml, not %s/%s.yaml!"%((nk.ename,)*4)) for nnk in needsnokids: if nnk.countDescendants() > 0: raise InvalidCommit("If %s has kids and is not toplevel, it needs to be in the form %s/%s.yaml, not %s.yaml!"%((nk.ename,)*4)) try: model.Element.get(u'Object') except NoResultFound: raise InvalidCommit("No root element Object!")