source: tracdarcs/tracdarcs/repository.py @ 147

Revision 147, 19.5 KB checked in by lele@…, 4 years ago (diff)

Better caching scheme
This is a dual-headed optimization. the starting point is that darcs
is faster computing the content of recent versions (because less
patches get commuted, I understand correctly), and much-much faster in
serving the "HEAD" version of an entry.

So, when asked about revision R, answer with the (possibly cached)
content of revision RC instead: RC is either the HEAD revision, if
there are no changes to the entry since revision R, or NR-1, the
revision that preceeds the next change to the entry.

This also means that the cache will contains a lot less entries:
instead of having multiple copies of the indentical content of an
entry for each revision between two distinct darcs patches, now only
one copy is actually stored in the database, the one corresponding to
the upper end of the range.

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
29
30from trac.util import TracError
31from trac.util.datefmt import to_timestamp, utc
32from trac.versioncontrol import (Repository, Node, Changeset,
33                                 NoSuchChangeset, NoSuchNode)
34
35from command import DarcsCommand
36from updatedb import update_darcsdb
37from dbutil import (CHANGE_ADDED, CHANGE_EDITED, CHANGE_MOVED,
38                    CHANGE_MOVED_EDITED, CHANGE_REMOVED,
39                    IS_TRAC_0_10_X, IS_TRAC_0_11_X,
40                    IS_TRAC_0_12_OR_BETTER, NODE_DIR_TYPE,
41                    NODE_FILE_TYPE, get_node_type, get_prev_path_rev,
42                    get_repository_id, query_nodes_for_revision)
43
44# mapping from node types used by the darcs backend and the types
45# used by the trac api
46_node_type_map = {
47    NODE_FILE_TYPE : Node.FILE,
48    NODE_DIR_TYPE : Node.DIRECTORY
49    }
50
51# mapping from change types used by the darcs backend and the types
52# used by the trac api
53_change_map = {
54    CHANGE_ADDED : Changeset.ADD,
55    CHANGE_REMOVED : Changeset.DELETE,
56    CHANGE_MOVED : Changeset.MOVE,
57    CHANGE_EDITED : Changeset.EDIT,
58    #FIXME: treat moved&edited as just moved?
59    CHANGE_MOVED_EDITED : Changeset.MOVE
60    }
61
62class DarcsRepository(Repository):
63    def __init__(self, db, path, log, darcscmd, possible_encodings):
64        Repository.__init__(self, path, None, log)
65        self.db = db
66        self.path = path
67        self.log = log
68        self.__cmd = DarcsCommand(darcscmd, path, log, possible_encodings)
69        self.repo_id = get_repository_id(db, path)
70        if IS_TRAC_0_10_X:
71            self.sync()
72
73    def close(self):
74        pass
75
76    def get_changeset(self, rev):
77        rev = self.normalize_rev(rev)
78        return DarcsChangeset(self.db, self.repo_id, rev)
79
80    def get_node(self, path, rev=None):
81        path = self.normalize_path(path)
82        rev = self.normalize_rev(rev)
83        # compute node_id, node_type and last_rev and then
84        # create a DarcsNode object.
85        # 'last_rev' is the last revision <= rev where this
86        # node was modified.
87        if path == '/':
88            node_id = None
89            node_type = NODE_DIR_TYPE
90            last_rev = rev
91        else:
92            c = self.db.cursor()
93            q = query_nodes_for_revision(self.repo_id, rev)
94            q += ' AND dnc.path = %s'
95            c.execute(q, (path,))
96            row = c.fetchone()
97            if row is None:
98                raise NoSuchNode(path, rev)
99            node_id,last_rev = row[:2]
100            node_type = get_node_type(self.db, self.repo_id, node_id)
101        return DarcsNode(node_id, node_type, path, last_rev,
102                         self.db, self.repo_id, self.__cmd, self.log)
103
104    def get_oldest_rev(self):
105        if self.get_youngest_rev() is None:
106            return None
107        return 1
108
109    def get_youngest_rev(self):
110        c = self.db.cursor()
111        c.execute('SELECT max(rev) FROM darcs_changesets '
112                  'WHERE repo_id = %s', (self.repo_id,))
113        row = c.fetchone()
114        return row and row[0] or None
115
116    def previous_rev(self, rev):
117        rev = self.normalize_rev(rev)
118        if rev > 1:
119            return rev-1
120        return None
121
122    def next_rev(self, rev, path=''):
123        rev = self.normalize_rev(rev)
124        if rev < self.get_youngest_rev():
125            return rev+1
126        return None
127
128    def rev_older_than(self, rev1, rev2):
129        return self.normalize_rev(rev1) < self.normalize_rev(rev2)
130
131    def get_path_history(self, path, rev=None, limit=None):
132        # FIXME: this is not correct
133        return self.get_node(path, rev).get_history(limit)
134
135    def normalize_path(self, path):
136        return path and path.strip('/') or '/'
137
138    def normalize_rev(self, rev):
139        if isinstance(rev, basestring) and len(rev) in (61,64):
140            if not rev.endswith('.gz'):
141                # We store the complete hashname in the db
142                rev = rev + '.gz'
143            c = self.db.cursor()
144            c.execute('SELECT rev FROM darcs_changesets '
145                      'WHERE repo_id = %s AND hash = %s', (self.repo_id, rev))
146            row = c.fetchone()
147            if row is None:
148                raise NoSuchChangeset(rev)
149            rev = int(row[0])
150        else:
151            youngest = self.get_youngest_rev()
152            if rev is None or rev == "":
153                return youngest
154            try:
155                rev = int(rev)
156            except ValueError, le:
157                raise TracError('Ill-formed revision: %s, error: %s' % (rev, le))
158            if rev > youngest:
159                rev = youngest
160        return rev
161
162    def get_changes(self, old_path, old_rev, new_path, new_rev, ignore_ancestry=1):
163        old_path = self.normalize_path(old_path)
164        old_rev = self.normalize_rev(old_rev)
165        new_path = self.normalize_path(new_path)
166        new_rev = self.normalize_rev(new_rev)
167        old_node = self.get_node(old_path, old_rev)
168        new_node = self.get_node(new_path, new_rev)
169
170        node_id = old_node._get_node_id()
171        if node_id != new_node._get_node_id():
172            raise TracError('Node mismatch: base is %s in rev %d '
173                            'and target is %s in rev %d' % (old_path,old_rev,
174                                                            new_path,new_rev))
175
176        if old_node.kind == Node.FILE:
177            if old_node.rev != new_node.rev:
178                yield (old_node,new_node,Node.FILE,Changeset.EDIT)
179            return
180
181        c = self.db.cursor()
182        if node_id is not None:
183            c.execute('SELECT rev,path FROM darcs_node_changes '
184                      'WHERE repo_id = %s AND node_id = %s AND rev >= %s AND rev <= %s',
185                      (self.repo_id,node_id,old_rev,new_rev))
186        else:
187            c.execute('SELECT rev,path FROM darcs_node_changes '
188                      'WHERE repo_id = %s AND rev >= %s AND rev <= %s',
189                      (self.repo_id,old_rev,new_rev))
190        node_set = dict()
191        node_list = []
192        c1 = self.db.cursor()
193        for rev,path in c:
194            c1.execute('SELECT node_id FROM darcs_node_changes '
195                       'WHERE repo_id = %s AND rev = %s AND path LIKE %s',
196                       (self.repo_id,rev,path+'/%'))
197            for nid, in c1:
198                if nid not in node_set:
199                    node_set[nid] = 1
200                    node_list.append(nid)
201        for nid in node_list:
202            old_node = new_node = None
203            c1.execute('SELECT rev,path FROM darcs_node_changes '
204                       'WHERE repo_id = %s AND node_id = %s AND rev < %s '
205                       'ORDER BY rev DESC LIMIT 1',
206                       (self.repo_id,nid,old_rev))
207            row = c1.fetchone()
208            if row is not None:
209                rev,path = row
210                old_node = self.get_node(path, rev)
211            c1.execute('SELECT rev,path,the_change FROM darcs_node_changes '
212                       'WHERE repo_id = %s AND node_id = %s AND rev >= %s AND rev <= %s '
213                       'ORDER BY rev DESC LIMIT 1',
214                       (self.repo_id,nid,old_rev,new_rev))
215            rev,path,change = c1.fetchone()
216            if change != CHANGE_REMOVED:
217                new_node = self.get_node(path, rev)
218            assert (old_node is not None) or (new_node is not None)
219            kind = old_node and old_node.kind or new_node.kind
220            if old_node is None:
221                change = Changeset.ADD
222            elif new_node is None:
223                change = Changeset.DELETE
224            elif old_node.path != new_node.path:
225                change = Changeset.MOVE
226            else:
227                change = Changeset.EDIT
228            yield (old_node,new_node,kind,change)
229
230    def sync(self, rev_callback=None):
231        # Import any new changesets, if any
232        update_darcsdb(self.db, self.__cmd, self.log, rev_callback=rev_callback)
233
234class DarcsNode(Node):
235    def __init__(self, node_id, node_type, path, rev,
236                 db, repo_id, cmd, log=None):
237        kind = _node_type_map[node_type]
238        Node.__init__(self, path, rev, kind)
239        self.__node_id = node_id
240        self.__node_type = node_type
241        self.__db = db
242        self.__repo_id = repo_id
243        self.__cmd = cmd
244        self.__log = log
245        self.created_path = path
246        self.created_rev = rev
247
248    def _get_node_id(self):
249        return self.__node_id
250
251    def _get_cached_rev(self):
252        # if there are no future versions, use the HEAD
253        nrev = self._get_next()
254        if nrev is None:
255            return None
256
257        prev = self.get_previous()
258        if prev is not None:
259            # ok, let's see if there is already a cache in the range
260            c = self.__db.cursor()
261            c.execute('SELECT max(rev) FROM darcs_cache '
262                      'WHERE repo_id = %s AND node_id = %s AND rev >= %s AND rev < %s AND content IS NOT NULL',
263                      (self.__repo_id, self.__node_id, prev[1], nrev[1]))
264            row = c.fetchone()
265            if row is not None:
266                return row[0]
267
268        # No luck, return the most recent revision before the next
269        return nrev[1]-1
270
271    def get_content(self):
272        if self.__node_type == NODE_DIR_TYPE:
273            return None
274        c = self.__db.cursor()
275
276        crev = self._get_cached_rev()
277        if crev is not None:
278            # check if the file content is there in the cache
279            c.execute('SELECT content FROM darcs_cache '
280                      'WHERE repo_id = %s AND node_id = %s AND rev = %s',
281                      (self.__repo_id,self.__node_id,crev))
282            row = c.fetchone()
283            if row is not None:
284                self.__log.debug('Cache hit %s at rev %s', self.path, crev)
285                # if present just return it
286                data = str(buffer(row[0]))
287            else:
288                self.__log.debug('Building cache for %s at rev %s', self.path, crev)
289
290                # load the file content from the repo
291                c.execute('SELECT hash FROM darcs_changesets '
292                          'WHERE repo_id = %s AND rev = %s', (self.__repo_id, crev,))
293                hash = c.fetchone()[0]
294                data = self.__cmd.cat(hash, self.path)
295
296                # save the file content in the cache
297                c = self.__db.cursor()
298                c.execute('INSERT INTO darcs_cache (repo_id,node_id,rev,content,size) '
299                          'VALUES (%s,%s,%s,%s,%s)',
300                          (self.__repo_id, self.__node_id, crev, buffer(data), len(data)))
301        else:
302            # Use the HEAD
303            self.__log.debug('Serving pristine file %s, no changes since rev %s', self.path, self.rev)
304            data = self.__cmd.cat(None, self.path)
305
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.__repo_id, 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, self.__repo_id, node_id)
320            yield DarcsNode(node_id, node_type, path, rev,
321                            self.__db, self.__repo_id, 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 repo_id = %s AND 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.__repo_id, self.__node_id, self.rev))
335        for path,rev,change in c:
336            yield (path, rev, _change_map[change])
337
338    def _get_next(self):
339        try:
340            return self._get_future(1).next()
341        except StopIteration:
342            return None
343
344    def _get_future(self, limit=None):
345        if self.path == '/':
346            youngest = self.get_youngest_rev()
347            for i in range(youngest, self.rev, -1):
348                yield (self.path, i, Changeset.EDIT)
349            return
350        c = self.__db.cursor()
351        q = ('SELECT path,rev,the_change FROM darcs_node_changes '
352             'WHERE repo_id = %s AND node_id = %s AND rev > %s '
353             'ORDER BY rev')
354        if limit is not None:
355            q += ' LIMIT %d' % limit
356        c.execute(q, (self.__repo_id, self.__node_id, self.rev))
357        for path,rev,change in c:
358            yield (path, rev, _change_map[change])
359
360    def get_annotations(self):
361        """Provide detailed backward history for the content of this Node.
362
363        Retrieve an array of revisions parsing `darcs annotate`.
364        """
365
366        from xml.sax import make_parser
367        from xml.sax.handler import ContentHandler, ErrorHandler
368        from datetime import datetime
369
370        c = self.__db.cursor()
371
372        class DarcsXMLAnnotateHandler(ContentHandler):
373            def __init__(self):
374                self.revisions = []
375                self.known_hashes = {}
376
377            def startElement(self, name, attributes):
378                if name == 'patch':
379                    self.current_hash = attributes['hash']
380
381            def endElement(self, name):
382                if name == 'normal_line':
383                    self.revisions.append(self.findRevision(self.current_hash))
384                elif name == 'added_line':
385                    self.revisions.append(self.findRevision(self.last_changed_hash))
386                elif name == 'modified':
387                    self.last_changed_hash = self.current_hash
388
389            def findRevision(self, hash):
390                # Return the trac revision for the given patch hash
391                try:
392                    return self.known_hashes[hash]
393                except KeyError:
394                    c.execute('SELECT rev FROM darcs_changesets '
395                              'WHERE hash = %s', (hash,))
396                    rev = self.known_hashes[hash] = c.fetchone()[0]
397                    return rev
398
399        # Get the hash of the patch
400        c.execute('SELECT hash FROM darcs_changesets '
401                  'WHERE rev = %s', (self.rev,))
402        hash = c.fetchone()[0]
403
404        # Get darcs annotate output for the given entry and patch hash
405        annotate = self.__cmd.annotate(hash, self.path)
406
407        parser = make_parser()
408        handler = DarcsXMLAnnotateHandler()
409        parser.setContentHandler(handler)
410        parser.setErrorHandler(ErrorHandler())
411
412        parser.feed(annotate)
413        parser.close()
414
415        return handler.revisions
416
417    def get_properties(self):
418        return {}
419
420    def get_content_length(self):
421        if self.isdir:
422            return None
423
424        # first check if the file is already in the cache
425        c = self.__db.cursor()
426        c.execute('SELECT size FROM darcs_cache '
427                  'WHERE repo_id = %s AND node_id = %s AND rev = %s',
428                  (self.__repo_id, self.__node_id,self.rev))
429        row = c.fetchone()
430        if row is not None:
431            return row[0]
432
433        # if it's not, get the whole content and count...
434        # next time you'll be luckier, promise!
435        return len(self.get_content().read())
436
437    def get_content_type(self):
438        if self.isdir:
439            return None
440        return mimetypes.guess_type(self.path)[0]
441
442    def get_name(self):
443        return os.path.split(self.path)[1]
444
445    def get_last_modified(self):
446        if self.__node_id is None:
447            return 0
448        c = self.__db.cursor()
449        c.execute('SELECT rev FROM darcs_node_changes '
450                  'WHERE repo_id = %s AND node_id = %s AND rev = %s',
451                  (self.__repo_id,self.__node_id,self.rev))
452        rev = c.fetchone()[0]
453        c.execute('SELECT time FROM revision '
454                  'WHERE repos = %s AND rev = %s', (self.__repo_id,rev,))
455        return datetime.fromtimestamp(c.fetchone()[0], utc)
456
457class DarcsChangeset(Changeset):
458    def __init__(self, db, repo_id, rev):
459        self.repo_id = repo_id
460        c = db.cursor()
461        if IS_TRAC_0_12_OR_BETTER:
462            c.execute('SELECT r.author,r.time,c.name,r.message,c.hash '
463                      'FROM revision as r, darcs_changesets as c '
464                      'WHERE r.repos = %s AND c.repo_id = r.repos '
465                      '  AND r.rev = %s AND c.rev = r.rev',
466                      (self.repo_id, rev))
467        else:
468            c.execute('SELECT r.author,r.time,c.name,r.message,c.hash '
469                      'FROM revision as r, darcs_changesets as c '
470                      'WHERE r.rev = %s '
471                      '  AND c.rev = r.rev AND c.repo_id = %s', (rev,''))
472        row = c.fetchone()
473        if row is None:
474            raise NoSuchChangeset(rev)
475        author,date,name,comment,hash = row
476        date = datetime.fromtimestamp(date, utc)
477        # Trac 0.10.x hack
478        if IS_TRAC_0_10_X:
479            date = time.mktime(date.timetuple())
480        msg = name
481        if comment:
482            msg += '\n' + comment
483        Changeset.__init__(self, rev, msg, author, date)
484        self.__db = db
485        self.__hash = hash
486
487    def get_changes(self):
488        c = self.__db.cursor()
489        c.execute('SELECT node_id,path,the_change FROM darcs_node_changes '
490                   'WHERE repo_id = %s AND rev = %s', (self.repo_id,self.rev,))
491        for node_id,path,change in c:
492            node_type = get_node_type(self.__db, self.repo_id, node_id)
493            kind = _node_type_map[node_type]
494            if change == CHANGE_ADDED:
495                prev_path = prev_rev = None
496            else:
497                prev_path,prev_rev = get_prev_path_rev(self.__db, self.repo_id,
498                                                       node_id, self.rev)
499            change = _change_map[change]
500            yield (path,kind,change,prev_path,prev_rev)
501
502    def get_properties(self):
503        # omit ending .gz, because under some configuration the Apache
504        # web server automatically tags such URLs with something like
505        # "Content-Encoding: gzip" that in turn may confuse the browser.
506        # Darcs recognizes also extension-stripped hashnames.
507
508        props = dict(Hashname=self.__hash[:-3])
509
510        c = self.__db.cursor()
511        c.execute('SELECT dcs.repo_id, dcs.rev '
512                  'FROM darcs_changesets dcs, darcs_changesets dcs2 '
513                  'WHERE dcs2.repo_id = %s AND dcs2.rev = %s '
514                  '  AND dcs.hash = dcs2.hash '
515                  '  AND dcs.repo_id <> dcs2.repo_id', (self.repo_id, self.rev))
516        eqcsets = [(repo, rev) for repo,rev in c.fetchall()]
517        if eqcsets:
518            props['EqChangesets'] = eqcsets
519
520        return props
Note: See TracBrowser for help on using the repository browser.