source: tracdarcs/tracdarcs/repository.py @ 163

Revision 163, 20.1 KB checked in by lele@…, 3 years ago (diff)

Assume repo_id==0 for older Tracs

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