source: tracdarcs/tracdarcs/repository.py @ 136

Revision 136, 18.0 KB checked in by lele@…, 4 years ago (diff)

Backward compatibility

Line 
1# -*- coding: utf-8 -*-
2#
3# Copyright (C) 2005 Edgewall Software
4# Copyright (C) 2006 K.S.Sreeram <sreeram@tachyontech.net>
5# Copyright (C) 2007,2008,2009 Lele Gaifax <lele@metapensiero.it>
6#
7# This software is licensed as described in the file COPYING, which
8# you should have received as part of this distribution. The terms
9# are also available at http://trac.edgewall.com/license.html.
10#
11# This software consists of voluntary contributions made by many
12# individuals. For the exact contribution history, see the revision
13# history and logs, available at http://projects.edgewall.com/trac/.
14#
15# Author: K.S.Sreeram <sreeram@tachyontech.net>
16
17'''
18This module implements the trac versioncontrol backend API.
19The API consists of 3 classes: DarcsRepository, DarcsNode
20and DarcsChangeset.
21
22Please see the docs in trac.versioncontrol.api for the interface
23which is implemented by this module.
24'''
25
26import os, StringIO, mimetypes
27from datetime import tzinfo, timedelta, datetime
28import time, re
29
30from trac.util import TracError
31from trac.util.datefmt import to_timestamp, utc
32from trac.versioncontrol import (Repository, Node, Changeset,
33                                 NoSuchChangeset, NoSuchNode)
34from trac import __version__ as tracVersion
35
36IS_TRAC_0_10_X = not not re.match(r'^0\.10', tracVersion)
37IS_TRAC_0_11_X = not not re.match(r'^0\.11', tracVersion)
38IS_TRAC_0_12_OR_BETTER = not not re.match(r'^0\.1[2-9]', tracVersion)
39
40from command import DarcsCommand
41from updatedb import update_darcsdb
42from dbutil import (CHANGE_ADDED, CHANGE_REMOVED, CHANGE_MOVED,
43                    CHANGE_EDITED, CHANGE_MOVED_EDITED,
44                    NODE_FILE_TYPE, NODE_DIR_TYPE,
45                    query_nodes_for_revision)
46
47# mapping from node types used by the darcs backend and the types
48# used by the trac api
49_node_type_map = {
50    NODE_FILE_TYPE : Node.FILE,
51    NODE_DIR_TYPE : Node.DIRECTORY
52    }
53
54# mapping from change types used by the darcs backend and the types
55# used by the trac api
56_change_map = {
57    CHANGE_ADDED : Changeset.ADD,
58    CHANGE_REMOVED : Changeset.DELETE,
59    CHANGE_MOVED : Changeset.MOVE,
60    CHANGE_EDITED : Changeset.EDIT,
61    #FIXME: treat moved&edited as just moved?
62    CHANGE_MOVED_EDITED : Changeset.MOVE
63    }
64
65def get_node_type(db, repo_id, node_id):
66    c = db.cursor()
67    c.execute('SELECT node_type FROM darcs_nodes '
68              'WHERE repo_id = %s AND node_id = %s', (repo_id,node_id,))
69    return c.fetchone()[0]
70
71def get_prev_path_rev(db, repo_id, node_id, rev):
72    c = db.cursor()
73    c.execute('SELECT path,rev FROM darcs_node_changes '
74              'WHERE repo_id = %s AND node_id = %s AND rev < %s '
75              'ORDER BY rev DESC LIMIT 1', (repo_id,node_id,rev))
76    path,rev = c.fetchone()
77    return path,rev
78
79def get_repository_id(db, path):
80    if IS_TRAC_0_10_X or IS_TRAC_0_11_X:
81        return ''
82
83    c = db.cursor()
84    c.execute('SELECT r1.id '
85              'FROM repository r1, repository r2 '
86              'WHERE r1.id = r2.id '
87              '  AND r1.name = %s  AND r1.value = %s '
88              '  AND r2.name = %s  AND r2.value = %s '
89              'LIMIT 1', ('dir', path,
90                          'type', 'darcs'))
91    row = c.fetchone()
92    if row is not None:
93        return row[0]
94    return ''
95
96class DarcsRepository(Repository):
97    def __init__(self, db, path, log, darcscmd, possible_encodings):
98        Repository.__init__(self, path, None, log)
99        self.db = db
100        self.path = path
101        self.log = log
102        self.__cmd = DarcsCommand(darcscmd, path, log, possible_encodings)
103        self.repo_id = get_repository_id(db, path)
104        if IS_TRAC_0_10_X:
105            self.sync()
106
107    def close(self):
108        pass
109
110    def get_changeset(self, rev):
111        rev = self.normalize_rev(rev)
112        return DarcsChangeset(self.db, self.repo_id, rev)
113
114    def get_node(self, path, rev=None):
115        path = self.normalize_path(path)
116        rev = self.normalize_rev(rev)
117        # compute node_id, node_type and last_rev and then
118        # create a DarcsNode object.
119        # 'last_rev' is the last revision <= rev where this
120        # node was modified.
121        if path == '/':
122            node_id = None
123            node_type = NODE_DIR_TYPE
124            last_rev = rev
125        else:
126            c = self.db.cursor()
127            q = query_nodes_for_revision(self.repo_id, rev)
128            q += ' AND dnc.path = %s'
129            c.execute(q, (path,))
130            row = c.fetchone()
131            if row is None:
132                raise NoSuchNode(path, rev)
133            node_id,last_rev = row[:2]
134            node_type = get_node_type(self.db, self.repo_id, node_id)
135        return DarcsNode(node_id, node_type, path, last_rev,
136                         self.db, self.repo_id, self.__cmd, self.log)
137
138    def get_oldest_rev(self):
139        if self.get_youngest_rev() is None:
140            return None
141        return 1
142
143    def get_youngest_rev(self):
144        c = self.db.cursor()
145        c.execute('SELECT max(rev) FROM darcs_changesets '
146                  'WHERE repo_id = %s', (self.repo_id,))
147        row = c.fetchone()
148        return row and row[0] or None
149
150    def previous_rev(self, rev):
151        rev = self.normalize_rev(rev)
152        if rev > 1:
153            return rev-1
154        return None
155
156    def next_rev(self, rev, path=''):
157        rev = self.normalize_rev(rev)
158        if rev < self.get_youngest_rev():
159            return rev+1
160        return None
161
162    def rev_older_than(self, rev1, rev2):
163        return self.normalize_rev(rev1) < self.normalize_rev(rev2)
164
165    def get_path_history(self, path, rev=None, limit=None):
166        # FIXME: this is not correct
167        return self.get_node(path, rev).get_history(limit)
168
169    def normalize_path(self, path):
170        return path and path.strip('/') or '/'
171
172    def normalize_rev(self, rev):
173        if isinstance(rev, basestring) and len(rev) in (61,64):
174            if not rev.endswith('.gz'):
175                # We store the complete hashname in the db
176                rev = rev + '.gz'
177            c = self.db.cursor()
178            c.execute('SELECT rev FROM darcs_changesets '
179                      'WHERE hash = %s', (rev,))
180            row = c.fetchone()
181            if row is None:
182                raise NoSuchChangeset(rev)
183            rev = int(row[0])
184        else:
185            youngest = self.get_youngest_rev()
186            if rev is None or rev == "":
187                return youngest
188            try:
189                rev = int(rev)
190            except ValueError, le:
191                raise TracError('Ill-formed revision: %s, error: %s' % (rev, le))
192            if rev > youngest:
193                rev = youngest
194        return rev
195
196    def get_changes(self, old_path, old_rev, new_path, new_rev, ignore_ancestry=1):
197        old_path = self.normalize_path(old_path)
198        old_rev = self.normalize_rev(old_rev)
199        new_path = self.normalize_path(new_path)
200        new_rev = self.normalize_rev(new_rev)
201        old_node = self.get_node(old_path, old_rev)
202        new_node = self.get_node(new_path, new_rev)
203
204        node_id = old_node._get_node_id()
205        if node_id != new_node._get_node_id():
206            raise TracError('Node mismatch: base is %s in rev %d '
207                            'and target is %s in rev %d' % (old_path,old_rev,
208                                                            new_path,new_rev))
209
210        if old_node.kind == Node.FILE:
211            if old_node.rev != new_node.rev:
212                yield (old_node,new_node,Node.FILE,Changeset.EDIT)
213            return
214
215        c = self.db.cursor()
216        c.execute('SELECT rev,path FROM darcs_node_changes '
217                  'WHERE repo_id = %s AND node_id = %s AND rev >= %s AND rev <= %s',
218                  (self.repo_id,node_id,old_rev,new_rev))
219        node_set = dict()
220        node_list = []
221        c1 = self.db.cursor()
222        for rev,path in c:
223            c1.execute('SELECT node_id FROM darcs_node_changes '
224                       'WHERE repo_id = %s AND rev = %s AND path LIKE %s',
225                       (self.repo_id,rev,path+'/%'))
226            for nid, in c1:
227                if nid not in node_set:
228                    node_set[nid] = 1
229                    node_list.append(nid)
230        for nid in node_list:
231            old_node = new_node = None
232            c1.execute('SELECT rev,path FROM darcs_node_changes '
233                       'WHERE repo_id = %s AND node_id = %s AND rev < %s '
234                       'ORDER BY rev DESC LIMIT 1',
235                       (self.repo_id,nid,old_rev))
236            row = c1.fetchone()
237            if row is not None:
238                rev,path = row
239                old_node = self.get_node(path, rev)
240            c1.execute('SELECT rev,path,the_change FROM darcs_node_changes '
241                       'WHERE repo_id = %s AND node_id = %s AND rev >= %s AND rev <= %s '
242                       'ORDER BY rev DESC LIMIT 1',
243                       (self.repo_id,nid,old_rev,new_rev))
244            rev,path,change = c1.fetchone()
245            if change != CHANGE_REMOVED:
246                new_node = self.get_node(path, rev)
247            assert (old_node is not None) or (new_node is not None)
248            kind = old_node and old_node.kind or new_node.kind
249            if old_node is None:
250                change = Changeset.ADD
251            elif new_node is None:
252                change = Changeset.DELETE
253            elif old_node.path != new_node.path:
254                change = Changeset.MOVE
255            else:
256                change = Changeset.EDIT
257            yield (old_node,new_node,kind,change)
258
259    def sync(self, rev_callback=None):
260        # Import any new changesets, if any
261        update_darcsdb(self.db, self.__cmd, self.log, rev_callback=rev_callback)
262
263class DarcsNode(Node):
264    def __init__(self, node_id, node_type, path, rev,
265                 db, repo_id, cmd, log=None):
266        kind = _node_type_map[node_type]
267        Node.__init__(self, path, rev, kind)
268        self.__node_id = node_id
269        self.__node_type = node_type
270        self.__db = db
271        self.__repo_id = repo_id
272        self.__cmd = cmd
273        self.__log = log
274        self.created_path = path
275        self.created_rev = rev
276
277    def _get_node_id(self):
278        return self.__node_id
279
280    def get_content(self):
281        if self.__node_type == NODE_DIR_TYPE:
282            return None
283        c = self.__db.cursor()
284        # check if the file content is there in the cache
285        c.execute('SELECT content FROM darcs_cache '
286                  'WHERE repo_id = %s AND node_id = %s AND rev = %s',
287                  (self.__repo_id,self.__node_id,self.rev))
288        row = c.fetchone()
289        if row is not None:
290            self.__log.debug('Cache hit %s at rev %s', self.path, self.rev)
291            # if present just return it
292            data = str(buffer(row[0]))
293        else:
294            self.__log.debug('Building cache for %s at rev %s', self.path, self.rev)
295
296            # load the file content from the repo
297            c.execute('SELECT hash FROM darcs_changesets '
298                      'WHERE repo_id = %s AND rev = %s', (self.__repo_id,self.rev,))
299            hash = c.fetchone()[0]
300            data = self.__cmd.cat(hash, self.path)
301
302            # save the file content in the cache
303
304            c = self.__db.cursor()
305            c.execute('INSERT INTO darcs_cache (repo_id,node_id,rev,content,size) '
306                      'VALUES (%s,%s,%s,%s,%s)',
307                      (self.__repo_id,self.__node_id,self.rev,buffer(data),len(data)))
308        return StringIO.StringIO(data)
309
310    def get_entries(self):
311        if self.__node_type == NODE_FILE_TYPE:
312            return
313        q = query_nodes_for_revision(self.__repo_id, self.rev)
314        if self.__node_id is None:
315            q += ' AND dnc.parent_id IS NULL'
316        else:
317            q += ' AND dnc.parent_id = %d' % self.__node_id
318        c = self.__db.cursor()
319        c.execute(q)
320        for node_id,rev,path,_ in c:
321            node_type = get_node_type(self.__db, self.__repo_id, node_id)
322            yield DarcsNode(node_id, node_type, path, rev,
323                            self.__db, self.__repo_id, self.__cmd, self.__log)
324
325    def get_history(self, limit=None):
326        if self.path == '/':
327            for i in range(self.rev,0,-1):
328                yield (self.path, i, Changeset.EDIT)
329            return
330        c = self.__db.cursor()
331        q = ('SELECT path,rev,the_change FROM darcs_node_changes '
332             'WHERE repo_id = %s AND node_id = %s AND rev <= %s '
333             'ORDER BY rev DESC')
334        if limit is not None:
335            q += ' LIMIT %d' % limit
336        c.execute(q, (self.__repo_id, self.__node_id, self.rev))
337        for path,rev,change in c:
338            yield (path, rev, _change_map[change])
339
340    def get_annotations(self):
341        """Provide detailed backward history for the content of this Node.
342
343        Retrieve an array of revisions parsing `darcs annotate`.
344        """
345
346        from xml.sax import make_parser
347        from xml.sax.handler import ContentHandler, ErrorHandler
348        from datetime import datetime
349
350        c = self.__db.cursor()
351
352        class DarcsXMLAnnotateHandler(ContentHandler):
353            def __init__(self):
354                self.revisions = []
355                self.known_hashes = {}
356
357            def startElement(self, name, attributes):
358                if name == 'patch':
359                    self.current_hash = attributes['hash']
360
361            def endElement(self, name):
362                if name == 'normal_line':
363                    self.revisions.append(self.findRevision(self.current_hash))
364                elif name == 'added_line':
365                    self.revisions.append(self.findRevision(self.last_changed_hash))
366                elif name == 'modified':
367                    self.last_changed_hash = self.current_hash
368
369            def findRevision(self, hash):
370                # Return the trac revision for the given patch hash
371                try:
372                    return self.known_hashes[hash]
373                except KeyError:
374                    c.execute('SELECT rev FROM darcs_changesets '
375                              'WHERE hash = %s', (hash,))
376                    rev = self.known_hashes[hash] = c.fetchone()[0]
377                    return rev
378
379        # Get the hash of the patch
380        c.execute('SELECT hash FROM darcs_changesets '
381                  'WHERE rev = %s', (self.rev,))
382        hash = c.fetchone()[0]
383
384        # Get darcs annotate output for the given entry and patch hash
385        annotate = self.__cmd.annotate(hash, self.path)
386
387        parser = make_parser()
388        handler = DarcsXMLAnnotateHandler()
389        parser.setContentHandler(handler)
390        parser.setErrorHandler(ErrorHandler())
391
392        parser.feed(annotate)
393        parser.close()
394
395        return handler.revisions
396
397    def get_properties(self):
398        return {}
399
400    def get_content_length(self):
401        if self.isdir:
402            return None
403
404        # first check if the file is already in the cache
405        c = self.__db.cursor()
406        c.execute('SELECT size FROM darcs_cache '
407                  'WHERE node_id = %s AND rev = %s',
408                  (self.__node_id,self.rev))
409        row = c.fetchone()
410        if row is not None:
411            return row[0]
412
413        # if it's not, get the whole content and count...
414        # next time you'll be luckier, promise!
415        return len(self.get_content().read())
416
417    def get_content_type(self):
418        if self.isdir:
419            return None
420        return mimetypes.guess_type(self.path)[0]
421
422    def get_name(self):
423        return os.path.split(self.path)[1]
424
425    def get_last_modified(self):
426        if self.__node_id is None:
427            return 0
428        c = self.__db.cursor()
429        c.execute('SELECT rev FROM darcs_node_changes '
430                  'WHERE repo_id = %s AND node_id = %s AND rev = %s',
431                  (self.__repo_id,self.__node_id,self.rev))
432        rev = c.fetchone()[0]
433        c.execute('SELECT time FROM revision '
434                  'WHERE repo_id = %s AND rev = %s', (self.__repo_id,rev,))
435        return datetime.fromtimestamp(c.fetchone()[0], utc)
436
437class DarcsChangeset(Changeset):
438    def __init__(self, db, repo_id, rev):
439        self.repo_id = repo_id
440        c = db.cursor()
441        if IS_TRAC_0_12_OR_BETTER:
442            c.execute('SELECT r.author,r.time,c.name,r.message,c.hash '
443                      'FROM revision as r, darcs_changesets as c '
444                      'WHERE r.repo_id = %s AND r.rev = %s '
445                      '  AND c.rev = r.rev AND c.repo = %s ',
446                      (self.repo_id, rev, self.repo_id))
447        else:
448            c.execute('SELECT r.author,r.time,c.name,r.message,c.hash '
449                      'FROM revision as r, darcs_changesets as c '
450                      'WHERE r.rev = %s '
451                      '  AND c.rev = r.rev', (rev,))
452        row = c.fetchone()
453        if row is None:
454            raise NoSuchChangeset(rev)
455        author,date,name,comment,hash = row
456        date = datetime.fromtimestamp(date, utc)
457        # Trac 0.10.x hack
458        if IS_TRAC_0_10_X:
459            date = time.mktime(date.timetuple())
460        msg = name
461        if comment:
462            msg += '\n' + comment
463        Changeset.__init__(self, rev, msg, author, date)
464        self.__db = db
465        self.__hash = hash
466
467    def get_changes(self):
468        c = self.__db.cursor()
469        c.execute('SELECT node_id,path,the_change FROM darcs_node_changes '
470                   'WHERE repo_id = %s AND rev = %s', (self.repo_id,self.rev,))
471        for node_id,path,change in c:
472            node_type = get_node_type(self.__db, self.repo_id, node_id)
473            kind = _node_type_map[node_type]
474            if change == CHANGE_ADDED:
475                prev_path = prev_rev = None
476            else:
477                prev_path,prev_rev = get_prev_path_rev(self.__db, self.repo_id,
478                                                       node_id, self.rev)
479            change = _change_map[change]
480            yield (path,kind,change,prev_path,prev_rev)
481
482    def get_properties(self):
483        # omit ending .gz, because under some configuration the Apache
484        # web server automatically tags such URLs with something like
485        # "Content-Encoding: gzip" that in turn may confuse the browser.
486        # Darcs recognizes also extension-stripped hashnames.
487        return dict(Hashname=self.__hash[:-3])
Note: See TracBrowser for help on using the repository browser.