from __future__ import generators
import types
import sqlite
import os
import string
import sets

class dirtiableProperty(property):
    def __init__(self, name, docstr, default=None):
        def getter(inst):
            if hasattr(inst, '__'+name):
                return getattr(inst, '__'+name)
            return default
        def setter(inst, val):
            if inst.killed is True:
                raise RuntimeError, 'Object has been killed'
            inst.dirty()
            setattr(inst, '__'+name, val)
        property.__init__(self, getter, setter, None, docstr)
class writeOnceProperty(property):
    def __init__(self, name, docstr, default=None):
        def getter(inst):
            if hasattr(inst, '__'+name):
                return getattr(inst, '__'+name)
            return default
        def setter(inst, val):
            if inst.killed is True:
                raise RuntimeError, 'Object has been killed'
            if hasattr(inst, '__'+name):
                if getattr(inst,'__'+name, val) is val:
                    return
                raise RuntimeError, '"%s" already set' % name
            inst.dirty()
            setattr(inst, '__'+name, val)
        property.__init__(self, getter, setter, None, docstr)

class DBAccessObj(object):
    db = None
    config = None
    def getCursor(clazz):
        if DBAccessObj.db is None:
            if clazz.config is not None:
                must_init_db = not os.access(clazz.config.cat_path, os.F_OK)
                DBAccessObj.db = sqlite.connect(clazz.config.cat_path)
                if must_init_db:
                    clazz.initdb()
                else:
                    clazz.updatedb()
                return DBAccessObj.db.cursor()
            raise RuntimeError, 'no database selected'
        return DBAccessObj.db.cursor()
    getCursor = classmethod(getCursor)
    def updatedb(clazz):
        """If necessary, make changes to the database to account for format changes"""
        clazz.getCursor().execute("""
        -- make adds sort before changes
        update revision set revtype=10 where revtype=1;
        update revision set revtype=11 where revtype=0;
        update revision set revtype=12 where revtype=2;
        update revision set revtype=13 where revtype=3;
        """)
    updatedb = classmethod(updatedb)
    def initdb(clazz):
        clazz.getCursor().execute("""
--PRAGMA default_synchronous = OFF;

create table changeset
(
  csnum		integer not null,	-- identifies the changeset within its branch
  branch	text not null,		-- identifies the branch the changeset is on
  creator	text not null,		-- creator of this changeset
  startdate	integer not null,	-- first time within the window of this changeset
  enddate	integer not null,	-- last time within the window of this changeset
  log		text not null,		-- log text
  primary key (csnum, branch)
);

-- FIXME:
-- (branch,csnum) <-> id
-- (branch,csnum)|(id) -> creator, startdate, enddate, log

create table revision
(
  csnum		integer not null,	-- identifies the changeset number only, here for sorting purposes
  branch	text not null,		-- branch for which csnum identifies the changeset this revision belongs to
  filename	text not null,		-- identifies the file being changed
  revision	text not null,		-- identifies the CVS revision number
  revtype	integer not null,	-- identifies the revision type (add, update, remove, etc)
  addcount      integer,       		-- number of lines added, if applicable
  delcount	integer,		-- number of lines deleted, if applicable
  primary key (filename, branch, revision),
  unique (filename, branch, csnum),
  foreign key (branch, csnum) references changeset(branch, csnum)
);


-- the same tag can occur only once per branch.
create table tag
(
  tag		text not null,		-- name of the tag
  branch	text not null,		-- branch on which this tag exists at the specified changeset
  csnum		integer not null,	-- changeset which this tag refers to on this branch
  primary key (tag, branch),
  foreign key (branch, csnum) references changeset(branch, csnum)
);

-- per-repository configuration and state
create table repositorysettings
(
  key		text not null,		-- key identifying the value stored
  value		text,			-- value matched with the preceding key
  primary key (key)
)
	""")
    initdb = classmethod(initdb)

