source: tracdarcs/tracdarcs/repository.py @ 198

Revision 198, 24.2 KB checked in by lele@…, 3 years ago (diff)

Removed obsolete comment, since we store .gz-less hashes now

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-2010 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, TimedDB,
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                 params, eager_annotations):
65        if IS_TRAC_0_12_OR_BETTER:
66            Repository.__init__(self, 'darcs:%s' % path, params, log)
67        else:
68            Repository.__init__(self, 'darcs:%s' % path, params, None, log)
69        self.db = TimedDB(db, log)
70        self.path = path
71        self.__cmd = DarcsCommand(darcscmd, path, log, possible_encodings)
72        if not IS_TRAC_0_12_OR_BETTER:
73            self.log = log
74            self.id = get_repository_id(db, path) or 0
75        self.eager_annotations = eager_annotations
76        if IS_TRAC_0_10_X:
77            self.sync()
78
79    def close(self):
80        pass
81
82    def get_changeset(self, rev):
83        rev = self.normalize_rev(rev)
84        return DarcsChangeset(self, rev)
85
86    def get_node(self, path, rev=None):
87        path = self.normalize_path(path)
88        rev = self.normalize_rev(rev)
89        # compute node_id, node_type and last_rev and then
90        # create a DarcsNode object.
91        # 'last_rev' is the last revision <= rev where this
92        # node was modified.
93        if path == '/':
94            node_id = None
95            node_type = NODE_DIR_TYPE
96            last_rev = rev
97        else:
98            c = self.db.cursor()
99            q,args = query_nodes_for_revision(self.id, rev, 'dnc.path = %s')
100            args.append(path)
101            c.execute(q, args)
102            row = c.fetchone()
103            if row is None:
104                raise NoSuchNode(path, rev)
105            node_id,last_rev = row[:2]
106            node_type = get_node_type(self.db, self.id, node_id)
107        return DarcsNode(node_id, node_type, path, last_rev,
108                         self, self.__cmd, self.log)
109
110    def get_oldest_rev(self):
111        if self.get_youngest_rev() is None:
112            return None
113        return 1
114
115    def get_youngest_rev(self):
116        c = self.db.cursor()
117        c.execute('SELECT max(rev) FROM darcs_changesets '
118                  'WHERE repo_id = %s', (self.id,))
119        row = c.fetchone()
120        return row and row[0] or None
121
122    def previous_rev(self, rev, path=''):
123        rev = self.normalize_rev(rev)
124        if rev > 1:
125            return rev-1
126        return None
127
128    def next_rev(self, rev, path=''):
129        rev = self.normalize_rev(rev)
130        if rev < self.get_youngest_rev():
131            return rev+1
132        return None
133
134    def rev_older_than(self, rev1, rev2):
135        return self.normalize_rev(rev1) < self.normalize_rev(rev2)
136
137    def get_path_history(self, path, rev=None, limit=None):
138        # FIXME: this is not correct
139        return self.get_node(path, rev).get_history(limit)
140
141    def normalize_path(self, path):
142        return path and path.strip('/') or '/'
143
144    def normalize_rev(self, rev):
145        if isinstance(rev, basestring) and len(rev) in (61,64):
146            if rev.endswith('.gz'):
147                # We don't store ending .gz in the db
148                rev = rev[:-3]
149            c = self.db.cursor()
150            c.execute('SELECT rev FROM darcs_changesets '
151                      'WHERE repo_id = %s AND hash = %s', (self.id, rev))
152            row = c.fetchone()
153            if row is None:
154                raise NoSuchChangeset(rev)
155            rev = int(row[0])
156        else:
157            youngest = self.get_youngest_rev()
158            if rev is None or rev == "":
159                return youngest
160            try:
161                rev = int(rev)
162            except ValueError, le:
163                raise TracError('Ill-formed revision: %s, error: %s' % (rev, le))
164            if rev > youngest:
165                rev = youngest
166        return rev
167
168    def get_changes(self, old_path, old_rev, new_path, new_rev, ignore_ancestry=1):
169        old_path = self.normalize_path(old_path)
170        old_rev = self.normalize_rev(old_rev)
171        new_path = self.normalize_path(new_path)
172        new_rev = self.normalize_rev(new_rev)
173        old_node = self.get_node(old_path, old_rev)
174        new_node = self.get_node(new_path, new_rev)
175
176        node_id = old_node._get_node_id()
177        if node_id != new_node._get_node_id():
178            raise TracError('Node mismatch: base is %s in rev %d '
179                            'and target is %s in rev %d' % (old_path, old_rev,
180                                                            new_path, new_rev))
181
182        if old_node.kind == Node.FILE:
183            if old_node.rev != new_node.rev:
184                yield (old_node, new_node, Node.FILE, Changeset.EDIT)
185            return
186
187        c = self.db.cursor()
188        if node_id is not None:
189            c.execute('SELECT rev,path FROM darcs_node_changes '
190                      'WHERE repo_id = %s AND node_id = %s AND rev >= %s AND rev <= %s',
191                      (self.id, node_id, old_rev, new_rev))
192        else:
193            c.execute('SELECT rev,path FROM darcs_node_changes '
194                      'WHERE repo_id = %s AND rev >= %s AND rev <= %s',
195                      (self.id, old_rev, new_rev))
196        node_set = dict()
197        node_list = []
198        c1 = self.db.cursor()
199        for rev,path in c:
200            c1.execute('SELECT node_id FROM darcs_node_changes '
201                       'WHERE repo_id = %s AND rev = %s AND path LIKE %s',
202                       (self.id, rev, path+'/%'))
203            for nid, in c1:
204                if nid not in node_set:
205                    node_set[nid] = 1
206                    node_list.append(nid)
207        for nid in node_list:
208            old_node = new_node = None
209            c1.execute('SELECT rev,path FROM darcs_node_changes '
210                       'WHERE repo_id = %s AND node_id = %s AND rev < %s '
211                       'ORDER BY rev DESC LIMIT 1',
212                       (self.id, nid, old_rev))
213            row = c1.fetchone()
214            if row is not None:
215                rev,path = row
216                old_node = self.get_node(path, rev)
217            c1.execute('SELECT rev,path,change_kind FROM darcs_node_changes '
218                       'WHERE repo_id = %s AND node_id = %s AND rev >= %s AND rev <= %s '
219                       'ORDER BY rev DESC LIMIT 1',
220                       (self.id, nid, old_rev, new_rev))
221            rev,path,change = c1.fetchone()
222            if change != CHANGE_REMOVED:
223                new_node = self.get_node(path, rev)
224            assert (old_node is not None) or (new_node is not None)
225            kind = old_node and old_node.kind or new_node.kind
226            if old_node is None:
227                change = Changeset.ADD
228            elif new_node is None:
229                change = Changeset.DELETE
230            elif old_node.path != new_node.path:
231                change = Changeset.MOVE
232            else:
233                change = Changeset.EDIT
234            yield (old_node,new_node,kind,change)
235
236    def sync(self, rev_callback=None, clean=False):
237        from dbutil import format_elapsed_time as trepr
238
239        # Import any new changesets, if any
240
241        newrevs = update_darcsdb(self.db, self.__cmd, self.log, self.id,
242                                 rev_callback=rev_callback, clean=clean)
243
244        # In eager mode, precompute the content and the annotations for
245        # each file modified or added by latest changesets
246
247        if self.eager_annotations and newrevs:
248            i = 1
249            count = len(newrevs)
250            for rev in newrevs:
251                t0 = time.time()
252
253                c = self.get_changeset(rev)
254                for path,kind,change,prev_path,prev_rev in c.get_changes():
255                    if kind==Node.FILE and (change==Changeset.EDIT or change==Changeset.ADD):
256                        node = self.get_node(path, rev)
257                        node.get_content()
258                        node.get_annotations()
259
260                t1 = time.time()
261                usec = (t1-t0) * 1e6
262                self.log.info('Preannotated changeset %d/%d: %s', i, count, trepr(usec))
263                i += 1
264
265class DarcsNode(Node):
266    def __init__(self, node_id, node_type, path, rev,
267                 repos, cmd, log=None):
268        kind = _node_type_map[node_type]
269        Node.__init__(self, repos, path, rev, kind)
270        self.__node_id = node_id
271        self.__node_type = node_type
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_cached_rev(self):
281        # if there are no future versions, use the HEAD
282        nrev = self._get_next()
283        if nrev is None:
284            return None
285
286        maxrange = nrev[1]-1
287
288        # if it's just one hop from node's revision, we're done
289        if maxrange == self.rev:
290            return maxrange
291
292        # ok, let's see if there is already a cache in the range
293        c = self.repos.db.cursor()
294        c.execute('SELECT max(rev) FROM darcs_cache '
295                  'WHERE repo_id = %s AND node_id = %s AND rev >= %s AND rev <= %s AND content IS NOT NULL',
296                  (self.repos.id, self.__node_id, self.rev, maxrange))
297        row = c.fetchone()
298        if row[0] is not None:
299            return row[0]
300
301        # No luck, return the most recent revision before the next
302        return maxrange
303
304    def get_content(self):
305        if self.__node_type == NODE_DIR_TYPE:
306            return None
307        c = self.repos.db.cursor()
308
309        # Since darcs is faster and faster in building the content
310        # of a file for more and more recent changes, compute the
311        # optimal revision to build the cache of
312        crev = self._get_cached_rev()
313
314        if crev is not None:
315            # check if the file content is there in the cache
316            c.execute('SELECT content FROM darcs_cache '
317                      'WHERE repo_id = %s AND node_id = %s AND rev = %s',
318                      (self.repos.id, self.__node_id, crev))
319            row = c.fetchone()
320            if row is not None:
321                self.__log.debug('Cache hit %s at rev %s', self.path, crev)
322                # if present just return it
323                data = str(buffer(row[0]))
324            else:
325                self.__log.debug('Building content cache for %s at rev %s', self.path, crev)
326
327                # load the file content from the repo
328                c.execute('SELECT hash FROM darcs_changesets '
329                          'WHERE repo_id = %s AND rev = %s', (self.repos.id, crev,))
330                hash = c.fetchone()[0]
331                data = self.__cmd.cat(hash, self.path)
332
333                # save the file content in the cache
334                c = self.repos.db.cursor()
335                try:
336                    c.execute('INSERT INTO darcs_cache (repo_id,node_id,rev,content,size) '
337                              'VALUES (%s,%s,%s,%s,%s)',
338                              (self.repos.id, self.__node_id, crev, buffer(data), len(data)))
339                except:
340                    self.repos.db.rollback()
341                    c = self.repos.db.cursor()
342                    # Maybe some other thread computed the same content
343                    c.execute('SELECT content FROM darcs_cache '
344                              'WHERE repo_id = %s AND node_id = %s AND rev = %s',
345                              (self.repos.id, self.__node_id, crev))
346                    row = c.fetchone()
347                    if row is not None:
348                        self.__log.debug('Late cache hit %s at rev %s', self.path, crev)
349                        data = str(buffer(row[0]))
350                    else:
351                        raise
352                else:
353                    self.repos.db.commit()
354        else:
355            # Use the HEAD
356            self.__log.debug('Serving pristine file %s, no changes since rev %s', self.path, self.rev)
357            data = self.__cmd.cat(None, self.path)
358
359        return StringIO.StringIO(data)
360
361    def get_entries(self):
362        if self.__node_type == NODE_FILE_TYPE:
363            return
364        if self.__node_id is None:
365            cond = 'dnc.parent_id IS NULL'
366        else:
367            cond = 'dnc.parent_id = %d' % self.__node_id
368        q,args = query_nodes_for_revision(self.repos.id, self.rev, cond)
369        c = self.repos.db.cursor()
370        c.execute(q, args)
371        for node_id,rev,path,_ in c:
372            node_type = get_node_type(self.repos.db, self.repos.id, node_id)
373            yield DarcsNode(node_id, node_type, path, rev,
374                            self.repos, self.__cmd, self.__log)
375
376    def get_history(self, limit=None):
377        if self.path == '/':
378            for i in range(self.rev,0,-1):
379                yield (self.path, i, Changeset.EDIT)
380            return
381        c = self.repos.db.cursor()
382        q = ('SELECT path,rev,change_kind FROM darcs_node_changes '
383             'WHERE repo_id = %s AND node_id = %s AND rev <= %s '
384             'ORDER BY rev DESC')
385        if limit is not None:
386            q += ' LIMIT %d' % limit
387        c.execute(q, (self.repos.id, self.__node_id, self.rev))
388        for path,rev,change in c:
389            yield (path, rev, _change_map[change])
390
391    def _get_next(self):
392        try:
393            return self._get_future(1).next()
394        except StopIteration:
395            return None
396
397    def _get_future(self, limit=None):
398        if self.path == '/':
399            youngest = self.get_youngest_rev()
400            for i in range(youngest, self.rev, -1):
401                yield (self.path, i, Changeset.EDIT)
402            return
403        c = self.repos.db.cursor()
404        q = ('SELECT path,rev,change_kind FROM darcs_node_changes '
405             'WHERE repo_id = %s AND node_id = %s AND rev > %s '
406             'ORDER BY rev')
407        if limit is not None:
408            q += ' LIMIT %d' % limit
409        c.execute(q, (self.repos.id, self.__node_id, self.rev))
410        for path,rev,change in c:
411            yield (path, rev, _change_map[change])
412
413    def get_annotations(self):
414        """Provide detailed backward history for the content of this Node.
415
416        Retrieve an array of revisions parsing `darcs annotate`. Since
417        that is (still) not fast enough for some repository, we write
418        a cache of the information: a future annotate on the same file
419        at the same revision won't reexecute `darcs annotate`.
420        """
421
422        from xml.sax import make_parser
423        from xml.sax.handler import ContentHandler, ErrorHandler
424        from datetime import datetime
425
426        c = self.repos.db.cursor()
427
428        # Since darcs is faster and faster in building the content
429        # of a file for more and more recent changes, compute the
430        # optimal revision to build the cache of
431        crev = self._get_cached_rev()
432        if crev is None:
433            crev = self.rev
434
435        # Check if the annotate cache is already present
436        c.execute('SELECT up_to_line,blame_rev FROM darcs_annotate_cache '
437                  'WHERE repo_id = %s AND node_id = %s AND rev = %s '
438                  'ORDER BY up_to_line', (self.repos.id, self.__node_id, crev))
439        row = c.fetchone()
440        if row is not None:
441            self.__log.debug('Annotate cache hit for %s at rev %s', self.path, crev)
442            revs = []
443            line = 0
444            # Expand the cache, producing a list of revisions, one per line
445            while row is not None:
446                while line<row[0]:
447                    revs.append(row[1])
448                    line += 1
449                row = c.fetchone()
450            return revs
451
452        # No cache, build it
453
454        self.__log.debug('Building annotate cache for %s at rev %s', self.path, crev)
455
456        class DarcsXMLAnnotateHandler(ContentHandler):
457            def __init__(self):
458                self.revisions = []
459                self.known_hashes = {}
460
461            def startElement(self, name, attributes):
462                if name == 'patch':
463                    self.current_hash = attributes['hash'][:-3]
464
465            def endElement(self, name):
466                if name == 'normal_line':
467                    self.revisions.append(self.findRevision(self.current_hash))
468                elif name == 'added_line':
469                    self.revisions.append(self.findRevision(self.last_changed_hash))
470                elif name == 'modified':
471                    self.last_changed_hash = self.current_hash
472
473            def findRevision(self, hash):
474                # Return the trac revision for the given patch hash
475                try:
476                    return self.known_hashes[hash]
477                except KeyError:
478                    c.execute('SELECT rev FROM darcs_changesets '
479                              'WHERE hash = %s', (hash,))
480                    rev = self.known_hashes[hash] = c.fetchone()[0]
481                    return rev
482
483        # Get the hash of the patch
484        c.execute('SELECT hash FROM darcs_changesets '
485                  'WHERE rev = %s', (self.rev,))
486        hash = c.fetchone()[0]
487
488        # Get darcs annotate output for the given entry and patch hash
489        annotate = self.__cmd.annotate(hash, self.path)
490
491        parser = make_parser()
492        handler = DarcsXMLAnnotateHandler()
493        parser.setContentHandler(handler)
494        parser.setErrorHandler(ErrorHandler())
495
496        parser.feed(annotate)
497        parser.close()
498
499        revs = handler.revisions
500        if not revs:
501            self.__log.debug('Empty file, no annotations for %s at rev %s', self.path, crev)
502            return revs
503
504        # Write a compressed representation
505
506        prev = None
507        for i,rev in enumerate(revs):
508            if prev is not None:
509                if prev != rev:
510                    c.execute('INSERT INTO darcs_annotate_cache (repo_id,node_id,rev,up_to_line,blame_rev) '
511                              'VALUES (%s,%s,%s,%s,%s)',
512                              (self.repos.id, self.__node_id, crev, i, prev))
513                    prev = rev
514                    lastline = i
515            else:
516                prev = rev
517                lastline = 0
518        if lastline != len(revs):
519            c.execute('INSERT INTO darcs_annotate_cache (repo_id,node_id,rev,up_to_line,blame_rev) '
520                      'VALUES (%s,%s,%s,%s,%s)',
521                      (self.repos.id, self.__node_id, crev, len(revs), revs[-1]))
522
523        self.repos.db.commit()
524
525        return revs
526
527    def get_properties(self):
528        return {}
529
530    def get_content_length(self):
531        if self.isdir:
532            return None
533
534        # first check if the file is already in the cache
535        c = self.repos.db.cursor()
536        c.execute('SELECT size FROM darcs_cache '
537                  'WHERE repo_id = %s AND node_id = %s AND rev = %s',
538                  (self.repos.id, self.__node_id, self.rev))
539        row = c.fetchone()
540        if row is not None:
541            return row[0]
542
543        # if it's not, get the whole content and count...
544        # next time you'll be luckier, promise!
545        return len(self.get_content().read())
546
547    def get_content_type(self):
548        if self.isdir:
549            return None
550        return mimetypes.guess_type(self.path)[0]
551
552    def get_name(self):
553        return os.path.split(self.path)[1]
554
555    def get_last_modified(self):
556        if self.__node_id is None:
557            return 0
558        c = self.repos.db.cursor()
559        c.execute('SELECT rev FROM darcs_node_changes '
560                  'WHERE repo_id = %s AND node_id = %s AND rev = %s',
561                  (self.repos.id, self.__node_id, self.rev))
562        rev = c.fetchone()[0]
563        if IS_TRAC_0_12_OR_BETTER:
564            c.execute('SELECT time FROM revision '
565                      'WHERE repos = %s AND rev = %s', (self.repos.id, rev))
566        else:
567            c.execute('SELECT time FROM revision '
568                      'WHERE rev = %s', (rev,))
569        return datetime.fromtimestamp(c.fetchone()[0], utc)
570
571class DarcsChangeset(Changeset):
572    def __init__(self, repos, rev):
573        repo_id = repos.id
574        c = repos.db.cursor()
575        if IS_TRAC_0_12_OR_BETTER:
576            c.execute('SELECT r.author,r.time,c.name,r.message,c.hash '
577                      'FROM revision as r, darcs_changesets as c '
578                      'WHERE r.repos = %s AND c.repo_id = r.repos '
579                      '  AND r.rev = %s AND c.rev = r.rev',
580                      (repo_id, rev))
581        else:
582            c.execute('SELECT r.author,r.time,c.name,r.message,c.hash '
583                      'FROM revision as r, darcs_changesets as c '
584                      'WHERE r.rev = %s '
585                      '  AND c.rev = r.rev AND c.repo_id = %s', (rev, 0))
586        row = c.fetchone()
587        if row is None:
588            raise NoSuchChangeset(rev)
589        author,date,name,comment,hash = row
590        date = datetime.fromtimestamp(date, utc)
591        # Trac 0.10.x hack
592        if IS_TRAC_0_10_X:
593            date = time.mktime(date.timetuple())
594        msg = name
595        if comment:
596            msg += '\n' + comment
597        Changeset.__init__(self, repos, rev, msg, author, date)
598        self.__hash = hash
599
600    def get_changes(self):
601        c = self.repos.db.cursor()
602        repo_id = self.repos.id
603        c.execute('SELECT node_id,path,change_kind FROM darcs_node_changes '
604                   'WHERE repo_id = %s AND rev = %s', (repo_id, self.rev,))
605        for node_id,path,change in c:
606            node_type = get_node_type(self.repos.db, repo_id, node_id)
607            kind = _node_type_map[node_type]
608            if change == CHANGE_ADDED:
609                prev_path = prev_rev = None
610            else:
611                prev_path,prev_rev = get_prev_path_rev(self.repos.db, repo_id,
612                                                       node_id, self.rev)
613            change = _change_map[change]
614            yield (path,kind,change,prev_path,prev_rev)
615
616    def get_properties(self):
617        props = dict(Hashname=self.__hash)
618
619        # Compute a list of "equivalent changesets", when the same
620        # changeset is present in other repositories.
621
622        c = self.repos.db.cursor()
623        c.execute('SELECT rep.value, dcs.rev '
624                  'FROM darcs_changesets dcs, darcs_changesets dcs2, repository rep '
625                  'WHERE dcs2.repo_id = %s AND dcs2.rev = %s '
626                  '  AND dcs.hash = dcs2.hash '
627                  '  AND dcs.repo_id <> dcs2.repo_id'
628                  '  AND rep.id = dcs.repo_id AND rep.name = \'name\''
629                  'ORDER BY rep.value', (self.repos.id, self.rev))
630        eqcsets = [(repo, rev) for repo,rev in c.fetchall()]
631        if eqcsets:
632            props['EqChangesets'] = eqcsets
633
634        return props
Note: See TracBrowser for help on using the repository browser.