source: tracdarcs/tracdarcs/repository.py @ 178

Revision 178, 23.0 KB checked in by lele@…, 3 years ago (diff)

Use a parametrized query

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