class DAO(object):
    def __init__(self):
        self.isDirty   = False
        self.isStored  = False
        self.killed    = False ## is this object supposed to be dead-and-gone?
    def _save(self):
        raise NotImplementedError
    def close(self):
        self.save()
    def save(self):
        if self.isBrandNew() or (self.isStored and (not self.isDirty)):
            return
        if self.killed:
            raise RuntimeError, 'trying to save no-longer-valid item'
        self._save()
    def clean(self):
        if not self.isDirty(): return
        if not self.isStored(): return
        self.save()
    def dirty(self):
        self.isDirty = True
    def isBrandNew(self):
        return not (self.isDirty or self.isStored)        
    def setDbMetadata(clazz, dbData):
        clazz.dbData = dbData
    setDbMetadata = classmethod(setDbMetadata)
    def getDbMetadata(clazz, dbData):
        clazz.dbData = dbData
    getDbMetadata = classmethod(getDbMetadata)

class CatalogDAO(DAO):
    def __init__(self, config):
        super(CatalogDAO, self).__init__()
	config.catalog = self
    def getBranchCount(self):
        """Return the number of branches in the catalog"""
        return len(self.getAllBranchesList())
    def getAuthorCount(self):
        """Return the number of authors in the catalog"""
        raise NotImplementedError
    def getAuthorBlame(self):
        """Determine how many changesets each author gets the blame for
	Uses the form {author:count}"""
	raise NotImplementedError
    def getAllBranchesIterator(self):
        """Return an iterator which will iterate over every available tag"""
        raise NotImplementedError
    def getAllBranchesList(self):
        """Return a list of available branches"""
        return list(self.getAllBranchesIterator())
    def getBranch(self, branchName):
        """Return the branch with the given name"""
        raise NotImplementedError
    def getLastUpdateTime(self):
        """Return the time of the last revision in the catalog"""
        raise NotImplementedError
    def getAllTags(self):
        """Return a data structure describing all tags in this repository
	   Uses the form {tag:{branch:changeset}}"""
	raise NotImplementedError
    def getCount(self):
        """Return the number of changesets (summed over all branches)"""
	raise NotImplementedError
    def alreadyHasRevision(self, rev):
        """True if we already have this revision, False otherwise"""
	raise NotImplementedError
    def getConfigItem(key):
    	"""Return the associated configuration value, or return None"""
    	raise NotImplementedError
    def setConfigItem(key, value):
    	"""Set the given key to the provided value, or delete the pair if value is None"""
	raise NotImplementedError
    def __getitem__(self, branchName):
        """Retrieve a branch"""
        return self.getBranch(branchName)
    branches    = property(lambda self: self.getAllBranchesList())
    time        = property(lambda self: self.getLastUpdateTime())
    tags	= property(lambda self: self.getAllTags())
    count	= property(lambda self: self.getCount())

