source: tracdarcs/tracdarcs/repository.py @ 69

Revision 69, 13.5 KB checked in by lele@…, 5 years ago (diff)

Bring the log to the DarcsNode?

Line 
1# -*- coding: iso-8859-1 -*-
2#
3# Copyright (C) 2005 Edgewall Software
4# Copyright (C) 2006 K.S.Sreeram <sreeram@tachyontech.net>
5#
6# This software is licensed as described in the file COPYING, which
7# you should have received as part of this distribution. The terms
8# are also available at http://trac.edgewall.com/license.html.
9#
10# This software consists of voluntary contributions made by many
11# individuals. For the exact contribution history, see the revision
12# history and logs, available at http://projects.edgewall.com/trac/.
13#
14# Author: K.S.Sreeram <sreeram@tachyontech.net>
15
16import os, StringIO, mimetypes
17from datetime import tzinfo, timedelta, datetime
18
19from trac.util import TracError
20from trac.versioncontrol import Repository, Node, Changeset, \
21        NoSuchChangeset, NoSuchNode
22
23from command import DarcsCommand
24from updatedb import update_darcsdb
25from dbutil import NODE_FILE_TYPE, NODE_DIR_TYPE
26from dbutil import CHANGE_ADDED, CHANGE_REMOVED, CHANGE_MOVED, \
27        CHANGE_EDITED, CHANGE_MOVED_EDITED
28from dbutil import query_nodes_for_revision
29
30'''
31This module implements the trac versioncontrol backend API.
32The API consists of 3 classes: DarcsRepository, DarcsNode
33and DarcsChangeset.
34
35Please see the docs in trac.versioncontrol.api for the interface
36which is implemented by this module.
37'''
38
39ZERO = timedelta(0)
40HOUR = timedelta(hours=1)
41
42class UTC(tzinfo):
43    """UTC"""
44
45    def utcoffset(self, dt):
46        return ZERO
47
48    def tzname(self, dt):
49        return "UTC"
50
51    def dst(self, dt):
52        return ZERO
53
54utc = UTC()
55
56# mapping from node types used by the darcs backend and the types
57# used by the trac api
58_node_type_map = {
59        NODE_FILE_TYPE : Node.FILE,
60        NODE_DIR_TYPE : Node.DIRECTORY
61        }
62
63# mapping from change types used by the darcs backend and the types
64# used by the trac api
65_change_map = {
66        CHANGE_ADDED : Changeset.ADD,
67        CHANGE_REMOVED : Changeset.DELETE,
68        CHANGE_MOVED : Changeset.MOVE,
69        CHANGE_EDITED : Changeset.EDIT,
70        #FIXME: treat moved&edited as just moved?
71        CHANGE_MOVED_EDITED : Changeset.MOVE
72        }
73
74def to_utc_datetime(dt):
75    if isinstance(dt, long):
76        try:
77            strptime = datetime.strptime
78        except AttributeError:
79            # Python <2.5
80            import time
81            def strptime(s,f):
82                tt = time.strptime(s, f)
83                return datetime(*tt[0:6])
84        dt = strptime(str(dt), '%Y%m%d%H%M%S')
85
86    return dt.replace(tzinfo = utc)
87
88def get_node_type( db, node_id ) :
89    c = db.cursor()
90    c.execute( 'SELECT node_type FROM darcs_nodes ' +
91            'WHERE node_id = %s', (node_id,) )
92    return c.fetchone()[0]
93
94def get_prev_path_rev( db, node_id, rev ) :
95    c = db.cursor()
96    c.execute( 'SELECT path,rev FROM darcs_node_changes ' +
97            'WHERE node_id = %s AND rev < %s ' +
98            'ORDER BY rev DESC LIMIT 1', (node_id,rev) )
99    path,rev = c.fetchone()
100    return path,rev
101
102class DarcsRepository( Repository ) :
103    def __init__( self, db, path, log, config ) :
104        Repository.__init__( self, path, None, log )
105        self.db = db
106        self.path = path
107        self.log = log
108        self.config = config
109        darcs = config.get('darcs', 'command', 'darcs')
110        if config.get('darcs', 'dont_escape_8bit'):
111            darcs = "DARCS_DONT_ESCAPE_8BIT=1 " + darcs
112        self.__cmd = DarcsCommand( darcs, path, log )
113        # import any new changesets, if any
114        update_darcsdb( db, self.__cmd, log )
115
116    def close( self ) :
117        pass
118
119    def get_changeset( self, rev ) :
120        rev = self.normalize_rev( rev )
121        return DarcsChangeset( self.db, rev )
122
123    def get_node( self, path, rev=None ) :
124        path = self.normalize_path( path )
125        rev = self.normalize_rev( rev )
126        # compute node_id, node_type and last_rev and then
127        # create a DarcsNode object.
128        # 'last_rev' is the last revision <= rev where this
129        # node was modified.
130        if path == '/' :
131            node_id = None
132            node_type = NODE_DIR_TYPE
133            last_rev = rev
134        else :
135            c = self.db.cursor()
136            q = query_nodes_for_revision( rev )
137            q += ' AND dnc.path = %s'
138            c.execute( q, (path,) )
139            row = c.fetchone()
140            if row is None :
141                raise NoSuchNode( path, rev )
142            node_id,last_rev = row[:2]
143            node_type = get_node_type( self.db, node_id )
144        return DarcsNode( node_id, node_type, path, last_rev,
145                self.db, self.__cmd, self.log )
146
147    def get_oldest_rev( self ) :
148        if self.get_youngest_rev() is None :
149            return None
150        return 1
151
152    def get_youngest_rev( self ) :
153        c = self.db.cursor()
154        c.execute( 'SELECT rev FROM darcs_revisions ' +
155                'ORDER BY rev DESC LIMIT 1' )
156        row = c.fetchone()
157        return row and row[0] or None
158
159    def previous_rev( self, rev ) :
160        rev = self.normalize_rev( rev )
161        if rev > 1 :
162            return rev-1
163        return None
164
165    def next_rev( self, rev, path='' ) :
166        rev = self.normalize_rev( rev )
167        if rev < self.get_youngest_rev() :
168            return rev+1
169        return None
170
171    def rev_older_than( self, rev1, rev2 ) :
172        return self.normalize_rev(rev1) < self.normalize_rev(rev2)
173
174    def get_path_history( self, path, rev=None, limit=None ) :
175        # FIXME: this is not correct
176        return self.get_node( path, rev ).get_history( limit )
177
178    def normalize_path( self, path ) :
179        return path and path.strip('/') or '/'
180
181    def normalize_rev( self, rev ) :
182        youngest = self.get_youngest_rev()
183        if rev is None or rev == "" :
184            return youngest
185        rev = int( rev )
186        if rev > youngest :
187            rev = youngest
188        return rev
189
190    def get_changes( self, old_path, old_rev, new_path, new_rev,
191            ignore_ancestry=1 ) :
192        old_path = self.normalize_path( old_path )
193        old_rev = self.normalize_rev( old_rev )
194        new_path = self.normalize_path( new_path )
195        new_rev = self.normalize_rev( new_rev )
196        old_node = self.get_node( old_path, old_rev )
197        new_node = self.get_node( new_path, new_rev )
198
199        node_id = old_node._get_node_id()
200        if node_id != new_node._get_node_id() :
201            raise TracError( 'Node mismatch: base is %s in rev %d '
202                    'and target is %s in rev %d' % (old_path,old_rev,
203                        new_path,new_rev) )
204
205        if old_node.kind == Node.FILE :
206            if old_node.rev != new_node.rev :
207                yield (old_node,new_node,Node.FILE,Changeset.EDIT)
208            return
209
210        c = self.db.cursor()
211        c.execute( 'SELECT rev,path FROM darcs_node_changes '
212                'WHERE node_id = %s AND rev >= %s AND rev <= %s',
213                (node_id,old_rev,new_rev) )
214        node_set = dict()
215        node_list = []
216        c1 = self.db.cursor()
217        for rev,path in c :
218            c1.execute( 'SELECT node_id FROM darcs_node_changes '
219                    'WHERE rev = %s AND path LIKE %s',
220                    (rev,path+'/%') )
221            for nid, in c1 :
222                if nid not in node_set :
223                    node_set[nid] = 1
224                    node_list.append( nid )
225        for nid in node_list :
226            old_node = new_node = None
227            c1.execute( 'SELECT rev,path FROM darcs_node_changes '
228                    'WHERE node_id = %s AND rev < %s '
229                    'ORDER BY rev DESC LIMIT 1',
230                    (nid,old_rev) )
231            row = c1.fetchone()
232            if row is not None :
233                rev,path = row
234                old_node = self.get_node( path, rev )
235            c1.execute( 'SELECT rev,path,the_change FROM darcs_node_changes '
236                    'WHERE node_id = %s AND rev >= %s AND rev <= %s '
237                    'ORDER BY rev DESC LIMIT 1',
238                    (nid,old_rev,new_rev) )
239            rev,path,change = c1.fetchone()
240            if change != CHANGE_REMOVED :
241                new_node = self.get_node( path, rev )
242            assert (old_node is not None) or (new_node is not None)
243            kind = old_node and old_node.kind or new_node.kind
244            if old_node is None :
245                change = Changeset.ADD
246            elif new_node is None :
247                change = Changeset.DELETE
248            elif old_node.path != new_node.path :
249                change = Changeset.MOVE
250            else :
251                change = Changeset.EDIT
252            yield (old_node,new_node,kind,change)
253
254    def sync( self, rev_callback=None ) :
255        # This is called by trac-admin resync
256        update_darcsdb(self.db, self.__cmd, self.log, rev_callback=rev_callback)
257
258class DarcsNode( Node ) :
259    def __init__( self, node_id, node_type, path, rev,
260            db, cmd, log=None ) :
261        kind = _node_type_map[node_type]
262        Node.__init__( self, path, rev, kind )
263        self.__node_id = node_id
264        self.__node_type = node_type
265        self.__db = db
266        self.__cmd = cmd
267        self.__log = log
268        self.created_path = path
269        self.created_rev = rev
270
271    def _get_node_id( self ) :
272        return self.__node_id
273
274    def get_content( self ) :
275        if self.__node_type == NODE_DIR_TYPE :
276            return None
277        c = self.__db.cursor()
278        # check if the file content is there in the cache
279        c.execute( 'SELECT content FROM darcs_cache '
280                'WHERE node_id = %s AND rev = %s',
281                (self.__node_id,self.rev) )
282        row = c.fetchone()
283        if row is not None :
284            # if present just return it
285            data = str(buffer(row[0]))
286            return StringIO.StringIO( data )
287        # load the file content from the repo
288        c.execute( 'SELECT hash FROM darcs_revisions ' +
289                'WHERE rev = %s', (self.rev,) )
290        hash = c.fetchone()[0]
291        data = self.__cmd.cat( hash, self.path )
292
293        # save the file content in the cache
294
295        # UGLY HACK: the following line is required otherwise
296        # db.commit() (pysqlite-2.3.2,winxp) throws
297        # OperationalError: 'SQL logic error or missing database'
298        #self.__db.cnx.cnx.isolation_level = None
299        # END UGLY HACK
300
301        c = self.__db.cursor()
302        c.execute( 'INSERT INTO darcs_cache(node_id,rev,content) '
303                'VALUES (%s,%s,%s)',
304                (self.__node_id,self.rev,buffer(data)) )
305        self.__db.commit()
306        return StringIO.StringIO( data )
307
308    def get_entries( self ) :
309        if self.__node_type == NODE_FILE_TYPE :
310            return
311        q = query_nodes_for_revision( self.rev )
312        if self.__node_id is None :
313            q += ' AND dnc.parent_id IS NULL'
314        else :
315            q += ' AND dnc.parent_id = %d' % self.__node_id
316        c = self.__db.cursor()
317        c.execute( q )
318        for node_id,rev,path,_ in c :
319            node_type = get_node_type( self.__db, node_id )
320            yield DarcsNode( node_id, node_type, path, rev,
321                    self.__db, self.__cmd, self.__log )
322
323    def get_history( self, limit=None ) :
324        if self.path == '/' :
325            for i in range(self.rev,0,-1) :
326                yield ( self.path, i, Changeset.EDIT )
327            return
328        c = self.__db.cursor()
329        q = 'SELECT path,rev,the_change FROM darcs_node_changes ' + \
330                'WHERE node_id = %s AND rev <= %s ' + \
331                'ORDER BY rev DESC'
332        if limit is not None :
333            q += ' LIMIT %d' % limit
334        c.execute( q, (self.__node_id,self.rev) )
335        for path,rev,change in c :
336            yield ( path, rev, _change_map[change] )
337
338    def get_properties( self ) :
339        return {}
340
341    def get_content_length( self ) :
342        if self.isdir :
343            return None
344        return len(self.get_content().read())
345
346    def get_content_type( self ) :
347        if self.isdir :
348            return None
349        return mimetypes.guess_type( self.path )[0]
350
351    def get_name( self ) :
352        return os.path.split( self.path )[1]
353
354    def get_last_modified( self ) :
355        if self.__node_id is None :
356            return 0
357        c = self.__db.cursor()
358        c.execute( 'SELECT rev FROM darcs_node_changes ' +
359                'WHERE node_id = %s AND rev = %s',
360                (self.__node_id,self.rev) )
361        rev = c.fetchone()[0]
362        c.execute( 'SELECT time FROM darcs_revisions ' +
363                'WHERE rev = %s', (rev,) )
364        return to_utc_datetime(c.fetchone()[0])
365
366class DarcsChangeset( Changeset ) :
367    def __init__( self, db, rev ) :
368        c = db.cursor()
369        c.execute( 'SELECT author,time,name,comment ' +
370                'FROM darcs_revisions WHERE rev = %s', (rev,) )
371        row = c.fetchone()
372        if row is None :
373            raise NoSuchChangeset( rev )
374        author,date,name,comment = row
375        date = to_utc_datetime(date)
376        msg = name
377        if comment :
378            msg += '\n' + comment
379        Changeset.__init__( self, rev, msg, author, date )
380        self.__db = db
381
382    def get_changes( self ) :
383        c = self.__db.cursor()
384        c.execute( 'SELECT node_id,path,the_change FROM darcs_node_changes ' +
385                'WHERE rev = %s', (self.rev,) )
386        for node_id,path,change in c :
387            node_type = get_node_type( self.__db, node_id )
388            kind = _node_type_map[node_type]
389            if change == CHANGE_ADDED :
390                prev_path = prev_rev = None
391            else :
392                prev_path,prev_rev = get_prev_path_rev( self.__db,
393                        node_id, self.rev )
394            change = _change_map[change]
395            yield (path,kind,change,prev_path,prev_rev)
396
397    def get_properties( self ) :
398        return []
Note: See TracBrowser for help on using the repository browser.