source: tracdarcs/tracdarcs/repository.py @ 144

Revision 144, 17.4 KB checked in by lele@…, 4 years ago (diff)

Show a link to the same changeset in other repositories

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        c.execute('SELECT rev,path FROM darcs_node_changes '
183                  'WHERE repo_id = %s AND node_id = %s AND rev >= %s AND rev <= %s',
184                  (self.repo_id,node_id,old_rev,new_rev))
185        node_set = dict()
186        node_list = []
187        c1 = self.db.cursor()
188        for rev,path in c:
189            c1.execute('SELECT node_id FROM darcs_node_changes '
190                       'WHERE repo_id = %s AND rev = %s AND path LIKE %s',
191                       (self.repo_id,rev,path+'/%'))
192            for nid, in c1:
193                if nid not in node_set:
194                    node_set[nid] = 1
195                    node_list.append(nid)
196        for nid in node_list:
197            old_node = new_node = None
198            c1.execute('SELECT rev,path FROM darcs_node_changes '
199                       'WHERE repo_id = %s AND node_id = %s AND rev < %s '
200                       'ORDER BY rev DESC LIMIT 1',
201                       (self.repo_id,nid,old_rev))
202            row = c1.fetchone()
203            if row is not None:
204                rev,path = row
205                old_node = self.get_node(path, rev)
206            c1.execute('SELECT rev,path,the_change FROM darcs_node_changes '
207                       'WHERE repo_id = %s AND node_id = %s AND rev >= %s AND rev <= %s '
208                       'ORDER BY rev DESC LIMIT 1',
209                       (self.repo_id,nid,old_rev,new_rev))
210            rev,path,change = c1.fetchone()
211            if change != CHANGE_REMOVED:
212                new_node = self.get_node(path, rev)
213            assert (old_node is not None) or (new_node is not None)
214            kind = old_node and old_node.kind or new_node.kind
215            if old_node is None:
216                change = Changeset.ADD
217            elif new_node is None:
218                change = Changeset.DELETE
219            elif old_node.path != new_node.path:
220                change = Changeset.MOVE
221            else:
222                change = Changeset.EDIT
223            yield (old_node,new_node,kind,change)
224
225    def sync(self, rev_callback=None):
226        # Import any new changesets, if any
227        update_darcsdb(self.db, self.__cmd, self.log, rev_callback=rev_callback)
228
229class DarcsNode(Node):
230    def __init__(self, node_id, node_type, path, rev,
231                 db, repo_id, cmd, log=None):
232        kind = _node_type_map[node_type]
233        Node.__init__(self, path, rev, kind)
234        self.__node_id = node_id
235        self.__node_type = node_type
236        self.__db = db
237        self.__repo_id = repo_id
238        self.__cmd = cmd
239        self.__log = log
240        self.created_path = path
241        self.created_rev = rev
242
243    def _get_node_id(self):
244        return self.__node_id
245
246    def get_content(self):
247        if self.__node_type == NODE_DIR_TYPE:
248            return None
249        c = self.__db.cursor()
250        # check if the file content is there in the cache
251        c.execute('SELECT content FROM darcs_cache '
252                  'WHERE repo_id = %s AND node_id = %s AND rev = %s',
253                  (self.__repo_id,self.__node_id,self.rev))
254        row = c.fetchone()
255        if row is not None:
256            self.__log.debug('Cache hit %s at rev %s', self.path, self.rev)
257            # if present just return it
258            data = str(buffer(row[0]))
259        else:
260            self.__log.debug('Building cache for %s at rev %s', self.path, self.rev)
261
262            # load the file content from the repo
263            c.execute('SELECT hash FROM darcs_changesets '
264                      'WHERE repo_id = %s AND rev = %s', (self.__repo_id,self.rev,))
265            hash = c.fetchone()[0]
266            data = self.__cmd.cat(hash, self.path)
267
268            # save the file content in the cache
269
270            c = self.__db.cursor()
271            c.execute('INSERT INTO darcs_cache (repo_id,node_id,rev,content,size) '
272                      'VALUES (%s,%s,%s,%s,%s)',
273                      (self.__repo_id,self.__node_id,self.rev,buffer(data),len(data)))
274        return StringIO.StringIO(data)
275
276    def get_entries(self):
277        if self.__node_type == NODE_FILE_TYPE:
278            return
279        q = query_nodes_for_revision(self.__repo_id, self.rev)
280        if self.__node_id is None:
281            q += ' AND dnc.parent_id IS NULL'
282        else:
283            q += ' AND dnc.parent_id = %d' % self.__node_id
284        c = self.__db.cursor()
285        c.execute(q)
286        for node_id,rev,path,_ in c:
287            node_type = get_node_type(self.__db, self.__repo_id, node_id)
288            yield DarcsNode(node_id, node_type, path, rev,
289                            self.__db, self.__repo_id, self.__cmd, self.__log)
290
291    def get_history(self, limit=None):
292        if self.path == '/':
293            for i in range(self.rev,0,-1):
294                yield (self.path, i, Changeset.EDIT)
295            return
296        c = self.__db.cursor()
297        q = ('SELECT path,rev,the_change FROM darcs_node_changes '
298             'WHERE repo_id = %s AND node_id = %s AND rev <= %s '
299             'ORDER BY rev DESC')
300        if limit is not None:
301            q += ' LIMIT %d' % limit
302        c.execute(q, (self.__repo_id, self.__node_id, self.rev))
303        for path,rev,change in c:
304            yield (path, rev, _change_map[change])
305
306    def get_annotations(self):
307        """Provide detailed backward history for the content of this Node.
308
309        Retrieve an array of revisions parsing `darcs annotate`.
310        """
311
312        from xml.sax import make_parser
313        from xml.sax.handler import ContentHandler, ErrorHandler
314        from datetime import datetime
315
316        c = self.__db.cursor()
317
318        class DarcsXMLAnnotateHandler(ContentHandler):
319            def __init__(self):
320                self.revisions = []
321                self.known_hashes = {}
322
323            def startElement(self, name, attributes):
324                if name == 'patch':
325                    self.current_hash = attributes['hash']
326
327            def endElement(self, name):
328                if name == 'normal_line':
329                    self.revisions.append(self.findRevision(self.current_hash))
330                elif name == 'added_line':
331                    self.revisions.append(self.findRevision(self.last_changed_hash))
332                elif name == 'modified':
333                    self.last_changed_hash = self.current_hash
334
335            def findRevision(self, hash):
336                # Return the trac revision for the given patch hash
337                try:
338                    return self.known_hashes[hash]
339                except KeyError:
340                    c.execute('SELECT rev FROM darcs_changesets '
341                              'WHERE hash = %s', (hash,))
342                    rev = self.known_hashes[hash] = c.fetchone()[0]
343                    return rev
344
345        # Get the hash of the patch
346        c.execute('SELECT hash FROM darcs_changesets '
347                  'WHERE rev = %s', (self.rev,))
348        hash = c.fetchone()[0]
349
350        # Get darcs annotate output for the given entry and patch hash
351        annotate = self.__cmd.annotate(hash, self.path)
352
353        parser = make_parser()
354        handler = DarcsXMLAnnotateHandler()
355        parser.setContentHandler(handler)
356        parser.setErrorHandler(ErrorHandler())
357
358        parser.feed(annotate)
359        parser.close()
360
361        return handler.revisions
362
363    def get_properties(self):
364        return {}
365
366    def get_content_length(self):
367        if self.isdir:
368            return None
369
370        # first check if the file is already in the cache
371        c = self.__db.cursor()
372        c.execute('SELECT size FROM darcs_cache '
373                  'WHERE node_id = %s AND rev = %s',
374                  (self.__node_id,self.rev))
375        row = c.fetchone()
376        if row is not None:
377            return row[0]
378
379        # if it's not, get the whole content and count...
380        # next time you'll be luckier, promise!
381        return len(self.get_content().read())
382
383    def get_content_type(self):
384        if self.isdir:
385            return None
386        return mimetypes.guess_type(self.path)[0]
387
388    def get_name(self):
389        return os.path.split(self.path)[1]
390
391    def get_last_modified(self):
392        if self.__node_id is None:
393            return 0
394        c = self.__db.cursor()
395        c.execute('SELECT rev FROM darcs_node_changes '
396                  'WHERE repo_id = %s AND node_id = %s AND rev = %s',
397                  (self.__repo_id,self.__node_id,self.rev))
398        rev = c.fetchone()[0]
399        c.execute('SELECT time FROM revision '
400                  'WHERE repos = %s AND rev = %s', (self.__repo_id,rev,))
401        return datetime.fromtimestamp(c.fetchone()[0], utc)
402
403class DarcsChangeset(Changeset):
404    def __init__(self, db, repo_id, rev):
405        self.repo_id = repo_id
406        c = db.cursor()
407        if IS_TRAC_0_12_OR_BETTER:
408            c.execute('SELECT r.author,r.time,c.name,r.message,c.hash '
409                      'FROM revision as r, darcs_changesets as c '
410                      'WHERE r.repos = %s AND c.repo_id = r.repos '
411                      '  AND r.rev = %s AND c.rev = r.rev',
412                      (self.repo_id, rev))
413        else:
414            c.execute('SELECT r.author,r.time,c.name,r.message,c.hash '
415                      'FROM revision as r, darcs_changesets as c '
416                      'WHERE r.rev = %s '
417                      '  AND c.rev = r.rev', (rev,))
418        row = c.fetchone()
419        if row is None:
420            raise NoSuchChangeset(rev)
421        author,date,name,comment,hash = row
422        date = datetime.fromtimestamp(date, utc)
423        # Trac 0.10.x hack
424        if IS_TRAC_0_10_X:
425            date = time.mktime(date.timetuple())
426        msg = name
427        if comment:
428            msg += '\n' + comment
429        Changeset.__init__(self, rev, msg, author, date)
430        self.__db = db
431        self.__hash = hash
432
433    def get_changes(self):
434        c = self.__db.cursor()
435        c.execute('SELECT node_id,path,the_change FROM darcs_node_changes '
436                   'WHERE repo_id = %s AND rev = %s', (self.repo_id,self.rev,))
437        for node_id,path,change in c:
438            node_type = get_node_type(self.__db, self.repo_id, node_id)
439            kind = _node_type_map[node_type]
440            if change == CHANGE_ADDED:
441                prev_path = prev_rev = None
442            else:
443                prev_path,prev_rev = get_prev_path_rev(self.__db, self.repo_id,
444                                                       node_id, self.rev)
445            change = _change_map[change]
446            yield (path,kind,change,prev_path,prev_rev)
447
448    def get_properties(self):
449        # omit ending .gz, because under some configuration the Apache
450        # web server automatically tags such URLs with something like
451        # "Content-Encoding: gzip" that in turn may confuse the browser.
452        # Darcs recognizes also extension-stripped hashnames.
453
454        props = dict(Hashname=self.__hash[:-3])
455
456        c = self.__db.cursor()
457        c.execute('SELECT dcs.repo_id, dcs.rev '
458                  'FROM darcs_changesets dcs, darcs_changesets dcs2 '
459                  'WHERE dcs2.repo_id = %s AND dcs2.rev = %s '
460                  '  AND dcs.hash = dcs2.hash '
461                  '  AND dcs.repo_id <> dcs2.repo_id', (self.repo_id, self.rev))
462        eqcsets = [(repo, rev) for repo,rev in c.fetchall()]
463        if eqcsets:
464            props['EqChangesets'] = eqcsets
465
466        return props
Note: See TracBrowser for help on using the repository browser.