class CatalogSQL(CatalogDAO, DBAccessObj):
    def __init__(self, config):
        CatalogDAO.__init__(self, config)
        self.getCursor().execute("delete from changeset where csnum in ( select distinct csnum from changeset except select distinct csnum from revision )")
    def __del__(self):
        if self.config.readOnly or self.config.dry_run != '': return
        try:
            self.db.commit()
        except Exception, e:
            print 'EXCEPTION SAVING DATABASE: ' + str(e)
    def getAuthorCount(self):
    	cursor = self.getCursor()
	cursor.execute("SELECT COUNT(*) FROM (SELECT DISTINCT creator FROM changeset WHERE CREATOR != 'none')")
	return int(cursor.fetchone()[0])
    def getAuthorBlame(self):
        cursor = self.getCursor()
	cursor.execute("SELECT creator, COUNT(*) FROM changeset GROUP BY creator ORDER BY creator")
	retval = {}
	for (author, count) in cursor.fetchall():
	    retval[author] = int(count)
	return retval
    def getAllTags(self):
        cursor = self.getCursor()
	cursor.execute("SELECT DISTINCT tag, branch, csnum FROM tag ORDER BY tag, branch")
	retval = {}
	for (tag, branch, csnum) in cursor.fetchall():
		if not retval.has_key(tag): retval[tag] = {}
		retval[tag][branch] = int(csnum)
	return retval
    def getCount(self):
        cursor = self.getCursor()
	cursor.execute("SELECT COUNT(*) FROM changeset")
	(retval,) = cursor.fetchone()
	return int(retval)
    def getAllBranchesIterator(self):
        cursor = self.getCursor()
        cursor.execute("SELECT DISTINCT branch FROM changeset ORDER BY branch")
        for (branch,) in cursor.fetchall():
            yield branch
    def hasBranch(self, branchName):
        cursor = self.getCursor()
        cursor.execute("SELECT DISTINCT branch FROM changeset WHERE branch=%(branch)s",
                       {'branch':branchName})
        return cursor.fetchone() is not None
    def getBranch(self, branchName):
        return BranchSQL(branchName)
    def getLastUpdateTime(self):
        cursor = self.getCursor()
        cursor.execute("SELECT MAX(enddate) FROM changeset")
        enddate = cursor.fetchone()[0]
        if enddate == None: enddate = 0
        return long(enddate)
    def getConfigItem(self, key):
    	cursor = self.getCursor()
	cursor.execute("SELECT value FROM repositorysettings WHERE key=%(key)s",
	               {'key':key})
	retval = cursor.fetchone()
	if retval is None: return None
	return retval[0]
    def setConfigItem(self, key, value):
    	cursor = self.getCursor()
	cursor.execute("DELETE FROM repositorysettings WHERE key=%(key)s",
	               {'key':key})
	cursor.execute("INSERT INTO repositorysettings(key, value) VALUES(%(key)s, %(value)s)",
	               {'key':key, 'value':str(value)})
    def alreadyHasRevision(self, rev):
        cursor = self.getCursor()
	cursor.execute("SELECT COUNT(*) FROM revision WHERE filename=%(filename)s AND revision=%(revision)s AND branch=%(branch)s",
	               {'filename':rev.filename,'revision':rev.revision,'branch':rev.branch})
	(retval,) = cursor.fetchone()
	return int(retval) > 0
    def _save(self):
        pass

class BranchDAO(DAO):
    def __init__(self):
        super(BranchDAO, self).__init__()
    def lastChangesetByAuthor(self, author):
        """Retrieve last changeset by this author"""
        raise NotImplementedError
    def getChangeset(self, id):
        """Retrieve a specific changeset based on its id"""
        raise NotImplementedError
    def getChangesetIterator(self):
        """Retrieve an iterator over all changesets in this branch"""
	for csid in self.getChangesetIdIterator():
	    yield self[csid]
    def getChangesetIdIterator(self):
        """Retrieve an iterator over all changeset IDs in this branch"""
        raise NotImplementedError
    def getChangesetForTag(self, tag):
        """Retrieve the changeset identified by this tag"""
        return self.getChangeset(self.getChangesetIdForTag(tag))
    def getChangesetIdForTag(self, tag):
        """Retrieve id number of the changeset identified by this tag"""
        raise NotImplementedError
    def getChangesetIdList(self):
        """Retrieve a list of all changesets in this branch"""
        return list(self.getChangesetIdIterator())
    def getChangesetList(self):
        """Retrieve a list of all changesets in this branch"""
        return list(self.getChangesetIterator())
    def getAllTagsIterator(self):
        """Retrieve an iterator which will visit every available tag"""
        raise NotImplementedError
    def getAllTagsList(self):
        """Retrieve a list of all available tags"""
        return list(self.getAllTagsIterator())
    def _addChangeset(self, changeset):
        """Add a new changeset to the branch"""
        raise NotImplementedError
    def getChangesetCount(self):
        """Return the number of changesets stored in this branch"""
        raise NotImplementedError
    def getNextChangesetNum(self):
        """Return the next available changeset number"""
        return self.getChangesetCount() + 1
    def __len__(self):
        """Return number of changesets"""
        return self.getChangesetCount()
    def __getitem__(self, id):
        """Retrieve a changeset"""
        return self.getChangeset(id)
    changesets  = property(lambda self: self.getChangesetList())
    branchName  = writeOnceProperty('branchName', 'name of this branch', None)

