46
from datetime import datetime
48
from genshi.builder import tag
50
from trac.config import ListOption
47
51
from trac.core import *
48
52
from trac.versioncontrol import Changeset, Node, Repository, \
49
53
IRepositoryConnector, \
50
54
NoSuchChangeset, NoSuchNode
51
55
from trac.versioncontrol.cache import CachedRepository
52
56
from trac.versioncontrol.svn_authz import SubversionAuthorizer
57
from trac.versioncontrol.web_ui.browser import IPropertyRenderer
58
from trac.util import sorted, embedded_numbers, reversed
53
59
from trac.util.text import to_unicode
60
from trac.util.translation import _
61
from trac.util.datefmt import utc
64
application_pool = None
68
global fs, repos, core, delta, _kindmap
56
69
from svn import fs, repos, core, delta
59
has_subversion = False
60
class dummy_svn(object):
63
def apr_pool_destroy(): pass
64
def apr_terminate(): pass
65
def apr_pool_clear(): pass
67
delta = core = dummy_svn()
70
_kindmap = {core.svn_node_dir: Node.DIRECTORY,
71
core.svn_node_file: Node.FILE}
74
application_pool = None
70
_kindmap = {core.svn_node_dir: Node.DIRECTORY,
71
core.svn_node_file: Node.FILE}
72
# Protect svn.core methods from GC
73
Pool.apr_pool_clear = staticmethod(core.apr_pool_clear)
74
Pool.apr_terminate = staticmethod(core.apr_terminate)
75
Pool.apr_pool_destroy = staticmethod(core.apr_pool_destroy)
76
77
def _to_svn(*args):
77
78
"""Expect a list of `unicode` path components.
78
Returns an UTF-8 encoded string suitable for the Subversion python bindings.
80
Returns an UTF-8 encoded string suitable for the Subversion python bindings
81
(the returned path never starts with a leading "/")
80
return '/'.join([path.strip('/') for path in args]).encode('utf-8')
83
return '/'.join([p for p in [p.strip('/') for p in args] if p]) \
82
86
def _from_svn(path):
83
"""Expect an UTF-8 encoded string and transform it to an `unicode` object"""
84
return path and path.decode('utf-8')
87
"""Expect an UTF-8 encoded string and transform it to an `unicode` object
88
But Subversion repositories built from conversion utilities can have
89
non-UTF-8, so we have to handle it.
91
return path and to_unicode(path, 'utf-8')
86
93
def _normalize_path(path):
87
94
"""Remove leading "/", except for the root."""
210
228
del self._weakref
213
# Initialize application-level pool
218
231
class SubversionConnector(Component):
220
233
implements(IRepositoryConnector)
235
branches = ListOption('svn', 'branches', 'trunk,branches/*', doc=
236
"""List of paths categorized as ''branches''.
237
If a path ends with '*', then all the directory entries found
238
below that path will be included.
241
tags = ListOption('svn', 'tags', 'tags/*', doc=
242
"""List of paths categorized as ''tags''.
243
If a path ends with '*', then all the directory entries found
244
below that path will be included.
252
self.log.debug('Subversion bindings imported')
254
self.log.info('Failed to load Subversion bindings', exc_info=True)
255
self.has_subversion = False
257
self.has_subversion = True
222
260
def get_supported_types(self):
223
global has_subversion
261
if self.has_subversion:
262
yield ("direct-svnfs", 4)
225
263
yield ("svnfs", 4)
228
266
def get_repository(self, type, dir, authname):
229
267
"""Return a `SubversionRepository`.
231
The repository is generally wrapped in a `CachedRepository`,
232
unless `direct-svn-fs` is the specified type.
269
The repository is wrapped in a `CachedRepository`, unless `type` is
234
repos = SubversionRepository(dir, None, self.log)
235
crepos = CachedRepository(self.env.get_db_cnx(), repos, None, self.log)
272
if not self._version:
273
self._version = self._get_version()
274
self.env.systeminfo.append(('Subversion', self._version))
275
fs_repos = SubversionRepository(dir, None, self.log,
277
'branches': self.branches})
278
if type == 'direct-svnfs':
281
repos = CachedRepository(self.env.get_db_cnx(), fs_repos, None,
283
repos.has_linear_changesets = True
237
authz = SubversionAuthorizer(self.env, crepos, authname)
238
repos.authz = crepos.authz = authz
285
authz = SubversionAuthorizer(self.env, weakref.proxy(repos),
287
repos.authz = fs_repos.authz = authz
290
def _get_version(self):
291
version = (core.SVN_VER_MAJOR, core.SVN_VER_MINOR, core.SVN_VER_MICRO)
292
version_string = '%d.%d.%d' % version + core.SVN_VER_TAG
294
raise TracError(_("Subversion >= 1.0 required: Found %(version)s",
295
version=version_string))
296
return version_string
299
class SubversionPropertyRenderer(Component):
300
implements(IPropertyRenderer)
303
self._externals_map = {}
305
# IPropertyRenderer methods
307
def match_property(self, name, mode):
308
return name in ('svn:externals', 'svn:needs-lock') and 4 or 0
310
def render_property(self, name, mode, context, props):
311
if name == 'svn:externals':
312
return self._render_externals(props[name])
313
elif name == 'svn:needs-lock':
314
return self._render_needslock(context)
316
def _render_externals(self, prop):
317
if not self._externals_map:
318
for dummykey, value in self.config.options('svn:externals'):
319
value = value.split()
321
self.env.warn("svn:externals entry %s doesn't contain "
322
"a space-separated key value pair, skipping.",
326
self._externals_map[key] = value.replace('%', '%%') \
327
.replace('$path', '%(path)s') \
328
.replace('$rev', '%(rev)s')
330
for external in prop.splitlines():
331
elements = external.split()
334
localpath, rev, url = elements[0], '', elements[-1]
335
if localpath.startswith('#'):
336
externals.append((external, None, None, None, None))
338
if len(elements) == 3:
340
rev = rev.replace('-r', '')
341
# retrieve a matching entry in the externals map
345
if base_url in self._externals_map or base_url==u'/':
347
base_url, pref = posixpath.split(base_url)
349
href = self._externals_map.get(base_url)
350
revstr = rev and ' at revision '+rev or ''
351
if not href and (url.startswith('http://') or
352
url.startswith('https://')):
353
href = url.replace('%', '%%')
355
remotepath = posixpath.join(*reversed(prefix))
356
externals.append((localpath, revstr, base_url, remotepath,
357
href % {'path': remotepath, 'rev': rev}))
359
externals.append((localpath, revstr, url, None, None))
361
for localpath, rev, url, remotepath, href in externals:
369
title = ''.join((remotepath, rev, url))
371
title = _('No svn:externals configured in trac.ini')
372
externals_data.append((label, href, title))
373
return tag.ul([tag.li(tag.a(label, href=href, title=title))
374
for label, href, title in externals_data])
376
def _render_needslock(self, context):
377
return tag.img(src=context.href.chrome('common/lock-locked.png'),
378
alt="needs lock", title="needs lock")
243
381
class SubversionRepository(Repository):
245
Repository implementation based on the svn.fs API.
382
"""Repository implementation based on the svn.fs API."""
248
def __init__(self, path, authz, log):
249
self.path = path # might be needed by __del__()/close()
384
def __init__(self, path, authz, log, options={}):
251
if core.SVN_VER_MAJOR < 1:
252
raise TracError("Subversion >= 1.0 required: Found %d.%d.%d" % \
386
self.options = options
256
387
self.pool = Pool()
258
389
# Remove any trailing slash or else subversion might abort
302
433
return _normalize_path(path)
304
435
def normalize_rev(self, rev):
307
except (ValueError, TypeError):
310
rev = self.youngest_rev
311
elif rev > self.youngest_rev:
436
if rev is None or isinstance(rev, basestring) and \
437
rev.lower() in ('', 'head', 'latest', 'youngest'):
438
return self.youngest_rev
442
if rev <= self.youngest_rev:
444
except (ValueError, TypeError):
312
446
raise NoSuchChangeset(rev)
449
self.repos = self.fs_ptr = self.pool = None
451
def _get_tags_or_branches(self, paths):
452
"""Retrieve known branches or tags."""
453
for path in self.options.get(paths, []):
454
if path.endswith('*'):
455
folder = posixpath.dirname(path)
457
entries = [n for n in self.get_node(folder).get_entries()]
458
for node in sorted(entries, key=lambda n:
459
embedded_numbers(n.path.lower())):
460
if node.kind == Node.DIRECTORY:
462
except: # no right (TODO: should use a specific Exception here)
466
yield self.get_node(path)
470
def get_quickjump_entries(self, rev):
471
"""Retrieve known branches, as (name, id) pairs.
473
Purposedly ignores `rev` and always takes the last revision.
475
for n in self._get_tags_or_branches('branches'):
476
yield 'branches', n.path, n.path, None
477
for n in self._get_tags_or_branches('tags'):
478
yield 'tags', n.path, n.created_path, n.created_rev
320
480
def get_changeset(self, rev):
321
481
rev = self.normalize_rev(rev)
473
646
if self.has_node(new_path, new_rev):
474
647
new_node = self.get_node(new_path, new_rev)
476
raise NoSuchNode(new_path, new_rev, 'The Target for Diff is invalid')
649
raise NoSuchNode(new_path, new_rev,
650
'The Target for Diff is invalid')
477
651
if new_node.kind != old_node.kind:
478
raise TracError('Diff mismatch: Base is a %s (%s in revision %s) '
479
'and Target is a %s (%s in revision %s).' \
480
% (old_node.kind, old_path, old_rev,
481
new_node.kind, new_path, new_rev))
652
raise TracError(_('Diff mismatch: Base is a %(oldnode)s '
653
'(%(oldpath)s in revision %(oldrev)s) and '
654
'Target is a %(newnode)s (%(newpath)s in '
655
'revision %(newrev)s).', oldnode=old_node.kind,
656
oldpath=old_path, oldrev=old_rev,
657
newnode=new_node.kind, newpath=new_path,
482
659
subpool = Pool(self.pool)
483
660
if new_node.isdir:
484
661
editor = DiffChangeEditor()
532
709
self._scoped_svn_path = _to_svn(self.scope, path)
533
710
self.pool = Pool(pool)
534
711
self._requested_rev = rev
536
self.root = fs.revision_root(self.fs_ptr, rev, self.pool())
537
node_type = fs.check_path(self.root, self._scoped_svn_path,
714
if parent and parent._requested_rev == self._requested_rev:
715
self.root = parent.root
717
self.root = fs.revision_root(self.fs_ptr, rev, self.pool())
718
node_type = fs.check_path(self.root, self._scoped_svn_path, pool)
539
719
if not node_type in _kindmap:
540
720
raise NoSuchNode(path, rev)
541
cr = fs.node_created_rev(self.root, self._scoped_svn_path, self.pool())
542
cp = fs.node_created_path(self.root, self._scoped_svn_path, self.pool())
721
cr = fs.node_created_rev(self.root, self._scoped_svn_path, pool)
722
cp = fs.node_created_path(self.root, self._scoped_svn_path, pool)
543
723
# Note: `cp` differs from `path` if the last change was a copy,
544
724
# In that case, `path` doesn't even exist at `cr`.
545
725
# The only guarantees are:
650
853
# we _hope_ it's UTF-8, but can't be 100% sure (#4321)
651
854
message = message and to_unicode(message, 'utf-8')
652
855
author = author and to_unicode(author, 'utf-8')
653
date = self._get_prop(core.SVN_PROP_REVISION_DATE)
655
date = core.svn_time_from_cstring(date, self.pool()) / 1000000
856
_date = self._get_prop(core.SVN_PROP_REVISION_DATE)
858
ts = core.svn_time_from_cstring(_date, self.pool()) / 1000000
859
date = datetime.fromtimestamp(ts, utc)
658
862
Changeset.__init__(self, rev, message, author, date)
864
def get_properties(self):
865
props = fs.revision_proplist(self.fs_ptr, self.rev, self.pool())
867
for k,v in props.iteritems():
868
if k not in (core.SVN_PROP_REVISION_LOG,
869
core.SVN_PROP_REVISION_AUTHOR,
870
core.SVN_PROP_REVISION_DATE):
871
properties[k] = to_unicode(v)
872
# Note: the above `to_unicode` has a small probability
873
# to mess-up binary properties, like icons.
660
876
def get_changes(self):
661
877
pool = Pool(self.pool)
751
967
# Note 2: the 'dir_baton' is the path of the parent directory
754
class DiffChangeEditor(delta.Editor):
971
def DiffChangeEditor():
973
class DiffChangeEditor(delta.Editor):
759
# -- svn.delta.Editor callbacks
761
def open_root(self, base_revision, dir_pool):
762
return ('/', Changeset.EDIT)
764
def add_directory(self, path, dir_baton, copyfrom_path, copyfrom_rev,
766
self.deltas.append((path, Node.DIRECTORY, Changeset.ADD))
767
return (path, Changeset.ADD)
769
def open_directory(self, path, dir_baton, base_revision, dir_pool):
770
return (path, dir_baton[1])
772
def change_dir_prop(self, dir_baton, name, value, pool):
773
path, change = dir_baton
774
if change != Changeset.ADD:
775
self.deltas.append((path, Node.DIRECTORY, change))
777
def delete_entry(self, path, revision, dir_baton, pool):
778
self.deltas.append((path, None, Changeset.DELETE))
780
def add_file(self, path, dir_baton, copyfrom_path, copyfrom_revision,
782
self.deltas.append((path, Node.FILE, Changeset.ADD))
784
def open_file(self, path, dir_baton, dummy_rev, file_pool):
785
self.deltas.append((path, Node.FILE, Changeset.EDIT))
978
# -- svn.delta.Editor callbacks
980
def open_root(self, base_revision, dir_pool):
981
return ('/', Changeset.EDIT)
983
def add_directory(self, path, dir_baton, copyfrom_path, copyfrom_rev,
985
self.deltas.append((path, Node.DIRECTORY, Changeset.ADD))
986
return (path, Changeset.ADD)
988
def open_directory(self, path, dir_baton, base_revision, dir_pool):
989
return (path, dir_baton[1])
991
def change_dir_prop(self, dir_baton, name, value, pool):
992
path, change = dir_baton
993
if change != Changeset.ADD:
994
self.deltas.append((path, Node.DIRECTORY, change))
996
def delete_entry(self, path, revision, dir_baton, pool):
997
self.deltas.append((path, None, Changeset.DELETE))
999
def add_file(self, path, dir_baton, copyfrom_path, copyfrom_revision,
1001
self.deltas.append((path, Node.FILE, Changeset.ADD))
1003
def open_file(self, path, dir_baton, dummy_rev, file_pool):
1004
self.deltas.append((path, Node.FILE, Changeset.EDIT))
1006
return DiffChangeEditor()