Gentoo Websites Logo
Go to: Gentoo Home Documentation Forums Lists Bugs Planet Store Wiki Get Gentoo!
View | Details | Raw Unified | Return to bug 109477 | Differences between
and this patch

Collapse All | Expand All

(-)trac/trac/db_default.py (+1 lines)
Lines 82-87 Link Here
82
        Column('time', type='int'),
82
        Column('time', type='int'),
83
        Column('author'),
83
        Column('author'),
84
        Column('message'),
84
        Column('message'),
85
        Column('hash'),
85
        Index(['time'])],
86
        Index(['time'])],
86
    Table('node_change', key=('rev', 'path', 'change'))[
87
    Table('node_change', key=('rev', 'path', 'change'))[
87
        Column('rev'),
88
        Column('rev'),
(-)trac/trac/env.py (-11 / +16 lines)
Lines 150-166 Link Here
150
        
150
        
151
        @param authname: user name for authorization
151
        @param authname: user name for authorization
152
        """
152
        """
153
        from trac.versioncontrol.cache import CachedRepository
153
        from trac.versioncontrol import get_repository
154
        from trac.versioncontrol.svn_authz import SubversionAuthorizer
154
        return get_repository(self, authname)
155
        from trac.versioncontrol.svn_fs import SubversionRepository
156
        repos_dir = self.config.get('trac', 'repository_dir')
157
        if not repos_dir:
158
            raise EnvironmentError, 'Path to repository not configured'
159
        authz = None
160
        if authname:
161
            authz = SubversionAuthorizer(self, authname)
162
        repos = SubversionRepository(repos_dir, authz, self.log)
163
        return CachedRepository(self.get_db_cnx(), repos, authz, self.log)
164
155
165
    def create(self, db_str=None):
156
    def create(self, db_str=None):
166
        """Create the basic directory structure of the environment, initialize
157
        """Create the basic directory structure of the environment, initialize
Lines 350-355 Link Here
350
                err = 'No upgrade module for version %i (%s.py)' % (i, name)
341
                err = 'No upgrade module for version %i (%s.py)' % (i, name)
351
                raise TracError, err
342
                raise TracError, err
352
            script.do_upgrade(self.env, i, cursor)
343
            script.do_upgrade(self.env, i, cursor)
344
345
            # Allow micro-upgrades: this is mainly intended to allow third
346
            # parties fixups
347
            from string import ascii_lowercase
348
            for l in ascii_lowercase:
349
                microname  = name + l
350
                try:
351
                    upgrades = __import__('upgrades', globals(), locals(),
352
                                          [microname])
353
                    script = getattr(upgrades, microname)
354
                except AttributeError:
355
                    break
356
                script.do_upgrade(self.env, i, cursor)
357
353
        cursor.execute("UPDATE system SET value=%s WHERE "
358
        cursor.execute("UPDATE system SET value=%s WHERE "
354
                       "name='database_version'", (db_default.db_version,))
359
                       "name='database_version'", (db_default.db_version,))
355
        self.log.info('Upgraded database version from %d to %d',
360
        self.log.info('Upgraded database version from %d to %d',
(-)trac/trac/upgrades/db14a.py (+16 lines)
Line 0 Link Here
1
sql = [
2
#-- Make the revision contain an hash, and suggest a resync
3
"""DROP TABLE revision;""",
4
"""CREATE TABLE revision (
5
    rev             text PRIMARY KEY,
6
    time            integer,
7
    author          text,
8
    message         text,
9
    hash            text
10
);""",
11
]
12
13
def do_upgrade(env, ver, cursor):
14
    for s in sql:
15
        cursor.execute(s)
16
    print 'Please perform a "resync" after this upgrade.'
(-)trac/trac/versioncontrol/api.py (-1 / +72 lines)
Lines 210-222 Link Here
210
210
211
    def get_changes(self):
211
    def get_changes(self):
212
        """
212
        """
213
        Generator that produces a (path, kind, change, base_rev, base_path)
213
        Generator that produces a (path, kind, change, base_path, base_rev)
214
        tuple for every change in the changeset, where change can be one of
214
        tuple for every change in the changeset, where change can be one of
215
        Changeset.ADD, Changeset.COPY, Changeset.DELETE, Changeset.EDIT or
215
        Changeset.ADD, Changeset.COPY, Changeset.DELETE, Changeset.EDIT or
216
        Changeset.MOVE, and kind is one of Node.FILE or Node.DIRECTORY.
216
        Changeset.MOVE, and kind is one of Node.FILE or Node.DIRECTORY.
217
        """
217
        """
218
        raise NotImplementedError
218
        raise NotImplementedError
219
219
220
    def insert_in_cache(self, cursor, kindmap, actionmap, log):
221
        """
222
        Insert this changeset in the cache db.
223
        """
224
        cursor.execute("INSERT INTO revision (rev,time,author,message) "
225
                       "VALUES (%s,%s,%s,%s)", (str(self.rev),
226
                       self.date, self.author,
227
                       self.message))
228
        for path,kind,action,base_path,base_rev in self.get_changes():
229
            log.debug("Caching node change in [%s]: %s"
230
                      % (self.rev, (path, kind, action,
231
                                    base_path, base_rev)))
232
            kind = kindmap[kind]
233
            action = actionmap[action]
234
            cursor.execute("INSERT INTO node_change (rev,path,kind,"
235
                           "change,base_path,base_rev) "
236
                           "VALUES (%s,%s,%s,%s,%s,%s)",
237
                           (str(self.rev), path, kind, action,
238
                           base_path, base_rev))
220
239
221
class PermissionDenied(PermissionError):
240
class PermissionDenied(PermissionError):
222
    """
241
    """
Lines 249-251 Link Here
249
    def has_permission_for_changeset(self, rev):
268
    def has_permission_for_changeset(self, rev):
250
        return 1
269
        return 1
251
270
271
272
def get_repository(env, authname):
273
    """
274
    Return the right repository backend wrapped by a CachedRepository.
275
276
    This looks for a ``darcs:`` or ``bzr:`` prefix on the
277
    'repository_dir' from the configuration: if present it qualifies
278
    respectively a Darcs or a Bazaar-NG repository, otherwise it uses
279
    the Subversion backend.
280
    """
281
    
282
    authz = None
283
    repos_dir = env.config.get('trac', 'repository_dir')
284
    db = env.get_db_cnx()
285
    if not repos_dir:
286
        raise EnvironmentError, 'Path to repository not configured'
287
    if repos_dir.startswith('darcs:'):
288
        from trac.versioncontrol.darcs import DarcsRepository, \
289
             DarcsCachedRepository as CachedRepository
290
        repos = DarcsRepository(db, repos_dir[6:], env.log, env.config)
291
    elif repos_dir.startswith('bzr:'):
292
        from trac.versioncontrol.bzr import BzrRepository, \
293
             BzrCachedRepository as CachedRepository
294
        #from trac.versioncontrol.cache import CachedRepository
295
        repos = BzrRepository(db, repos_dir[4:], env.log)
296
    else:
297
        from trac.versioncontrol.svn_authz import SubversionAuthorizer
298
        from trac.versioncontrol.svn_fs import SubversionRepository
299
        from trac.versioncontrol.cache import CachedRepository
300
        if authname:
301
            authz = SubversionAuthorizer(env, authname)
302
        repos = SubversionRepository(repos_dir, authz, env.log)
303
    return CachedRepository(db, repos, authz, env.log)
304
305
306
def get_authorizer(env, authname):
307
    """
308
    Return the right authorizer for the configured repository.
309
310
    Like ``get_repository()`` this looks for a ``darcs:`` or ``bzr:``
311
    prefix on the 'repository_dir' setting: if present it returns
312
    a basic Authorizer(), otherwise a SubversionAuthorizer.
313
    """
314
    
315
    authz = Authorizer()
316
    repos_dir = env.config.get('trac', 'repository_dir')
317
    if not repos_dir.startswith('darcs:') \
318
           and not repos_dir.startswith('bzr:'):
319
        from trac.versioncontrol.svn_authz import SubversionAuthorizer
320
        if authname:
321
            authz = SubversionAuthorizer(env, authname)
322
    return authz
(-)trac/trac/versioncontrol/bzr.py (+383 lines)
Line 0 Link Here
1
# -*- coding: iso-8859-1 -*-
2
#
3
# Copyright (C) 2005 Edgewall Software
4
# Copyright (C) 2005 Johan Rydberg <jrydberg@gnu.org>
5
#
6
# This software is licensed as described in the file COPYING, which
7
# you should have received as part of this distribution. The terms
8
# are also available at http://trac.edgewall.com/license.html.
9
#
10
# This software consists of voluntary contributions made by many
11
# individuals. For the exact contribution history, see the revision
12
# history and logs, available at http://projects.edgewall.com/trac/.
13
#
14
# Author: Johan Rydberg <jrydberg@gnu.org>
15
16
from __future__ import generators
17
18
from trac.util import TracError, NaivePopen
19
from trac.versioncontrol import Changeset, Node, Repository
20
from trac.versioncontrol.cache import CachedRepository, CachedChangeset
21
22
import os
23
24
25
class BzrRepository(Repository):
26
    """
27
    A Bazaar-NG repository.
28
    """
29
    def __init__(self, db, path, log):
30
        from bzrlib.branch import Branch
31
        self.branch = Branch.open(path)
32
        self.db = db
33
        Repository.__init__(self, path, None, log)
34
35
    def get_base_rev(self, file_id, start_rev = None):
36
        """
37
        Lookup what revision file with file id specified by file_id was
38
        last modified by.  start_rev gives the revision where to start
39
        scanning from (in reverese order.)  start_rev is _EXCLUSIVE_.
40
        """
41
        def _enumerate_history(branch):
42
            rh = []
43
            revno = 1
44
            for rev_id in branch.revision_history():
45
                rh.append((revno, rev_id))
46
                revno += 1
47
            return rh
48
49
        if start_rev:
50
            start_rev = self.normalize_rev(start_rev)
51
            if start_rev and start_rev <= 1:
52
                return 0
53
54
        which_revs = _enumerate_history(self.branch)
55
        if start_rev:
56
            which_revs = which_revs[:start_rev - 1]
57
58
        which_revs.reverse()
59
60
        for revno, rev_id in which_revs:
61
            delta = self.branch.get_revision_delta(revno)
62
63
            if delta.touches_file_id(file_id):
64
                return revno
65
66
        return 0
67
68
    def get_oldest_rev(self):
69
        """
70
        Revisions are numbered from 1 to the highest revision number,
71
        return by branch.revno().
72
        """
73
        return 1
74
75
    def get_youngest_rev(self):
76
        """
77
        Return the youngest revision in the repository.
78
        """
79
        return self.branch.revno()
80
81
    def get_kind(self, path, rev):
82
        """
83
        Determine the kind of the path at given revision.
84
        """
85
86
        kind = 'F'
87
        if not path.endswith('/') and path != "":
88
            cursor = self.db.cursor()
89
            cursor.execute("SELECT kind "
90
                           "FROM node_change "
91
                           "WHERE ((LENGTH(rev)<LENGTH(%s)) OR "
92
                           "       (LENGTH(rev)=LENGTH(%s) AND rev<=%s)) "
93
                           "  AND path=%s "
94
                           "ORDER BY -LENGTH(rev),rev DESC "
95
                           "LIMIT 1", (rev, rev, rev, path))
96
            val = cursor.fetchone()
97
            if val:
98
                kind, = val
99
            if kind == 'D':
100
                kind = Node.DIRECTORY
101
            else:
102
                kind = Node.FILE
103
        else:
104
            kind = Node.DIRECTORY
105
        return kind
106
107
    def get_node(self, path, rev):
108
        """
109
        Retrieve a Node (directory or file) from the repository at the
110
        given path. If the rev parameter is specified, the version of the
111
        node at that revision is returned, otherwise the latest version
112
        of the node is returned.
113
        """
114
        rev = self.normalize_rev(rev)
115
        path = self.normalize_path(path)
116
        kind = self.get_kind(path, rev)
117
118
        if kind == Node.DIRECTORY:
119
            cursor = self.db.cursor()
120
            cursor.execute("SELECT rev,change "
121
                           "  FROM node_change "
122
                           "WHERE ((LENGTH(rev)<LENGTH(%s)) OR "
123
                           "       (LENGTH(rev)=LENGTH(%s) AND rev<=%s)) "
124
                           "  AND path LIKE %s "
125
                           "ORDER BY -LENGTH(rev),rev DESC "
126
                           "LIMIT 1", (rev, rev, rev, path+'%'))
127
            lastchange = cursor.fetchone()
128
            if lastchange:
129
                rev, change = lastchange
130
            else:
131
                raise TracError, "No node at %r in revision %s" % (path, rev)
132
        else:
133
            cursor = self.db.cursor()
134
            cursor.execute("SELECT rev FROM node_change "
135
                           "  WHERE((LENGTH(rev)<LENGTH(%d)) OR"
136
                           "         (LENGTH(rev)=LENGTH(%d) AND rev<=%d))"
137
                           "  AND path='%s' "
138
                           "ORDER BY -LENGTH(rev),rev DESC LIMIT 1;"
139
                           % (rev, rev, rev, path))
140
            val = cursor.fetchone()
141
            if val:
142
                rev, = val
143
                
144
        rev = self.normalize_rev(rev)
145
        return BzrNode(self, path, kind, rev)
146
147
    def get_path_history(self, path, rev=None, limit=None):
148
        """
149
        Retrieve all the revisions containing this path (no newer than 'rev').
150
        The result format should be the same as the one of Node.get_history()
151
        """
152
        node = self.get_node(path, rev)
153
        return node.get_history()
154
155
    def get_changeset(self, rev):
156
        """
157
        Retrieve a Changeset object that describes the changes made in
158
        revision 'rev'.
159
        """
160
        revobj = self.branch.get_revision(self.branch.get_rev_id(rev))        
161
        return BzrChangeset(self, revobj, rev)
162
163
    def normalize_path(self, path):
164
        """
165
        Return a canonical representation of path in the repos.
166
        """
167
        if not path:
168
            return ''
169
170
        if path.startswith('/'):
171
            return path[1:]
172
        else:
173
            return path
174
175
    def normalize_rev(self, rev):
176
        """
177
        Return a canonical representation of a revision in the repos.
178
        'None' is a valid revision value and represents the youngest revision.
179
        """
180
        try:
181
            rev =  int(rev)
182
        except (ValueError, TypeError):
183
            rev = None
184
        if rev is None:
185
            rev = self.get_youngest_rev()
186
        elif rev > self.get_youngest_rev():
187
            rev = self.get_youngest_rev()
188
        return rev
189
190
    def previous_rev(self, rev):
191
        """
192
        Return the revision immediately preceding the specified revision.
193
        """
194
        rev = self.normalize_rev(rev)
195
        if rev == 1:
196
            return None
197
        else:
198
            return rev-1
199
200
    def next_rev(self, rev):
201
        """
202
        Return the revision immediately following the specified revision.
203
        """
204
        rev = self.normalize_rev(rev)
205
        if rev < self.get_youngest_rev():
206
            return rev+1
207
        else:
208
            return None
209
210
    def rev_older_than(self, rev1, rev2):
211
        """
212
        Return True if rev1 is older than rev2, i.e. if rev1 comes before rev2
213
        in the revision sequence.
214
        """
215
        return self.normalize_rev(rev1) < self.normalize_rev(rev2)
216
217
class BzrNode(Node):
218
    def __init__(self, repo, path, kind, rev):
219
        Node.__init__(self, path, rev, kind)
220
        self.repo = repo
221
        revision_id = repo.branch.get_rev_id(rev)
222
        self.tree = repo.branch.revision_tree(revision_id)
223
224
    def get_history(self, limit=None):
225
        """
226
        Generator that yields (path, rev, chg) tuples, one for each
227
        revision in which the node was changed. This generator will
228
        follow copies and moves of a node (if the underlying version
229
        control system supports that), which will be indicated by the
230
        first element of the tuple (i.e. the path) changing.
231
        """
232
233
        cursor = self.repo.db.cursor()
234
235
        rev = self.rev
236
        path = self.path
237
238
        actions = { 'A': Changeset.ADD,
239
                    'E': Changeset.EDIT,
240
                    'M': Changeset.MOVE,
241
                    'D': Changeset.DELETE }
242
243
        if self.kind == Node.DIRECTORY:
244
            revdone = {}
245
            cursor.execute("SELECT rev,change,base_path"
246
                           "  FROM node_change "
247
                           "WHERE ((LENGTH(rev)<LENGTH(%d)) OR "
248
                           "       (LENGTH(rev)=LENGTH(%d) AND rev<=%d)) "
249
                           "  AND path LIKE %s "
250
                           "ORDER BY -LENGTH(rev),rev DESC ",
251
                           (rev, rev, rev, path+'%'))
252
            path = path.rstrip('/')
253
            for row in cursor:
254
                rev, change, base_path = row
255
                if not rev in revdone:
256
                    revdone[rev] = True
257
                    yield path, rev, actions[change]
258
                    if limit:
259
                        limit -= 1
260
                        if limit == 0:
261
                            break
262
        else:
263
            while rev>=1:
264
                cursor.execute("SELECT kind,change,base_path,base_rev "
265
                               "FROM node_change WHERE rev=%d AND path=%s",
266
                               (rev, path))
267
                base_rev = None
268
                row = cursor.fetchone()
269
                if row:
270
                    kind, change, base_path, base_rev = row
271
                    yield path, rev, actions[change]
272
                    if limit:
273
                        limit -= 1
274
                        if limit == 0:
275
                            break
276
                    base_rev = base_rev and int(base_rev) or 0
277
                    path = base_path
278
279
                if base_rev is None:
280
                    rev -= 1
281
                else:
282
                    rev = int(base_rev)
283
            
284
    def get_content(self):
285
        """
286
        Return a stream for reading the content of the node. This method
287
        will return None for directories. The returned object should provide
288
        a read([len]) function.
289
        """
290
        return self.tree.get_file(self.tree.inventory.path2id(self.path))
291
292
    def get_entries(self):
293
        """
294
        Generator that yields the immediate child entries of a directory, in no
295
        particular order. If the node is a file, this method returns None.
296
        """
297
        if self.kind == Node.DIRECTORY:
298
            file_id = self.tree.inventory.path2id(self.path)
299
            ie = self.tree.inventory[file_id]
300
            for x, kid in ie.sorted_children():
301
                yield self.repo.get_node(os.path.join(self.path, kid.name),
302
                                         self.rev)
303
304
    def get_name(self):
305
        return os.path.split(self.path)[1]
306
    
307
    def get_content_length(self):
308
        if self.kind == Node.DIRECTORY:
309
            return None
310
311
        file_id = self.tree.inventory.path2id(self.path)
312
        ie = self.tree.inventory[file_id]
313
        return ie.text_size
314
315
    def get_content_type(self):
316
        if self.kind == Node.DIRECTORY:
317
            return None
318
        # FIXME: guess type here.
319
        return None
320
321
    def get_last_modified(self):
322
        # FIXME: add
323
        return 0
324
325
    def get_properties(self):
326
        """
327
        Returns a dictionary containing the properties (meta-data) of the node.
328
        The set of properties depends on the version control system.
329
        """
330
        return {}
331
332
333
class BzrChangeset(Changeset):
334
    def __init__(self, repo, rev, revno):
335
        """
336
        Initialize bazaar changeset.  rev is the revision object provided
337
        by bzrlib, revno is its revision number.
338
        """
339
        self.revobj = rev
340
        self.repo = repo
341
        Changeset.__init__(self, revno, rev.message.encode('utf-8'),
342
                           rev.committer, rev.timestamp)
343
344
    def get_changes(self):
345
        """
346
        Generator that produces a (path, kind, change, base_path, base_rev)
347
        tuple for every change in the changeset, where change can be one of
348
        Changeset.ADD, Changeset.COPY, Changeset.DELETE, Changeset.EDIT or
349
        Changeset.MOVE, and kind is one of Node.FILE or Node.DIRECTORY.
350
        """
351
        delta = self.repo.branch.get_revision_delta(self.rev)
352
353
        kinds = { 'file': Node.FILE, 'directory': Node.DIRECTORY,
354
                  'root_directory': Node.DIRECTORY }
355
        
356
        for path, file_id, kind in delta.added:
357
            base_rev = self.repo.get_base_rev(file_id, self.rev)
358
            yield path, kinds[kind], Changeset.ADD, path, base_rev
359
360
        for path, file_id, kind, _, _ in delta.modified:
361
            base_rev = self.repo.get_base_rev(file_id, self.rev)
362
            yield path, kinds[kind], Changeset.EDIT, path, base_rev
363
364
        for path, file_id, kind in delta.removed:
365
            base_rev = self.repo.get_base_rev(file_id, self.rev)
366
            yield path, kinds[kind], Changeset.DELETE, path, base_rev
367
368
        for old_path, new_path, file_id, kind, modified, _ in delta.renamed:
369
            base_rev = self.repo.get_base_rev(file_id, self.rev)
370
            yield new_path, kinds[kind], Changeset.MOVE, old_path, base_rev
371
        
372
class BzrCachedRepository(CachedRepository):
373
    """
374
    Bazaar-NG version of the cached repository, that serves BzrCachedChangesets
375
    """
376
377
    def get_changeset(self, rev):
378
        if not self.synced:
379
            self.sync()
380
            self.synced = 1
381
        return CachedChangeset(self.repos.normalize_rev(rev), self.db,
382
                               self.authz)
383
(-)trac/trac/versioncontrol/cache.py (-15 / +1 lines)
Lines 76-96 Link Here
76
                current_rev = self.repos.oldest_rev
76
                current_rev = self.repos.oldest_rev
77
            while current_rev is not None:
77
            while current_rev is not None:
78
                changeset = self.repos.get_changeset(current_rev)
78
                changeset = self.repos.get_changeset(current_rev)
79
                cursor.execute("INSERT INTO revision (rev,time,author,message) "
79
                changeset.insert_in_cache(cursor, kindmap, actionmap, self.log)
80
                               "VALUES (%s,%s,%s,%s)", (str(current_rev),
81
                               changeset.date, changeset.author,
82
                               changeset.message))
83
                for path,kind,action,base_path,base_rev in changeset.get_changes():
84
                    self.log.debug("Caching node change in [%s]: %s"
85
                                   % (current_rev, (path, kind, action,
86
                                      base_path, base_rev)))
87
                    kind = kindmap[kind]
88
                    action = actionmap[action]
89
                    cursor.execute("INSERT INTO node_change (rev,path,kind,"
90
                                   "change,base_path,base_rev) "
91
                                   "VALUES (%s,%s,%s,%s,%s,%s)",
92
                                   (str(current_rev), path, kind, action,
93
                                   base_path, base_rev))
94
                current_rev = self.repos.next_rev(current_rev)
80
                current_rev = self.repos.next_rev(current_rev)
95
            self.db.commit()
81
            self.db.commit()
96
            self.repos.authz = authz # restore permission checking
82
            self.repos.authz = authz # restore permission checking
(-)trac/trac/versioncontrol/darcs.py (+951 lines)
Line 0 Link Here
1
# -*- coding: iso-8859-1 -*-
2
#
3
# Copyright (C) 2005 Edgewall Software
4
# Copyright (C) 2005 Lele Gaifax <lele@metapensiero.it>
5
#
6
# This software is licensed as described in the file COPYING, which
7
# you should have received as part of this distribution. The terms
8
# are also available at http://trac.edgewall.com/license.html.
9
#
10
# This software consists of voluntary contributions made by many
11
# individuals. For the exact contribution history, see the revision
12
# history and logs, available at http://projects.edgewall.com/trac/.
13
#
14
# Author: Lele Gaifax <lele@metapensiero.it>
15
16
from __future__ import generators
17
18
from trac.util import TracError, NaivePopen
19
from trac.versioncontrol import Changeset, Node, Repository
20
from trac.versioncontrol.cache import CachedRepository, CachedChangeset
21
from os import listdir, makedirs, utime, stat
22
from os.path import join, isdir, split, exists
23
from time import gmtime, mktime, strftime, timezone
24
from shutil import copyfile
25
from mimetypes import guess_type
26
27
# Python 2.3+ compatibility
28
try:
29
    reversed
30
except:
31
    def reversed(x):
32
        if hasattr(x, 'keys'):
33
            raise ValueError("mappings do not support reverse iteration")
34
        i = len(x)
35
        while i > 0:
36
            i -= 1
37
            yield x[i]
38
39
class DarcsRepository(Repository):
40
    """
41
    Darcs concrete implementation of a Repository.
42
43
    Darcs (http://www.abridgegame.org/darcs/) is a distribuited SCM,
44
    patch-centric instead of snapshot-centric like Subversion. The
45
    approach here is to assume that the history in the repository
46
    followed by Trac is immutable, which is not a requirement for the
47
    underlaying darcs. This means that on that repository should never
48
    be executed a `darcs unpull`, or `darcs unrecord` and the like.
49
50
    Given it's nature, this class tries to cache as much as possible
51
    the requests coming from the above Trac machinery, to avoid or
52
    minimize the external calls to Darcs that tend to be somewhat time
53
    expensive.  In particular, _getNodeContent() caches any particular
54
    version of any file Trac asks to it, kept on the filesystem,
55
    inside the ``_darcs`` metadir of the controlled repository in the
56
    directory ``trac_cache`` (or where specified by the option
57
    ``cachedir`` in the section ``darcs`` of the configuration.  This
58
    may require some kind of control over the size of the cache, that
59
    OTOH may be completely removed whenever needed, it will be
60
    recreated as soon as the first request come in.
61
    """
62
63
    def __init__(self, db, path, log, config):
64
        Repository.__init__(self, path, None, log)
65
        self.path = path
66
        self.db = db
67
        self.__youngest_rev = 0
68
        self.__history = None
69
        self.__history_start = 0
70
        self.__no_pristine = exists(join(self.path, '_darcs', 'current.none'))
71
        self.__darcs = config.get('darcs', 'command', 'darcs')
72
        if config.get('darcs', 'dont_escape_8bit'):
73
            self.__darcs = "DARCS_DONT_ESCAPE_8BIT=1 " + self.__darcs
74
        self.__cachedir = config.get('darcs', 'cachedir',
75
                                     join(self.path, '_darcs', 'trac_cache'))
76
77
    ## Low level stuff: CamelCase is used to avoid clash with inherited
78
    ## interface namespace.
79
80
    def _darcs (self, command, args=''):
81
        """
82
        Execute a some query on the repository running an external darcs.
83
84
        Return the XML output of the command.
85
        """
86
87
        command = "cd %r; TZ=UTC %s %s %s" % \
88
                  (self.path, self.__darcs, command, args)
89
        self.log.debug(command)
90
        np = NaivePopen (command, capturestderr=True)
91
        if np.errorlevel:
92
            err = 'Running (%s) failed: %s, %s.' % (command,
93
                                                    np.errorlevel, np.err)
94
            raise TracError (err, title='Darcs execution failed')
95
        return np.out
96
97
    def _changes(self, args, startfrom=1):
98
        """
99
        Get changes information parsing the XML output of ``darcs changes``.
100
101
        Return a sequence of Changesets.
102
        """
103
104
        return changesets_from_darcschanges(
105
            self._darcs('changes --reverse --summary --xml', args),
106
            self, startfrom)
107
108
    def _diff(self, path, rev1, rev2=None, patch=None):
109
        """
110
        Return a darcs diff between two revisions.
111
        """
112
113
        diff = "diff --unified"
114
        rev1 = self.normalize_rev(rev1)
115
        cset1 = DarcsCachedChangeset(rev1, self.db)
116
        diff += " --from-match 'hash %s'" % cset1.hash
117
        if rev2:
118
            rev2 = self.normalize_rev(rev2)
119
            cset2 = DarcsCachedChangeset(rev2, self.db)
120
            diff += "  --to-match 'hash %s'" % cset2.hash
121
        if patch:
122
            path = path + patch
123
        return self._darcs(diff, path)
124
125
    def _parseInventory(self, inventory):
126
        """
127
        Parse an inventory, and return a dictionary of its content.
128
        """
129
130
        from sha import new
131
132
        csmap = {}
133
        start = 0
134
        length = len(inventory)
135
        index = self.__youngest_rev
136
        while start<length:
137
            start = inventory.find('[', start)
138
            if start<0:
139
                break
140
            end = inventory.index('\n', start)
141
            patchname = inventory[start+1:end]
142
            start = end
143
            end = inventory.index('*', start)
144
            author = inventory[start+1:end]
145
            inv = inventory[end+1] == '*' and 'f' or 't'
146
            date = inventory[end+2:end+16]
147
            y = int(date[:4])
148
            m = int(date[4:6])
149
            d = int(date[6:8])
150
            hh = int(date[8:10])
151
            mm = int(date[10:12])
152
            ss = int(date[12:14])
153
            unixtime = int(mktime((y, m, d, hh, mm, ss, 0, 0, 0)))-timezone
154
            end += 16
155
            log = ''
156
            if inventory[end] <> ']':
157
                start = end
158
                end = inventory.index('\n', start+1)
159
                while True:
160
                    log += inventory[start+2:end]
161
                    start = end
162
                    if inventory[end+1] == ']':
163
                        break
164
                    end = inventory.index('\n', start+1)
165
                end += 1
166
167
            index += 1
168
            phash = new()
169
            phash.update(patchname)
170
            phash.update(author)
171
            phash.update(date)
172
            phash.update(log)
173
            phash.update(inv)
174
            patchid = '%s-%s-%s.gz' % (date,
175
                                       new(author).hexdigest()[:5],
176
                                       phash.hexdigest())
177
            csmap[index] = patchid
178
            csmap[patchid] = index
179
            start = end+1
180
        self.__youngest_rev = index
181
        return csmap
182
183
    def _loadChangesetsIndex(self):
184
        """
185
        Load the index of changesets in the repository, assigning an unique
186
        integer number to each one, ala Subversion, for easier references.
187
188
        This is done by parsing the ``_darcs/inventory`` file using the
189
        position of each changeset as its `revision number`, **assuming**
190
        that **nobody's never** going to do anything that alters its order,
191
        such as ``darcs optimize`` or ``darcs unpull``.
192
        """
193
194
        self.__youngest_rev = 0
195
        inventories = [join(self.path, '_darcs', 'inventories', i) for i
196
                       in listdir(join(self.path, '_darcs', 'inventories'))]
197
        inventories.sort()
198
        inventories.append(join(self.path, '_darcs', 'inventory'))
199
        for inventory in inventories:
200
            f = open(inventory, 'rU')
201
            try:
202
                index = self._parseInventory(f.read())
203
            finally:
204
                f.close()
205
206
    ## Medium level, work-horse methods
207
208
    def _getCachedContentLocation(self, node):
209
        """
210
        Return the location of the cache for the given node. If it does
211
        not exist, compute it by applying a diff to the current version.
212
        This may return None, if the node does not actually exist.
213
        """
214
215
        rev = self.normalize_rev(node.rev)
216
217
        if self.__no_pristine:
218
            current = join(self.path, node.path)
219
        else:
220
            current = join(self.path, '_darcs', 'current', node.path)
221
222
        # Iterate over history to find out which is the revision of
223
        # the given path that last changed the it. We need to find
224
        # both a 'last revision' and 'second last', because later
225
        # we may apply either a r1:last diff or a 2nd:current diff.
226
        history = self._getPathHistory(node.path, None)
227
        try:
228
            lastnode = history.next()
229
        except StopIteration:
230
            lastnode = None
231
232
        if lastnode is None:
233
            return None
234
        elif lastnode.rev <= rev:
235
            # Content hasn't changed, return current version
236
            if exists(current):
237
                return current
238
239
        prevlast = lastnode
240
        for oldnode in history:
241
            if oldnode.rev <= rev:
242
                lastnode = oldnode
243
                break
244
            prevlast = oldnode
245
246
        cachedir = join(self.__cachedir, str(lastnode.rev))
247
        cache = join(cachedir, lastnode.path)
248
249
        # One may never know: should by any chance an absolute path survived
250
        # in lastnode.path, or in some clever way introduced some trick like
251
        # 'somepath/../../../etc/passwd'...
252
        from os.path import normpath
253
        assert normpath(cache).startswith(cachedir)
254
255
        if not exists(cache):
256
            self.log.debug('Caching revision %d of %s' % (lastnode.rev,
257
                                                          lastnode.path))
258
            dir = split(cache)[0]
259
            if not exists(dir):
260
                makedirs(dir)
261
262
            # If the file doesn't current exist, create an empty file
263
            # and apply a patch from revision 1 to the node revision,
264
            # otherwise apply a reversed patch from the current revision
265
            # and to node revision+1.
266
            try:
267
                if not exists(current):
268
                    self.log.debug('Applying a direct patch from revision 1 up'
269
                                   ' to %d to %s' % (lastnode.rev, node.path))
270
                    open(cache, 'w').close()
271
                    patch = "| patch -p1 -d %s" % cachedir
272
                    self._diff(node.path, 1, lastnode.rev, patch=patch)
273
                else:
274
                    self.log.debug('Applying a reverse patch from current'
275
                                   ' revision back to %d to %s' %
276
                                   (lastnode.rev, node.path))
277
                    copyfile(current, cache)
278
                    patch = "| patch -p1 -R -d %s" % cachedir
279
                    self._diff(node.path, prevlast.rev, patch=patch)
280
            except TracError, exc:
281
                if 'Only garbage was found in the patch input' in exc.message:
282
                    pass
283
                else:
284
                    raise
285
286
            # Adjust the times of the just created cache file, to match
287
            # the timestamp of the associated changeset.
288
            cursor = self.db.cursor()
289
            cursor.execute("SELECT time FROM revision "
290
                           "WHERE rev = %s", (lastnode.rev,))
291
            cstimestamp = int(cursor.fetchone()[0])
292
            utime(cache, (cstimestamp, cstimestamp))
293
294
        if exists(cache):
295
            return cache
296
        else:
297
            return None
298
299
    def _getNodeContent(self, node):
300
        """
301
        Return the content of the node, loading it from the cache.
302
        """
303
304
        from cStringIO import StringIO
305
306
        location = self._getCachedContentLocation(node)
307
        if location:
308
            return file(location)
309
        else:
310
            return StringIO('')
311
312
    def _getNodeSize(self, node):
313
        """
314
        Return the content of the node, loading it from the cache.
315
        """
316
317
        location = self._getCachedContentLocation(node)
318
        if location:
319
            return stat(location).st_size
320
        else:
321
            return None
322
323
    def _getNodeEntries(self, node):
324
        """
325
        Generate the the immediate child entries of a directory at given
326
        revision, in alpha order.
327
        """
328
329
        from cache import _actionmap
330
331
        # Loop over nodes touched before given rev that falls in the
332
        # given path. We effectively want to look at the whole subtree,
333
        # because when a child is a directory we annotate it with the
334
        # latest change happened below that, instead with the revision
335
        # that actually touched the directory itself.
336
337
        cursor = self.db.cursor()
338
        path = node.path.strip('/')
339
        cursor.execute("SELECT rev, path, change, base_path "
340
                       "  FROM node_change "
341
                       "WHERE ((LENGTH(rev)<LENGTH(%s)) OR "
342
                       "       (LENGTH(rev)=LENGTH(%s) AND rev<=%s)) "
343
                       "  AND (path GLOB %s OR base_path GLOB %s) "
344
                       "ORDER BY -LENGTH(rev),rev DESC,path ",
345
                       (node.rev, node.rev, node.rev, path+'*', path+'*'))
346
347
        if path:
348
            path += '/'
349
350
        done = {}
351
        for entry in cursor:
352
            rev, subtreepath, change, oldpath = entry
353
            if oldpath and oldpath.startswith(path):
354
                subpath = oldpath[len(path):].lstrip('/')
355
            elif subtreepath.startswith(path):
356
                subpath = subtreepath[len(path):].lstrip('/')
357
            else:
358
                # The GLOB above may return either entries in the
359
                # directory or entries in other directories (usually
360
                # the one that contains the folder we are listing)
361
                # that share a common chunk of the name with the
362
                # directory itself.
363
                continue
364
365
            if '/' in subpath:
366
                child, rest = subpath.split('/', 1)
367
            else:
368
                child, rest = subpath, None
369
370
            if not child or child in done:
371
                continue
372
373
            done[child] = True
374
375
            # Return the node only if the entry is not a direct child
376
            # of the directory, otherwise only if it's not a deletion
377
            # or a rename.
378
            if rest or not ((_actionmap[change]==Changeset.MOVE
379
                             and oldpath and oldpath.startswith(path)) or
380
                            _actionmap[change]==Changeset.DELETE):
381
                yield self.get_node(path + child, rev)
382
383
    def _getNodeLastModified(self, node):
384
        """
385
        Return the timestamp of the last modification to the given node.
386
        """
387
388
        location = self._getCachedContentLocation(node)
389
        if location:
390
            return stat(location).st_mtime
391
        else:
392
            return 0
393
394
    def _getPathHistory(self, path, rev=None, limit=None):
395
        """
396
        Iterate over the nodes that compose the history of the given
397
        path not newer than rev.
398
        """
399
400
        from trac.versioncontrol.cache import _kindmap, _actionmap
401
402
        rev = self.normalize_rev(rev)
403
404
        # Start with the concrete history, if present
405
        if self.__history and rev>self.__history_start:
406
            node = None
407
            for cs in reversed(self.__history):
408
                if cs.rev > rev:
409
                    continue
410
                node = cs.get_node(path)
411
                if node:
412
                    yield node
413
                    if limit:
414
                        limit -= 1
415
                        if limit==0:
416
                            break
417
                    # Expand renames
418
                    if node.change == Changeset.MOVE:
419
                        for node in self._getPathHistory(node.oldpath,
420
                                                         node.rev-1, limit):
421
                            yield node
422
                            if limit:
423
                                limit -= 1
424
            if node is not None:
425
                rev = node.rev-1
426
427
        # Keep going with the cache stored in the DB
428
        kind = self._getNodeKind(path, rev)
429
        cursor = self.db.cursor()
430
431
        path = path.rstrip('/')
432
        if kind == Node.DIRECTORY:
433
            revdone = {}
434
            cursor.execute("SELECT rev,kind,change,base_path"
435
                           "  FROM node_change "
436
                           "WHERE ((LENGTH(rev)<LENGTH(%s)) OR "
437
                           "       (LENGTH(rev)=LENGTH(%s) AND rev<=%s)) "
438
                           "  AND (path=%s OR path GLOB %s) "
439
                           "ORDER BY -LENGTH(rev),rev DESC ",
440
                           (rev, rev, rev, path, path+'/*'))
441
            for row in cursor:
442
                rev, kind, change, base_path = row
443
                if not rev in revdone:
444
                    revdone[rev] = True
445
                    node = DarcsNode(path, rev, _kindmap[kind],
446
                                     _actionmap[change], self,
447
                                     oldpath=base_path)
448
                    yield node
449
                    if limit:
450
                        limit -= 1
451
                        if limit==0:
452
                            break
453
        else:
454
            while rev>=1:
455
                cursor.execute("SELECT kind,change,base_path,base_rev "
456
                               "FROM node_change WHERE rev=%s AND path=%s",
457
                               (rev, path))
458
                base_rev = None
459
                for row in cursor:
460
                    kind, change, base_path, base_rev = row
461
                    node = DarcsNode(path, rev, _kindmap[kind],
462
                                     _actionmap[change], self,
463
                                     oldpath=base_path)
464
                    yield node
465
                    if limit:
466
                        limit -= 1
467
                        if limit==0:
468
                            break
469
                    base_rev = base_rev and int(base_rev) or 0
470
                    # Expand renames
471
                    if node.change == Changeset.MOVE:
472
                        for node in self._getPathHistory(node.oldpath,
473
                                                         base_rev, limit):
474
                            yield node
475
                            if limit:
476
                                limit -= 1
477
478
                if base_rev is None:
479
                    rev -= 1
480
                else:
481
                    rev = base_rev
482
483
    def _getNodeKind(self, path, rev):
484
        """
485
        Determine the kind of the path at given revision.
486
        """
487
488
        # Determine if the path is really a directory, except when it's
489
        # already known: it is, when its name ends with a slash (a fake
490
        # one introduced by changesets_from_darcschanges()) or it is the
491
        # empty string, resulted from normalize_path('/').
492
        if not path.endswith("/") and path <> "":
493
            cursor = self.db.cursor()
494
            cursor.execute("SELECT path "
495
                           "FROM node_change "
496
                           "WHERE ((LENGTH(rev)<LENGTH(%s)) OR "
497
                           "       (LENGTH(rev)=LENGTH(%s) AND rev<=%s)) "
498
                           "  AND path=%s "
499
                           "ORDER BY -LENGTH(rev),rev DESC "
500
                           "LIMIT 1", (rev, rev, rev, path+'/'))
501
            if cursor.fetchone():
502
                kind = Node.DIRECTORY
503
            else:
504
                kind = Node.FILE
505
        else:
506
            kind = Node.DIRECTORY
507
        return kind
508
509
    ## Interface API
510
511
    def close(self):
512
        """
513
        Close the connection to the repository.
514
515
        Darcs: no-op.
516
        """
517
518
    def get_changeset(self, rev):
519
        """
520
        Retrieve a Changeset object that describes the changes made in
521
        revision 'rev'.
522
        """
523
524
        if not self.__history:
525
            youngest = self.normalize_rev(self.youngest_rev)
526
            youngest_cache = self.get_youngest_rev_in_cache(self.db) or '0'
527
            self.__history_start = int(youngest_cache)
528
            self.__history = self._changes('--last=%d' %
529
                                           (youngest - self.__history_start,),
530
                                           self.__history_start+1)
531
        rev = self.normalize_rev(rev)
532
        return self.__history[rev-self.__history_start-1]
533
534
    def get_node(self, path, rev=None):
535
        """
536
        Retrieve a Node (directory or file) from the repository at the
537
        given path. If the rev parameter is specified, the version of the
538
        node at that revision is returned, otherwise the latest version
539
        of the node is returned.
540
        """
541
542
        rev = self.normalize_rev(rev)
543
        path = self.normalize_path(path)
544
545
        if path == '':
546
            return DarcsNode('', rev, Node.DIRECTORY, None, self)
547
548
        kind = self._getNodeKind(path, rev)
549
550
        if kind == Node.DIRECTORY:
551
            cursor = self.db.cursor()
552
            path = path.rstrip('/')
553
            cursor.execute("SELECT rev,change "
554
                           "  FROM node_change "
555
                           "WHERE ((LENGTH(rev)<LENGTH(%s)) OR "
556
                           "       (LENGTH(rev)=LENGTH(%s) AND rev<=%s)) "
557
                           "  AND (path=%s OR path GLOB %s) "
558
                           "ORDER BY -LENGTH(rev),rev DESC "
559
                           "LIMIT 1", (rev, rev, rev, path, path+'/*'))
560
            lastchange = cursor.fetchone()
561
            if lastchange:
562
                rev, change = lastchange
563
            else:
564
                raise TracError, "No node at %r in revision %s" % (path, rev)
565
            node = DarcsNode(path, rev, Node.DIRECTORY, change, self)
566
        else:
567
            history = self._getPathHistory(path, rev, limit=1)
568
            try:
569
                node = history.next()
570
            except StopIteration:
571
                raise TracError, "No node at %r in revision %s" % (path, rev)
572
573
        return node
574
575
    def get_oldest_rev(self):
576
        """
577
        Return the oldest revision stored in the repository.
578
        """
579
580
        # If the repository is empty, return None
581
        if not self.__youngest_rev:
582
            return None
583
        return 1
584
585
    def get_youngest_rev(self):
586
        """
587
        Return the youngest revision in the repository.
588
        """
589
590
        if not self.__youngest_rev:
591
            self._loadChangesetsIndex()
592
        return self.__youngest_rev
593
594
    def previous_rev(self, rev):
595
        """
596
        Return the revision immediately preceding the specified revision.
597
        """
598
599
        rev = self.normalize_rev(rev)
600
        if rev == 1:
601
            return None
602
        else:
603
            return rev-1
604
605
    def next_rev(self, rev):
606
        """
607
        Return the revision immediately following the specified revision.
608
        """
609
610
        rev = self.normalize_rev(rev)
611
        if rev < self.get_youngest_rev():
612
            return rev+1
613
        else:
614
            return None
615
616
    def rev_older_than(self, rev1, rev2):
617
        """
618
        Return True if rev1 is older than rev2, i.e. if rev1 comes before rev2
619
        in the revision sequence.
620
        """
621
622
        return self.normalize_rev(rev1) < self.normalize_rev(rev2)
623
624
    def get_youngest_rev_in_cache(self, db):
625
        """
626
        Return the youngest revision currently cached.
627
628
        For darcs, this is the last applied revision, not necessarily
629
        the youngest one. We are numbering darcs patches in order of
630
        application.
631
        """
632
633
        cursor = db.cursor()
634
        cursor.execute("SELECT rev "
635
                       "FROM revision "
636
                       "ORDER BY -LENGTH(rev),rev DESC " # rev is a string
637
                       "LIMIT 1")                        # in the database
638
        row = cursor.fetchone()
639
        return row and row[0] or None
640
641
    def get_path_history(self, path, rev=None, limit=None):
642
        """
643
        Retrieve all the revisions containing this path (no newer than 'rev').
644
        The result format should be the same as the one of Node.get_history()
645
        """
646
647
        path = self.normalize_path(path)
648
        rev = self.normalize_rev(rev)
649
        for node in self._getPathHistory(path, rev, limit):
650
            yield (node.path, node.rev, node.change)
651
652
    def normalize_path(self, path):
653
        """
654
        Return a canonical representation of path in the repos.
655
        """
656
657
        if path.startswith('/'):
658
            return path[1:]
659
        else:
660
            return path
661
662
    def normalize_rev(self, rev):
663
        """
664
        Return a canonical representation of a revision in the repos.
665
        'None' is a valid revision value and represents the youngest revision.
666
        """
667
668
        try:
669
            rev =  int(rev)
670
        except (ValueError, TypeError):
671
            rev = None
672
        if rev is None:
673
            rev = self.get_youngest_rev()
674
        elif rev > self.get_youngest_rev():
675
            rev = self.get_youngest_rev()
676
        return rev
677
678
679
class DarcsChangeset(Changeset):
680
    """
681
    Represents a set of changes of a repository.
682
    """
683
684
    def __init__(self, rev, patchname, message, author, date, changes, hash):
685
        if message:
686
            log = patchname + '\n' + message
687
        else:
688
            log = patchname
689
        Changeset.__init__(self, rev, log, author, date)
690
        self.patchname = patchname
691
        self.changes = changes
692
        self.hash = hash
693
        # fix up changes rev slot
694
        for c in self.changes:
695
            c.rev = rev
696
697
    def get_changes(self):
698
        """
699
        Generator that produces a (path, kind, change, base_path, base_rev)
700
        """
701
702
        moves = {}
703
        for c in self.changes:
704
            last = c.get_history(limit=2)
705
            try:
706
                last.next()
707
                basepath,baserev,basechg = last.next()
708
            except StopIteration:
709
                basepath = None
710
                baserev = -1
711
            yield (c.path, c.kind, c.change, c.oldpath or basepath, baserev)
712
713
    def get_node(self, path, maybedir=False):
714
        """
715
        Find and return the node relative to given path.
716
        """
717
718
        for c in self.changes:
719
            if c.path == path or c.oldpath == path:
720
                return c
721
            if maybedir and not path.endswith('/'):
722
                path += '/'
723
                if c.path == path or c.oldpath == path:
724
                    return c
725
726
    def insert_in_cache(self, cursor, kindmap, actionmap, log):
727
        """
728
        Augment standard metadata with darcs patch hash.
729
        """
730
731
        Changeset.insert_in_cache(self, cursor, kindmap, actionmap, log)
732
        cursor.execute("UPDATE revision SET hash = %s "
733
                       "WHERE rev = %s", (self.hash, self.rev))
734
735
736
class DarcsNode(Node):
737
    """
738
    Represent a single item changed within a Changeset.
739
    """
740
741
    def __init__(self, path, rev, kind, change, repository, oldpath=None):
742
        Node.__init__(self, path, rev, kind)
743
        self.change = change
744
        self.repository = repository
745
        self.oldpath = oldpath
746
747
    def __cmp__(self, other):
748
        res = cmp(self.rev, other.rev)
749
        if res:
750
            return res
751
        res = cmp(self.path, other.path)
752
        if res == 0:
753
            if self.change==Changeset.MOVE and other.change==Changeset.DELETE:
754
                res = 1
755
            elif self.change==Changeset.DELETE and other.change==Changeset.MOVE:
756
                res = -1
757
        return res
758
759
    def get_content(self):
760
        """
761
        Return a stream for reading the content of the node. This method
762
        will return None for directories. The returned object should provide
763
        a read([len]) function.
764
        """
765
766
        if self.isdir:
767
            return None
768
769
        return self.repository._getNodeContent(self)
770
771
    def get_entries(self):
772
        """
773
        Generator that yields the immediate child entries of a directory, in no
774
        particular order. If the node is a file, this method returns None.
775
        """
776
777
        if self.isdir:
778
            return self.repository._getNodeEntries(self)
779
780
    def get_history(self, limit=None):
781
        """
782
        Generator that yields (path, rev, chg) tuples, one for each
783
        revision in which the node was changed. This generator will
784
        follow copies and moves of a node (if the underlying version
785
        control system supports that), which will be indicated by the
786
        first element of the tuple (i.e. the path) changing.
787
        """
788
789
        # Start with current version
790
        yield (self.path, self.rev, self.change)
791
792
        # Keep going with the previous steps, possibly following the old
793
        # name of the entry if this is a move.
794
        prevpath = self.oldpath or self.path
795
        prevrev = self.repository.normalize_rev(self.rev)-1
796
        prevhist = self.repository.get_path_history(prevpath, prevrev, limit-1)
797
        for path, rev, chg in prevhist:
798
            yield (path, rev, chg)
799
800
    def get_properties(self):
801
        """
802
        Returns a dictionary containing the properties (meta-data) of the node.
803
        The set of properties depends on the version control system.
804
        """
805
806
        return {}
807
808
    def get_content_length(self):
809
        if self.isdir:
810
            return None
811
        return self.repository._getNodeSize(self)
812
813
    def get_content_type(self):
814
        if self.isdir:
815
            return None
816
        return guess_type(self.path)[0]
817
818
    def get_name(self):
819
        return split(self.path)[1]
820
821
    def get_last_modified(self):
822
        return self.repository._getNodeLastModified(self)
823
824
825
def changesets_from_darcschanges(changes, repository, start_revision):
826
    """
827
    Parse XML output of ``darcs changes``.
828
829
    Return a list of ``Changeset`` instances.
830
    """
831
832
    from xml.sax import parseString, SAXException
833
    from xml.sax.handler import ContentHandler
834
835
    class DarcsXMLChangesHandler(ContentHandler):
836
        def __init__(self):
837
            self.changesets = []
838
            self.index = start_revision-1
839
            self.current = None
840
            self.current_field = []
841
842
        def startElement(self, name, attributes):
843
            if name == 'patch':
844
                self.current = {}
845
                self.current['author'] = attributes['author']
846
                date = attributes['date']
847
                # 20040619130027
848
                y = int(date[:4])
849
                m = int(date[4:6])
850
                d = int(date[6:8])
851
                hh = int(date[8:10])
852
                mm = int(date[10:12])
853
                ss = int(date[12:14])
854
                unixtime = int(mktime((y, m, d, hh, mm, ss, 0, 0, 0)))-timezone
855
                self.current['date'] = unixtime
856
                self.current['comment'] = ''
857
                self.current['hash'] = attributes['hash']
858
                self.current['entries'] = []
859
            elif name in ['name', 'comment', 'add_file', 'add_directory',
860
                          'remove_directory', 'modify_file', 'remove_file']:
861
                self.current_field = []
862
            elif name == 'move':
863
                self.old_name = attributes['from']
864
                self.new_name = attributes['to']
865
866
        def endElement(self, name):
867
            if name == 'patch':
868
                # Sort the paths to make tests easier
869
                self.current['entries'].sort()
870
                self.index += 1
871
                cset = DarcsChangeset(self.index,
872
                                      self.current['name'],
873
                                      self.current['comment'],
874
                                      self.current['author'],
875
                                      self.current['date'],
876
                                      self.current['entries'],
877
                                      self.current['hash'])
878
                self.changesets.append(cset)
879
                self.current = None
880
            elif name in ['name', 'comment']:
881
                self.current[name] = ''.join(self.current_field)
882
            elif name == 'move':
883
                kind = None
884
                for cs in reversed(self.changesets):
885
                    node = cs.get_node(self.old_name, maybedir=True)
886
                    if node:
887
                        kind = node.kind
888
                        break
889
                if kind is None:
890
                    kind = repository._getNodeKind(self.old_name, self.index)
891
                if kind == Node.DIRECTORY:
892
                    self.new_name += '/'
893
                    self.old_name += '/'
894
                entry = DarcsNode(self.new_name, None, kind, Changeset.MOVE,
895
                                  repository, self.old_name)
896
                self.current['entries'].append(entry)
897
            elif name in ['add_file', 'add_directory', 'modify_file',
898
                          'remove_file', 'remove_directory']:
899
                path = ''.join(self.current_field).strip()
900
                change = { 'add_file': Changeset.ADD,
901
                           'add_directory': Changeset.ADD,
902
                           'modify_file': Changeset.EDIT,
903
                           'remove_file': Changeset.DELETE,
904
                           'remove_directory': Changeset.DELETE
905
                         }[name]
906
                isdir = name in ('add_directory', 'remove_directory')
907
                kind = isdir and Node.DIRECTORY or Node.FILE
908
                # Eventually add one final '/' to identify directories.
909
                # This is because Trac brings around simple tuples at times,
910
                # that cannot carry that flag with them.
911
                if isdir:
912
                    path += '/'
913
                entry = DarcsNode(path, None, kind, change, repository)
914
                self.current['entries'].append(entry)
915
916
        def characters(self, data):
917
            self.current_field.append(data)
918
919
    handler = DarcsXMLChangesHandler()
920
    try:
921
        parseString(changes, handler)
922
    except SAXException, le:
923
        raise TracError('Unable to parse "darcs changes" output: ' + str(le))
924
925
    return handler.changesets
926
927
class DarcsCachedRepository(CachedRepository):
928
    """
929
    Darcs version of the cached repository, that serves DarcsCachedChangesets
930
    """
931
932
    def get_changeset(self, rev):
933
        if not self.synced:
934
            self.sync()
935
            self.synced = 1
936
        return DarcsCachedChangeset(self.repos.normalize_rev(rev), self.db,
937
                                    self.authz)
938
939
class DarcsCachedChangeset(CachedChangeset):
940
    """
941
    Darcs version of the CachedChangeset that knows about the hash.
942
    """
943
944
    def __init__(self, rev, db, authz=None):
945
        CachedChangeset.__init__(self, rev, db, authz)
946
        cursor = self.db.cursor()
947
        cursor.execute("SELECT hash FROM revision "
948
                       "WHERE rev=%s", (rev,))
949
        row = cursor.fetchone()
950
        if row:
951
            self.hash = row[0]
(-)trac/trac/versioncontrol/web_ui/changeset.py (-5 / +4 lines)
Lines 25-32 Link Here
25
from trac.perm import IPermissionRequestor
25
from trac.perm import IPermissionRequestor
26
from trac.Search import ISearchSource, query_to_sql, shorten_result
26
from trac.Search import ISearchSource, query_to_sql, shorten_result
27
from trac.Timeline import ITimelineEventProvider
27
from trac.Timeline import ITimelineEventProvider
28
from trac.versioncontrol import Changeset, Node
28
from trac.versioncontrol import Changeset, Node, get_authorizer
29
from trac.versioncontrol.svn_authz import SubversionAuthorizer
30
from trac.versioncontrol.diff import get_diff_options, hdf_diff, unified_diff
29
from trac.versioncontrol.diff import get_diff_options, hdf_diff, unified_diff
31
from trac.web import IRequestHandler
30
from trac.web import IRequestHandler
32
from trac.web.chrome import add_link, add_stylesheet, INavigationContributor
31
from trac.web.chrome import add_link, add_stylesheet, INavigationContributor
Lines 64-70 Link Here
64
63
65
        rev = req.args.get('rev')
64
        rev = req.args.get('rev')
66
        repos = self.env.get_repository(req.authname)
65
        repos = self.env.get_repository(req.authname)
67
        authzperm = SubversionAuthorizer(self.env, req.authname)
66
        authzperm = get_authorizer(self.env, req.authname)
68
        authzperm.assert_permission_for_changeset(rev)
67
        authzperm.assert_permission_for_changeset(rev)
69
68
70
        diff_options = get_diff_options(req)
69
        diff_options = get_diff_options(req)
Lines 106-112 Link Here
106
                                             'changeset_show_files'))
105
                                             'changeset_show_files'))