class BranchSQL(BranchDAO, DBAccessObj):
    def __init__(self, branchName):
        BranchDAO.__init__(self)
        self.branchName = branchName
    def _save(self):
        pass
    def getChangeset(self, id):
        return ChangesetSQL(self.branchName, id)
    def lastChangesetByAuthor(self, author):
        cursor = self.getCursor()
        cursor.execute("SELECT max(csnum) AS csnum FROM changeset WHERE branch=%(branch)s AND creator=%(creator)s",
                       {'branch':self.branchName, 'creator':author})
        csnum = cursor.fetchone()[0]
        if csnum is None: return None
        return self.getChangeset(int(csnum))
    def getChangesetIdForTag(self, tag):
        cursor = self.getCursor()
        cursor.execute("SELECT csnum FROM tag WHERE branch=%(branch)s AND tag=%(tag)s",
                       {'branch':self.branchName, 'tag':tag})
        result = cursor.fetchone()
        if result is None: raise KeyError, 'tag %s does not exist for branch %s' % (tag, self.branchName)
        return int(result[0])
    def getChangesetIdIterator(self):
        cursor = self.getCursor()
        cursor.execute("SELECT csnum FROM changeset WHERE branch=%(branch)s",
                       {'branch':self.branchName})
        for (csnum,) in cursor.fetchall():
            yield int(csnum)
    def getAllTagsIterator(self):
        cursor = self.getCursor()
        cursor.execute("SELECT DISTINCT tag.tag FROM tag, changeset WHERE tag.csnum=changeset.csnum AND changeset.branch=%(branch)s",
                       {'branch':self.branchName})
        for (tag,) in cursor.fetchall():
            yield tag
    def createChangesetFor(self, rev):
        cursor = self.getCursor()
        csnum = self.getNextChangesetNum()
        ## TODO: move this save functionality into the changeset object?
        cursor.execute("INSERT INTO changeset(csnum, branch, creator, startdate, enddate, log) VALUES(%(csnum)i, %(branch)s, %(creator)s, %(startdate)i, %(enddate)i, %(log)s)",
                       {'csnum':csnum,
                        'branch':self.branchName,
                        'creator':rev.author,
                        'startdate':rev.time,
                        'enddate':rev.time,
                        'log':rev.log})
        for tag in rev.tags:
            cursor.execute("INSERT OR REPLACE INTO tag(tag, branch, csnum) VALUES(%(tag)s, %(branch)s, %(csnum)i)",
                           {'tag':tag, 'branch':self.branchName, 'csnum':csnum})
        newCS = self.getChangeset(csnum)
        newCS._addRevision(rev)
        return newCS
    def getChangesetCount(self):
        cursor = self.getCursor()
        cursor.execute("SELECT COUNT(*) FROM changeset WHERE branch=%(branch)s",
                       {'branch':self.branchName})
        return int(cursor.fetchone()[0])

