Edgewall Software

Ticket #3752: svn_fs.py

File svn_fs.py, 19.0 KB (added by andreas.l@…, 2 years ago)

Modified svn_fs.py

Line 
1# -*- coding: iso-8859-1 -*-
2#
3# Copyright (C) 2005 Edgewall Software
4# Copyright (C) 2005 Christopher Lenz <cmlenz@gmx.de>
5# All rights reserved.
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: Christopher Lenz <cmlenz@gmx.de>
16
17from trac.util import TracError
18from trac.versioncontrol import Changeset, Node, Repository
19
20import os.path
21import time
22import weakref
23import posixpath
24
25from svn import fs, repos, core, delta
26
27_kindmap = {core.svn_node_dir: Node.DIRECTORY,
28            core.svn_node_file: Node.FILE}
29
30application_pool = None
31
32   
33def _get_history(path, authz, fs_ptr, pool, start, end, limit=None):
34    history = []
35    if hasattr(repos, 'svn_repos_history2'):
36        # For Subversion >= 1.1
37        def authz_cb(root, path, pool):
38            if limit and len(history) >= limit:
39                return 0
40            return authz.has_permission(path) and 1 or 0
41        def history2_cb(path, rev, pool):
42            history.append((path, rev))
43        repos.svn_repos_history2(fs_ptr, path, history2_cb, authz_cb,
44                                 start, end, 1, pool())
45    else:
46        # For Subversion 1.0.x
47        def history_cb(path, rev, pool):
48            if authz.has_permission(path):
49                history.append((path, rev))
50        repos.svn_repos_history(fs_ptr, path, history_cb, start, end, 1, pool())
51    for item in history:
52        yield item
53
54
55def _normalize_path(path):
56    """Remove leading "/", except for the root"""
57    return path and path.strip('/') or '/'
58
59def _scoped_path(scope, fullpath):
60    """Remove the leading scope from repository paths"""
61    if fullpath:
62        if scope == '/':
63            return _normalize_path(fullpath)
64        elif fullpath.startswith(scope.rstrip('/')):
65            return fullpath[len(scope):] or '/'
66
67
68def _mark_weakpool_invalid(weakpool):
69    if weakpool():
70        weakpool()._mark_invalid()
71
72
73class Pool(object):
74    """A Pythonic memory pool object"""
75
76    # Protect svn.core methods from GC
77    apr_pool_destroy = staticmethod(core.apr_pool_destroy)
78    apr_terminate = staticmethod(core.apr_terminate)
79    apr_pool_clear = staticmethod(core.apr_pool_clear)
80   
81    def __init__(self, parent_pool=None):
82        """Create a new memory pool"""
83
84        global application_pool
85        self._parent_pool = parent_pool or application_pool
86
87        # Create pool
88        if self._parent_pool:
89            self._pool = core.svn_pool_create(self._parent_pool())
90        else:
91            # If we are an application-level pool,
92            # then initialize APR and set this pool
93            # to be the application-level pool
94            core.apr_initialize()
95            application_pool = self
96
97            self._pool = core.svn_pool_create(None)
98        self._mark_valid()
99
100    def __call__(self):
101        return self._pool
102
103    def valid(self):
104        """Check whether this memory pool and its parents
105        are still valid"""
106        return hasattr(self,"_is_valid")
107
108    def assert_valid(self):
109        """Assert that this memory_pool is still valid."""
110        assert self.valid();
111
112    def clear(self):
113        """Clear embedded memory pool. Invalidate all subpools."""
114        self.apr_pool_clear(self._pool)
115        self._mark_valid()
116
117    def destroy(self):
118        """Destroy embedded memory pool. If you do not destroy
119        the memory pool manually, Python will destroy it
120        automatically."""
121
122        global application_pool
123
124        self.assert_valid()
125
126        # Destroy pool
127        self.apr_pool_destroy(self._pool)
128
129        # Clear application pool and terminate APR if necessary
130        if not self._parent_pool:
131            application_pool = None
132            self.apr_terminate()
133
134        self._mark_invalid()
135
136    def __del__(self):
137        """Automatically destroy memory pools, if necessary"""
138        if self.valid():
139            self.destroy()
140
141    def _mark_valid(self):
142        """Mark pool as valid"""
143        if self._parent_pool:
144            # Refer to self using a weakreference so that we don't
145            # create a reference cycle
146            weakself = weakref.ref(self)
147
148            # Set up callbacks to mark pool as invalid when parents
149            # are destroyed
150            self._weakref = weakref.ref(self._parent_pool._is_valid,
151                                        lambda x: \
152                                        _mark_weakpool_invalid(weakself));
153
154        # mark pool as valid
155        self._is_valid = lambda: 1
156
157    def _mark_invalid(self):
158        """Mark pool as invalid"""
159        if self.valid():
160            # Mark invalid
161            del self._is_valid
162
163            # Free up memory
164            del self._parent_pool
165            if hasattr(self, "_weakref"):
166                del self._weakref
167
168# Initialize application-level pool
169Pool()
170
171
172class SubversionRepository(Repository):
173    """
174    Repository implementation based on the svn.fs API.
175    """
176
177    def __init__(self, path, authz, log):
178        if core.SVN_VER_MAJOR < 1:
179            raise TracError, \
180                  "Subversion >= 1.0 required: Found %d.%d.%d" % \
181                  (core.SVN_VER_MAJOR, core.SVN_VER_MINOR, core.SVN_VER_MICRO)
182
183        self.pool = Pool()
184       
185        # Remove any trailing slash or else subversion might abort
186        path = os.path.normpath(path).replace('\\', '/')
187        self.path = repos.svn_repos_find_root_path(path, self.pool())
188        if self.path is None:
189            raise TracError, \
190                  "%s does not appear to be a Subversion repository." % path
191
192        self.repos = repos.svn_repos_open(self.path, self.pool())
193        self.fs_ptr = repos.svn_repos_fs(self.repos)
194       
195        uuid = fs.get_uuid(self.fs_ptr, self.pool())
196        name = 'svn:%s:%s' % (uuid, path)
197
198        Repository.__init__(self, name, authz, log)
199
200        if self.path != path:
201            self.scope = path[len(self.path):]
202            if not self.scope[-1] == '/':
203                self.scope += '/'
204        else:
205            self.scope = '/'
206        self.log.debug("Opening subversion file-system at %s with scope %s" \
207                       % (self.path, self.scope))
208
209        self.rev = fs.youngest_rev(self.fs_ptr, self.pool())
210
211        self.history = None
212        if self.scope != '/':
213            self.history = []
214            for path,rev in _get_history(self.scope[1:], self.authz,
215                                         self.fs_ptr, self.pool, 0, self.rev):
216                self.history.append(rev)
217
218    def __del__(self):
219        self.close()
220
221    def has_node(self, path, rev, pool=None):
222        if not pool:
223            pool = self.pool
224        rev_root = fs.revision_root(self.fs_ptr, rev, pool())
225        node_type = fs.check_path(rev_root, self.scope + path, pool())
226        return node_type in _kindmap
227
228    def normalize_path(self, path):
229        return _normalize_path(path)
230
231    def normalize_rev(self, rev):
232        try:
233            rev =  int(rev)
234        except (ValueError, TypeError):
235            rev = None
236        if rev is None:
237            rev = self.youngest_rev
238        elif rev > self.youngest_rev:
239            raise TracError, "Revision %s doesn't exist yet" % rev
240        return rev
241
242    def close(self):
243        self.log.debug("Closing subversion file-system at %s" % self.path)
244        self.repos = None
245        self.fs_ptr = None
246        self.rev = None
247        self.pool = None
248
249    def get_changeset(self, rev):
250        return SubversionChangeset(int(rev), self.authz, self.scope,
251                                   self.fs_ptr, self.pool)
252
253    def get_node(self, path, rev=None):
254        self.authz.assert_permission(posixpath.join(self.scope, path))
255        if path and path[-1] == '/':
256            path = path[:-1]
257
258        rev = self.normalize_rev(rev)
259
260        return SubversionNode(path, rev, self.authz, self.scope, self.fs_ptr,
261                              self.pool)
262
263    def _history(self, path, start, end, limit=None, pool=None):
264        scoped_path = posixpath.join(self.scope[1:], path)
265        return _get_history(scoped_path, self.authz, self.fs_ptr,
266                            pool or self.pool, start, end, limit)
267
268    def _previous_rev(self, rev, path='', pool=None):
269        if rev > 1: # don't use oldest here, as it's too expensive
270            try:
271                for _, prev in self._history(path, 0, rev-1, limit=1,
272                                             pool=pool):
273                    return prev
274            except (SystemError, # "null arg to internal routine" in 1.2.x
275                    core.SubversionException): # in 1.3.x
276                pass
277        return None
278   
279
280    def get_oldest_rev(self):
281        rev = 0
282        if self.scope == '/':
283            return rev
284        return self.history[-1]
285
286    def get_youngest_rev(self):
287        rev = self.rev
288        if self.scope == '/':
289            return rev
290        return self.history[0]
291
292    def previous_rev(self, rev):
293        rev = int(rev)
294        if rev == 0:
295            return None
296        if self.scope == '/':
297            return rev - 1
298        idx = self.history.index(rev)
299        if idx + 1 < len(self.history):
300            return self.history[idx + 1]
301        return None
302
303    def next_rev(self, rev):
304        rev = int(rev)
305        if rev == self.rev:
306            return None
307        if self.scope == '/':
308            return rev + 1
309        if rev == 0:
310            return self.oldest_rev
311        try:
312            idx = self.history.index(rev)
313            if idx > 0:
314                return self.history[idx - 1]
315        except ValueError:
316            return rev + 1 # for scoped repos.
317        return None
318
319    def rev_older_than(self, rev1, rev2):
320        return self.normalize_rev(rev1) < self.normalize_rev(rev2)
321
322    def get_youngest_rev_in_cache(self, db):
323        """Get the latest stored revision by sorting the revision strings
324        numerically
325        """
326        cursor = db.cursor()
327        cursor.execute("SELECT rev FROM revision "
328                       "ORDER BY -LENGTH(rev), rev DESC LIMIT 1")
329        row = cursor.fetchone()
330        return row and row[0] or None
331
332    def get_path_history(self, path, rev=None, limit=None):
333        path = self.normalize_path(path)
334        rev = self.normalize_rev(rev)
335        expect_deletion = False
336        subpool = Pool(self.pool)
337        while rev:
338            subpool.clear()
339            if self.has_node(path, rev, subpool):
340                if expect_deletion:
341                    # it was missing, now it's there again:
342                    #  rev+1 must be a delete
343                    yield path, rev+1, Changeset.DELETE
344                newer = None # 'newer' is the previously seen history tuple
345                older = None # 'older' is the currently examined history tuple
346                for p, r in _get_history(self.scope + path, self.authz,
347                                         self.fs_ptr, subpool, 0, rev, limit):
348                    older = (_scoped_path(self.scope, p), r, Changeset.ADD)
349                    rev = self._previous_rev(r, pool=subpool)
350                    if newer:
351                        if older[0] == path:
352                            # still on the path: 'newer' was an edit
353                            yield newer[0], newer[1], Changeset.EDIT
354                        else:
355                            # the path changed: 'newer' was a copy
356                            rev = self._previous_rev(newer[1], pool=subpool)
357                            # restart before the copy op
358                            yield newer[0], newer[1], Changeset.COPY
359                            older = (older[0], older[1], 'unknown')
360                            break
361                    newer = older
362                if older:
363                    # either a real ADD or the source of a COPY
364                    yield older
365            else:
366                expect_deletion = True
367                rev = self._previous_rev(rev, pool=subpool)
368
369
370class SubversionNode(Node):
371
372    def __init__(self, path, rev, authz, scope, fs_ptr, pool=None):
373        self.authz = authz
374        self.scope = scope
375        if scope != '/':
376            self.scoped_path = scope + path
377        else:
378            self.scoped_path = path
379        self.fs_ptr = fs_ptr
380        self.pool = Pool(pool)
381        self._requested_rev = rev
382
383        self.root = fs.revision_root(fs_ptr, rev, self.pool())
384        node_type = fs.check_path(self.root, self.scoped_path, self.pool())
385        if not node_type in _kindmap:
386            raise TracError, "No node at %s in revision %s" % (path, rev)
387        self.created_rev = fs.node_created_rev(self.root, self.scoped_path,
388                                               self.pool())
389        self.created_path = fs.node_created_path(self.root, self.scoped_path,
390                                                 self.pool())
391        # 'created_path' differs from 'path' if the last operation is a copy,
392        # and furthermore, 'path' might not exist at 'create_rev'
393        self.rev = self.created_rev
394       
395        Node.__init__(self, path, self.rev, _kindmap[node_type])
396
397    def get_content(self):
398        if self.isdir:
399            return None
400        s = core.Stream(fs.file_contents(self.root, self.scoped_path,
401                                         self.pool()))
402        # Make sure the stream object references the pool to make sure the pool
403        # is not destroyed before the stream object.
404        s._pool = self.pool
405        return s
406
407    def get_entries(self):
408        if self.isfile:
409            return
410        pool = Pool(self.pool)
411        entries = fs.dir_entries(self.root, self.scoped_path, pool())
412        for item in entries.keys():
413            path = '/'.join((self.path, item))
414            if not self.authz.has_permission(path):
415                continue
416            yield SubversionNode(path, self._requested_rev, self.authz,
417                                 self.scope, self.fs_ptr, self.pool)
418
419    def get_history(self,limit=None):
420        newer = None # 'newer' is the previously seen history tuple
421        older = None # 'older' is the currently examined history tuple
422        pool = Pool(self.pool)
423        for path, rev in _get_history(self.scoped_path, self.authz, self.fs_ptr,
424                                      pool, 0, self._requested_rev, limit):
425            scoped_path = _scoped_path(self.scope, path)
426            if rev > 0 and scoped_path:
427                older = (scoped_path, rev, Changeset.ADD)
428                if newer:
429                    change = newer[0] == older[0] and Changeset.EDIT or \
430                             Changeset.COPY
431                    newer = (newer[0], newer[1], change)
432                    yield newer
433                newer = older
434        if newer:
435            yield newer
436
437    def get_properties(self):
438        props = fs.node_proplist(self.root, self.scoped_path, self.pool())
439        for name,value in props.items():
440            props[name] = str(value) # Make sure the value is a proper string
441        return props
442
443    def get_content_length(self):
444        if self.isdir:
445            return None
446        return fs.file_length(self.root, self.scoped_path, self.pool())
447
448    def get_content_type(self):
449        if self.isdir:
450            return None
451        return self._get_prop(core.SVN_PROP_MIME_TYPE)
452
453    def get_last_modified(self):
454        date = fs.revision_prop(self.fs_ptr, self.created_rev,
455                                core.SVN_PROP_REVISION_DATE, self.pool())
456        return core.svn_time_from_cstring(date, self.pool()) / 1000000
457
458    def _get_prop(self, name):
459        return fs.node_prop(self.root, self.scoped_path, name, self.pool())
460
461
462class SubversionChangeset(Changeset):
463
464    def __init__(self, rev, authz, scope, fs_ptr, pool=None):
465        self.rev = rev
466        self.authz = authz
467        self.scope = scope
468        self.fs_ptr = fs_ptr
469        self.pool = Pool(pool)
470        message = self._get_prop(core.SVN_PROP_REVISION_LOG)
471        author = self._get_prop(core.SVN_PROP_REVISION_AUTHOR)
472        date = self._get_prop(core.SVN_PROP_REVISION_DATE)
473        date = core.svn_time_from_cstring(date, self.pool()) / 1000000
474        Changeset.__init__(self, rev, message, author, date)
475
476    def get_changes(self):
477        pool = Pool(self.pool)
478        tmp = Pool(pool)
479        root = fs.revision_root(self.fs_ptr, self.rev, pool())
480        editor = repos.RevisionChangeCollector(self.fs_ptr, self.rev, pool())
481        e_ptr, e_baton = delta.make_editor(editor, pool())
482        repos.svn_repos_replay(root, e_ptr, e_baton, pool())
483
484        idx = 0
485        copies, deletions = {}, {}
486        changes = []
487        revroots = {}
488        for path, change in editor.changes.items():
489            tmp.clear()
490            if not self.authz.has_permission(path):
491                # FIXME: what about base_path?
492                continue
493            if not (path+'/').startswith(self.scope[1:]):
494                continue
495            action = ''
496            if not change.path and change.base_path:
497                action = Changeset.DELETE
498                deletions[change.base_path] = idx
499            elif change.added:
500                if change.base_path and change.base_rev:
501                    action = Changeset.COPY
502                    copies[change.base_path] = idx
503                else:
504                    action = Changeset.ADD
505            else:
506                action = Changeset.EDIT
507                b_path, b_rev = change.base_path, change.base_rev
508                if revroots.has_key(b_rev):
509                    b_root = revroots[b_rev]
510                else:
511                    b_root = fs.revision_root(self.fs_ptr, b_rev, pool())
512                    revroots[b_rev] = b_root
513                change.base_path = fs.node_created_path(b_root, b_path, tmp())
514                change.base_rev = fs.node_created_rev(b_root, b_path, tmp())
515            kind = _kindmap[change.item_kind]
516            path = path[len(self.scope) - 1:]
517            base_path = _scoped_path(self.scope, change.base_path)
518            changes.append([path, kind, action, base_path, change.base_rev])
519            idx += 1
520
521        moves = []
522        for k,v in copies.items():
523            if k in deletions:
524                changes[v][2] = Changeset.MOVE
525                moves.append(deletions[k])
526        offset = 0
527        moves.sort()
528        for i in moves:
529            del changes[i - offset]
530            offset += 1
531
532        changes.sort()
533        for change in changes:
534            yield tuple(change)
535
536    def _get_prop(self, name):
537        return fs.revision_prop(self.fs_ptr, self.rev, name, self.pool())