source: tracdarcs/tracdarcs/repository.py @ 150

Revision 150, 19.8 KB checked in by lele@…, 4 years ago (diff)

Refined and commented the optimization

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        maxrange = nrev[1]-1
258
259        # if it's just one hop from node's revision, we're done
260        if maxrange == self.rev:
261            return maxrange
262
263        # ok, let's see if there is already a cache in the range
264        c = self.__db.cursor()
265        c.execute('SELECT max(rev) FROM darcs_cache '
266                  'WHERE repo_id = %s AND node_id = %s AND rev >= %s AND rev <= %s AND content IS NOT NULL',
267                  (self.__repo_id, self.__node_id, self.rev, maxrange))
268        row = c.fetchone()
269        if row[0] is not None:
270            return row[0]
271
272        # No luck, return the most recent revision before the next
273        return maxrange
274
275    def get_content(self):
276        if self.__node_type == NODE_DIR_TYPE:
277            return None
278        c = self.__db.cursor()
279
280        # Since darcs is faster and faster in building the content
281        # of a file for more and more recent changes, compute the
282        # optimal revision to build the cache of
283        crev = self._get_cached_rev()
284
285        if crev is not None:
286            # check if the file content is there in the cache
287            c.execute('SELECT content FROM darcs_cache '
288                      'WHERE repo_id = %s AND node_id = %s AND rev = %s',
289                      (self.__repo_id,self.__node_id,crev))
290            row = c.fetchone()
291            if row is not None:
292                self.__log.debug('Cache hit %s at rev %s', self.path, crev)
293                # if present just return it
294                data = str(buffer(row[0]))
295            else:
296                self.__log.debug('Building cache for %s at rev %s', self.path, crev)
297
298                # load the file content from the repo
299                c.execute('SELECT hash FROM darcs_changesets '
300                          'WHERE repo_id = %s AND rev = %s', (self.__repo_id, crev,))
301                hash = c.fetchone()[0]
302                data = self.__cmd.cat(hash, self.path)
303
304                # save the file content in the cache
305                c = self.__db.cursor()
306                c.execute('INSERT INTO darcs_cache (repo_id,node_id,rev,content,size) '
307                          'VALUES (%s,%s,%s,%s,%s)',
308                          (self.__repo_id, self.__node_id, crev, buffer(data), len(data)))
309        else:
310            # Use the HEAD
311            self.__log.debug('Serving pristine file %s, no changes since rev %s', self.path, self.rev)
312            data = self.__cmd.cat(None, self.path)
313
314        return StringIO.StringIO(data)
315
316    def get_entries(self):
317        if self.__node_type == NODE_FILE_TYPE:
318            return
319        q = query_nodes_for_revision(self.__repo_id, self.rev)
320        if self.__node_id is None:
321            q += ' AND dnc.parent_id IS NULL'
322        else:
323            q += ' AND dnc.parent_id = %d' % self.__node_id
324        c = self.__db.cursor()
325        c.execute(q)
326        for node_id,rev,path,_ in c:
327            node_type = get_node_type(self.__db, self.__repo_id, node_id)
328            yield DarcsNode(node_id, node_type, path, rev,
329                            self.__db, self.__repo_id, self.__cmd, self.__log)
330
331    def get_history(self, limit=None):
332        if self.path == '/':
333            for i in range(self.rev,0,-1):
334                yield (self.path, i, Changeset.EDIT)
335            return
336        c = self.__db.cursor()
337        q = ('SELECT path,rev,the_change FROM darcs_node_changes '
338             'WHERE repo_id = %s AND node_id = %s AND rev <= %s '
339             'ORDER BY rev DESC')
340        if limit is not None:
341            q += ' LIMIT %d' % limit
342        c.execute(q, (self.__repo_id, self.__node_id, self.rev))
343        for path,rev,change in c:
344            yield (path, rev, _change_map[change])
345
346    def _get_next(self):
347        try:
348            return self._get_future(1).next()
349        except StopIteration:
350            return None
351
352    def _get_future(self, limit=None):
353        if self.path == '/':
354            youngest = self.get_youngest_rev()
355            for i in range(youngest, self.rev, -1):
356                yield (self.path, i, Changeset.EDIT)
357            return
358        c = self.__db.cursor()
359        q = ('SELECT path,rev,the_change FROM darcs_node_changes '
360             'WHERE repo_id = %s AND node_id = %s AND rev > %s '
361             'ORDER BY rev')
362        if limit is not None:
363            q += ' LIMIT %d' % limit
364        c.execute(q, (self.__repo_id, self.__node_id, self.rev))
365        for path,rev,change in c:
366            yield (path, rev, _change_map[change])
367
368    def get_annotations(self):
369        """Provide detailed backward history for the content of this Node.
370
371        Retrieve an array of revisions parsing `darcs annotate`.
372        """
373
374        from xml.sax import make_parser
375        from xml.sax.handler import ContentHandler, ErrorHandler
376        from datetime import datetime
377
378        c = self.__db.cursor()
379
380        class DarcsXMLAnnotateHandler(ContentHandler):
381            def __init__(self):
382                self.revisions = []
383                self.known_hashes = {}
384
385            def startElement(self, name, attributes):
386                if name == 'patch':
387                    self.current_hash = attributes['hash']
388
389            def endElement(self, name):
390                if name == 'normal_line':
391                    self.revisions.append(self.findRevision(self.current_hash))
392                elif name == 'added_line':
393                    self.revisions.append(self.findRevision(self.last_changed_hash))
394                elif name == 'modified':
395                    self.last_changed_hash = self.current_hash
396
397            def findRevision(self, hash):
398                # Return the trac revision for the given patch hash
399                try:
400                    return self.known_hashes[hash]
401                except KeyError:
402                    c.execute('SELECT rev FROM darcs_changesets '
403                              'WHERE hash = %s', (hash,))
404                    rev = self.known_hashes[hash] = c.fetchone()[0]
405                    return rev
406
407        # Get the hash of the patch
408        c.execute('SELECT hash FROM darcs_changesets '
409                  'WHERE rev = %s', (self.rev,))
410        hash = c.fetchone()[0]
411
412        # Get darcs annotate output for the given entry and patch hash
413        annotate = self.__cmd.annotate(hash, self.path)
414
415        parser = make_parser()
416        handler = DarcsXMLAnnotateHandler()
417        parser.setContentHandler(handler)
418        parser.setErrorHandler(ErrorHandler())
419
420        parser.feed(annotate)
421        parser.close()
422
423        return handler.revisions
424
425    def get_properties(self):
426        return {}
427
428    def get_content_length(self):
429        if self.isdir:
430            return None
431
432        # first check if the file is already in the cache
433        c = self.__db.cursor()
434        c.execute('SELECT size FROM darcs_cache '
435                  'WHERE repo_id = %s AND node_id = %s AND rev = %s',
436                  (self.__repo_id, self.__node_id,self.rev))
437        row = c.fetchone()
438        if row is not None:
439            return row[0]
440
441        # if it's not, get the whole content and count...
442        # next time you'll be luckier, promise!
443        return len(self.get_content().read())
444
445    def get_content_type(self):
446        if self.isdir:
447            return None
448        return mimetypes.guess_type(self.path)[0]
449
450    def get_name(self):
451        return os.path.split(self.path)[1]
452
453    def get_last_modified(self):
454        if self.__node_id is None:
455            return 0
456        c = self.__db.cursor()
457        c.execute('SELECT rev FROM darcs_node_changes '
458                  'WHERE repo_id = %s AND node_id = %s AND rev = %s',
459                  (self.__repo_id,self.__node_id,self.rev))
460        rev = c.fetchone()[0]
461        c.execute('SELECT time FROM revision '
462                  'WHERE repos = %s AND rev = %s', (self.__repo_id,rev,))
463        return datetime.fromtimestamp(c.fetchone()[0], utc)
464
465class DarcsChangeset(Changeset):
466    def __init__(self, db, repo_id, rev):
467        self.repo_id = repo_id
468        c = db.cursor()
469        if IS_TRAC_0_12_OR_BETTER:
470            c.execute('SELECT r.author,r.time,c.name,r.message,c.hash '
471                      'FROM revision as r, darcs_changesets as c '
472                      'WHERE r.repos = %s AND c.repo_id = r.repos '
473                      '  AND r.rev = %s AND c.rev = r.rev',
474                      (self.repo_id, rev))
475        else:
476            c.execute('SELECT r.author,r.time,c.name,r.message,c.hash '
477                      'FROM revision as r, darcs_changesets as c '
478                      'WHERE r.rev = %s '
479                      '  AND c.rev = r.rev AND c.repo_id = %s', (rev,''))
480        row = c.fetchone()
481        if row is None:
482            raise NoSuchChangeset(rev)
483        author,date,name,comment,hash = row
484        date = datetime.fromtimestamp(date, utc)
485        # Trac 0.10.x hack
486        if IS_TRAC_0_10_X:
487            date = time.mktime(date.timetuple())
488        msg = name
489        if comment:
490            msg += '\n' + comment
491        Changeset.__init__(self, rev, msg, author, date)
492        self.__db = db
493        self.__hash = hash
494
495    def get_changes(self):
496        c = self.__db.cursor()
497        c.execute('SELECT node_id,path,the_change FROM darcs_node_changes '
498                   'WHERE repo_id = %s AND rev = %s', (self.repo_id,self.rev,))
499        for node_id,path,change in c:
500            node_type = get_node_type(self.__db, self.repo_id, node_id)
501            kind = _node_type_map[node_type]
502            if change == CHANGE_ADDED:
503                prev_path = prev_rev = None
504            else:
505                prev_path,prev_rev = get_prev_path_rev(self.__db, self.repo_id,
506                                                       node_id, self.rev)
507            change = _change_map[change]
508            yield (path,kind,change,prev_path,prev_rev)
509
510    def get_properties(self):
511        # omit ending .gz, because under some configuration the Apache
512        # web server automatically tags such URLs with something like
513        # "Content-Encoding: gzip" that in turn may confuse the browser.
514        # Darcs recognizes also extension-stripped hashnames.
515
516        props = dict(Hashname=self.__hash[:-3])
517
518        c = self.__db.cursor()
519        c.execute('SELECT dcs.repo_id, dcs.rev '
520                  'FROM darcs_changesets dcs, darcs_changesets dcs2 '
521                  'WHERE dcs2.repo_id = %s AND dcs2.rev = %s '
522                  '  AND dcs.hash = dcs2.hash '
523                  '  AND dcs.repo_id <> dcs2.repo_id', (self.repo_id, self.rev))
524        eqcsets = [(repo, rev) for repo,rev in c.fetchall()]
525        if eqcsets:
526            props['EqChangesets'] = eqcsets
527
528        return props
Note: See TracBrowser for help on using the repository browser.