class ChangesetDAO(DAO):
    def __init__(self):
        super(ChangesetDAO, self).__init__()
    def getFilesWithDifferingRevisionsIterator(self, other):
        raise NotImplementedError
    def containsNewRevisionForFile(self, filename):
        """True if we have a revision for this file, False otherwise"""
        return self.getNewRevisionForFile(filename) != None
    def getNumberOfNewRevisions(self):
        """Return the number of files for which we have new revisions"""
        raise NotImplementedError
    def getNewRevisionForFile(self, filename):
        """Retrieve any new Revision we have for a given file"""
        raise NotImplementedError
    def getAllRevisionsIterator(self):
        """Iterate over the current revision for every file in the repository"""
        raise NotImplementedError
    def getAllRevisions(self):
        """Retrieve the current revision for every file in the repository"""
        return list(self.getAllRevisionsIterator())
    def getAllRevisionsDict(self):
        """Retrieve a dictionary mapping each file's name to the current revision
	of that file for every file in the repository"""
	retval = {}
        for revision in self.getAllRevisionsIterator():
		retval[revision.filename] = revision
	return retval
    def getAllNewRevisionsIterator(self):
        """Iterate over all new revisions included in this changeset"""
        raise NotImplementedError
    def getAllNewRevisions(self):
        """Retrieve all new revisions included in this changeset"""
        return list(self.getAllNewRevisionsIterator())
    def getLatestRevisionForFile(self, filename):
        """Retrieve the revision defining current state for the given file"""
        raise NotImplementedError
    def getTags(self):
        """Return a list of tags on this changeset"""
        raise NotImplementedError
    def addTag(self, tag):
        """Add given tag to this object"""
        raise NotImplementedError
    def removeTag(self, tag):
        """Remove a given tag from this object"""
        raise NotImplementedError
    def getBranch(self):
        """Retrieve the branch this changeset is on"""
        raise NotImplementedError
    def _addRevision(self, revision):
        """Add the given revision object (already filled out) to this changeset.
        Invalidates the added revision object; should be retrieved from changeset.
        """
        revision.isKilled = True
        raise NotImplementedError
    def __contains__(self, revision):
        """Do we contain the given revision?"""
        raise NotImplementedError
    
    author      = writeOnceProperty('author', 'individual who made these changes', None)
    log         = writeOnceProperty('log', 'log message associated with these changes', '')
    branch      = writeOnceProperty('branch', 'branch this changeset exists on', None)
    index       = writeOnceProperty('index', 'index of this changeset within its branch', 0)
    starttime   = dirtiableProperty('time', 'time of first revision in this changeset', 2147483647L)
    endtime     = dirtiableProperty('endtime', 'time of last revision in this changeset', 0L)
    tags        = property(lambda self: self.getTags())
    branch      = property(lambda self: self.getBranch())
    branchName  = dirtiableProperty('branchName', 'name of the branch this object is on', None)
    time        = property(lambda self: (self.starttime, self.endtime))