107
            db = self.env.get_db_cnx()
106
            db = self.env.get_db_cnx()
108
            repos = self.env.get_repository()
107
            repos = self.env.get_repository()
109
            authzperm = SubversionAuthorizer(self.env, req.authname)
108
            authzperm = get_authorizer(self.env, req.authname)
110
            rev = repos.youngest_rev
109
            rev = repos.youngest_rev
111
            while rev:
110
            while rev:
112
                if not authzperm.has_permission_for_changeset(rev):
111
                if not authzperm.has_permission_for_changeset(rev):
Lines 383-389 Link Here
383
    def get_search_results(self, req, query, filters):
382
    def get_search_results(self, req, query, filters):
384
        if not 'changeset' in filters:
383
        if not 'changeset' in filters:
385
            return
384
            return
386
        authzperm = SubversionAuthorizer(self.env, req.authname)
385
        authzperm = get_authorizer(self.env, req.authname)
387
        db = self.env.get_db_cnx()
386
        db = self.env.get_db_cnx()
388
        sql = "SELECT rev,time,author,message " \
387
        sql = "SELECT rev,time,author,message " \
389
              "FROM revision WHERE %s" % \
388
              "FROM revision WHERE %s" % \
(-)trac/wiki-default/TracIni (+5 lines)
Lines 88-93 Link Here
88
== [wiki] ==
88
== [wiki] ==
89
|| `ignore_missing_pages` || enable/disable highlighting CamelCase links to missing pages (''since 0.9'') ||
89
|| `ignore_missing_pages` || enable/disable highlighting CamelCase links to missing pages (''since 0.9'') ||
90
90
91
== [darcs] ==
92
|| `cachedir` || By default the darcs backend keeps a cache of the visited files at various revision inside the repository itself, in `_darcs/trac_cache`, that may be overridden by this option, setting it to the desired directory that needs to be writeable by the trac process. ||
93
|| `command` || Name of the external darcs executable, default to `darcs`. This can be used to set up the environment as well, like in "`DARCS_DONT_ESCAPE_ANYTHING=1 /usr/local/bin/darcs`" ||
94
|| `dont_escape_8bit` || This is a shortcut for "`command=DARCS_DONT_ESCAPE_8BIT=1 darcs`" (true, false). Default to false. ||
95
91
== [components] ==
96
== [components] ==
92
(''since 0.9'')
97
(''since 0.9'')
93
98

Return to bug 109477