42
from trac import versioncontrol, core, mimeview, wiki
43
from trac import versioncontrol, core, config, mimeview, wiki
43
44
from trac.web.chrome import Chrome
44
45
from trac.util.html import html, Markup
71
73
from bzrlib.revision import (
78
config_section = 'tracbzr'
77
80
class BzrConnector(core.Component):
78
"""The necessary glue between our repository and trac."""
80
core.implements(versioncontrol.IRepositoryConnector,
81
wiki.IWikiMacroProvider,
85
# IRepositoryConnector
81
"""The necessary glue between our set of bzr branches and trac."""
83
core.implements(versioncontrol.IRepositoryConnector)
86
if hasattr(self.env, 'systeminfo'):
87
bzr_version = '%i.%i.%i' % bzrlib.api.get_current_api_version(None)
88
self.env.systeminfo.append(('Bazaar', bzr_version))
87
90
def get_supported_types(self):
88
91
"""Support for `repository_type = bzr`"""
90
yield ('bzr+debug', 8)
92
94
def get_repository(self, repos_type, repos_dir, authname):
93
95
"""Return a `BzrRepository`"""
94
96
assert repos_type == 'bzr'
95
return BzrRepository(repos_dir, self.log)
97
return BzrRepository(repos_dir, self)
99
class BzrWikiMacros(core.Component):
100
"""Component for macros related to bzr."""
102
core.implements(wiki.IWikiMacroProvider)
99
104
def get_macros(self):
118
123
if not isinstance(repo, BzrRepository):
119
124
raise core.TracError('Configured repository is not a bzr repository')
121
branches = sorted(repo._get_branches())
126
branches = repo.get_branches()
123
128
for loc, target in branches:
124
129
revid = target.last_revision()
151
class BzrPropertyRenderer(core.Component):
152
"""Renderer for bzr-specific properties."""
154
core.implements(IPropertyRenderer)
148
156
def match_property(self, name, mode):
149
157
if name == 'parents' and mode == 'revprop':
206
215
"""Present a bzr branch as a trac repository."""
208
def __init__(self, location, log):
209
versioncontrol.Repository.__init__(self, location, None, log)
217
primary_branches = config.ListOption(
218
config_section, 'primary_branches', ',trunk', keep_empty = True,
219
doc = """Ordered list of primary branches.
221
These will be listed first in the Branches macro. When viewing
222
the timeline, each changeset will be associated with the first
223
primary branch that contains it. The value is a comma
224
separated list of globs, as used by the fnmatch module. An
225
empty list element can be used to denote the branch at the
226
root of the repository.
228
Defaults to ',trunk'.""")
230
def __init__(self, location, component):
231
versioncontrol.Repository.__init__(self, location, None, component.log)
232
self.component = component
233
self.config = component.config
210
234
self.root_transport = transport.get_transport(location)
211
235
self._tree_cache = {}
212
236
self._locked_branches = LockedBranches()
235
259
branch is None or a bzr branch.
237
261
if branch is None:
238
# No "safe" chars (make sure "/" is escaped)
239
return self._escape(revid, ':')
262
return self._escape_revid(revid)
240
263
relpath = self.branch_path(branch)
242
265
return self._string_rev_revno(relpath, branch.revision_id_to_revno(revid))
244
267
dotted = self.dotted_revno(branch, revid)
245
268
if dotted is not None:
246
269
return self._string_rev_revno(relpath, dotted)
247
if revid in branch.repository.get_ancestry(branch.last_revision()):
248
271
return self._string_rev_revid(relpath, revid)
250
return self._escape(revid, ':')
253
def _escape(string, safe=''):
254
return urllib.quote(string, safe)
257
def _string_rev_revid(relpath, revid):
258
branch_name = urllib.quote(relpath, '')
259
revid = urllib.quote(revid, '')
274
def _escape_revid(revid):
275
return urllib.quote(revid, ':')
278
def _unescape_revid(revid):
279
return urllib.unquote(revid)
282
def _escape_branch(relpath):
283
if '%' in relpath or ',' in relpath:
284
relpath = urllib.quote(relpath, '')
286
relpath = relpath.replace('/', ',')
290
def _unescape_branch(relpath):
292
relpath = urllib.unquote(relpath)
294
relpath = relpath.replace(',', '/')
298
def _string_rev_revid(cls, relpath, revid):
299
branch_name = cls._escape_branch(relpath)
300
revid = cls._escape_revid(revid)
261
302
return '%s,%s' % (branch_name, revid)
263
304
return '%s' % revid
266
def _string_rev_revno(relpath, revno):
267
branch_name = urllib.quote(relpath, ':')
268
if branch_name != "":
307
def _string_rev_revno(cls, relpath, revno):
308
branch_name = cls._escape_branch(relpath)
269
310
return '%s,%s' % (branch_name, revno)
271
312
return str(revno)
283
324
(currently revid is assumed to be in the branch ancestry!)
284
325
- "spork," is latest revision in the spork branch.
285
326
- "spork,1.2.3" is either revno or revid 1.2.3 in the spork branch.
327
- "foo,bar,123" is revno 123 in the foo/bar branch.
287
Branch paths and revids are urlencoded.
329
Branch paths have / replaced by , and existing , escaped by doubling.
330
Revids are urlencoded.
290
333
# Make sure our rev is a string
295
338
if len(split) == 1:
298
elif len(split) == 2:
299
rev_branch, rev_rev = split
341
elif len(split) >= 2:
342
rev_branch = self._unescape_branch(','.join(split[:-1]))
301
345
raise versioncontrol.NoSuchChangeset(rev)
303
347
# unquote revision part, and treat special cases of current: and null:
304
rev_rev = urllib.unquote(rev_rev)
348
rev_rev = self._unescape_revid(rev_rev)
305
349
if len(split) == 1 and rev_rev in (CURRENT_REVISION, NULL_REVISION):
306
350
return None, rev_rev
341
385
if location in self._branch_cache:
342
386
return self._branch_cache[location].branch
343
387
trans = self.root_transport
344
for piece in urllib.unquote(location).split('/'):
388
for piece in location.split('/'):
346
390
trans = trans.clone(piece)
347
391
target_bzrdir = bzrdir.BzrDir.open_from_transport(trans)
348
392
branch = target_bzrdir.open_branch()
350
393
self._locked_branches.append(branch)
351
394
self._branch_cache[location] = BranchCache(self, branch)
356
399
real_location = location[:-len(relpath)].rstrip('/')
357
400
if real_location not in self._branch_cache:
358
401
self._branch_cache[real_location] = BranchCache(self, branch)
360
402
self._locked_branches.append(branch)
361
403
# return the cached version, possibly throwing away the one we just
363
405
return self._branch_cache[real_location].branch, relpath
407
def get_branches(self):
408
"""Get an ordered list of all branches in the repository.
410
Returns a list of (relpath, branch) pairs. Branches will be
411
locked for reading. Branches inside other branches will not be
413
primary = self.primary_branches + ['*']
415
for loc, branch in self._get_branches():
416
for index, pattern in enumerate(primary):
417
if fnmatch.fnmatch(loc, pattern):
418
branches.append((index, loc, branch))
421
for index, loc, branch in branches:
422
self._locked_branches.append(branch)
423
return [(loc, branch) for index, loc, branch in branches]
365
425
def _get_branches(self, trans=None, loc=()):
366
426
"""Find branches under a listable transport.
396
456
except errors.NoSuchRevision, e:
397
457
raise versioncontrol.NoSuchChangeset(rev)
399
# TODO: get_changesets?
459
def get_changesets(self, start, stop):
460
"""Retrieve all changesets in a cetain time span."""
462
rhss = [] # right hand side revids, one iterable for each branch
464
branches = self.get_branches()
465
def walk(cache, revid):
466
rhs = [] # all right hand side revids we encounter
467
while revid not in seen:
469
revision = cache.get_revision(revid)
470
time = trac_timestamp(revision.timestamp)
474
chgset = BzrChangeset(self, branch, revid, self.log)
475
inrange.append((time, revid, chgset))
476
parents = revision.parent_ids
480
rhs.extend(parents[1:])
483
for relpath, branch in branches:
484
cache = self.get_branch_cache(branch)
485
revid = branch.last_revision()
486
rhs = walk(cache, revid)
488
for (relpath, branch), rhs in zip(branches, rhss):
489
cache = self.get_branch_cache(branch)
492
new_rhs = walk(cache, revid)
496
return [chgset for time, revid, chgset in inrange]
401
498
def has_node(self, path, rev=None):
402
499
"""Return a boolean indicating if the node is present in a rev."""
411
508
"""Return a Node object or raise NoSuchNode or NoSuchChangeset."""
412
509
path = self.normalize_path(path)
414
rev = self._escape(CURRENT_REVISION)
511
rev = self._escape_revid(CURRENT_REVISION)
415
512
revbranch, revid = self._parse_rev(rev)
417
514
branch, relpath = self.get_containing_branch(path)
418
515
except errors.NotBranchError:
419
516
if not self.root_transport.has(path):
420
517
raise versioncontrol.NoSuchNode(path, rev)
421
return UnversionedDirNode(self, path)
518
return UnversionedDirNode(self, revbranch, revid, path)
422
519
if revbranch is None:
423
520
revbranch = branch
469
566
revisions = sorted(revision_set, key=lambda x: x.timestamp)
470
567
return [(r.revision_id, branches[r.revision_id]) for r in revisions]
472
def previous_rev(self, rev):
569
def previous_rev(self, rev, path=''):
571
prev = self.get_node(path, rev).get_previous()
473
575
branch, revid = self._parse_rev(rev)
474
if revid == NULL_REVISION:
576
if revid in (NULL_REVISION, CURRENT_REVISION) or branch is None:
476
if self._previous is None:
477
if self._history is None:
478
self._history = self._repo_history()
481
for rev, branch in reversed(self._history):
483
raise repr(self._history)
485
self._previous[last] = (branch, rev)
487
if revid == CURRENT_REVISION:
488
return self.string_rev(self._history[-1][1], self._history[-1][0])
578
ancestry = iter(branch.repository.iter_reverse_revision_history(revid))
490
return self.string_rev(*self._previous[revid])
581
return self.string_rev(branch, ancestry.next())
582
except StopIteration:
583
return self.string_rev(branch, NULL_REVISION)
494
585
def next_rev(self, rev, path=''):
495
586
# TODO path is ignored.
497
588
if revid == CURRENT_REVISION:
499
590
if revid == NULL_REVISION:
500
return CURRENT_REVISION
592
return CURRENT_REVISION
501
594
if branch is None:
502
ancestry = self.repo.get_ancestry(self.branch.last_revision())
504
ancestry = branch.repository.get_ancestry(branch.last_revision())
506
idx = ancestry.index(revid)
508
# XXX this revision is not in the branch ancestry. Now what?
511
next_revid = ancestry[idx + 1]
513
# There is no next rev. Now what?
597
revno = branch.revision_id_to_revno(revid)
598
except errors.NoSuchRevision:
599
return None # non-mainline branches don't have a future (yet).
601
next_revid = branch.get_rev_id(revno + 1)
602
except errors.NoSuchRevision:
603
return None # this was the last revision on that branch
515
604
return self.string_rev(branch, next_revid)
517
606
def rev_older_than(self, rev1, rev2):
520
609
branch1, rrev1 = self._parse_rev(rev1)
521
610
branch2, rrev2 = self._parse_rev(rev2)
522
if rrev2 == CURRENT_REVISION:
524
first_before_second = rrev1 in branch2.repository.get_ancestry(rrev2)
525
second_before_first = rrev2 in branch1.repository.get_ancestry(rrev1)
526
if first_before_second and second_before_first:
527
raise core.TracError('%s and %s precede each other?' %
529
if first_before_second:
531
if second_before_first:
613
both = frozenset([rrev1, rrev2])
614
if NULL_REVISION in both:
615
return NULL_REVISION == rrev1
616
if CURRENT_REVISION in both:
617
return CURRENT_REVISION == rrev2
619
if branch1.repository.has_revisions(both) == both:
620
heads = branch1.repository.get_graph().heads(both)
621
elif branch2.repository.has_revisions(both) == both:
622
heads = branch2.repository.get_graph().heads(both)
626
return rrev2 in heads
533
627
# Bah, unrelated revisions. Fall back to comparing timestamps.
534
628
return (branch1.repository.get_revision(rrev1).timestamp <
535
629
branch2.repository.get_revision(rrev2).timestamp)
572
666
def short_rev(self, rev):
573
667
"""Attempt to shorten a rev.
575
This returns the revno if there is one, otherwise returns a
576
"nearby" revno with a ~ prefix.
669
This returns the last 6 characters of the dotted revno.
578
671
The result of this method is used above the line number
579
672
columns in the diff/changeset viewer. There is *very* little
580
room there. Our ~revno results are actually a little too wide
581
already. Tricks like using the branch nick simply do not fit.
673
room there. Tricks like using the branch nick simply do not fit.
675
Those 6 chars seem to fit, at least if two of them are dots,
676
and should be enough to clearly identify almost any two
677
revisions named in a changeset.
583
679
branch, revid = self._parse_rev(rev)
584
680
if branch is None:
682
dotted = self.dotted_revno(branch, revid)
586
history = branch.revision_history()
587
# First try if it is a revno.
589
return str(history.index(revid) + 1)
591
# Get the closest thing that *is* a revno.
592
ancestry = branch.repository.get_ancestry(revid)
593
# We've already tried the current one.
595
for ancestor in reversed(ancestry):
597
return '~%s' % (history.index(ancestor) + 1,)
600
# XXX unrelated branch. Now what?
603
687
def get_changes(self, old_path, old_rev, new_path, new_rev,
604
688
ignore_ancestry=1):
677
761
class UnversionedDirNode(BzrNode):
678
def __init__(self, bzr_repo, path):
679
rev_string = urllib.quote(CURRENT_REVISION)
762
def __init__(self, bzr_repo, branch, revid, path):
763
rev_string = BzrRepository._escape_revid(CURRENT_REVISION)
680
764
BzrNode.__init__(self, path, rev_string, versioncontrol.Node.DIRECTORY)
681
765
self.transport = bzr_repo.root_transport.clone(path)
682
766
self.bzr_repo = bzr_repo
685
771
def __repr__(self):
702
788
branch = self.bzr_repo.get_branch(child_path)
703
789
except errors.NotBranchError:
704
result.append(UnversionedDirNode(self.bzr_repo, child_path))
790
result.append(UnversionedDirNode(self.bzr_repo, self.branch,
791
self.revid, child_path))
706
793
tree = branch.basis_tree()
707
794
if tree.inventory.root:
708
795
node = BzrDirNode(self.bzr_repo, branch, tree,
709
796
tree.inventory.root, child_path)
711
node = UnversionedDirNode(self.bzr_repo, child_path)
798
node = UnversionedDirNode(self.bzr_repo, self.branch,
799
self.revid, child_path)
712
800
result.append(node)
722
810
return 'application/octet-stream'
724
812
def get_history(self, limit=None):
725
return [(self.path, BzrRepository._escape(CURRENT_REVISION), 'add')]
813
if self.branch is not None:
814
bpath = self.bzr_repo.branch_path(self.branch)
815
if self.path == '' or bpath.startswith(self.path + '/'):
816
repo = self.branch.repository
817
ancestry = repo.iter_reverse_revision_history(self.revid)
818
ancestry = iter(ancestry)
819
for rev_id in ancestry:
821
self.bzr_repo.string_rev(self.branch, rev_id),
822
versioncontrol.Changeset.EDIT)
824
self.bzr_repo.string_rev(self.branch, NULL_REVISION),
825
versioncontrol.Changeset.ADD)
827
yield (self.path, BzrRepository._escape_revid(CURRENT_REVISION), 'add')
728
831
class BzrVersionedNode(BzrNode):