1
# -*- coding: iso-8859-1 -*-
3
# Copyright (C) 2005 Edgewall Software
4
# Copyright (C) 2005-2006 Christian Boos <cboos@neuf.fr>
5
# Copyright (C) 2005 Johan Rydberg <jrydberg@gnu.org>
6
# Copyright (C) 2006 Yann Hodique <hodique@lifl.fr>
7
# Copyright (C) 2006 Lukas Lalinsky <lalinsky@gmail.com>
8
# Copyright (C) 2006 Marien Zwart <marienz@gentoo.org>
11
# This software may be used and distributed according to the terms
12
# of the GNU General Public License, incorporated herein by reference.
14
# Author: Yann Hodique <hodique@lifl.fr>
17
"""Bazaar-ng backend for trac's versioncontrol."""
22
from itertools import izip
26
from trac import versioncontrol, core, mimeview, wiki
27
from trac.util.html import html, Markup
30
branch as bzrlib_branch,
41
class BzrConnector(core.Component):
43
"""The necessary glue between our repository and trac."""
45
core.implements(versioncontrol.IRepositoryConnector,
46
wiki.IWikiMacroProvider)
48
# IRepositoryConnector
50
def get_supported_types(self):
51
"""Support for `repository_type = bzr`"""
53
yield ('bzr+debug', 8)
55
def get_repository(self, repos_type, repos_dir, authname):
56
"""Return a `BzrRepository`"""
57
assert repos_type in ('bzr', 'bzr+debug')
58
if repos_type == 'bzr+debug':
59
# HACK: this causes logging to be applied to all BzrRepositories,
60
# BzrChangesets, and BzrNodes
61
BzrRepository.__getattribute__ = getattribute
62
BzrChangeset.__getattribute__ = getattribute
63
BzrNode.__getattribute__ = getattribute
64
return BzrRepository(repos_dir, self.log)
71
def get_macro_description(self, name):
72
assert name == 'Branches'
73
return 'Render a list of available branches.'
75
def render_macro(self, req, name, content):
76
assert name == 'Branches'
77
# This is pretty braindead but adding an option for this is too.
78
manager = versioncontrol.RepositoryManager(self.env)
79
if manager.repository_type != 'bzr':
80
raise core.TracError('Configured repo is not a bzr repo')
81
temp_branch = bzrlib_branch.Branch.open(manager.repository_dir)
82
trans = temp_branch.repository.bzrdir.root_transport
83
branches = sorted(self._get_branches(trans))
84
# Slight hack. We know all these branches are in the same
85
# repo, so we can read lock that once.
86
repo = bzrdir.BzrDir.open_from_transport(trans).open_repository()
89
return html.TABLE(class_='listing')(
91
html.TH('Path'), html.TH('Nick'),
92
html.TH('Last Change'))),
95
html.TD(html.A(loc, href=req.href.browser(
96
rev=':%s' % (urllib.quote(loc, ''),
100
datetime.datetime.fromtimestamp(
102
target.last_revision()).timestamp
105
for loc, target in branches]))
110
class BzrRepository(versioncontrol.Repository):
112
"""Present a bzr branch as a trac repository."""
114
def __init__(self, location, log):
115
versioncontrol.Repository.__init__(self, location, None, log)
116
self.root_transport = transport.get_transport(location)
117
self._tree_cache = {}
118
self._locked_branches = []
119
self._branch_cache = {}
121
self._previous = None
122
self._revision_cache = {}
125
return 'BzrRepository(%r)' % self.root_transport.base
127
def branch_path(self, branch):
128
"""Determine the relative path to a branch from the root"""
129
repo_path = self.root_transport.base
130
branch_path = branch.bzrdir.root_transport.base
131
if branch_path.startswith(repo_path):
132
return branch_path[len(repo_path):].rstrip('/')
134
repo_path = osutil.normalizepath(repo_path)
135
branch_path = osutil.normalizepath(branch_path)
136
return osutils.relpath(repo_path, branch_path)
138
def string_rev(self, branch, revid):
139
"""Create a trac rev string.
141
branch is None or a bzr branch.
144
# No "safe" chars (make sure "/" is escaped)
145
return self._escape(revid)
146
relpath = self.branch_path(branch)
148
return '%s,%s' % (urllib.quote(relpath, ':'),
149
branch.revision_id_to_revno(revid))
150
except errors.NoSuchRevision:
151
dotted = self.dotted_revno(branch, revid)
152
if dotted is not None:
153
return '%s,%s' % (urllib.quote(relpath, ':'), dotted)
154
if revid in branch.repository.get_ancestry(branch.last_revision()):
155
return '%s,%s' % (urllib.quote(relpath, ''),
156
self._escape(revid, ':'))
158
return self._escape(revid, ':')
162
return urllib.quote(string, '')
165
def _string_rev_revid(relpath, revid):
166
return '%s,%s' % (urllib.quote(relpath, ''), urllib.quote(revid, ''))
168
def _parse_rev(self, rev):
169
"""Translate a trac rev string into a (branch, revid) tuple.
171
branch is None or a bzr branch object.
174
- "spork,123" is revno 123 in the spork branch.
175
- "spork,revid" is a revid in the spork branch.
176
(currently revid is assumed to be in the branch ancestry!)
178
Branch paths and revids are urlencoded.
180
# Try integer revno to revid conversion.
182
raise versioncontrol.NoSuchChangeset(rev)
184
# Try path-to-branch-in-repo.
186
split = rev.split(',')
188
raise versioncontrol.NoSuchChangeset(rev)
189
rev_branch,rev_rev = split
191
branch = self.get_branch(rev_branch)
192
except errors.NotBranchError:
193
raise versioncontrol.NoSuchChangeset(rev)
196
if rev_rev.isdigit():
198
revid = branch.get_rev_id(int(rev_rev))
199
except errors.NoSuchRevision:
200
raise versioncontrol.NoSuchChangeset(rev)
202
dotted = rev_rev.split('.')
203
for segment in dotted:
204
if not segment.isdigit():
205
revid = urllib.unquote(rev_rev)
208
cache = self.get_branch_cache(branch)
209
revid = cache.revid_from_dotted(rev_rev)
212
revid = urllib.unquote(rev_rev)
214
revid = branch.last_revision()
219
revid = urllib.unquote(rev)
220
if revid in ('current:', 'null:'):
223
if self.repo.has_revision(revid):
226
# Unsupported format.
227
raise versioncontrol.NoSuchChangeset(rev)
230
# XXX Eeeeeww. Unfortunately for us trac does not actually call the
231
# close method. So we do this. Quite silly, since bzr does the same
232
# thing (printing a warning...)
238
"""Release our branches. Trac does not *have* to call this!"""
239
for branch in self._locked_branches:
242
def get_branch(self, location):
243
if location in self._branch_cache:
244
return self._branch_cache[location].branch
245
trans = self.root_transport
246
for piece in urllib.unquote(location).split('/'):
248
trans = trans.clone(piece)
249
target_bzrdir = bzrdir.BzrDir.open_from_transport(trans)
250
branch = target_bzrdir.open_branch()
252
self._locked_branches.append(branch)
253
self._branch_cache[location] = BranchCache(self, branch)
256
def get_containing_branch(self, location):
257
branch, relpath = containing_branch(self.root_transport, location)
258
real_location = location[:-len(relpath)].rstrip('/')
259
if real_location not in self._branch_cache:
260
self._branch_cache[real_location] = BranchCache(self, branch)
262
self._locked_branches.append(branch)
263
# return the cached version, possibly throwing away the one we just
265
return self._branch_cache[real_location].branch, relpath
267
def _get_branches(self, trans=None, loc=()):
268
"""Find branches under a listable transport.
270
Does not descend into control directories or branch directories.
271
(branches inside other branches will not be listed)
274
trans = self.root_transport
276
children = trans.list_dir('.')
277
if '.bzr' in children:
278
children.remove('.bzr')
280
target_bzrdir = bzrdir.BzrDir.open_from_transport(trans)
281
yield '/'.join(loc), target_bzrdir.open_branch()
282
except errors.NotBranchError:
286
for child in children:
287
for child_loc, child_branch in self._get_branches(
288
trans.clone(child), loc + (child,)):
289
yield child_loc, child_branch
290
except errors.NoSuchFile, e:
293
def get_changeset(self, rev):
294
"""Retrieve a Changeset."""
295
branch, revid = self._parse_rev(rev)
297
return BzrChangeset(self, branch, revid, self.log)
298
except errors.NoSuchRevision, e:
299
assert e.revision == revid
300
raise versioncontrol.NoSuchChangeset(rev)
302
# TODO: get_changesets?
304
def has_node(self, path, rev=None):
305
"""Return a boolean indicating if the node is present in a rev."""
307
self.get_node(path, rev)
308
except versioncontrol.NoSuchNode:
313
def get_node(self, path, rev=None):
314
"""Return a Node object or raise NoSuchNode or NoSuchChangeset."""
315
path = self.normalize_path(path)
318
revbranch, revid = self._parse_rev(rev)
320
branch, relpath = self.get_containing_branch(path)
321
except errors.NotBranchError:
322
if not self.root_transport.has(path):
323
raise versioncontrol.NoSuchNode(path, rev)
324
return UnversionedDirNode(self, path)
325
if revbranch is None:
328
if revid == 'current:':
329
tree = revbranch.basis_tree()
331
tree = revbranch.repository.revision_tree(revid)
332
except (errors.NoSuchRevision, errors.RevisionNotPresent):
333
raise versioncontrol.NoSuchChangeset(rev)
334
file_id = tree.inventory.path2id(relpath)
336
raise versioncontrol.NoSuchNode(path, rev)
337
entry = tree.inventory[file_id]
338
klass = NODE_MAP[entry.kind]
339
return klass(self, branch, tree, entry, path)
341
def get_oldest_rev(self):
342
# TODO just use revno here
343
# (by definition, this revision is always in the branch history)
344
return self.string_rev(None, 'null:')
346
def get_youngest_rev(self):
347
# TODO just use revno here
348
# (by definition, this revision is always in the branch history)
349
return self.string_rev(None, 'current:')
351
def _repo_history(self):
356
for loc, branch in self._get_branches():
357
repo_base = branch.repository.bzrdir.transport.base
358
repos[repo_base] = branch.repository
359
for revision_id in reversed(branch.revision_history()):
360
if revision_id in seen:
362
revisions.setdefault(repo_base, []).append(revision_id)
363
branches[revision_id] = branch
364
seen.add(revision_id)
366
for repo_base, revision_ids in revisions.iteritems():
367
revision_set.update(repos[repo_base].get_revisions(revision_ids))
368
revisions = sorted(revision_set, key=lambda x: x.timestamp)
369
return [(r.revision_id, branches[r.revision_id]) for r in revisions]
371
def previous_rev(self, rev):
372
branch, revid = self._parse_rev(rev)
375
if self._history is None:
376
self._history = self._repo_history()
379
for rev, branch in reversed(self._history):
381
raise repr(self._history)
383
self._previous[last] = (branch, rev)
385
if revid == 'current:':
386
return self.string_rev(self._history[-1][1], self._history[-1][0])
388
return self.string_rev(*self._previous[revid])
392
def next_rev(self, rev, path=''):
393
# TODO path is ignored.
394
branch, revid = self._parse_rev(rev)
395
if revid == 'current:':
400
ancestry = self.repo.get_ancestry(self.branch.last_revision())
402
ancestry = branch.repository.get_ancestry(branch.last_revision())
404
idx = ancestry.index(revid)
406
# XXX this revision is not in the branch ancestry. Now what?
409
next_revid = ancestry[idx + 1]
411
# There is no next rev. Now what?
413
return self.string_rev(branch, next_revid)
415
def rev_older_than(self, rev1, rev2):
418
branch1, rrev1 = self._parse_rev(rev1)
419
branch2, rrev2 = self._parse_rev(rev2)
420
if rrev2 == 'current:':
422
first_before_second = rrev1 in branch2.repo.get_ancestry(rrev2)
423
second_before_first = rrev2 in branch1.repo.get_ancestry(rrev1)
424
if first_before_second and second_before_first:
425
raise core.TracError('%s and %s precede each other?' %
427
if first_before_second:
429
if second_before_first:
431
# Bah, unrelated revisions. Fall back to comparing timestamps.
432
return (self.repo.get_revision(rrev1).timestamp <
433
self.repo.get_revision(rrev2).timestamp)
435
# XXX what is get_youngest_rev_in_cache doing in here
437
def get_path_history(self, path, rev=None, limit=None):
438
"""Shortcut for Node's get_history."""
439
# XXX I think the intention for this one is probably different:
440
# it should track the state of this filesystem location across time.
441
# That is, it should keep tracking the same path as stuff is moved
442
# on to / away from that path.
444
# No need to normalize/unquote, get_node is a trac api method
445
# so it takes quoted values.
446
return self.get_node(path, rev).get_history(limit)
448
def normalize_path(self, path):
449
"""Remove leading and trailing '/'"""
450
# Also turns None into '', just in case.
451
return path and path.strip('/') or ''
453
def normalize_rev(self, rev):
454
"""Turn a user-specified rev into a "normalized" rev.
456
This turns None into a rev, and may convert a revid-based rev into
463
branch, revid = self._parse_rev(rev)
464
if branch is not None:
465
repository = branch.repository
468
return self.string_rev(branch, revid)
470
def short_rev(self, rev):
471
"""Attempt to shorten a rev.
473
This returns the revno if there is one, otherwise returns a
474
"nearby" revno with a ~ prefix.
476
The result of this method is used above the line number
477
columns in the diff/changeset viewer. There is *very* little
478
room there. Our ~revno results are actually a little too wide
479
already. Tricks like using the branch nick simply do not fit.
481
branch, revid = self._parse_rev(rev)
484
history = branch.revision_history()
485
# First try if it is a revno.
487
return str(history.index(revid) + 1)
489
# Get the closest thing that *is* a revno.
490
ancestry = branch.repository.get_ancestry(revid)
491
# We've already tried the current one.
493
for ancestor in reversed(ancestry):
495
return '~%s' % (history.index(ancestor) + 1,)
498
# XXX unrelated branch. Now what?
501
def get_changes(self, old_path, old_rev, new_path, new_rev,
503
"""yields (old_node, new_node, kind, change) tuples."""
504
# ignore_ancestry is ignored, don't know what it's for.
505
if old_path != new_path:
506
raise core.TracError(
507
'Currently the bzr plugin does not support this between '
508
'different directories. Sorry.')
509
old_branch, old_revid = self._parse_rev(old_rev)
510
new_branch, new_revid = self._parse_rev(new_rev)
511
old_tree = old_branch.repository.revision_tree(old_revid)
512
new_tree = new_branch.repository.revision_tree(new_revid)
513
delta = new_tree.changes_from(old_tree)
514
for path, file_id, kind in delta.added:
515
entry = new_tree.inventory[file_id]
516
node = NODE_MAP[kind](self, new_branch, new_tree, entry, path)
517
cur_path = new_tree.id2path(file_id)
518
node._history_cache[(new_revid, old_revid, file_id)] = \
519
cur_path, new_rev, versioncontrol.Changeset.ADD
520
yield None, node, node.kind, versioncontrol.Changeset.ADD
521
for path, file_id, kind in delta.removed:
522
entry = old_tree.inventory[file_id]
523
node = NODE_MAP[kind](self, old_branch, old_tree, entry, path)
524
yield node, None, node.kind, versioncontrol.Changeset.DELETE
525
for oldpath, newpath, file_id, kind, textmod, metamod in delta.renamed:
526
oldnode = NODE_MAP[kind](self, old_branch, old_tree,
527
old_tree.inventory[file_id], oldpath)
528
newnode = NODE_MAP[kind](self, new_branch, new_tree,
529
new_tree.inventory[file_id], newpath)
530
if oldnode.kind != newnode.kind:
531
raise core.TracError(
532
'%s changed kinds, I do not know how to handle that' % (
534
yield oldnode, newnode, oldnode.kind, versioncontrol.Changeset.MOVE
535
for path, file_id, kind, textmod, metamod in delta.modified:
536
# Bzr won't report a changed path as a rename but trac wants that.
537
oldpath = old_tree.id2path(file_id)
538
oldnode = NODE_MAP[kind](self, old_branch, old_tree,
539
old_tree.inventory[file_id], oldpath)
540
newnode = NODE_MAP[kind](self, new_branch, new_tree,
541
new_tree.inventory[file_id], path)
542
if oldnode.kind != newnode.kind:
543
raise core.TracError(
544
'%s changed kinds, I do not know how to handle that' % (
547
action = versioncontrol.Changeset.MOVE
549
action = versioncontrol.Changeset.EDIT
550
cur_path = new_tree.id2path(file_id)
551
newnode._history_cache[(new_revid, old_revid, file_id)] = \
552
cur_path, new_rev, action
553
yield oldnode, newnode, oldnode.kind, action
555
def dotted_revno(self, branch, revid):
556
return self.get_branch_cache(branch).dotted_revno(revid)
558
def get_branch_cache(self, branch):
559
branch_key = branch.bzrdir.root_transport.base
560
if branch_key not in self._branch_cache:
561
self._branch_cache[branch_key] = BranchCache(self, branch)
562
return self._branch_cache[branch_key]
564
def sorted_revision_history(self, branch):
565
return self.get_branch_cache(branch).sorted_revision_history()
568
"""Dummy to satisfy interface requirements"""
569
# XXX should we be dumping in-mem caches? Seems unlikely.
574
class BzrNode(versioncontrol.Node):
578
class UnversionedDirNode(BzrNode):
579
def __init__(self, bzr_repo, path):
580
rev_string = urllib.quote('current:')
581
BzrNode.__init__(self, path, rev_string, versioncontrol.Node.DIRECTORY)
582
self.transport = bzr_repo.root_transport.clone(path)
583
self.bzr_repo = bzr_repo
587
return 'UnversionedDirNode(path=%r)' % self.path
589
def get_properties(self):
592
def get_entries(self):
594
for name in self.transport.list_dir(''):
597
stat_mode = self.transport.stat(name).st_mode
598
kind = osutils.file_kind_from_stat_mode(stat_mode)
599
if not kind == 'directory':
601
child_path = osutils.pathjoin(self.path, name)
603
branch = self.bzr_repo.get_branch(child_path)
604
except errors.NotBranchError:
605
result.append(UnversionedDirNode(self.bzr_repo, child_path))
607
tree = branch.basis_tree()
608
node = BzrDirNode(self.bzr_repo, branch, tree,
609
tree.inventory.root, child_path)
613
def get_content_length(self):
616
def get_content(self):
617
return StringIO.StringIO('')
619
def get_content_type(self):
620
return 'application/octet-stream'
622
def get_history(self, limit=None):
623
return [(self.path, 'current%3A', 'add')]
626
def sorted_revision_history(branch, generate_revno=False):
627
history = branch.revision_history()
628
graph = branch.repository.get_revision_graph(history[-1])
629
return tsort.merge_sort(graph, history[-1], generate_revno=generate_revno)
632
class BzrVersionedNode(BzrNode):
636
'modified': versioncontrol.Changeset.EDIT,
637
'unchanged': versioncontrol.Changeset.EDIT,
638
'added': versioncontrol.Changeset.ADD,
639
inventory.InventoryEntry.RENAMED: versioncontrol.Changeset.MOVE,
640
inventory.InventoryEntry.MODIFIED_AND_RENAMED:
641
versioncontrol.Changeset.MOVE
644
def __init__(self, bzr_repo, branch, revisiontree, entry, path):
645
"""Initialize. path has to be a normalized path."""
646
rev_string = bzr_repo.string_rev(branch, entry.revision)
647
BzrNode.__init__(self, path, rev_string, self.kind)
648
self.bzr_repo = bzr_repo
649
self.log = bzr_repo.log
650
self.repo = branch.repository
652
self.tree = revisiontree
654
# XXX I am not sure if this makes any sense but it does make
655
# the links in the changeset viewer work.
656
self.created_rev = self.rev
657
self.created_path = self.path
658
self.root_path = path[:-len(self.tree.id2path(self.entry.file_id))]
660
def get_properties(self):
661
# Must at least return an empty dict here (base class version raises).
663
if self.entry.executable:
664
result['executable'] = 'True'
667
def _merging_history(self):
668
"""Iterate through history revisions that merged changes to this node
670
This includes all revisions in which the revision_id changed.
671
It may also include a few revisions in which the revision_id did not
672
change, if the modification was subsequently undone.
674
weave = self.tree.get_weave(self.entry.file_id)
675
file_ancestry = weave.get_ancestry(self.entry.revision)
676
# Can't use None here, because it's a legitimate revision id.
677
last_yielded = 'bogus:'
678
for num, revision_id, depth, revno, eom in \
679
self.bzr_repo.sorted_revision_history(self.branch):
681
last_mainline = revision_id
682
if last_mainline == last_yielded:
684
if revision_id in file_ancestry:
686
last_yielded = last_mainline
689
def get_history(self, limit=None):
692
yields (path, revid, chg) tuples.
694
path is the path to this entry.
696
revid is the revid string. It is the revision in which the change
697
was applied to the branch, not necessarily the revision that originated
698
the change. In SVN terms, it is a changeset, not a file revision.
700
chg is a Changeset.ACTION thing.
702
First thing should be for the current revision.
704
limit is an int cap on how many entries to return.
706
history_iter = self._get_history()
710
return (y for x, y in izip(range(limit), history_iter))
712
def _get_history(self, limit=None):
713
file_id = self.entry.file_id
715
history = list(self._merging_history())
716
cache = self.bzr_repo.get_branch_cache(self.branch)
717
trees = cache.revision_trees(history)
718
for prev_tree in trees:
719
previous_revision = prev_tree.get_revision_id()
721
prev_file_revision = prev_tree.inventory[file_id].revision
722
except errors.NoSuchId:
723
prev_file_revision = None
724
if (revision is not None and
725
prev_file_revision != file_revision):
726
path, rev_str, chg = \
727
self.get_change(revision, previous_revision, file_id)
728
branch_revision = self.bzr_repo.string_rev(self.branch,
730
yield (osutils.pathjoin(self.root_path, path),
731
branch_revision, chg)
732
if prev_file_revision is None:
734
revision = previous_revision
735
file_revision = prev_file_revision
737
def get_change(self, revision, previous_revision, file_id):
738
key = (revision, previous_revision, file_id)
739
if key not in self._history_cache or False:
740
self._history_cache[key] = self.calculate_history(revision,
741
previous_revision, file_id)
742
return self._history_cache[key]
744
def calculate_history(self, revision, previous_revision, file_id):
745
cache = self.bzr_repo.get_branch_cache(self.branch)
746
tree = cache.revision_tree(revision)
747
current_entry = tree.inventory[file_id]
748
current_path = tree.id2path(file_id)
749
if previous_revision not in (None, 'null:'):
750
previous_tree = cache.revision_tree(previous_revision)
751
previous_entry = previous_tree.inventory[file_id]
753
previous_entry = None
754
# We should only get revisions in the ancestry for which
755
# we exist, so this should succeed..
756
return self.compare_entries(current_path, current_entry,
759
def compare_entries(self, current_path, current_entry, previous_entry):
760
diff = current_entry.describe_change(previous_entry, current_entry)
761
rev = self.bzr_repo.string_rev(self.branch, current_entry.revision)
763
return current_path, rev, self._diff_map[diff]
765
raise Exception('unknown describe_change %r' % (diff,))
767
def get_last_modified(self):
768
return self.tree.get_file_mtime(self.entry.file_id)
771
class BzrDirNode(BzrVersionedNode):
775
kind = versioncontrol.Node.DIRECTORY
777
def __init__(self, bzr_repo, branch, revisiontree, entry, path,
779
BzrVersionedNode.__init__(self, bzr_repo, branch, revisiontree, entry,
782
ancestry = self.repo.get_ancestry(revisiontree.get_revision_id())
785
best = self._get_cache(self.revcache, ancestry, entry)
786
self._orig_rev = ancestry[best]
787
self.rev = bzr_repo.string_rev(self.branch, (ancestry[best]))
789
self.revcache = revcache
790
self._orig_rev = revcache[entry.file_id]
791
self.rev = bzr_repo.string_rev(self.branch, self._orig_rev)
794
return 'BzrDirNode(path=%r, relpath=%r)' % (self.path, self.entry.name)
797
def _get_cache(cls, cache, ancestry, entry, ancestry_idx=None):
798
"""Populate a file_id <-> revision_id mapping.
800
This mapping is different from InventoryEntry.revision, but only for
801
directories. In this scheme, directories are considered modified
802
if their contents are modified.
804
The revision ids are not guaranteed to be in the mainline revision
807
cache: The cache to populate
808
ancestry: A topologically-sorted list of revisions, with more recent
809
revisions having lower indexes.
810
entry: The InventoryEntry to start at
811
ancestry_idx: A mapping of revision_id <-> ancestry index.
813
if ancestry_idx is None:
814
ancestry_idx = dict((r, n) for n, r in enumerate(ancestry))
815
# best ~= most recent revision to modify a child of this directory
816
best = ancestry_idx[entry.revision]
817
for child in entry.children.itervalues():
818
if child.kind == 'directory':
819
index = cls._get_cache(cache, ancestry, child, ancestry_idx)
820
cache[child.file_id] = ancestry[index]
822
index = ancestry_idx[child.revision]
823
best = min(best, index)
826
def get_content(self):
827
"""Return a file-like (read(length)) for a file, None for a dir."""
830
def get_entries(self):
831
"""Yield child Nodes if a dir, return None if a file."""
832
for name, entry in self.entry.children.iteritems():
833
childpath = '/'.join((self.path, name))
834
klass = NODE_MAP[entry.kind]
835
if klass is BzrDirNode:
836
yield klass(self.bzr_repo, self.branch, self.tree, entry,
837
childpath, self.revcache)
839
yield klass(self.bzr_repo, self.branch, self.tree, entry,
842
def get_content_length(self):
845
def get_content_type(self):
848
def _get_revision_history(self, limit=None):
849
history = self.branch.revision_history()
851
if limit is not None:
852
history = history[-limit:]
853
self.bzr_repo.get_branch_cache(self.branch).cache_revisions(history)
854
for rev_id in reversed(history):
856
operation = versioncontrol.Changeset.ADD
858
operation = versioncontrol.Changeset.EDIT
860
self.bzr_repo.string_rev(self.branch, rev_id),
863
def get_history(self, limit=None):
866
yields (path, rev, chg) tuples.
868
path is the path to this entry, rev is the revid string.
869
chg is a Changeset.ACTION thing.
871
First thing should be for the current revision.
873
This is special because it checks for changes recursively,
874
not just to this directory. bzr only treats the dir as changed
875
if it is renamed, not if its contents changed. Trac needs
876
this recursive behaviour.
878
limit is an int cap on how many entries to return.
880
current_entry = self.entry
881
file_id = current_entry.file_id
882
if current_entry.parent_id == None:
883
for r in self._get_revision_history(limit):
888
# We need the rev we were created with, not the rev the entry
889
# specifies (our contents may have changed between that rev
890
# and our own current rev).
891
current_revid = self._orig_rev
893
if self.branch is not None:
894
history = self.branch.revision_history()
897
if current_revid == 'current:':
898
current_revid = history[-1]
899
# If the current_revid we start from is in the branch history,
900
# limit our view to just the history, not the full ancestry.
902
index = history.index(current_revid)
904
ancestry = self.repo.get_ancestry(current_revid)
905
# The last entry is this rev, skip it.
908
# The last entry is None, skip it.
911
ancestry = ['null:'] + history[:index]
913
# Load a bunch of trees in one go. We do not know how many we
914
# need: we may end up skipping some trees because they do not
916
chunksize = limit or 100
917
current_tree = self.tree
918
current_path = current_tree.id2path(file_id)
919
path_prefix = self.path[:-len(current_path)]
921
chunk, ancestry = ancestry[:chunksize], ancestry[chunksize:]
922
cache = self.bzr_repo.get_branch_cache(self.branch)
923
for previous_revid, previous_tree in izip(
924
chunk, cache.revision_trees(chunk)):
925
if file_id in previous_tree.inventory:
926
previous_entry = previous_tree.inventory[file_id]
928
previous_entry = None
929
delta = current_tree.changes_from(
930
previous_tree, specific_files=[current_path])
931
if not delta.has_changed():
932
current_entry = previous_entry
933
current_path = previous_tree.inventory.id2path(file_id)
934
current_revid = previous_revid
935
current_tree = previous_tree
937
diff = current_entry.describe_change(previous_entry,
940
yield (path_prefix+current_path,
941
self.bzr_repo.string_rev(self.branch,
943
versioncontrol.Changeset.ADD)
944
# There is no history before this point, we're done.
946
elif diff == 'modified' or diff == 'unchanged':
947
# We want the entry anyway.
948
yield (path_prefix+current_path,
949
self.bzr_repo.string_rev(self.branch,
951
versioncontrol.Changeset.EDIT)
952
elif diff in (current_entry.RENAMED,
953
current_entry.MODIFIED_AND_RENAMED):
954
yield (path_prefix+current_path,
955
self.bzr_repo.string_rev(self.branch,
957
versioncontrol.Changeset.MOVE)
959
raise Exception('unknown describe_change %r' % (diff,))
961
if limit is not None and count >= limit:
963
current_entry = previous_entry
964
current_path = previous_tree.inventory.id2path(file_id)
965
current_revid = previous_revid
966
current_tree = previous_tree
968
def get_previous(self):
969
"""Equivalent to i=iter(get_history(2));i.next();return i.next().
971
The default implementation does essentially that, but we specialcase
972
it because we can skip the loading of all the trees.
974
# Special case: if this is the root node it (well, its
975
# contents) change every revision.
976
if not self.tree.id2path(self.entry.file_id):
977
return self.path, self.rev, versioncontrol.Changeset.EDIT
978
if self._orig_rev != self.entry.revision:
979
# The last change did not affect this dir directly, it changed
981
return self.path, self.rev, versioncontrol.Changeset.EDIT
982
# We were affected directly. Get a delta to figure out how.
983
delta = self.repo.get_revision_delta(self._orig_rev)
984
for path, file_id, kind in delta.added:
985
if file_id == self.entry.file_id:
986
return path, self.rev, versioncontrol.Changeset.ADD
987
for oldpath, newpath, file_id, kind, textmod, metamod in delta.renamed:
988
if file_id == self.entry.file_id:
989
return newpath, self.rev, versioncontrol.Changeset.MOVE
990
# We were removed (which does not make any sense,
991
# the tree we were constructed from is newer and has us)
992
raise core.TracError('should not get here, %r %r %r' %
993
(self.entry, delta, self._orig_rev))
996
class BzrFileNode(BzrVersionedNode):
1000
kind = versioncontrol.Node.FILE
1003
return 'BzrFileNode(path=%r)' % self.path
1005
def get_content(self):
1006
"""Return a file-like (read(length)) for a file, None for a dir."""
1007
return self.tree.get_file(self.entry.file_id)
1009
def get_entries(self):
1012
def get_content_length(self):
1013
return self.entry.text_size
1015
def get_content_type(self):
1016
return mimeview.get_mimetype(self.name)
1019
class BzrSymlinkNode(BzrVersionedNode):
1021
"""Kinda like a file only not really. Empty, properties only."""
1025
kind = versioncontrol.Node.FILE
1028
return 'BzrSymlinkNode(path=%r)' % self.path
1030
def get_content(self):
1031
return StringIO.StringIO('')
1033
def get_entries(self):
1036
def get_content_length(self):
1039
def get_content_type(self):
1042
def get_properties(self):
1043
return {'destination': self.entry.symlink_target}
1047
'directory': BzrDirNode,
1048
'file': BzrFileNode,
1049
'symlink': BzrSymlinkNode,
1053
class BzrChangeset(versioncontrol.Changeset):
1055
def __init__(self, bzr_repo, branch, revid, log):
1056
"""Initialize from a bzr repo, an unquoted revid and a logger."""
1058
self.bzr_repo = bzr_repo
1060
if revid in ('current:', 'null:'):
1061
self.revision = revision.Revision(revid, committer='',
1062
message='', timezone='')
1063
versioncontrol.Changeset.__init__(self, urllib.quote(revid),
1064
'', '', time.time())
1066
raise errors.NoSuchRevision(None, revid)
1068
self.revision = bzr_repo.get_branch_cache(branch).get_revision(revid)
1069
versioncontrol.Changeset.__init__(self,
1070
bzr_repo.string_rev(
1072
self.revision.message,
1073
self.revision.committer,
1074
self.revision.timestamp)
1075
self.branch = branch
1078
return 'BzrChangeset(%r)' % (self.revision.revision_id)
1080
def get_properties(self):
1081
"""Return an iterator of (name, value, is wikitext, html class)."""
1082
for name, value in self.revision.properties.iteritems():
1083
yield name, value, False, ''
1084
if len(self.revision.parent_ids) > 1:
1085
for name, link in [('parent trees', ' * source:@%s'),
1086
('changesets', ' * [changeset:%s]')]:
1087
yield name, '\n'.join(
1088
link % (self.bzr_repo.string_rev(self.branch, parent),)
1089
for parent in self.revision.parent_ids), True, ''
1091
def get_changes(self):
1094
Return tuples are (path, kind, change, base_path, base_rev).
1095
change is self.ADD/COPY/DELETE/EDIT/MOVE.
1096
kind is Node.FILE or Node.DIRECTORY.
1097
base_path and base_rev are the location and revision of the file
1100
if self.revision.revision_id in ('current:', 'null:'):
1102
branchpath = osutils.relpath(
1103
osutils.normalizepath(self.bzr_repo.root_transport.base),
1104
osutils.normalizepath(self.branch.bzrdir.root_transport.base))
1105
for path, kind, change, base_path, base_rev in self._get_changes():
1106
if path is not None:
1107
path = osutils.pathjoin(branchpath, path)
1108
if base_path is not None:
1109
base_path = osutils.pathjoin(branchpath, base_path)
1110
yield (path, kind, change, base_path, base_rev)
1112
def _get_changes(self):
1113
if self.revision.revision_id in ('current:', 'null:'):
1115
this = self.branch.repository.revision_tree(self.revision.revision_id)
1116
parents = self.revision.parent_ids
1118
parent_revid = parents[0]
1121
other = self.branch.repository.revision_tree(parent_revid)
1122
delta = this.changes_from(other)
1124
kindmap = {'directory': versioncontrol.Node.DIRECTORY,
1125
'file': versioncontrol.Node.FILE,
1126
'symlink': versioncontrol.Node.FILE, # gotta do *something*
1129
# We have to make sure our base_path/base_rev combination
1130
# exists (get_node succeeds). If we use
1131
# other.inventory[file_id].revision as base_rev we cannot use
1132
# what bzr hands us or what's in other.inventory as base_path
1133
# since the parent node may have been renamed between what we
1134
# return as base_rev and the revision the "other" inventory
1135
# corresponds to (parent_revid).
1136
# So we can either return other.id2path(file_id) and parent_revid
1137
# or use entry.revision and pull up the inventory for that revision
1138
# to get the path. Currently the code does the former,
1139
# remember to update the paths if it turns out returning the other
1140
# revision works better.
1142
for path, file_id, kind in delta.added:
1143
# afaict base_{path,rev} *should* be ignored for this one.
1144
yield path, kindmap[kind], self.ADD, None, None
1145
for path, file_id, kind in delta.removed:
1146
yield (path, kindmap[kind], self.DELETE, path,
1147
self.bzr_repo.string_rev(self.branch, parent_revid))
1148
for oldpath, newpath, file_id, kind, textmod, metamod in delta.renamed:
1149
yield (newpath, kindmap[kind], self.MOVE, other.id2path(file_id),
1150
self.bzr_repo.string_rev(self.branch, parent_revid))
1151
for path, file_id, kind, textmod, metamod in delta.modified:
1152
# "path" may not be accurate for base_path: the directory
1153
# it is in may have been renamed. So pull the right path from
1154
# the old inventory.
1155
yield (path, kindmap[kind], self.EDIT, other.id2path(file_id),
1156
self.bzr_repo.string_rev(self.branch, parent_revid))
1159
def getattribute(self, attr):
1160
"""If a callable is requested log the call."""
1161
obj = object.__getattribute__(self, attr)
1162
if not callable(obj):
1165
log = object.__getattribute__(self, 'log')
1166
except AttributeError:
1171
def _wrap(*args, **kwargs):
1172
arg_list = [repr(a) for a in args] + \
1173
['%s=%r' % i for i in kwargs.iteritems() ]
1174
call_str = '%s.%s(%s)' % \
1175
(self, attr, ', '.join(arg_list))
1176
if len(call_str) > 200:
1177
call_str = '%s...' % call_str[:200]
1178
log.debug('CALL %s' % call_str)
1180
result = obj(*args, **kwargs)
1181
except Exception, e:
1182
log.debug('FAILURE of %s: %s' % (call_str, e) )
1184
str_result = str(result)
1185
if len(str_result) > 200:
1186
str_result = '%s...' % str_result[:200]
1187
log.debug('RESULT of %s: %s' % (call_str, str_result))
1192
def containing_branch(transport, path):
1193
child_transport = transport.clone(path)
1194
my_bzrdir, relpath = \
1195
bzrdir.BzrDir.open_containing_from_transport(child_transport)
1196
return my_bzrdir.open_branch(), relpath
1199
class BranchCache(object):
1201
def __init__(self, bzr_repo, branch):
1202
self.bzr_repo = bzr_repo
1203
self.branch = branch
1204
self._sorted_revision_history = None
1205
self._dotted_revno = None
1206
self._revno_revid = None
1208
def sorted_revision_history(self):
1209
if self._sorted_revision_history is None:
1210
self._sorted_revision_history = \
1211
sorted_revision_history(self.branch, generate_revno=True)
1212
return self._sorted_revision_history
1214
def _populate_dotted_maps(self):
1215
if self._dotted_revno is None:
1216
self._dotted_revno = {}
1217
self._revno_revid = {}
1218
for s, revision_id, m, revno, e in \
1219
self.sorted_revision_history():
1220
dotted = '.'.join([str(s) for s in revno])
1221
self._dotted_revno[revision_id] = dotted
1222
self._revno_revid[dotted] = revision_id
1224
def dotted_revno(self, revid):
1225
self._populate_dotted_maps()
1226
return self._dotted_revno.get(revid)
1228
def revid_from_dotted(self, dotted_revno):
1229
self._populate_dotted_maps()
1230
return self._revno_revid.get(dotted_revno)
1232
def revision_tree(self, revision_id):
1233
if revision_id not in self.bzr_repo._tree_cache:
1234
self.bzr_repo._tree_cache[revision_id] = \
1235
self.branch.repository.revision_tree(revision_id)
1236
if revision_id == 'null:':
1237
self.bzr_repo._tree_cache[None] = \
1238
self.bzr_repo._tree_cache['null:']
1239
return self.bzr_repo._tree_cache[revision_id]
1241
def revision_trees(self, revision_ids):
1242
if None in revision_ids or 'null:' in revision_ids:
1243
self.revision_tree('null:')
1244
missing = [r for r in revision_ids if r not in
1245
self.bzr_repo._tree_cache]
1246
if len(missing) > 0:
1247
trees = self.branch.repository.revision_trees(missing)
1249
self.bzr_repo._tree_cache[tree.get_revision_id()] = tree
1250
return [self.bzr_repo._tree_cache[r] for r in revision_ids]
1252
def cache_revisions(self, revision_ids):
1253
if self.bzr_repo.log:
1254
self.bzr_repo.log.debug('caching %d revisions' % len(revision_ids))
1255
missing = [r for r in revision_ids if r not in
1256
self.bzr_repo._revision_cache]
1257
revisions = self.branch.repository.get_revisions(missing)
1258
self.bzr_repo._revision_cache.update(dict((r.revision_id, r) for r in
1260
if self.bzr_repo.log:
1261
self.bzr_repo.log.debug('done caching %d revisions' %
1264
def get_revision(self, revision_id):
1266
return self.bzr_repo._revision_cache[revision_id]
1268
self.cache_revisions([revision_id])
1269
return self.bzr_repo._revision_cache[revision_id]