# -*- coding: iso-8859-1 -*- # # Copyright (C) 2005 Edgewall Software # Copyright (C) 2006 K.S.Sreeram # # This software is licensed as described in the file COPYING, which # you should have received as part of this distribution. The terms # are also available at http://trac.edgewall.com/license.html. # # This software consists of voluntary contributions made by many # individuals. For the exact contribution history, see the revision # history and logs, available at http://projects.edgewall.com/trac/. # # Author: K.S.Sreeram import os, StringIO, mimetypes from datetime import tzinfo, timedelta, datetime from trac.util import TracError from trac.versioncontrol import Repository, Node, Changeset, \ NoSuchChangeset, NoSuchNode from command import DarcsCommand from updatedb import update_darcsdb from dbutil import NODE_FILE_TYPE, NODE_DIR_TYPE from dbutil import CHANGE_ADDED, CHANGE_REMOVED, CHANGE_MOVED, \ CHANGE_EDITED, CHANGE_MOVED_EDITED from dbutil import query_nodes_for_revision ''' This module implements the trac versioncontrol backend API. The API consists of 3 classes: DarcsRepository, DarcsNode and DarcsChangeset. Please see the docs in trac.versioncontrol.api for the interface which is implemented by this module. ''' ZERO = timedelta(0) HOUR = timedelta(hours=1) class UTC(tzinfo): """UTC""" def utcoffset(self, dt): return ZERO def tzname(self, dt): return "UTC" def dst(self, dt): return ZERO utc = UTC() # mapping from node types used by the darcs backend and the types # used by the trac api _node_type_map = { NODE_FILE_TYPE : Node.FILE, NODE_DIR_TYPE : Node.DIRECTORY } # mapping from change types used by the darcs backend and the types # used by the trac api _change_map = { CHANGE_ADDED : Changeset.ADD, CHANGE_REMOVED : Changeset.DELETE, CHANGE_MOVED : Changeset.MOVE, CHANGE_EDITED : Changeset.EDIT, #FIXME: treat moved&edited as just moved? CHANGE_MOVED_EDITED : Changeset.MOVE } def to_utc_datetime(dt): if isinstance(dt, long): dt = datetime.strptime(str(dt), '%Y%m%d%H%M%S') return dt.replace(tzinfo = utc) def get_node_type( db, node_id ) : c = db.cursor() c.execute( 'SELECT node_type FROM darcs_nodes ' + 'WHERE node_id = %s', (node_id,) ) return c.fetchone()[0] def get_prev_path_rev( db, node_id, rev ) : c = db.cursor() c.execute( 'SELECT path,rev FROM darcs_node_changes ' + 'WHERE node_id = %s AND rev < %s ' + 'ORDER BY rev DESC LIMIT 1', (node_id,rev) ) path,rev = c.fetchone() return path,rev class DarcsRepository( Repository ) : def __init__( self, db, path, log, config ) : Repository.__init__( self, path, None, log ) self.db = db self.path = path self.log = log self.config = config self.__cmd = DarcsCommand( 'darcs', path, log ) # import any new changesets, if any update_darcsdb( db, self.__cmd, log ) def close( self ) : pass def get_changeset( self, rev ) : rev = self.normalize_rev( rev ) return DarcsChangeset( self.db, rev ) def get_node( self, path, rev=None ) : path = self.normalize_path( path ) rev = self.normalize_rev( rev ) # compute node_id, node_type and last_rev and then # create a DarcsNode object. # 'last_rev' is the last revision <= rev where this # node was modified. if path == '/' : node_id = None node_type = NODE_DIR_TYPE last_rev = rev else : c = self.db.cursor() q = query_nodes_for_revision( rev ) q += ' AND dnc.path = %s' c.execute( q, (path,) ) row = c.fetchone() if row is None : raise NoSuchNode( path, rev ) node_id,last_rev = row[:2] node_type = get_node_type( self.db, node_id ) return DarcsNode( node_id, node_type, path, last_rev, self.db, self.__cmd ) def get_oldest_rev( self ) : if self.get_youngest_rev() is None : return None return 1 def get_youngest_rev( self ) : c = self.db.cursor() c.execute( 'SELECT rev FROM darcs_revisions ' + 'ORDER BY rev DESC LIMIT 1' ) row = c.fetchone() return row and row[0] or None def previous_rev( self, rev ) : rev = self.normalize_rev( rev ) if rev > 1 : return rev-1 return None def next_rev( self, rev, path='' ) : rev = self.normalize_rev( rev ) if rev < self.get_youngest_rev() : return rev+1 return None def rev_older_than( self, rev1, rev2 ) : return self.normalize_rev(rev1) < self.normalize_rev(rev2) def get_path_history( self, path, rev=None, limit=None ) : # FIXME: this is not correct return self.get_node( path, rev ).get_history( limit ) def normalize_path( self, path ) : return path and path.strip('/') or '/' def normalize_rev( self, rev ) : youngest = self.get_youngest_rev() if rev is None or rev == "" : return youngest rev = int( rev ) if rev > youngest : rev = youngest return rev def get_changes( self, old_path, old_rev, new_path, new_rev, ignore_ancestry=1 ) : old_path = self.normalize_path( old_path ) old_rev = self.normalize_rev( old_rev ) new_path = self.normalize_path( new_path ) new_rev = self.normalize_rev( new_rev ) old_node = self.get_node( old_path, old_rev ) new_node = self.get_node( new_path, new_rev ) node_id = old_node._get_node_id() if node_id != new_node._get_node_id() : raise TracError( 'Node mismatch: base is %s in rev %d ' 'and target is %s in rev %d' % (old_path,old_rev, new_path,new_rev) ) if old_node.kind == Node.FILE : if old_node.rev != new_node.rev : yield (old_node,new_node,Node.FILE,Changeset.EDIT) return c = self.db.cursor() c.execute( 'SELECT rev,path FROM darcs_node_changes ' 'WHERE node_id = %s AND rev >= %s AND rev <= %s', (node_id,old_rev,new_rev) ) node_set = dict() node_list = [] c1 = self.db.cursor() for rev,path in c : c1.execute( 'SELECT node_id FROM darcs_node_changes ' 'WHERE rev = %s AND path LIKE %s', (rev,path+'/%') ) for nid, in c1 : if nid not in node_set : node_set[nid] = 1 node_list.append( nid ) for nid in node_list : old_node = new_node = None c1.execute( 'SELECT rev,path FROM darcs_node_changes ' 'WHERE node_id = %s AND rev < %s ' 'ORDER BY rev DESC LIMIT 1', (nid,old_rev) ) row = c1.fetchone() if row is not None : rev,path = row old_node = self.get_node( path, rev ) c1.execute( 'SELECT rev,path,the_change FROM darcs_node_changes ' 'WHERE node_id = %s AND rev >= %s AND rev <= %s ' 'ORDER BY rev DESC LIMIT 1', (nid,old_rev,new_rev) ) rev,path,change = c1.fetchone() if change != CHANGE_REMOVED : new_node = self.get_node( path, rev ) assert (old_node is not None) or (new_node is not None) kind = old_node and old_node.kind or new_node.kind if old_node is None : change = Changeset.ADD elif new_node is None : change = Changeset.DELETE elif old_node.path != new_node.path : change = Changeset.MOVE else : change = Changeset.EDIT yield (old_node,new_node,kind,change) def sync( self, rev_callback=None ) : # This is called by trac-admin resync update_darcsdb(self.db, self.__cmd, self.log, rev_callback=rev_callback) class DarcsNode( Node ) : def __init__( self, node_id, node_type, path, rev, db, cmd ) : kind = _node_type_map[node_type] Node.__init__( self, path, rev, kind ) self.__node_id = node_id self.__node_type = node_type self.__db = db self.__cmd = cmd self.created_path = path self.created_rev = rev def _get_node_id( self ) : return self.__node_id def get_content( self ) : if self.__node_type == NODE_DIR_TYPE : return None c = self.__db.cursor() # check if the file content is there in the cache c.execute( 'SELECT content FROM darcs_cache ' 'WHERE node_id = %s AND rev = %s', (self.__node_id,self.rev) ) row = c.fetchone() if row is not None : # if present just return it data = str(buffer(row[0])) return StringIO.StringIO( data ) # load the file content from the repo c.execute( 'SELECT hash FROM darcs_revisions ' + 'WHERE rev = %s', (self.rev,) ) hash = c.fetchone()[0] data = self.__cmd.cat( hash, self.path ) # save the file content in the cache # UGLY HACK: the following line is required otherwise # db.commit() (pysqlite-2.3.2,winxp) throws # OperationalError: 'SQL logic error or missing database' #self.__db.cnx.cnx.isolation_level = None # END UGLY HACK c = self.__db.cursor() c.execute( 'INSERT INTO darcs_cache(node_id,rev,content) ' 'VALUES (%s,%s,%s)', (self.__node_id,self.rev,buffer(data)) ) self.__db.commit() return StringIO.StringIO( data ) def get_entries( self ) : if self.__node_type == NODE_FILE_TYPE : return q = query_nodes_for_revision( self.rev ) if self.__node_id is None : q += ' AND dnc.parent_id IS NULL' else : q += ' AND dnc.parent_id = %d' % self.__node_id c = self.__db.cursor() c.execute( q ) for node_id,rev,path,_ in c : node_type = get_node_type( self.__db, node_id ) yield DarcsNode( node_id, node_type, path, rev, self.__db, self.__cmd ) def get_history( self, limit=None ) : if self.path == '/' : for i in range(self.rev,0,-1) : yield ( self.path, i, Changeset.EDIT ) return c = self.__db.cursor() q = 'SELECT path,rev,the_change FROM darcs_node_changes ' + \ 'WHERE node_id = %s AND rev <= %s ' + \ 'ORDER BY rev DESC' if limit is not None : q += ' LIMIT %d' % limit c.execute( q, (self.__node_id,self.rev) ) for path,rev,change in c : yield ( path, rev, _change_map[change] ) def get_properties( self ) : return {} def get_content_length( self ) : if self.isdir : return None return len(self.get_content().read()) def get_content_type( self ) : if self.isdir : return None return mimetypes.guess_type( self.path )[0] def get_name( self ) : return os.path.split( self.path )[1] def get_last_modified( self ) : if self.__node_id is None : return 0 c = self.__db.cursor() c.execute( 'SELECT rev FROM darcs_node_changes ' + 'WHERE node_id = %s AND rev = %s', (self.__node_id,self.rev) ) rev = c.fetchone()[0] c.execute( 'SELECT time FROM darcs_revisions ' + 'WHERE rev = %s', (rev,) ) return to_utc_datetime(c.fetchone()[0]) class DarcsChangeset( Changeset ) : def __init__( self, db, rev ) : c = db.cursor() c.execute( 'SELECT author,time,name,comment ' + 'FROM darcs_revisions WHERE rev = %s', (rev,) ) row = c.fetchone() if row is None : raise NoSuchChangeset( rev ) author,date,name,comment = row date = to_utc_datetime(date) msg = name if comment : msg += '\n' + comment Changeset.__init__( self, rev, msg, author, date ) self.__db = db def get_changes( self ) : c = self.__db.cursor() c.execute( 'SELECT node_id,path,the_change FROM darcs_node_changes ' + 'WHERE rev = %s', (self.rev,) ) for node_id,path,change in c : node_type = get_node_type( self.__db, node_id ) kind = _node_type_map[node_type] if change == CHANGE_ADDED : prev_path = prev_rev = None else : prev_path,prev_rev = get_prev_path_rev( self.__db, node_id, self.rev ) change = _change_map[change] yield (path,kind,change,prev_path,prev_rev) def get_properties( self ) : return []