39
|
1 # -*- coding: utf-8 -*-
|
|
2 #
|
|
3 # Copyright (C) 2005-2006 Edgewall Software
|
|
4 # Copyright (C) 2005 Christopher Lenz <cmlenz@gmx.de>
|
|
5 # Copyright (C) 2005-2006 Christian Boos <cboos@neuf.fr>
|
|
6 # All rights reserved.
|
|
7 #
|
|
8 # This software is licensed as described in the file COPYING, which
|
|
9 # you should have received as part of this distribution. The terms
|
|
10 # are also available at http://trac.edgewall.com/license.html.
|
|
11 #
|
|
12 # This software consists of voluntary contributions made by many
|
|
13 # individuals. For the exact contribution history, see the revision
|
|
14 # history and logs, available at http://projects.edgewall.com/trac/.
|
|
15 #
|
|
16 # Author: Christopher Lenz <cmlenz@gmx.de>
|
|
17 # Christian Boos <cboos@neuf.fr>
|
|
18
|
|
19 """
|
|
20 Note about Unicode:
|
|
21 All paths (or strings) manipulated by the Subversion bindings are
|
|
22 assumed to be UTF-8 encoded.
|
|
23
|
|
24 All paths manipulated by Trac are `unicode` objects.
|
|
25
|
|
26 Therefore:
|
|
27 * before being handed out to SVN, the Trac paths have to be encoded to UTF-8,
|
|
28 using `_to_svn()`
|
|
29 * before being handed out to Trac, a SVN path has to be decoded from UTF-8,
|
|
30 using `_from_svn()`
|
|
31
|
|
32 Warning: `SubversionNode.get_content` returns an object from which one
|
|
33 can read a stream of bytes.
|
|
34 NO guarantees can be given about what that stream of bytes
|
|
35 represents.
|
|
36 It might be some text, encoded in some way or another.
|
|
37 SVN properties __might__ give some hints about the content,
|
|
38 but they actually only reflect the beliefs of whomever set
|
|
39 those properties...
|
|
40 """
|
|
41
|
|
42 import os.path
|
|
43 import time
|
|
44 import weakref
|
|
45 import posixpath
|
|
46
|
|
47 from trac.core import *
|
|
48 from trac.versioncontrol import Changeset, Node, Repository, \
|
|
49 IRepositoryConnector, \
|
|
50 NoSuchChangeset, NoSuchNode
|
|
51 from trac.versioncontrol.cache import CachedRepository
|
|
52 from trac.versioncontrol.svn_authz import SubversionAuthorizer
|
|
53 from trac.util.text import to_unicode
|
|
54
|
|
55 try:
|
|
56 from svn import fs, repos, core, delta
|
|
57 has_subversion = True
|
|
58 except ImportError:
|
|
59 has_subversion = False
|
|
60 class dummy_svn(object):
|
|
61 svn_node_dir = 1
|
|
62 svn_node_file = 2
|
|
63 def apr_pool_destroy(): pass
|
|
64 def apr_terminate(): pass
|
|
65 def apr_pool_clear(): pass
|
|
66 Editor = object
|
|
67 delta = core = dummy_svn()
|
|
68
|
|
69
|
|
70 _kindmap = {core.svn_node_dir: Node.DIRECTORY,
|
|
71 core.svn_node_file: Node.FILE}
|
|
72
|
|
73
|
|
74 application_pool = None
|
|
75
|
|
76 def _get_history(svn_path, authz, fs_ptr, pool, start, end, limit=None):
|
|
77 """`svn_path` is assumed to be a UTF-8 encoded string.
|
|
78 Returned history paths will be `unicode` objects though."""
|
|
79 history = []
|
|
80 if hasattr(repos, 'svn_repos_history2'):
|
|
81 # For Subversion >= 1.1
|
|
82 def authz_cb(root, path, pool):
|
|
83 if limit and len(history) >= limit:
|
|
84 return 0
|
|
85 return authz.has_permission(_from_svn(path)) and 1 or 0
|
|
86 def history2_cb(path, rev, pool):
|
|
87 history.append((_from_svn(path), rev))
|
|
88 repos.svn_repos_history2(fs_ptr, svn_path, history2_cb, authz_cb,
|
|
89 start, end, 1, pool())
|
|
90 else:
|
|
91 # For Subversion 1.0.x
|
|
92 def history_cb(path, rev, pool):
|
|
93 path = _from_svn(path)
|
|
94 if authz.has_permission(path):
|
|
95 history.append((path, rev))
|
|
96 repos.svn_repos_history(fs_ptr, svn_path, history_cb,
|
|
97 start, end, 1, pool())
|
|
98 for item in history:
|
|
99 yield item
|
|
100
|
|
101 def _to_svn(*args):
|
|
102 """Expect a list of `unicode` path components.
|
|
103 Returns an UTF-8 encoded string suitable for the Subversion python bindings.
|
|
104 """
|
|
105 return '/'.join([path.strip('/') for path in args]).encode('utf-8')
|
|
106
|
|
107 def _from_svn(path):
|
|
108 """Expect an UTF-8 encoded string and transform it to an `unicode` object"""
|
|
109 return path and path.decode('utf-8')
|
|
110
|
|
111 def _normalize_path(path):
|
|
112 """Remove leading "/", except for the root."""
|
|
113 return path and path.strip('/') or '/'
|
|
114
|
|
115 def _path_within_scope(scope, fullpath):
|
|
116 """Remove the leading scope from repository paths.
|
|
117
|
|
118 Return `None` if the path is not is scope.
|
|
119 """
|
|
120 if fullpath is not None:
|
|
121 fullpath = fullpath.lstrip('/')
|
|
122 if scope == '/':
|
|
123 return _normalize_path(fullpath)
|
|
124 scope = scope.strip('/')
|
|
125 if (fullpath + '/').startswith(scope + '/'):
|
|
126 return fullpath[len(scope) + 1:] or '/'
|
|
127
|
|
128 def _is_path_within_scope(scope, fullpath):
|
|
129 """Check whether the given `fullpath` is within the given `scope`"""
|
|
130 if scope == '/':
|
|
131 return fullpath is not None
|
|
132 fullpath = fullpath and fullpath.lstrip('/') or ''
|
|
133 scope = scope.strip('/')
|
|
134 return (fullpath + '/').startswith(scope + '/')
|
|
135
|
|
136
|
|
137 def _mark_weakpool_invalid(weakpool):
|
|
138 if weakpool():
|
|
139 weakpool()._mark_invalid()
|
|
140
|
|
141
|
|
142 class Pool(object):
|
|
143 """A Pythonic memory pool object"""
|
|
144
|
|
145 # Protect svn.core methods from GC
|
|
146 apr_pool_destroy = staticmethod(core.apr_pool_destroy)
|
|
147 apr_terminate = staticmethod(core.apr_terminate)
|
|
148 apr_pool_clear = staticmethod(core.apr_pool_clear)
|
|
149
|
|
150 def __init__(self, parent_pool=None):
|
|
151 """Create a new memory pool"""
|
|
152
|
|
153 global application_pool
|
|
154 self._parent_pool = parent_pool or application_pool
|
|
155
|
|
156 # Create pool
|
|
157 if self._parent_pool:
|
|
158 self._pool = core.svn_pool_create(self._parent_pool())
|
|
159 else:
|
|
160 # If we are an application-level pool,
|
|
161 # then initialize APR and set this pool
|
|
162 # to be the application-level pool
|
|
163 core.apr_initialize()
|
|
164 application_pool = self
|
|
165
|
|
166 self._pool = core.svn_pool_create(None)
|
|
167 self._mark_valid()
|
|
168
|
|
169 def __call__(self):
|
|
170 return self._pool
|
|
171
|
|
172 def valid(self):
|
|
173 """Check whether this memory pool and its parents
|
|
174 are still valid"""
|
|
175 return hasattr(self,"_is_valid")
|
|
176
|
|
177 def assert_valid(self):
|
|
178 """Assert that this memory_pool is still valid."""
|
|
179 assert self.valid();
|
|
180
|
|
181 def clear(self):
|
|
182 """Clear embedded memory pool. Invalidate all subpools."""
|
|
183 self.apr_pool_clear(self._pool)
|
|
184 self._mark_valid()
|
|
185
|
|
186 def destroy(self):
|
|
187 """Destroy embedded memory pool. If you do not destroy
|
|
188 the memory pool manually, Python will destroy it
|
|
189 automatically."""
|
|
190
|
|
191 global application_pool
|
|
192
|
|
193 self.assert_valid()
|
|
194
|
|
195 # Destroy pool
|
|
196 self.apr_pool_destroy(self._pool)
|
|
197
|
|
198 # Clear application pool and terminate APR if necessary
|
|
199 if not self._parent_pool:
|
|
200 application_pool = None
|
|
201 self.apr_terminate()
|
|
202
|
|
203 self._mark_invalid()
|
|
204
|
|
205 def __del__(self):
|
|
206 """Automatically destroy memory pools, if necessary"""
|
|
207 if self.valid():
|
|
208 self.destroy()
|
|
209
|
|
210 def _mark_valid(self):
|
|
211 """Mark pool as valid"""
|
|
212 if self._parent_pool:
|
|
213 # Refer to self using a weakreference so that we don't
|
|
214 # create a reference cycle
|
|
215 weakself = weakref.ref(self)
|
|
216
|
|
217 # Set up callbacks to mark pool as invalid when parents
|
|
218 # are destroyed
|
|
219 self._weakref = weakref.ref(self._parent_pool._is_valid,
|
|
220 lambda x: \
|
|
221 _mark_weakpool_invalid(weakself));
|
|
222
|
|
223 # mark pool as valid
|
|
224 self._is_valid = lambda: 1
|
|
225
|
|
226 def _mark_invalid(self):
|
|
227 """Mark pool as invalid"""
|
|
228 if self.valid():
|
|
229 # Mark invalid
|
|
230 del self._is_valid
|
|
231
|
|
232 # Free up memory
|
|
233 del self._parent_pool
|
|
234 if hasattr(self, "_weakref"):
|
|
235 del self._weakref
|
|
236
|
|
237
|
|
238 # Initialize application-level pool
|
|
239 if has_subversion:
|
|
240 Pool()
|
|
241
|
|
242
|
|
243 class SubversionConnector(Component):
|
|
244
|
|
245 implements(IRepositoryConnector)
|
|
246
|
|
247 def get_supported_types(self):
|
|
248 global has_subversion
|
|
249 if has_subversion:
|
|
250 yield ("svnfs", 4)
|
|
251 yield ("svn", 2)
|
|
252
|
|
253 def get_repository(self, type, dir, authname):
|
|
254 """Return a `SubversionRepository`.
|
|
255
|
|
256 The repository is generally wrapped in a `CachedRepository`,
|
|
257 unless `direct-svn-fs` is the specified type.
|
|
258 """
|
|
259 authz = None
|
|
260 if authname:
|
|
261 authz = SubversionAuthorizer(self.env, authname)
|
|
262 repos = SubversionRepository(dir, authz, self.log)
|
|
263 return CachedRepository(self.env.get_db_cnx(), repos, authz, self.log)
|
|
264
|
|
265
|
|
266 class SubversionRepository(Repository):
|
|
267 """
|
|
268 Repository implementation based on the svn.fs API.
|
|
269 """
|
|
270
|
|
271 def __init__(self, path, authz, log):
|
|
272 self.path = path # might be needed by __del__()/close()
|
|
273 self.log = log
|
|
274 if core.SVN_VER_MAJOR < 1:
|
|
275 raise TracError("Subversion >= 1.0 required: Found %d.%d.%d" % \
|
|
276 (core.SVN_VER_MAJOR,
|
|
277 core.SVN_VER_MINOR,
|
|
278 core.SVN_VER_MICRO))
|
|
279 self.pool = Pool()
|
|
280
|
|
281 # Remove any trailing slash or else subversion might abort
|
|
282 if isinstance(path, unicode):
|
|
283 path = path.encode('utf-8')
|
|
284 path = os.path.normpath(path).replace('\\', '/')
|
|
285 self.path = repos.svn_repos_find_root_path(path, self.pool())
|
|
286 if self.path is None:
|
|
287 raise TracError("%s does not appear to be a Subversion repository." \
|
|
288 % path)
|
|
289
|
|
290 self.repos = repos.svn_repos_open(self.path, self.pool())
|
|
291 self.fs_ptr = repos.svn_repos_fs(self.repos)
|
|
292
|
|
293 uuid = fs.get_uuid(self.fs_ptr, self.pool())
|
|
294 name = 'svn:%s:%s' % (uuid, path)
|
|
295
|
|
296 Repository.__init__(self, name, authz, log)
|
|
297
|
|
298 if self.path != path:
|
|
299 self.scope = path[len(self.path):]
|
|
300 if not self.scope[-1] == '/':
|
|
301 self.scope += '/'
|
|
302 else:
|
|
303 self.scope = '/'
|
|
304 assert self.scope[0] == '/'
|
|
305
|
|
306 self.log.debug("Opening subversion file-system at %s with scope %s" \
|
|
307 % (self.path, self.scope))
|
|
308 self.youngest = None
|
|
309 self.oldest = None
|
|
310
|
|
311 def __del__(self):
|
|
312 self.close()
|
|
313
|
|
314 def has_node(self, path, rev, pool=None):
|
|
315 if not pool:
|
|
316 pool = self.pool
|
|
317 rev_root = fs.revision_root(self.fs_ptr, rev, pool())
|
|
318 node_type = fs.check_path(rev_root, _to_svn(self.scope, path), pool())
|
|
319 return node_type in _kindmap
|
|
320
|
|
321 def normalize_path(self, path):
|
|
322 return _normalize_path(path)
|
|
323
|
|
324 def normalize_rev(self, rev):
|
|
325 try:
|
|
326 rev = int(rev)
|
|
327 except (ValueError, TypeError):
|
|
328 rev = None
|
|
329 if rev is None:
|
|
330 rev = self.youngest_rev
|
|
331 elif rev > self.youngest_rev:
|
|
332 raise NoSuchChangeset(rev)
|
|
333 return rev
|
|
334
|
|
335 def close(self):
|
|
336 self.log.debug("Closing subversion file-system at %s" % self.path)
|
|
337 self.repos = None
|
|
338 self.fs_ptr = None
|
|
339 self.pool = None
|
|
340
|
|
341 def get_changeset(self, rev):
|
|
342 return SubversionChangeset(int(rev), self.authz, self.scope,
|
|
343 self.fs_ptr, self.pool)
|
|
344
|
|
345 def get_node(self, path, rev=None):
|
|
346 path = path or ''
|
|
347 self.authz.assert_permission(posixpath.join(self.scope, path))
|
|
348 if path and path[-1] == '/':
|
|
349 path = path[:-1]
|
|
350
|
|
351 rev = self.normalize_rev(rev)
|
|
352
|
|
353 return SubversionNode(path, rev, self.authz, self.scope, self.fs_ptr,
|
|
354 self.pool)
|
|
355
|
|
356 def _history(self, path, start, end, limit=None, pool=None):
|
|
357 return _get_history(_to_svn(self.scope, path), self.authz, self.fs_ptr,
|
|
358 pool or self.pool, start, end, limit)
|
|
359
|
|
360 def _previous_rev(self, rev, path='', pool=None):
|
|
361 if rev > 1: # don't use oldest here, as it's too expensive
|
|
362 try:
|
|
363 for _, prev in self._history(path, 0, rev-1, limit=1,
|
|
364 pool=pool):
|
|
365 return prev
|
|
366 except (SystemError, # "null arg to internal routine" in 1.2.x
|
|
367 core.SubversionException): # in 1.3.x
|
|
368 pass
|
|
369 return None
|
|
370
|
|
371
|
|
372 def get_oldest_rev(self):
|
|
373 if self.oldest is None:
|
|
374 self.oldest = 1
|
|
375 if self.scope != '/':
|
|
376 self.oldest = self.next_rev(0, find_initial_rev=True)
|
|
377 return self.oldest
|
|
378
|
|
379 def get_youngest_rev(self):
|
|
380 if not self.youngest:
|
|
381 self.youngest = fs.youngest_rev(self.fs_ptr, self.pool())
|
|
382 if self.scope != '/':
|
|
383 for path, rev in self._history('', 0, self.youngest, limit=1):
|
|
384 self.youngest = rev
|
|
385 return self.youngest
|
|
386
|
|
387 def previous_rev(self, rev, path=''):
|
|
388 rev = self.normalize_rev(rev)
|
|
389 return self._previous_rev(rev, path)
|
|
390
|
|
391 def next_rev(self, rev, path='', find_initial_rev=False):
|
|
392 rev = self.normalize_rev(rev)
|
|
393 next = rev + 1
|
|
394 youngest = self.youngest_rev
|
|
395 subpool = Pool(self.pool)
|
|
396 while next <= youngest:
|
|
397 subpool.clear()
|
|
398 try:
|
|
399 for _, next in self._history(path, rev+1, next, limit=1,
|
|
400 pool=subpool):
|
|
401 return next
|
|
402 except (SystemError, # "null arg to internal routine" in 1.2.x
|
|
403 core.SubversionException): # in 1.3.x
|
|
404 if not find_initial_rev:
|
|
405 return next # a 'delete' event is also interesting...
|
|
406 next += 1
|
|
407 return None
|
|
408
|
|
409 def rev_older_than(self, rev1, rev2):
|
|
410 return self.normalize_rev(rev1) < self.normalize_rev(rev2)
|
|
411
|
|
412 def get_youngest_rev_in_cache(self, db):
|
|
413 """Get the latest stored revision by sorting the revision strings
|
|
414 numerically
|
|
415 """
|
|
416 cursor = db.cursor()
|
|
417 cursor.execute("SELECT rev FROM revision "
|
|
418 "ORDER BY -LENGTH(rev), rev DESC LIMIT 1")
|
|
419 row = cursor.fetchone()
|
|
420 return row and row[0] or None
|
|
421
|
|
422 def get_path_history(self, path, rev=None, limit=None):
|
|
423 path = self.normalize_path(path)
|
|
424 rev = self.normalize_rev(rev)
|
|
425 expect_deletion = False
|
|
426 subpool = Pool(self.pool)
|
|
427 while rev:
|
|
428 subpool.clear()
|
|
429 if self.has_node(path, rev, subpool):
|
|
430 if expect_deletion:
|
|
431 # it was missing, now it's there again:
|
|
432 # rev+1 must be a delete
|
|
433 yield path, rev+1, Changeset.DELETE
|
|
434 newer = None # 'newer' is the previously seen history tuple
|
|
435 older = None # 'older' is the currently examined history tuple
|
|
436 for p, r in _get_history(_to_svn(self.scope, path), self.authz,
|
|
437 self.fs_ptr, subpool, 0, rev, limit):
|
|
438 older = (_path_within_scope(self.scope, p), r,
|
|
439 Changeset.ADD)
|
|
440 rev = self._previous_rev(r, pool=subpool)
|
|
441 if newer:
|
|
442 if older[0] == path:
|
|
443 # still on the path: 'newer' was an edit
|
|
444 yield newer[0], newer[1], Changeset.EDIT
|
|
445 else:
|
|
446 # the path changed: 'newer' was a copy
|
|
447 rev = self._previous_rev(newer[1], pool=subpool)
|
|
448 # restart before the copy op
|
|
449 yield newer[0], newer[1], Changeset.COPY
|
|
450 older = (older[0], older[1], 'unknown')
|
|
451 break
|
|
452 newer = older
|
|
453 if older:
|
|
454 # either a real ADD or the source of a COPY
|
|
455 yield older
|
|
456 else:
|
|
457 expect_deletion = True
|
|
458 rev = self._previous_rev(rev, pool=subpool)
|
|
459
|
|
460 def get_changes(self, old_path, old_rev, new_path, new_rev,
|
|
461 ignore_ancestry=0):
|
|
462 old_node = new_node = None
|
|
463 old_rev = self.normalize_rev(old_rev)
|
|
464 new_rev = self.normalize_rev(new_rev)
|
|
465 if self.has_node(old_path, old_rev):
|
|
466 old_node = self.get_node(old_path, old_rev)
|
|
467 else:
|
|
468 raise NoSuchNode(old_path, old_rev, 'The Base for Diff is invalid')
|
|
469 if self.has_node(new_path, new_rev):
|
|
470 new_node = self.get_node(new_path, new_rev)
|
|
471 else:
|
|
472 raise NoSuchNode(new_path, new_rev, 'The Target for Diff is invalid')
|
|
473 if new_node.kind != old_node.kind:
|
|
474 raise TracError('Diff mismatch: Base is a %s (%s in revision %s) '
|
|
475 'and Target is a %s (%s in revision %s).' \
|
|
476 % (old_node.kind, old_path, old_rev,
|
|
477 new_node.kind, new_path, new_rev))
|
|
478 subpool = Pool(self.pool)
|
|
479 if new_node.isdir:
|
|
480 editor = DiffChangeEditor()
|
|
481 e_ptr, e_baton = delta.make_editor(editor, subpool())
|
|
482 old_root = fs.revision_root(self.fs_ptr, old_rev, subpool())
|
|
483 new_root = fs.revision_root(self.fs_ptr, new_rev, subpool())
|
|
484 def authz_cb(root, path, pool): return 1
|
|
485 text_deltas = 0 # as this is anyway re-done in Diff.py...
|
|
486 entry_props = 0 # "... typically used only for working copy updates"
|
|
487 repos.svn_repos_dir_delta(old_root,
|
|
488 _to_svn(self.scope, old_path), '',
|
|
489 new_root,
|
|
490 _to_svn(self.scope + new_path),
|
|
491 e_ptr, e_baton, authz_cb,
|
|
492 text_deltas,
|
|
493 1, # directory
|
|
494 entry_props,
|
|
495 ignore_ancestry,
|
|
496 subpool())
|
|
497 for path, kind, change in editor.deltas:
|
|
498 path = _from_svn(path)
|
|
499 old_node = new_node = None
|
|
500 if change != Changeset.ADD:
|
|
501 old_node = self.get_node(posixpath.join(old_path, path),
|
|
502 old_rev)
|
|
503 if change != Changeset.DELETE:
|
|
504 new_node = self.get_node(posixpath.join(new_path, path),
|
|
505 new_rev)
|
|
506 else:
|
|
507 kind = _kindmap[fs.check_path(old_root,
|
|
508 _to_svn(self.scope,
|
|
509 old_node.path),
|
|
510 subpool())]
|
|
511 yield (old_node, new_node, kind, change)
|
|
512 else:
|
|
513 old_root = fs.revision_root(self.fs_ptr, old_rev, subpool())
|
|
514 new_root = fs.revision_root(self.fs_ptr, new_rev, subpool())
|
|
515 if fs.contents_changed(old_root, _to_svn(self.scope, old_path),
|
|
516 new_root, _to_svn(self.scope, new_path),
|
|
517 subpool()):
|
|
518 yield (old_node, new_node, Node.FILE, Changeset.EDIT)
|
|
519
|
|
520
|
|
521 class SubversionNode(Node):
|
|
522
|
|
523 def __init__(self, path, rev, authz, scope, fs_ptr, pool=None):
|
|
524 self.authz = authz
|
|
525 self.scope = scope
|
|
526 self._scoped_svn_path = _to_svn(scope, path)
|
|
527 self.fs_ptr = fs_ptr
|
|
528 self.pool = Pool(pool)
|
|
529 self._requested_rev = rev
|
|
530
|
|
531 self.root = fs.revision_root(fs_ptr, rev, self.pool())
|
|
532 node_type = fs.check_path(self.root, self._scoped_svn_path,
|
|
533 self.pool())
|
|
534 if not node_type in _kindmap:
|
|
535 raise NoSuchNode(path, rev)
|
|
536 cr = fs.node_created_rev(self.root, self._scoped_svn_path, self.pool())
|
|
537 cp = fs.node_created_path(self.root, self._scoped_svn_path, self.pool())
|
|
538 # Note: `cp` differs from `path` if the last change was a copy,
|
|
539 # In that case, `path` doesn't even exist at `cr`.
|
|
540 # The only guarantees are:
|
|
541 # * this node exists at (path,rev)
|
|
542 # * the node existed at (created_path,created_rev)
|
|
543 # Also, `cp` might well be out of the scope of the repository,
|
|
544 # in this case, we _don't_ use the ''create'' information.
|
|
545 if _is_path_within_scope(self.scope, cp):
|
|
546 self.created_rev = cr
|
|
547 self.created_path = _path_within_scope(self.scope, _from_svn(cp))
|
|
548 else:
|
|
549 self.created_rev, self.created_path = rev, path
|
|
550 self.rev = self.created_rev
|
|
551 # TODO: check node id
|
|
552 Node.__init__(self, path, self.rev, _kindmap[node_type])
|
|
553
|
|
554 def get_content(self):
|
|
555 if self.isdir:
|
|
556 return None
|
|
557 s = core.Stream(fs.file_contents(self.root, self._scoped_svn_path,
|
|
558 self.pool()))
|
|
559 # Make sure the stream object references the pool to make sure the pool
|
|
560 # is not destroyed before the stream object.
|
|
561 s._pool = self.pool
|
|
562 return s
|
|
563
|
|
564 def get_entries(self):
|
|
565 if self.isfile:
|
|
566 return
|
|
567 pool = Pool(self.pool)
|
|
568 entries = fs.dir_entries(self.root, self._scoped_svn_path, pool())
|
|
569 for item in entries.keys():
|
|
570 path = posixpath.join(self.path, _from_svn(item))
|
|
571 if not self.authz.has_permission(path):
|
|
572 continue
|
|
573 yield SubversionNode(path, self._requested_rev, self.authz,
|
|
574 self.scope, self.fs_ptr, self.pool)
|
|
575
|
|
576 def get_history(self,limit=None):
|
|
577 newer = None # 'newer' is the previously seen history tuple
|
|
578 older = None # 'older' is the currently examined history tuple
|
|
579 pool = Pool(self.pool)
|
|
580 for path, rev in _get_history(self._scoped_svn_path, self.authz,
|
|
581 self.fs_ptr, pool,
|
|
582 0, self._requested_rev, limit):
|
|
583 path = _path_within_scope(self.scope, path)
|
|
584 if rev > 0 and path:
|
|
585 older = (path, rev, Changeset.ADD)
|
|
586 if newer:
|
|
587 change = newer[0] == older[0] and Changeset.EDIT or \
|
|
588 Changeset.COPY
|
|
589 newer = (newer[0], newer[1], change)
|
|
590 yield newer
|
|
591 newer = older
|
|
592 if newer:
|
|
593 yield newer
|
|
594
|
|
595 # def get_previous(self):
|
|
596 # # FIXME: redo it with fs.node_history
|
|
597
|
|
598 def get_properties(self):
|
|
599 props = fs.node_proplist(self.root, self._scoped_svn_path, self.pool())
|
|
600 for name, value in props.items():
|
|
601 # Note that property values can be arbitrary binary values
|
|
602 # so we can't assume they are UTF-8 strings...
|
|
603 props[_from_svn(name)] = to_unicode(value)
|
|
604 return props
|
|
605
|
|
606 def get_content_length(self):
|
|
607 if self.isdir:
|
|
608 return None
|
|
609 return fs.file_length(self.root, self._scoped_svn_path, self.pool())
|
|
610
|
|
611 def get_content_type(self):
|
|
612 if self.isdir:
|
|
613 return None
|
|
614 return self._get_prop(core.SVN_PROP_MIME_TYPE)
|
|
615
|
|
616 def get_last_modified(self):
|
|
617 date = fs.revision_prop(self.fs_ptr, self.created_rev,
|
|
618 core.SVN_PROP_REVISION_DATE, self.pool())
|
|
619 return core.svn_time_from_cstring(date, self.pool()) / 1000000
|
|
620
|
|
621 def _get_prop(self, name):
|
|
622 return fs.node_prop(self.root, self._scoped_svn_path, name, self.pool())
|
|
623
|
|
624
|
|
625 class SubversionChangeset(Changeset):
|
|
626
|
|
627 def __init__(self, rev, authz, scope, fs_ptr, pool=None):
|
|
628 self.rev = rev
|
|
629 self.authz = authz
|
|
630 self.scope = scope
|
|
631 self.fs_ptr = fs_ptr
|
|
632 self.pool = Pool(pool)
|
|
633 message = self._get_prop(core.SVN_PROP_REVISION_LOG)
|
|
634 author = self._get_prop(core.SVN_PROP_REVISION_AUTHOR)
|
|
635 date = self._get_prop(core.SVN_PROP_REVISION_DATE)
|
|
636 date = core.svn_time_from_cstring(date, self.pool()) / 1000000
|
|
637 Changeset.__init__(self, rev, message, author, date)
|
|
638
|
|
639 def get_changes(self):
|
|
640 pool = Pool(self.pool)
|
|
641 tmp = Pool(pool)
|
|
642 root = fs.revision_root(self.fs_ptr, self.rev, pool())
|
|
643 editor = repos.RevisionChangeCollector(self.fs_ptr, self.rev, pool())
|
|
644 e_ptr, e_baton = delta.make_editor(editor, pool())
|
|
645 repos.svn_repos_replay(root, e_ptr, e_baton, pool())
|
|
646
|
|
647 idx = 0
|
|
648 copies, deletions = {}, {}
|
|
649 changes = []
|
|
650 revroots = {}
|
|
651 for path, change in editor.changes.items():
|
|
652 #assert path == change.path or change.base_path
|
|
653
|
|
654 # Filtering on `path`
|
|
655 if not (_is_path_within_scope(self.scope, path) and \
|
|
656 self.authz.has_permission(path)):
|
|
657 continue
|
|
658
|
|
659 path = change.path
|
|
660 base_path = change.base_path
|
|
661 base_rev = change.base_rev
|
|
662
|
|
663 # Ensure `base_path` is within the scope
|
|
664 if not (_is_path_within_scope(self.scope, base_path) and \
|
|
665 self.authz.has_permission(base_path)):
|
|
666 base_path, base_rev = None, -1
|
|
667
|
|
668 # Determine the action
|
|
669 if not path: # deletion
|
|
670 if base_path:
|
|
671 action = Changeset.DELETE
|
|
672 deletions[base_path] = idx
|
|
673 elif self.scope: # root property change
|
|
674 action = Changeset.EDIT
|
|
675 else: # deletion outside of scope, ignore
|
|
676 continue
|
|
677 elif change.added or not base_path: # add or copy
|
|
678 action = Changeset.ADD
|
|
679 if base_path and base_rev:
|
|
680 action = Changeset.COPY
|
|
681 copies[base_path] = idx
|
|
682 else:
|
|
683 action = Changeset.EDIT
|
|
684 # identify the most interesting base_path/base_rev
|
|
685 # in terms of last changed information (see r2562)
|
|
686 if revroots.has_key(base_rev):
|
|
687 b_root = revroots[base_rev]
|
|
688 else:
|
|
689 b_root = fs.revision_root(self.fs_ptr, base_rev, pool())
|
|
690 revroots[base_rev] = b_root
|
|
691 tmp.clear()
|
|
692 cbase_path = fs.node_created_path(b_root, base_path, tmp())
|
|
693 cbase_rev = fs.node_created_rev(b_root, base_path, tmp())
|
|
694 # give up if the created path is outside the scope
|
|
695 if _is_path_within_scope(self.scope, cbase_path):
|
|
696 base_path, base_rev = cbase_path, cbase_rev
|
|
697
|
|
698 kind = _kindmap[change.item_kind]
|
|
699 path = _path_within_scope(self.scope, _from_svn(path or base_path))
|
|
700 base_path = _path_within_scope(self.scope, _from_svn(base_path))
|
|
701 changes.append([path, kind, action, base_path, base_rev])
|
|
702 idx += 1
|
|
703
|
|
704 moves = []
|
|
705 for k,v in copies.items():
|
|
706 if k in deletions:
|
|
707 changes[v][2] = Changeset.MOVE
|
|
708 moves.append(deletions[k])
|
|
709 offset = 0
|
|
710 moves.sort()
|
|
711 for i in moves:
|
|
712 del changes[i - offset]
|
|
713 offset += 1
|
|
714
|
|
715 changes.sort()
|
|
716 for change in changes:
|
|
717 yield tuple(change)
|
|
718
|
|
719 def _get_prop(self, name):
|
|
720 return fs.revision_prop(self.fs_ptr, self.rev, name, self.pool())
|
|
721
|
|
722
|
|
723 #
|
|
724 # Delta editor for diffs between arbitrary nodes
|
|
725 #
|
|
726 # Note 1: the 'copyfrom_path' and 'copyfrom_rev' information is not used
|
|
727 # because 'repos.svn_repos_dir_delta' *doesn't* provide it.
|
|
728 #
|
|
729 # Note 2: the 'dir_baton' is the path of the parent directory
|
|
730 #
|
|
731
|
|
732 class DiffChangeEditor(delta.Editor):
|
|
733
|
|
734 def __init__(self):
|
|
735 self.deltas = []
|
|
736
|
|
737 # -- svn.delta.Editor callbacks
|
|
738
|
|
739 def open_root(self, base_revision, dir_pool):
|
|
740 return ('/', Changeset.EDIT)
|
|
741
|
|
742 def add_directory(self, path, dir_baton, copyfrom_path, copyfrom_rev,
|
|
743 dir_pool):
|
|
744 self.deltas.append((path, Node.DIRECTORY, Changeset.ADD))
|
|
745 return (path, Changeset.ADD)
|
|
746
|
|
747 def open_directory(self, path, dir_baton, base_revision, dir_pool):
|
|
748 return (path, dir_baton[1])
|
|
749
|
|
750 def change_dir_prop(self, dir_baton, name, value, pool):
|
|
751 path, change = dir_baton
|
|
752 if change != Changeset.ADD:
|
|
753 self.deltas.append((path, Node.DIRECTORY, change))
|
|
754
|
|
755 def delete_entry(self, path, revision, dir_baton, pool):
|
|
756 self.deltas.append((path, None, Changeset.DELETE))
|
|
757
|
|
758 def add_file(self, path, dir_baton, copyfrom_path, copyfrom_revision,
|
|
759 dir_pool):
|
|
760 self.deltas.append((path, Node.FILE, Changeset.ADD))
|
|
761
|
|
762 def open_file(self, path, dir_baton, dummy_rev, file_pool):
|
|
763 self.deltas.append((path, Node.FILE, Changeset.EDIT))
|
|
764
|