class ChangesetSQL(ChangesetDAO, DBAccessObj):
    def __init__(self, branch, csnum):
        ChangesetDAO.__init__(self)
        if branch.__class__ is not types.StringType:
            branch = branch.branchName
        self.branchName = branch
        self.index = csnum
        cursor = self.getCursor()
        cursor.execute("SELECT creator, startdate, enddate, log FROM changeset WHERE csnum=%(csnum)i AND branch=%(branch)s",
                       {'csnum':csnum, 'branch':branch})
        result = cursor.fetchone()
        if result is None: raise KeyError, 'no such changeset %i in branch %s' % (csnum, branch)
        (creator, startdate, enddate, log) = result
        startdate = int(startdate)
        enddate   = int(enddate)
        (self.author, self.starttime, self.enddtime, self.log) = (creator, startdate, enddate, log)
    def _save(self):
        self.getCursor().execute('INSERT INTO changeset(csnum, branch, creator, startdate, enddate, log) VALUES(%(csnum)i, %(branch)s, %(creator)s, %(startdate)i, %(enddate)i, %(log)s)',
                                 {'creator':self.author,
                                  'startdate':self.starttime,
                                  'enddate':self.endtime,
                                  'log':self.log,
                                  'csnum':self.index,
                                  'branch':self.branchName})                                     
    def getFilesWithDifferingRevisionsIterator(self, other, flip=0):
        if other.index < self.index:
		for value in other.getFilesWithDifferingRevisionsIterator(self, flip=1): yield value
        cursor = self.getCursor()
	## getAllRevisionsIterator(self) except getAllRevisionsIterator(other)
	cursor.execute("""SELECT filename, MAX(csnum) AS csnum, branch
			  FROM revision
			  WHERE branch=%(branch2)s
			    AND csnum <= %(csnum2)i
			  GROUP BY filename
			EXCEPT
			  SELECT filename, MAX(csnum) AS csnum, branch
			  FROM revision
			  WHERE branch=%(branch1)s
			    AND csnum <= %(csnum1)i
			  GROUP BY FILENAME""",
		       {'branch1':self.branch.branchName,
		        'branch2':other.branch.branchName,
			'csnum1':self.index,
			'csnum2':other.index})
	for (filename,_,_) in cursor.fetchall():
		yield filename
    def containsNewRevisionForFile(self, filename):
        cursor = self.getCursor()
        cursor.execute("SELECT COUNT(*) FROM revision WHERE csnum=%(csnum)i AND branch=%(branch)s AND filename=%(filename)s",
                       {'csnum':self.index,
                        'branch':self.branchName,
                        'filename':filename})
        return int(cursor.fetchone()[0]) > 0
    def getNumberOfNewRevisions(self):
        cursor = self.getCursor()
        cursor.execute("SELECT COUNT(*) FROM revision WHERE csnum=%(csnum)i AND branch=%(branch)s",
                       {'csnum':self.index, 'branch':self.branchName})
        return int(cursor.fetchone()[0])
    def getAllNewRevisionsIterator(self):
        cursor = self.getCursor()
        cursor.execute("""
select revision.filename, revision.revision, revision.revtype, revision.addcount, revision.delcount
  from (select filename, csnum, branch
          from revision
	 where branch=%(branch)s
	   and csnum=%(changeset)i)
  as newestvers
  natural join revision
  natural join changeset;
""", {'branch':self.branchName, 'changeset':self.index})        
        for (filename, revision, revtype, addcount, delcount) in cursor.fetchall():
            yield RevisionSQL(self.branch, self.index, filename, revision, revtype, addcount, delcount)
    def getAllRevisionsIterator(self):
        cursor = self.getCursor()
        cursor.execute("""
select revision.csnum, revision.filename, revision.revision, revision.revtype, revision.addcount, revision.delcount
  from (select filename, max(csnum) as csnum, branch
          from revision
	 where branch=%(branch)s
	   and csnum <= %(changeset)i
      group by filename)
  as newestvers
  natural join revision
  natural join changeset;
""", {'branch':self.branchName,'changeset':self.index})
        for (csnum, filename, revision, revtype, addcount, delcount) in cursor.fetchall():
            yield RevisionSQL(self.branch, csnum, filename, revision, revtype, addcount, delcount)
    def getNewRevisionForFile(self, filename):
        cursor = self.getCursor()
        cursor.execute("select csnum, revision, revtype, addcount, delcount from revision where branch=%(branch)s and filename=%(filename)s and csnum=%(changeset)s",
                       {'branch':self.branch.branchName,'filename':filename,'changeset':self.index})
        (csnum, revision, revtype, addcount, delcount) = cursor.fetchone()
        return RevisionSQL(self.branch, csnum, filename, revision, revtype, addcount, delcount)
    def getLatestRevisionForFile(self, filename):
        cursor = self.getCursor()
        cursor.execute("""
select revision.csnum, revision.revision, revision.revtype, revision.addcount, revision.delcount
  from (select max(csnum) as csnum
          from revision
         where branch=%(branch)s
           and filename=%(filename)s
           and csnum <= %(changeset)i)
  as newestver
  natural join revision
  natural join changeset where revision.branch=%(branch)s and changeset.branch=%(branch)s
""", {'branch':self.branch.branchName,'filename':filename,'changeset':self.index})
	result = cursor.fetchone()
	if result is None: return None
        (csnum, revision, revtype, addcount, delcount) = result
        return RevisionSQL(self.branch, csnum, filename, revision, revtype, addcount, delcount)
    def getTags(self):
        cursor = self.getCursor()
        cursor.execute("SELECT DISTINCT tag FROM tag WHERE csnum=%(csnum)i AND branch=%(branch)s",
                       {'csnum':self.index, 'branch':self.branchName})
        return [n[0] for n in cursor.fetchall()]
    def addTag(self, tag):
        cursor = self.getCursor()
        cursor.execute("INSERT OR IGNORE INTO tag(tag, branch, csnum) VALUES(%(tag)s, %(branch)s, %(csnum)i",
                       {'tag':tag,'banch':self.branchName,'csnum':self.index})
    def removeTag(self, tag):
        cursor = self.getCursor()
        cursor.execute("DELETE FROM tag WHERE tag=%(tag)s AND branch=%(branch)s AND csnum=%(csnum)i",
                       {'tag':tag, 'branch':self.branchName, 'csnum':self.index})
    def getBranch(self):
        return BranchSQL(self.branchName)
    def _addRevision(self, revision):
        cursor = self.getCursor()
        cursor.execute("INSERT INTO revision(csnum, branch, filename, revision, revtype, addcount, delcount) VALUES(%(csnum)i, %(branch)s, %(filename)s, %(revision)s, %(revtype)i, %(addcount)i, %(delcount)i)",
                       {'csnum':self.index,'branch':self.branchName,'filename':revision.filename,'revision':revision.revision,'revtype':revision.type,'addcount':revision.addcount,'delcount':revision.delcount})
        revision.__changeset = self.index
        revision.isKilled = True
    def __contains__(self, revision):
        cursor = self.getCursor()
        cursor.execute("SELECT COUNT(*) FROM revision WHERE csnum=%(csnum)i AND branch=%(branch)s AND filename=%(filename)s AND revision=%(revision)s",
                       {'csnum':self.index,'branch':self.branchName,'filename':revision.filename,'revision':revision.revision})
        
