Rev 5695: (spiv) Add Branch.heads_to_fetch API and HPSS request. (Andrew Bennetts) in file:///home/pqm/archives/thelove/bzr/%2Btrunk/
Canonical.com Patch Queue Manager
pqm at pqm.ubuntu.com
Thu Mar 3 06:02:53 UTC 2011
At file:///home/pqm/archives/thelove/bzr/%2Btrunk/
------------------------------------------------------------
revno: 5695 [merge]
revision-id: pqm at pqm.ubuntu.com-20110303060249-l2zou9i59742nrqf
parent: pqm at pqm.ubuntu.com-20110303043323-76p182iwtfnaqbkt
parent: andrew.bennetts at canonical.com-20110301080749-2fv6l3d4t9o7umfm
committer: Canonical.com Patch Queue Manager <pqm at pqm.ubuntu.com>
branch nick: +trunk
timestamp: Thu 2011-03-03 06:02:49 +0000
message:
(spiv) Add Branch.heads_to_fetch API and HPSS request. (Andrew Bennetts)
modified:
bzrlib/branch.py branch.py-20050309040759-e4baf4e0d046576e
bzrlib/fetch.py fetch.py-20050818234941-26fea6105696365d
bzrlib/remote.py remote.py-20060720103555-yeeg2x51vn0rbtdp-1
bzrlib/smart/branch.py branch.py-20061124031907-mzh3pla28r83r97f-1
bzrlib/smart/request.py request.py-20061108095550-gunadhxmzkdjfeek-1
bzrlib/tests/per_branch/test_branch.py testbranch.py-20050711070244-121d632bc37d7253
bzrlib/tests/test_remote.py test_remote.py-20060720103555-yeeg2x51vn0rbtdp-2
doc/en/release-notes/bzr-2.4.txt bzr2.4.txt-20110114053217-k7ym9jfz243fddjm-1
=== modified file 'bzrlib/branch.py'
--- a/bzrlib/branch.py 2011-02-26 15:35:01 +0000
+++ b/bzrlib/branch.py 2011-03-03 06:02:49 +0000
@@ -1531,6 +1531,26 @@
else:
raise AssertionError("invalid heads: %r" % (heads,))
+ def heads_to_fetch(self):
+ """Return the heads that must and that should be fetched to copy this
+ branch into another repo.
+
+ :returns: a 2-tuple of (must_fetch, if_present_fetch). must_fetch is a
+ set of heads that must be fetched. if_present_fetch is a set of
+ heads that must be fetched if present, but no error is necessary if
+ they are not present.
+ """
+ # For bzr native formats must_fetch is just the tip, and if_present_fetch
+ # are the tags.
+ must_fetch = set([self.last_revision()])
+ try:
+ if_present_fetch = set(self.tags.get_reverse_tag_dict())
+ except errors.TagsNotSupported:
+ if_present_fetch = set()
+ must_fetch.discard(_mod_revision.NULL_REVISION)
+ if_present_fetch.discard(_mod_revision.NULL_REVISION)
+ return must_fetch, if_present_fetch
+
class BranchFormat(controldir.ControlComponentFormat):
"""An encapsulation of the initialization and open routines for a format.
=== modified file 'bzrlib/fetch.py'
--- a/bzrlib/fetch.py 2011-02-07 04:14:29 +0000
+++ b/bzrlib/fetch.py 2011-02-25 03:00:35 +0000
@@ -350,7 +350,8 @@
Factors that go into determining the sort of fetch to perform:
* did the caller specify any revision IDs?
- * did the caller specify a source branch (need to fetch the tip + tags)
+ * did the caller specify a source branch (need to fetch its
+ heads_to_fetch(), usually the tip + tags)
* is there an existing target repo (don't need to refetch revs it
already has)
* target is stacked? (similar to pre-existing target repo: even if
@@ -391,27 +392,29 @@
return graph.EverythingNotInOther(
self.target_repo, self.source_repo).execute()
heads_to_fetch = set(self._explicit_rev_ids)
- tags_to_fetch = set()
if self.source_branch is not None:
- try:
- tags_to_fetch.update(
- self.source_branch.tags.get_reverse_tag_dict())
- except errors.TagsNotSupported:
- pass
+ must_fetch, if_present_fetch = self.source_branch.heads_to_fetch()
if self.source_branch_stop_revision_id is not None:
- heads_to_fetch.add(self.source_branch_stop_revision_id)
- else:
- heads_to_fetch.add(self.source_branch.last_revision())
+ # Replace the tip rev from must_fetch with the stop revision
+ # XXX: this might be wrong if the tip rev is also in the
+ # must_fetch set for other reasons (e.g. it's the tip of
+ # multiple loom threads?), but then it's pretty unclear what it
+ # should mean to specify a stop_revision in that case anyway.
+ must_fetch.discard(self.source_branch.last_revision())
+ must_fetch.add(self.source_branch_stop_revision_id)
+ heads_to_fetch.update(must_fetch)
+ else:
+ if_present_fetch = set()
if self.target_repo_kind == TargetRepoKinds.EMPTY:
# PendingAncestryResult does not raise errors if a requested head
# is absent. Ideally it would support the
# required_ids/if_present_ids distinction, but in practice
# heads_to_fetch will almost certainly be present so this doesn't
# matter much.
- all_heads = heads_to_fetch.union(tags_to_fetch)
+ all_heads = heads_to_fetch.union(if_present_fetch)
return graph.PendingAncestryResult(all_heads, self.source_repo)
return graph.NotInOtherForRevs(self.target_repo, self.source_repo,
- required_ids=heads_to_fetch, if_present_ids=tags_to_fetch
+ required_ids=heads_to_fetch, if_present_ids=if_present_fetch
).execute()
=== modified file 'bzrlib/remote.py'
--- a/bzrlib/remote.py 2011-03-03 04:33:23 +0000
+++ b/bzrlib/remote.py 2011-03-03 06:02:49 +0000
@@ -2194,6 +2194,19 @@
self._ensure_real()
return self._custom_format.supports_set_append_revisions_only()
+ def _use_default_local_heads_to_fetch(self):
+ # If the branch format is a metadir format *and* its heads_to_fetch
+ # implementation is not overridden vs the base class, we can use the
+ # base class logic rather than use the heads_to_fetch RPC. This is
+ # usually cheaper in terms of net round trips, as the last-revision and
+ # tags info fetched is cached and would be fetched anyway.
+ self._ensure_real()
+ if isinstance(self._custom_format, branch.BranchFormatMetadir):
+ branch_class = self._custom_format._branch_class()
+ heads_to_fetch_impl = branch_class.heads_to_fetch.im_func
+ if heads_to_fetch_impl is branch.Branch.heads_to_fetch.im_func:
+ return True
+ return False
class RemoteBranch(branch.Branch, _RpcHelper, lock._RelockDebugMixin):
"""Branch stored on a server accessed by HPSS RPC.
@@ -2781,6 +2794,33 @@
self._ensure_real()
return self._real_branch.set_push_location(location)
+ def heads_to_fetch(self):
+ if self._format._use_default_local_heads_to_fetch():
+ # We recognise this format, and its heads-to-fetch implementation
+ # is the default one (tip + tags). In this case it's cheaper to
+ # just use the default implementation rather than a special RPC as
+ # the tip and tags data is cached.
+ return branch.Branch.heads_to_fetch(self)
+ medium = self._client._medium
+ if medium._is_remote_before((2, 4)):
+ return self._vfs_heads_to_fetch()
+ try:
+ return self._rpc_heads_to_fetch()
+ except errors.UnknownSmartMethod:
+ medium._remember_remote_is_before((2, 4))
+ return self._vfs_heads_to_fetch()
+
+ def _rpc_heads_to_fetch(self):
+ response = self._call('Branch.heads_to_fetch', self._remote_path())
+ if len(response) != 2:
+ raise errors.UnexpectedSmartServerResponse(response)
+ must_fetch, if_present_fetch = response
+ return set(must_fetch), set(if_present_fetch)
+
+ def _vfs_heads_to_fetch(self):
+ self._ensure_real()
+ return self._real_branch.heads_to_fetch()
+
class RemoteConfig(object):
"""A Config that reads and writes from smart verbs.
=== modified file 'bzrlib/smart/branch.py'
--- a/bzrlib/smart/branch.py 2010-05-13 16:17:54 +0000
+++ b/bzrlib/smart/branch.py 2011-02-25 04:44:53 +0000
@@ -142,6 +142,20 @@
self.branch.unlock()
+class SmartServerBranchHeadsToFetch(SmartServerBranchRequest):
+
+ def do_with_branch(self, branch):
+ """Return the heads-to-fetch for a Branch as two bencoded lists.
+
+ See Branch.heads_to_fetch.
+
+ New in 2.4.
+ """
+ must_fetch, if_present_fetch = branch.heads_to_fetch()
+ return SuccessfulSmartServerResponse(
+ (list(must_fetch), list(if_present_fetch)))
+
+
class SmartServerBranchRequestGetStackedOnURL(SmartServerBranchRequest):
def do_with_branch(self, branch):
=== modified file 'bzrlib/smart/request.py'
--- a/bzrlib/smart/request.py 2011-03-02 20:39:58 +0000
+++ b/bzrlib/smart/request.py 2011-03-03 06:02:49 +0000
@@ -505,6 +505,9 @@
'Branch.set_tags_bytes', 'bzrlib.smart.branch',
'SmartServerBranchSetTagsBytes')
request_handlers.register_lazy(
+ 'Branch.heads_to_fetch', 'bzrlib.smart.branch',
+ 'SmartServerBranchHeadsToFetch')
+request_handlers.register_lazy(
'Branch.get_stacked_on_url', 'bzrlib.smart.branch', 'SmartServerBranchRequestGetStackedOnURL')
request_handlers.register_lazy(
'Branch.last_revision_info', 'bzrlib.smart.branch', 'SmartServerBranchRequestLastRevisionInfo')
=== modified file 'bzrlib/tests/per_branch/test_branch.py'
--- a/bzrlib/tests/per_branch/test_branch.py 2011-02-19 17:37:45 +0000
+++ b/bzrlib/tests/per_branch/test_branch.py 2011-02-21 07:27:20 +0000
@@ -467,6 +467,26 @@
br.set_revision_history([])
self.assertEquals(br.revision_history(), [])
+ def test_heads_to_fetch(self):
+ # heads_to_fetch is a method that returns a collection of revids that
+ # need to be fetched to copy this branch into another repo. At a
+ # minimum this will include the tip.
+ # (In native formats, this is the tip + tags, but other formats may
+ # have other revs needed)
+ tree = self.make_branch_and_tree('a')
+ tree.commit('first commit', rev_id='rev1')
+ tree.commit('second commit', rev_id='rev2')
+ must_fetch, should_fetch = tree.branch.heads_to_fetch()
+ self.assertTrue('rev2' in must_fetch)
+
+ def test_heads_to_fetch_not_null_revision(self):
+ # NULL_REVISION does not appear in the result of heads_to_fetch, even
+ # for an empty branch.
+ tree = self.make_branch_and_tree('a')
+ must_fetch, should_fetch = tree.branch.heads_to_fetch()
+ self.assertFalse(revision.NULL_REVISION in must_fetch)
+ self.assertFalse(revision.NULL_REVISION in should_fetch)
+
class TestBranchFormat(per_branch.TestCaseWithBranch):
=== modified file 'bzrlib/tests/test_remote.py'
--- a/bzrlib/tests/test_remote.py 2011-03-03 04:33:23 +0000
+++ b/bzrlib/tests/test_remote.py 2011-03-03 06:02:49 +0000
@@ -1143,6 +1143,71 @@
[('set_tags_bytes', 'tags bytes')] * 2, real_branch.calls)
+class TestBranchHeadsToFetch(RemoteBranchTestCase):
+
+ def test_uses_last_revision_info_and_tags_by_default(self):
+ transport = MemoryTransport()
+ client = FakeClient(transport.base)
+ client.add_expected_call(
+ 'Branch.get_stacked_on_url', ('quack/',),
+ 'error', ('NotStacked',))
+ client.add_expected_call(
+ 'Branch.last_revision_info', ('quack/',),
+ 'success', ('ok', '1', 'rev-tip'))
+ # XXX: this will break if the default format's serialization of tags
+ # changes, or if the RPC for fetching tags changes from get_tags_bytes.
+ client.add_expected_call(
+ 'Branch.get_tags_bytes', ('quack/',),
+ 'success', ('d5:tag-17:rev-foo5:tag-27:rev-bare',))
+ transport.mkdir('quack')
+ transport = transport.clone('quack')
+ branch = self.make_remote_branch(transport, client)
+ result = branch.heads_to_fetch()
+ self.assertFinished(client)
+ self.assertEqual(
+ (set(['rev-tip']), set(['rev-foo', 'rev-bar'])), result)
+
+ def test_uses_rpc_for_formats_with_non_default_heads_to_fetch(self):
+ transport = MemoryTransport()
+ client = FakeClient(transport.base)
+ client.add_expected_call(
+ 'Branch.get_stacked_on_url', ('quack/',),
+ 'error', ('NotStacked',))
+ client.add_expected_call(
+ 'Branch.heads_to_fetch', ('quack/',),
+ 'success', (['tip'], ['tagged-1', 'tagged-2']))
+ transport.mkdir('quack')
+ transport = transport.clone('quack')
+ branch = self.make_remote_branch(transport, client)
+ branch._format._use_default_local_heads_to_fetch = lambda: False
+ result = branch.heads_to_fetch()
+ self.assertFinished(client)
+ self.assertEqual((set(['tip']), set(['tagged-1', 'tagged-2'])), result)
+
+ def test_backwards_compatible(self):
+ self.setup_smart_server_with_call_log()
+ # Make a branch with a single revision.
+ builder = self.make_branch_builder('foo')
+ builder.start_series()
+ builder.build_snapshot('tip', None, [
+ ('add', ('', 'root-id', 'directory', ''))])
+ builder.finish_series()
+ branch = builder.get_branch()
+ # Add two tags to that branch
+ branch.tags.set_tag('tag-1', 'rev-1')
+ branch.tags.set_tag('tag-2', 'rev-2')
+ self.addCleanup(branch.lock_read().unlock)
+ # Disable the heads_to_fetch verb
+ verb = 'Branch.heads_to_fetch'
+ self.disable_verb(verb)
+ self.reset_smart_call_log()
+ result = branch.heads_to_fetch()
+ self.assertEqual((set(['tip']), set(['rev-1', 'rev-2'])), result)
+ self.assertEqual(
+ ['Branch.last_revision_info', 'Branch.get_tags_bytes'],
+ [call.call.method for call in self.hpss_calls])
+
+
class TestBranchLastRevisionInfo(RemoteBranchTestCase):
def test_empty_branch(self):
=== modified file 'doc/en/release-notes/bzr-2.4.txt'
--- a/doc/en/release-notes/bzr-2.4.txt 2011-03-03 04:33:23 +0000
+++ b/doc/en/release-notes/bzr-2.4.txt 2011-03-03 06:02:49 +0000
@@ -44,6 +44,10 @@
the dirstate file to be rebuilt, rather than using a ``bzr checkout``
workaround. (John Arbash Meinel)
+* Added a ``Branch.heads_to_fetch`` RPC to the smart server protocol.
+ This allows formats from plugins (such as looms) to efficiently tell the
+ client which revisions need to be fetched. (Andrew Bennetts)
+
* Branching, merging and pulling a branch now copies revisions named in
tags, not just the tag metadata. (Andrew Bennetts, #309682)
@@ -133,6 +137,9 @@
.. Changes that may require updates in plugins or other code that uses
bzrlib.
+* Added ``Branch.heads_to_fetch`` method. Implementions of the Branch API
+ must now inherit or implement this method. (Andrew Bennetts, #721328)
+
* Added ``bzrlib.mergetools`` module with helper functions for working with
the list of external merge tools. (Gordon Tyler, #489915)
More information about the bazaar-commits
mailing list