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

RevLine 
[126]1# -*- coding: utf-8 -*-
[36]2#
3# Copyright (C) 2005 Edgewall Software
4# Copyright (C) 2006 K.S.Sreeram <sreeram@tachyontech.net>
[126]5# Copyright (C) 2007,2008,2009 Lele Gaifax <lele@metapensiero.it>
[36]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
[106]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
[36]26import os, StringIO, mimetypes
[61]27from datetime import tzinfo, timedelta, datetime
[137]28import time
[61]29
30from trac.util import TracError
[104]31from trac.util.datefmt import to_timestamp, utc
32from trac.versioncontrol import (Repository, Node, Changeset,
33                                 NoSuchChangeset, NoSuchNode)
[136]34
[36]35from command import DarcsCommand
36from updatedb import update_darcsdb
[137]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)
[36]43
44# mapping from node types used by the darcs backend and the types
45# used by the trac api
46_node_type_map = {
[88]47    NODE_FILE_TYPE : Node.FILE,
48    NODE_DIR_TYPE : Node.DIRECTORY
49    }
[36]50
51# mapping from change types used by the darcs backend and the types
52# used by the trac api
53_change_map = {
[88]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    }
[36]61
[88]62class DarcsRepository(Repository):
[123]63    def __init__(self, db, path, log, darcscmd, possible_encodings):
[88]64        Repository.__init__(self, path, None, log)
[36]65        self.db = db
66        self.path = path
67        self.log = log
[132]68        self.__cmd = DarcsCommand(darcscmd, path, log, possible_encodings)
69        self.repo_id = get_repository_id(db, path)
[110]70        if IS_TRAC_0_10_X:
71            self.sync()
[36]72
[88]73    def close(self):
[36]74        pass
75
[88]76    def get_changeset(self, rev):
77        rev = self.normalize_rev(rev)
[131]78        return DarcsChangeset(self.db, self.repo_id, rev)
[36]79
[88]80    def get_node(self, path, rev=None):
81        path = self.normalize_path(path)
82        rev = self.normalize_rev(rev)
[36]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.
[88]87        if path == '/':
[36]88            node_id = None
89            node_type = NODE_DIR_TYPE
90            last_rev = rev
[88]91        else:
[36]92            c = self.db.cursor()
[131]93            q = query_nodes_for_revision(self.repo_id, rev)
[36]94            q += ' AND dnc.path = %s'
[88]95            c.execute(q, (path,))
[36]96            row = c.fetchone()
[88]97            if row is None:
98                raise NoSuchNode(path, rev)
[36]99            node_id,last_rev = row[:2]
[131]100            node_type = get_node_type(self.db, self.repo_id, node_id)
[88]101        return DarcsNode(node_id, node_type, path, last_rev,
[132]102                         self.db, self.repo_id, self.__cmd, self.log)
[36]103
[88]104    def get_oldest_rev(self):
105        if self.get_youngest_rev() is None:
[36]106            return None
107        return 1
108
[88]109    def get_youngest_rev(self):
[36]110        c = self.db.cursor()
[132]111        c.execute('SELECT max(rev) FROM darcs_changesets '
112                  'WHERE repo_id = %s', (self.repo_id,))
[36]113        row = c.fetchone()
114        return row and row[0] or None
115
[88]116    def previous_rev(self, rev):
117        rev = self.normalize_rev(rev)
118        if rev > 1:
[36]119            return rev-1
120        return None
121
[88]122    def next_rev(self, rev, path=''):
123        rev = self.normalize_rev(rev)
124        if rev < self.get_youngest_rev():
[36]125            return rev+1
126        return None
127
[88]128    def rev_older_than(self, rev1, rev2):
[36]129        return self.normalize_rev(rev1) < self.normalize_rev(rev2)
130
[88]131    def get_path_history(self, path, rev=None, limit=None):
[36]132        # FIXME: this is not correct
[88]133        return self.get_node(path, rev).get_history(limit)
[36]134
[88]135    def normalize_path(self, path):
[36]136        return path and path.strip('/') or '/'
137
[88]138    def normalize_rev(self, rev):
[121]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'
[120]143            c = self.db.cursor()
144            c.execute('SELECT rev FROM darcs_changesets '
[138]145                      'WHERE repo_id = %s AND hash = %s', (self.repo_id, rev))
[120]146            row = c.fetchone()
147            if row is None:
148                raise NoSuchChangeset(rev)
[121]149            rev = int(row[0])
[120]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
[36]160        return rev
161
[88]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)
[36]169
170        node_id = old_node._get_node_id()
[88]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))
[36]175
[88]176        if old_node.kind == Node.FILE:
177            if old_node.rev != new_node.rev:
[36]178                yield (old_node,new_node,Node.FILE,Changeset.EDIT)
179            return
180
181        c = self.db.cursor()
[88]182        c.execute('SELECT rev,path FROM darcs_node_changes '
[131]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))
[36]185        node_set = dict()
186        node_list = []
187        c1 = self.db.cursor()
[88]188        for rev,path in c:
189            c1.execute('SELECT node_id FROM darcs_node_changes '
[131]190                       'WHERE repo_id = %s AND rev = %s AND path LIKE %s',
191                       (self.repo_id,rev,path+'/%'))
[88]192            for nid, in c1:
193                if nid not in node_set:
[36]194                    node_set[nid] = 1
[88]195                    node_list.append(nid)
196        for nid in node_list:
[36]197            old_node = new_node = None
[88]198            c1.execute('SELECT rev,path FROM darcs_node_changes '
[131]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))
[36]202            row = c1.fetchone()
[88]203            if row is not None:
[36]204                rev,path = row
[88]205                old_node = self.get_node(path, rev)
206            c1.execute('SELECT rev,path,the_change FROM darcs_node_changes '
[131]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))
[36]210            rev,path,change = c1.fetchone()
[88]211            if change != CHANGE_REMOVED:
212                new_node = self.get_node(path, rev)
[36]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
[88]215            if old_node is None:
[36]216                change = Changeset.ADD
[88]217            elif new_node is None:
[36]218                change = Changeset.DELETE
[88]219            elif old_node.path != new_node.path:
[36]220                change = Changeset.MOVE
[88]221            else:
[36]222                change = Changeset.EDIT
223            yield (old_node,new_node,kind,change)
224
[88]225    def sync(self, rev_callback=None):
[73]226        # Import any new changesets, if any
[132]227        update_darcsdb(self.db, self.__cmd, self.log, rev_callback=rev_callback)
[37]228
[88]229class DarcsNode(Node):
[131]230    def __init__(self, node_id, node_type, path, rev,
231                 db, repo_id, cmd, log=None):
[36]232        kind = _node_type_map[node_type]
[88]233        Node.__init__(self, path, rev, kind)
[36]234        self.__node_id = node_id
235        self.__node_type = node_type
236        self.__db = db
[131]237        self.__repo_id = repo_id
[36]238        self.__cmd = cmd
[69]239        self.__log = log
[36]240        self.created_path = path
241        self.created_rev = rev
242
[88]243    def _get_node_id(self):
[36]244        return self.__node_id
245
[88]246    def get_content(self):
247        if self.__node_type == NODE_DIR_TYPE:
[36]248            return None
249        c = self.__db.cursor()
250        # check if the file content is there in the cache
[75]251        c.execute('SELECT content FROM darcs_cache '
[131]252                  'WHERE repo_id = %s AND node_id = %s AND rev = %s',
253                  (self.__repo_id,self.__node_id,self.rev))
[36]254        row = c.fetchone()
[88]255        if row is not None:
[75]256            self.__log.debug('Cache hit %s at rev %s', self.path, self.rev)
[36]257            # if present just return it
[60]258            data = str(buffer(row[0]))
[75]259        else:
260            self.__log.debug('Building cache for %s at rev %s', self.path, self.rev)
[88]261
[75]262            # load the file content from the repo
[132]263            c.execute('SELECT hash FROM darcs_changesets '
[131]264                      'WHERE repo_id = %s AND rev = %s', (self.__repo_id,self.rev,))
[75]265            hash = c.fetchone()[0]
266            data = self.__cmd.cat(hash, self.path)
[36]267
[75]268            # save the file content in the cache
[36]269
[75]270            c = self.__db.cursor()
[134]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)))
[88]274        return StringIO.StringIO(data)
[36]275
[88]276    def get_entries(self):
277        if self.__node_type == NODE_FILE_TYPE:
[36]278            return
[131]279        q = query_nodes_for_revision(self.__repo_id, self.rev)
[88]280        if self.__node_id is None:
[36]281            q += ' AND dnc.parent_id IS NULL'
[88]282        else:
[36]283            q += ' AND dnc.parent_id = %d' % self.__node_id
284        c = self.__db.cursor()
[88]285        c.execute(q)
286        for node_id,rev,path,_ in c:
[131]287            node_type = get_node_type(self.__db, self.__repo_id, node_id)
[88]288            yield DarcsNode(node_id, node_type, path, rev,
[131]289                            self.__db, self.__repo_id, self.__cmd, self.__log)
[36]290
[88]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)
[36]295            return
296        c = self.__db.cursor()
[88]297        q = ('SELECT path,rev,the_change FROM darcs_node_changes '
[131]298             'WHERE repo_id = %s AND node_id = %s AND rev <= %s '
[88]299             'ORDER BY rev DESC')
300        if limit is not None:
[36]301            q += ' LIMIT %d' % limit
[132]302        c.execute(q, (self.__repo_id, self.__node_id, self.rev))
[88]303        for path,rev,change in c:
304            yield (path, rev, _change_map[change])
[112]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
[36]362
[88]363    def get_properties(self):
[36]364        return {}
365
[88]366    def get_content_length(self):
367        if self.isdir:
[36]368            return None
[134]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!
[36]381        return len(self.get_content().read())
382
[88]383    def get_content_type(self):
384        if self.isdir:
[36]385            return None
[88]386        return mimetypes.guess_type(self.path)[0]
[36]387
[88]388    def get_name(self):
389        return os.path.split(self.path)[1]
[36]390
[88]391    def get_last_modified(self):
392        if self.__node_id is None:
[36]393            return 0
394        c = self.__db.cursor()
[88]395        c.execute('SELECT rev FROM darcs_node_changes '
[131]396                  'WHERE repo_id = %s AND node_id = %s AND rev = %s',
397                  (self.__repo_id,self.__node_id,self.rev))
[36]398        rev = c.fetchone()[0]
[132]399        c.execute('SELECT time FROM revision '
[140]400                  'WHERE repos = %s AND rev = %s', (self.__repo_id,rev,))
[102]401        return datetime.fromtimestamp(c.fetchone()[0], utc)
[59]402
[88]403class DarcsChangeset(Changeset):
[131]404    def __init__(self, db, repo_id, rev):
405        self.repo_id = repo_id
[36]406        c = db.cursor()
[136]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 '
[140]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))
[136]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,))
[36]418        row = c.fetchone()
[88]419        if row is None:
420            raise NoSuchChangeset(rev)
[118]421        author,date,name,comment,hash = row
[102]422        date = datetime.fromtimestamp(date, utc)
[78]423        # Trac 0.10.x hack
424        if IS_TRAC_0_10_X:
425            date = time.mktime(date.timetuple())
[36]426        msg = name
[88]427        if comment:
[36]428            msg += '\n' + comment
[88]429        Changeset.__init__(self, rev, msg, author, date)
[36]430        self.__db = db
[118]431        self.__hash = hash
[36]432
[88]433    def get_changes(self):
[36]434        c = self.__db.cursor()
[88]435        c.execute('SELECT node_id,path,the_change FROM darcs_node_changes '
[131]436                   'WHERE repo_id = %s AND rev = %s', (self.repo_id,self.rev,))
[88]437        for node_id,path,change in c:
[131]438            node_type = get_node_type(self.__db, self.repo_id, node_id)
[36]439            kind = _node_type_map[node_type]
[88]440            if change == CHANGE_ADDED:
[36]441                prev_path = prev_rev = None
[88]442            else:
[131]443                prev_path,prev_rev = get_prev_path_rev(self.__db, self.repo_id,
444                                                       node_id, self.rev)
[36]445            change = _change_map[change]
446            yield (path,kind,change,prev_path,prev_rev)
447
[88]448    def get_properties(self):
[121]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.
[144]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.