class RevisionDAO(DAO):
    ADD    = 10
    CHANGE = 11
    REMOVE = 12
    RENAME = 13
    PLACEHOLDER = 14
    
    def __init__(self):
        super(RevisionDAO, self).__init__()
    def __cmp__(self, other):
	if other is None: return True
    
        # sort first by branch...
        rv = cmp(self.branch, other.branch)
        if rv != 0: return rv
        
	# ...then by time...
        rv = cmp(self.time, other.time)
        if rv != 0: return rv
        
	# ...then by type...
	# This is *before* filename to prevent problems in the case where both
	# adds and updates happen on multiple files at the same time.
	rv = cmp(self.type, other.type)
	if rv != 0: return rv

	# ...then by log entry...
	# This is *before* filename to prevent new changesets from being
	# unnecessarily started when changes are checked in at the same time
	# with different comments.
	rv = cmp(self.log, other.log)
	if rv != 0: return rv

	# ...then by filename...
        rv = cmp(self.filename, other.filename)
        if rv != 0: return rv
        
	# ...then by revision depth...
        rv = cmp(string.count(self.revision, '.'),
                 string.count(other.revision, '.'))
        return rv
    def getPredecessor(self):
        raise NotImplementedError

    changeset   = writeOnceProperty('changeset', 'changeset this revision belongs to', None)
    addcount    = dirtiableProperty('addcount', 'number of lines added', None)
    delcount    = dirtiableProperty('delcount', 'number of lines removed', None)
    revision    = writeOnceProperty('revision', 'revision number', '')
    type        = writeOnceProperty('type', 'variety of file operation', -1)
    filename    = writeOnceProperty('filename', 'name of file this revision touches', '')
    tags        = property(lambda self: self.changeset.tags, doc='tags against this revision')
    author      = property(lambda self: self.changeset.author, doc='creator of this revision')
    log         = property(lambda self: self.changeset.log, doc='log message for this revision')
    branch      = property(lambda self: self.changeset.branch, doc='branch this revision is on')
    time        = property(lambda self: self.changeset.starttime, doc='time this revision is stamped with')
    csnum       = property(lambda self: self.changeset.index, doc='number of the changeset this revision belongs to')
    predecessor = property(lambda self: self.getPredecessor(), doc='previous revision of this file on this branch')

class WIPRevision(RevisionDAO):
    """
    A work-in-progress revision; particularly, one which does not
    yet belong to a changeset or have database backing
    """
    def __init__(self):
        super(WIPRevision, self).__init__()
        self.tags = []
    def _save(self):
        pass
    tags        = writeOnceProperty('tags', 'tags presently on this revision')
    author      = writeOnceProperty('author', 'individual who made this change')
    log         = writeOnceProperty('log', 'log message associated with changeset')
    branch      = writeOnceProperty('branch', 'branch this revision is on', 'MAIN')
    time        = writeOnceProperty('time', 'integer representing revision time')

class RevisionSQL(RevisionDAO, DBAccessObj):
    def __init__(self, branch, csnum, filename, revision, revtype=None, addcount=None, delcount=None):
        RevisionDAO.__init__(self)
        self.changeset = branch.getChangeset(csnum)
        self.filename = filename
        self.revision = revision
        if revtype is not None:
            self.type = revtype
            self.addcount = addcount
            self.delcount = delcount
    def _save(self):
        return
