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.flavors import FLAVORS from bazbase import db, custom, structure from bazyaml import format from . import share class InvalidCommit(Exception): pass class format_handler(object): def __init__(self, cat, cwd, is_toplevel): self.cat = cat self.cwd = cwd self.is_toplevel = is_toplevel self.saw_parent = False def start_element(self,e,parent=None): if parent is not None: pare = structure.get_element(parent) elm = structure.get_element(e) if elm is None: pare.create_child(e) else: elm.set_parent(pare) self.present_props = set() def end_element(self, e): """Remove any props that weren't present.""" elm = structure.get_element(e) for pname in elm.list_props(): if pname not in self.present_props: elm.remove_prop(pname) def prop(self, e, p, contents): if p == u'parent': if not self.is_toplevel: raise InvalidCommit( "'parent' specified in non-toplevel element '%s'!" % e) # This is a toplevel element. parent = structure.get_element(contents) elm = structure.get_element(e) if elm is None: elm = parent.create_child(e) else: elm.set_parent(parent) elm.set_orgmode(u'toplevel') self.saw_parent = True else: if self.is_toplevel and not self.saw_parent: raise InvalidCommit("'parent' not specified at start of " "toplevel element %s/%s.yaml!" % (e, e)) self.present_props.add(p) if isinstance(contents, format.Include): fmt = contents.ext contents = contents.val else: contents = contents.encode('utf-8') fmt = 'creole' structure.get_element(e).set_prop(p, contents, fmt) 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 prop = structure.get_prop(p) if prop is None: structure.create_prop(p) def end_element(self, p): pass def prop(self,p,a,contents): getattr(structure.get_prop(p), 'set_' + a)( format.interpret_as_prop(a, contents)) 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): # Handled by loading the config. #import bazsql #bazsql.activate() 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 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 = [] delprops = [] 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] e = structure.get_element(delparent) if e is not None: parent_path = "%s/%s.yaml" % (fil, delparent) if hook._elementpath(e) == parent_path: # We deleted a whole tree. for descendant in [e] + e.get_descendants(): 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... parent = fil.rsplit('/', 1)[0] if parent not in changes or changes[parent][0] != 'D': for neighbor in tree(parent, non_recursive=True): if (neighbor.endswith('.yaml') and neighbor not in changes): changes[neighbor] = ('R', pysvn.node_kind.file, False, False) keys = changes.keys() # We want added props, then fewer path segments first, then defs # followed by the rest alphabetically, followed by deleted props 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('Object/')) keys.sort(key=lambda s:not s.startswith('props/')) print >>sys.stderr,"Keys: ",keys # 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/'))): e = structure.get_element(ename) if e is not None: oldpaths[ename] = hook._elementpath(e) 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' is_toplevel = False if len(path) <= 0: assert action in ('A','R') continue if (path[-1].rsplit('.', 1)[-1] != 'yaml' or len(path) <= 1 or ' ' in path[0] or '.' in path[0]): 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 = structure.Prop else: ename = path[-1].rsplit('.',1)[0] cls = structure.Element if action == 'D': if cls == structure.Prop: delprops.append(pname) else: if ename == u'Object': raise InvalidCommit( "You can't delete the root element.") if len(path) >= 2 and not is_def: parent = structure.get_element(path[-2]) elif len(path) >= 3 and is_def: parent = structure.get_element(path[-3]) else: assert is_def and len(path) == 2, (is_def, path) parent = None if parent is not None and parent.ename not in needsnokids: needskids.add(parent.ename) delfiles.insert(0, fil) else: assert action in ('A','R'),action if (cls == structure.Element and len(path) < 3 and is_def and ename != u'Object'): is_toplevel = True if action == 'A' and cls == structure.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: # Could be Object or another toplevel. parname = None if parname is not None: parent = structure.get_element(parname) if parent is None: raise InvalidCommit( "Parent '%s' for element '%s' undefined!" % (parname, ename)) else: parent = None e = structure.get_element(ename) if e is None: if parent is not None: parent.create_child(ename) elif ename == u'Object': structure.create_root_element(ename) 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)) if not is_toplevel: e.set_orgmode(u'normal') e.set_parent(parent) if not is_def: needsnokids.add(ename) if ename in needskids: needskids.remove(ename) if (is_def and len(path) != 2): needskids.add(ename) if ename in needsnokids: needsnokids.remove(ename) catio = codecs.getreader('utf-8')(StringIO(cat(fil))) # Now do the appropriate mods if cls == structure.Prop: format.parse(fil,prop_handler(),inf=catio) else: format.parse(fil, format_handler(cat, os.path.dirname(fil), is_toplevel), inf=catio) # Do cleanup for f in delfiles: ename = f.rsplit('/',1)[-1].rsplit('.',1)[0] print >>sys.stderr, "Deleting element %s." % ename e = structure.get_element(ename) db.edelete(e) if ename in needskids: needskids.remove(ename) if ename in needsnokids: needsnokids.remove(ename) for pname in delprops: db.pdelete(structure.get_prop(pname)) for nk in needskids: if len(structure.get_children(nk)) <= 0: raise InvalidCommit("If %s has no kids, it needs to be in the form %s.yaml, not %s/%s.yaml!"%((nk,)*4)) for nnk in needsnokids: if len(structure.get_children(nnk)) > 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,)*4)) object = structure.get_element(u'Object') if object is None: raise InvalidCommit("No root element Object!")