##         if not self.isStored:
##             self.getCursor().execute('INSERT INTO revision(csnum, branch, filename, revision, revtype, addcount, delcount) VALUES(%(csnum)i, %(branch)s, %(filename)s, %(revision)s, %(revtype)i, %(addcount)i, %(delcount)i',
##                                      {'csnum':self.changeset.index,
##                                       'branch':self.changeset.branch.branchName,
##                                       'filename':self.filename,
##                                       'revision':self.revision,
##                                       'revtype':self.type,
##                                       'addcount':self.addcount,
##                                       'delcount':self.delcount})
##         else:
##             self.getCursor().execute('UPDATE revision SET csnum=%(csnum)i, branch=%(branch)s, revtype=%(revtype)i, addcount=%(addcount)i, delcount=%(delcount)i WHERE csnum=%(csnum)i AND branch=%(branch) WHERE filename=%(filename)s AND revision=%(revision)s',
##                                      {'csnum':self.changeset.index,
##                                       'branch':self.changeset.branch.branchName,
##                                       'filename':self.filename,
##                                       'revision':self.revision,
##                                       'revtype':self.type,
##                                       'addcount':self.addcount,
##                                       'delcount':self.delcount})
    def getPredecessor(self):
        cursor = self.getCursor()
	cursor.execute("SELECT csnum, revision, revtype, addcount, delcount FROM revision WHERE filename=%(filename)s AND branch=%(branch)s AND csnum < %(csnum)i ORDER BY csnum DESC LIMIT 1", {'filename': self.filename, 'branch':self.branch.branchName, 'csnum':self.csnum})
	values = cursor.fetchone()
	if values is None: return  None
	(csnum, revision, revtype, addcount, delcount) = values
	return RevisionSQL(self.branch, csnum, self.filename, revision, revtype=revtype, addcount=addcount, delcount=delcount)
        

### there's some magic below here. It's ugly right now. Ignore it.

import new
import SCM

SQLClasses = {
    CatalogDAO          : CatalogSQL,
    BranchDAO           : BranchSQL,
    ChangesetDAO        : SCM.ChangeSet.ChangeSet,
    RevisionDAO         : SCM.Revision.Revision
}
ignoreClasses = {
    WIPRevision         : None
}

class StorageEnabler(object):
    def __init__(self, config, classMap = SQLClasses, ignoreList = ignoreClasses):
        self.config = config
        self.classMap = classMap
        self.convertedObjects = {} ## really should be a set
        self.ignoreList = ignoreList
    def convertingImport(self, module):
        if type(module) is types.StringType:
            module = __import__(module)
        if self.convertedObjects.has_key(module):
            return module
        self.convertedObjects[module] = None
        for objname in dir(module):
            object = getattr(module, objname)
            if type(object) is types.ModuleType:
                setattr(module, objname, self.convertingImport(object))
            elif type(object) is types.TypeType:
                if object not in self.ignoreList:
                    setattr(module, objname, self.getParentClass(object))
        return module
    def getParentClass(self, targetClass):
        newParentClasses = [ self.classMap[clazz] for clazz in targetClass.__bases__ if self.classMap.has_key(clazz) ]
        if len(newParentClasses) > 0:
            ## combine namespaces
            nsDict = targetClass.__dict__.copy()
            for clazz in newParentClasses:
                nsDict.update(clazz.__dict__)
            ## create a new parent class
            newParentClasses.append(targetClass)
            newClass = new.classobj(targetClass.__name__+'_CONV', tuple(sets.Set(newParentClasses)), nsDict)
            ## provide it with the system configuration
            newClass.config = self.config
            return newClass
        return targetClass

_storageEnabler = None
def getStorageEnabler():
    global _storageEnabler
    if _storageEnabler is None:
        raise RuntimeError, 'storage layer unconfigured'
    return _storageEnabler
def convertingImport(module):
    return getStorageEnabler().convertingImport(module)
def setConfig(config):
    global _storageEnabler
    if _storageEnabler is not None:
        raise RuntimeError, 'storage layer already passed configuration'
    _storageEnabler = StorageEnabler(config)
