Merge lp:~spiv/bzr/fetch-all-tags-309682 into lp:bzr

Proposed by Andrew Bennetts
Status: Merged
Merged at revision: 5648
Proposed branch: lp:~spiv/bzr/fetch-all-tags-309682
Merge into: lp:bzr
Prerequisite: lp:~spiv/bzr/fetch-integration
Diff against target: 776 lines (+339/-98)
15 files modified
bzrlib/controldir.py (+58/-49)
bzrlib/fetch.py (+93/-17)
bzrlib/graph.py (+8/-6)
bzrlib/remote.py (+3/-3)
bzrlib/repofmt/knitrepo.py (+1/-1)
bzrlib/repofmt/weaverepo.py (+1/-1)
bzrlib/repository.py (+9/-9)
bzrlib/tests/blackbox/test_branch.py (+33/-2)
bzrlib/tests/per_branch/test_sprout.py (+19/-0)
bzrlib/tests/per_controldir/test_controldir.py (+101/-1)
bzrlib/tests/per_interrepository/test_interrepository.py (+1/-1)
bzrlib/tests/test_bzrdir.py (+4/-0)
bzrlib/tests/test_remote.py (+3/-3)
doc/en/release-notes/bzr-2.3.txt (+0/-5)
doc/en/release-notes/bzr-2.4.txt (+5/-0)
To merge this branch: bzr merge lp:~spiv/bzr/fetch-all-tags-309682
Reviewer Review Type Date Requested Status
John A Meinel Needs Fixing
Review via email: mp+42910@code.launchpad.net

Commit message

'bzr branch' now fetches revisions referred to by tags.

Description of the change

This branch fixes <https://bugs.launchpad.net/bzr/+bug/309682>: bzr branch should copy revisions referred to in tags, not just the tip.

The main difficulty is the ugliness of sprout and to a lesser extent fetch. We don't want fetching the tags to regress our performance: i.e. no extra roundtrips for redundant locking/unlocking/locking again, no multiple calls to fetch for one sprout, no errors from bzr branch simply because a tag refers to a ghost, etc. At the same time we don't want to make the code any more complex and harder to understand than it already is!

The prerequisite branch addresses much of that, so that this patch on its own is fairly small. (The prerequisite branch is simply lp:~spiv/bzr/sprout-does-not-reopen-repo and lp:~spiv/bzr/fetch-spec-everything-not-in-other merged together. Both of those are in the review queue.)

I've taken the approach of moving much of the complexity out of the sprout method, into more focussed methods and objects which are hopefully easy to understand isolation.

Part of the problem is that the interface of sprout is pretty confusing, as demonstrated by the one test failure I've currently got in this branch: bzrlib.tests.per_controldir.test_controldir.TestControlDir.test_sprout_bzrdir_repository_branch_only_source_under_shared

That test fails because it verifies that bzrdir.sprout(url) from a bzrdir with a branch in a shared repo copies the entire repository. I think this behaviour is wrong, because it's likely to surprise callers. A quick check on IRC suggests that lifeless didn't expect this behaviour either. And quick skim of the bzrlib source suggests that at least 'bzr switch --create-branch' is encountering this behaviour without intending it. So I intend to remove or adjust the test for the new behaviour, but I thought I should get some reviewer feedback first.

More generally, the interaction between the various combinations of passing or not passing revision_id and source_branch and various configurations of source branches and repositories is too complex. It's not a friendly interface. Unfortunately adding consideration of tags into the mix is probably only making it messier. FWIW the behaviour now is:

 * If a source branch is passed or found, only its tip and tags are fetched
   * if revision_id is passed, then it overrides the tip
 * otherwise if a revision_id is passed, then that is fetched
 * otherwise the whole repository is fetched.

I think much of the mess stems from the weirdness of sprouting a bzrdir rather than a repository or branch directly. I'm starting to think we should remove ControlDir.sprout.

Finally, while this addresses the case reported in bug 309682 (bzr branch), I think a complete fix requires that pull, merge, update, etc also fetch tags. I have a follow-up branch for that.

To post a comment you must log in.
Revision history for this message
John A Meinel (jameinel) wrote :
Download full text (9.0 KiB)

-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1

> I think much of the mess stems from the weirdness of sprouting a bzrdir rather than a repository or branch directly. I'm starting to think we should remove ControlDir.sprout.
>

I'm in favor of this. It will mess up our test suite a ton, because that
has become the defacto method of creating a new branch. However using:

tree.branch.sprout(new_branch) makes a lot more sense to me than
tree.bzrdir.sprout(new_something)

I think some of it stems from, do you want a WT in the target or not,
etc, etc. I would be happier with something that you can compose. This
is also cluttered because you can't just open the WT from a Branch, it
will re-open the branch, etc. (Note that there was also bugs with
branch.sprout() which didn't happen with bzrdir.sprout(), something with
Trees and merge, and double-locking the WT, IIRC)

Things we want to keep in mind:

a) In the ideal interface, we would have a way to copy any number of
   branches simultaneously into a target. This gives us a way to
   implement some form of Repository cloning that people would like.
   (Being able to grab the stack of a loom, or trunk+main features,
   etc.)
b) I think we do like the ability to branch from a source, and preserve
   the type. However, many extensions probably explicitly work around
   this (bzr branch svn:// is not creating an svn branch, same for
   hg/git/etc)

On 12/6/2010 11:28 PM, Andrew Bennetts wrote:
> Andrew Bennetts has proposed merging lp:~spiv/bzr/fetch-all-tags-309682 into lp:bzr with lp:~spiv/bzr/fetch-integration as a prerequisite.
>
> Requested reviews:
> bzr-core (bzr-core)
> Related bugs:
> #309682 tags are copied but their revisions may not be
> https://bugs.launchpad.net/bugs/309682
>
>
> This branch fixes <https://bugs.launchpad.net/bzr/+bug/309682>: bzr branch should copy revisions referred to in tags, not just the tip.
>
> The main difficulty is the ugliness of sprout and to a lesser extent fetch. We don't want fetching the tags to regress our performance: i.e. no extra roundtrips for redundant locking/unlocking/locking again, no multiple calls to fetch for one sprout, no errors from bzr branch simply because a tag refers to a ghost, etc. At the same time we don't want to make the code any more complex and harder to understand than it already is!
>
> The prerequisite branch addresses much of that, so that this patch on its own is fairly small. (The prerequisite branch is simply lp:~spiv/bzr/sprout-does-not-reopen-repo and lp:~spiv/bzr/fetch-spec-everything-not-in-other merged together. Both of those are in the review queue.)
>
> I've taken the approach of moving much of the complexity out of the sprout method, into more focussed methods and objects which are hopefully easy to understand isolation.
>
> Part of the problem is that the interface of sprout is pretty confusing, as demonstrated by the one test failure I've currently got in this branch: bzrlib.tests.per_controldir.test_controldir.TestControlDir.test_sprout_bzrdir_repository_branch_only_source_under_shared
>
> That test fails because it verifies that bzrdir.sprout(url) from a bzrdir with a branch in a shared repo co...

Read more...

review: Needs Fixing
Revision history for this message
Martin Pool (mbp) wrote :

On 15 December 2010 09:56, John A Meinel <email address hidden> wrote:
> Review: Needs Fixing
> -----BEGIN PGP SIGNED MESSAGE-----
> Hash: SHA1
>
>> I think much of the mess stems from the weirdness of sprouting a bzrdir rather than a repository or branch directly.  I'm starting to think we should remove ControlDir.sprout.
>>
>
> I'm in favor of this. It will mess up our test suite a ton, because that
> has become the defacto method of creating a new branch. However using:
>
> tree.branch.sprout(new_branch) makes a lot more sense to me than
> tree.bzrdir.sprout(new_something)
>
> I think some of it stems from, do you want a WT in the target or not,
> etc, etc. I would be happier with something that you can compose. This
> is also cluttered because you can't just open the WT from a Branch, it
> will re-open the branch, etc. (Note that there was also bugs with
> branch.sprout() which didn't happen with bzrdir.sprout(), something with
> Trees and merge, and double-locking the WT, IIRC)

+1 to removing it.

Years ago, Robert and I had the idea it would be good to be able to
just clone any object as it currently exists. So for a bzrdir, that
would imply copying the whole repository (if any), plus branches, etc.
 However I don't think that makes much sense give how things have now
evolved.

The term 'sprout' is intended to denote 'I'm making a new divergent
line of development' vs 'I'm copying exactly what's there'. At one
conceptual level there is a distinction. But we don't really expose
that in the UI; we don't use this name for it in the UI or user model;
and I'm not sure it makes sense to have as a separate method anyhow.

--
Martin

Revision history for this message
Andrew Bennetts (spiv) wrote :

Martin Pool wrote:
> >> I think much of the mess stems from the weirdness of sprouting a
> >> bzrdir rather than a repository or branch directly.  I'm starting
> >> to think we should remove ControlDir.sprout.
[...]
> +1 to removing it.
>
> Years ago, Robert and I had the idea it would be good to be able to
> just clone any object as it currently exists. So for a bzrdir, that
> would imply copying the whole repository (if any), plus branches, etc.
> However I don't think that makes much sense give how things have now
> evolved.

I agree that it doesn't make much sense now. It already produces
something that is not an exact clone, in hard to predict ways. e.g. it
will create a branch even if the source doesn't have one, and bzr-svn
chooses to make sprout from a SVN checkout produce a bzr-format bzrdir
rather than try to preserve format.

> The term 'sprout' is intended to denote 'I'm making a new divergent
> line of development' vs 'I'm copying exactly what's there'. At one
> conceptual level there is a distinction. But we don't really expose
> that in the UI; we don't use this name for it in the UI or user model;
> and I'm not sure it makes sense to have as a separate method anyhow.

I agree with this also.

Revision history for this message
Andrew Bennetts (spiv) wrote :
Download full text (7.3 KiB)

John A Meinel wrote:
[...]
> I think some of it stems from, do you want a WT in the target or not,
> etc, etc. I would be happier with something that you can compose. This
> is also cluttered because you can't just open the WT from a Branch, it
> will re-open the branch, etc. (Note that there was also bugs with
> branch.sprout() which didn't happen with bzrdir.sprout(), something with
> Trees and merge, and double-locking the WT, IIRC)

Perhaps we need an object that explicitly composes together all the components
of a controldir? i.e. rather than getting a controldir that you can call
open_branch etc on, and but then to go from the branch to the working tree you
need to reopen the working tree, you grab all the objects at once.

Without thinking about it too hard, maybe something like:

   bag_of_things = open_bag(location) # strawman name
   branch = bag_of_things.get_branch()
   wt = bag_of_things.get_workingtree() # does not reopen branch; it's opened
                                        # once and remembered for the lifetime
                                        # of the bag.

   [etc etc]

I'm not sure that's better, but I am sure the existing design isn't
great :)

The current API design makes it hard to avoid reopening the same object
over and over. Ideally the design would require you to go out of your way to
get two different in-memory objects representing the same on-disk
object, because that's usually not what you want.

> Things we want to keep in mind:
>
> a) In the ideal interface, we would have a way to copy any number of
> branches simultaneously into a target. This gives us a way to
> implement some form of Repository cloning that people would like.
> (Being able to grab the stack of a loom, or trunk+main features,
> etc.)

We at least have a workable way to do the fetch part of that operation,
but we certainly lack nice ways to find, open, copy or create sets of
branch objects.

[...]
> +class FetchSpecFactory(object):
[...]
> ^- I really prefer calling them "source_branch_stop_revision_id". or
> "source_tip_revision_id". I'd like the id to be in there, to make it
> clear that this is a string rev_id, not a tuple key, not a Revision
> object, etc.

Good point, I'll make that change.

> However, shouldn't it be "source_tip_revision_ids" (a list of strings).
> To allow more flexibility? We've often been hampered by having a single
> revision_id to work with.

Hmm. Probably, although I'm inclined to delay complicating this further
until we have concrete use cases to drive the design. YAGNI, in other
words :)

I wouldn't be surprised to find at the point someone refactors to add
that sort of feature that the result turns out to be a deeper change.

> I see that you have "explicit_rev_ids = set()" which isn't documented.

Hmm, it's class-private variable really, I'll prefix it with an
underscore.

> Is there a reason to keep "source_branch_stop_revision" at all?

Yes. It's used by
<https://code.launchpad.net/~spiv/bzr/fetch-tags-from-non-sprout-too/+merge/42911>,
to support operations like “bzr pull -r 99”. That fetch should not pull
revno 100. So source_branch_stop_revision is the variable that
overri...

Read more...

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'bzrlib/controldir.py'
2--- bzrlib/controldir.py 2011-01-26 19:34:58 +0000
3+++ bzrlib/controldir.py 2011-02-07 04:18:21 +0000
4@@ -29,6 +29,7 @@
5 from bzrlib import (
6 cleanup,
7 errors,
8+ fetch,
9 graph,
10 revision as _mod_revision,
11 transport as _mod_transport,
12@@ -378,53 +379,41 @@
13 accelerator_tree=None, hardlink=False, stacked=False,
14 source_branch=None, create_tree_if_local=True):
15 add_cleanup = op.add_cleanup
16+ fetch_spec_factory = fetch.FetchSpecFactory()
17+ if revision_id is not None:
18+ fetch_spec_factory.add_revision_ids([revision_id])
19+ fetch_spec_factory.source_branch_stop_revision_id = revision_id
20 target_transport = _mod_transport.get_transport(url,
21 possible_transports)
22 target_transport.ensure_base()
23 cloning_format = self.cloning_metadir(stacked)
24 # Create/update the result branch
25 result = cloning_format.initialize_on_transport(target_transport)
26+ source_branch, source_repository = self._find_source_repo(
27+ add_cleanup, source_branch)
28+ fetch_spec_factory.source_branch = source_branch
29 # if a stacked branch wasn't requested, we don't create one
30 # even if the origin was stacked
31- stacked_branch_url = None
32- if source_branch is not None:
33- add_cleanup(source_branch.lock_read().unlock)
34- if stacked:
35- stacked_branch_url = self.root_transport.base
36- source_repository = source_branch.repository
37+ if stacked and source_branch is not None:
38+ stacked_branch_url = self.root_transport.base
39 else:
40- try:
41- source_branch = self.open_branch()
42- source_repository = source_branch.repository
43- if stacked:
44- stacked_branch_url = self.root_transport.base
45- except errors.NotBranchError:
46- source_branch = None
47- try:
48- source_repository = self.open_repository()
49- except errors.NoRepositoryPresent:
50- source_repository = None
51- else:
52- add_cleanup(source_repository.lock_read().unlock)
53- else:
54- add_cleanup(source_branch.lock_read().unlock)
55+ stacked_branch_url = None
56 repository_policy = result.determine_repository_policy(
57 force_new_repo, stacked_branch_url, require_stacking=stacked)
58 result_repo, is_new_repo = repository_policy.acquire_repository()
59 add_cleanup(result_repo.lock_write().unlock)
60- is_stacked = stacked or (len(result_repo._fallback_repositories) != 0)
61- if is_new_repo and revision_id is not None and not is_stacked:
62- fetch_spec = graph.PendingAncestryResult(
63- [revision_id], source_repository)
64+ fetch_spec_factory.source_repo = source_repository
65+ fetch_spec_factory.target_repo = result_repo
66+ if stacked or (len(result_repo._fallback_repositories) != 0):
67+ target_repo_kind = fetch.TargetRepoKinds.STACKED
68+ elif is_new_repo:
69+ target_repo_kind = fetch.TargetRepoKinds.EMPTY
70 else:
71- fetch_spec = None
72+ target_repo_kind = fetch.TargetRepoKinds.PREEXISTING
73+ fetch_spec_factory.target_repo_kind = target_repo_kind
74 if source_repository is not None:
75- # Fetch while stacked to prevent unstacked fetch from
76- # Branch.sprout.
77- if fetch_spec is None:
78- result_repo.fetch(source_repository, revision_id=revision_id)
79- else:
80- result_repo.fetch(source_repository, fetch_spec=fetch_spec)
81+ fetch_spec = fetch_spec_factory.make_fetch_spec()
82+ result_repo.fetch(source_repository, fetch_spec=fetch_spec)
83
84 if source_branch is None:
85 # this is for sprouting a controldir without a branch; is that
86@@ -455,34 +444,54 @@
87 else:
88 wt = None
89 if recurse == 'down':
90+ basis = None
91 if wt is not None:
92 basis = wt.basis_tree()
93- basis.lock_read()
94- subtrees = basis.iter_references()
95 elif result_branch is not None:
96 basis = result_branch.basis_tree()
97- basis.lock_read()
98- subtrees = basis.iter_references()
99 elif source_branch is not None:
100 basis = source_branch.basis_tree()
101- basis.lock_read()
102+ if basis is not None:
103+ add_cleanup(basis.lock_read().unlock)
104 subtrees = basis.iter_references()
105 else:
106 subtrees = []
107- basis = None
108- try:
109- for path, file_id in subtrees:
110- target = urlutils.join(url, urlutils.escape(path))
111- sublocation = source_branch.reference_parent(file_id, path)
112- sublocation.bzrdir.sprout(target,
113- basis.get_reference_revision(file_id, path),
114- force_new_repo=force_new_repo, recurse=recurse,
115- stacked=stacked)
116- finally:
117- if basis is not None:
118- basis.unlock()
119+ for path, file_id in subtrees:
120+ target = urlutils.join(url, urlutils.escape(path))
121+ sublocation = source_branch.reference_parent(file_id, path)
122+ sublocation.bzrdir.sprout(target,
123+ basis.get_reference_revision(file_id, path),
124+ force_new_repo=force_new_repo, recurse=recurse,
125+ stacked=stacked)
126 return result
127
128+ def _find_source_repo(self, add_cleanup, source_branch):
129+ """Find the source branch and repo for a sprout operation.
130+
131+ This is helper intended for use by _sprout.
132+
133+ :returns: (source_branch, source_repository). Either or both may be
134+ None. If not None, they will be read-locked (and their unlock(s)
135+ scheduled via the add_cleanup param).
136+ """
137+ if source_branch is not None:
138+ add_cleanup(source_branch.lock_read().unlock)
139+ return source_branch, source_branch.repository
140+ try:
141+ source_branch = self.open_branch()
142+ source_repository = source_branch.repository
143+ except errors.NotBranchError:
144+ source_branch = None
145+ try:
146+ source_repository = self.open_repository()
147+ except errors.NoRepositoryPresent:
148+ source_repository = None
149+ else:
150+ add_cleanup(source_repository.lock_read().unlock)
151+ else:
152+ add_cleanup(source_branch.lock_read().unlock)
153+ return source_branch, source_repository
154+
155 def push_branch(self, source, revision_id=None, overwrite=False,
156 remember=False, create_prefix=False):
157 """Push the source branch into this ControlDir."""
158
159=== modified file 'bzrlib/fetch.py'
160--- bzrlib/fetch.py 2011-01-14 17:18:23 +0000
161+++ bzrlib/fetch.py 2011-02-07 04:18:21 +0000
162@@ -55,6 +55,8 @@
163
164 :param last_revision: If set, try to limit to the data this revision
165 references.
166+ :param fetch_spec: A SearchResult specifying which revisions to fetch.
167+ If set, this overrides last_revision.
168 :param find_ghosts: If True search the entire history for ghosts.
169 """
170 # repository.fetch has the responsibility for short-circuiting
171@@ -93,12 +95,12 @@
172 pb.show_pct = pb.show_count = False
173 try:
174 pb.update("Finding revisions", 0, 2)
175- search = self._revids_to_fetch()
176- mutter('fetching: %s', search)
177- if search.is_empty():
178+ search_result = self._revids_to_fetch()
179+ mutter('fetching: %s', search_result)
180+ if search_result.is_empty():
181 return
182 pb.update("Fetching revisions", 1, 2)
183- self._fetch_everything_for_search(search)
184+ self._fetch_everything_for_search(search_result)
185 finally:
186 pb.finished()
187
188@@ -151,18 +153,9 @@
189 install self._last_revision in self.to_repository.
190
191 :returns: A SearchResult of some sort. (Possibly a
192- PendingAncestryResult, EmptySearchResult, etc.)
193+ PendingAncestryResult, EmptySearchResult, etc.)
194 """
195- mutter("self._fetch_spec, self._last_revision: %r, %r",
196- self._fetch_spec, self._last_revision)
197- get_search_result = getattr(self._fetch_spec, 'get_search_result', None)
198- if get_search_result is not None:
199- mutter(
200- 'resolving fetch_spec into search result: %s', self._fetch_spec)
201- # This is EverythingNotInOther or a similar kind of fetch_spec.
202- # Turn it into a search result.
203- return get_search_result()
204- elif self._fetch_spec is not None:
205+ if self._fetch_spec is not None:
206 # The fetch spec is already a concrete search result.
207 return self._fetch_spec
208 elif self._last_revision == NULL_REVISION:
209@@ -172,11 +165,11 @@
210 elif self._last_revision is not None:
211 return graph.NotInOtherForRevs(self.to_repository,
212 self.from_repository, [self._last_revision],
213- find_ghosts=self.find_ghosts).get_search_result()
214+ find_ghosts=self.find_ghosts).execute()
215 else: # self._last_revision is None:
216 return graph.EverythingNotInOther(self.to_repository,
217 self.from_repository,
218- find_ghosts=self.find_ghosts).get_search_result()
219+ find_ghosts=self.find_ghosts).execute()
220
221
222 class Inter1and2Helper(object):
223@@ -339,3 +332,86 @@
224 selected_ids.append(parent_id)
225 parent_keys = [(root_id, parent_id) for parent_id in selected_ids]
226 return parent_keys
227+
228+
229+class TargetRepoKinds(object):
230+ """An enum-like set of constants.
231+
232+ They are the possible values of FetchSpecFactory.target_repo_kinds.
233+ """
234+
235+ PREEXISTING = 'preexisting'
236+ STACKED = 'stacked'
237+ EMPTY = 'empty'
238+
239+
240+class FetchSpecFactory(object):
241+ """A helper for building the best fetch spec for a sprout call.
242+
243+ Factors that go into determining the sort of fetch to perform:
244+ * did the caller specify any revision IDs?
245+ * did the caller specify a source branch (need to fetch the tip + tags)
246+ * is there an existing target repo (don't need to refetch revs it
247+ already has)
248+ * target is stacked? (similar to pre-existing target repo: even if
249+ the target itself is new don't want to refetch existing revs)
250+
251+ :ivar source_branch: the source branch if one specified, else None.
252+ :ivar source_branch_stop_revision_id: fetch up to this revision of
253+ source_branch, rather than its tip.
254+ :ivar source_repo: the source repository if one found, else None.
255+ :ivar target_repo: the target repository acquired by sprout.
256+ :ivar target_repo_kind: one of the TargetRepoKinds constants.
257+ """
258+
259+ def __init__(self):
260+ self._explicit_rev_ids = set()
261+ self.source_branch = None
262+ self.source_branch_stop_revision_id = None
263+ self.source_repo = None
264+ self.target_repo = None
265+ self.target_repo_kind = None
266+
267+ def add_revision_ids(self, revision_ids):
268+ """Add revision_ids to the set of revision_ids to be fetched."""
269+ self._explicit_rev_ids.update(revision_ids)
270+
271+ def make_fetch_spec(self):
272+ """Build a SearchResult or PendingAncestryResult or etc."""
273+ if self.target_repo_kind is None or self.source_repo is None:
274+ raise AssertionError(
275+ 'Incomplete FetchSpecFactory: %r' % (self.__dict__,))
276+ if len(self._explicit_rev_ids) == 0 and self.source_branch is None:
277+ # Caller hasn't specified any revisions or source branch
278+ if self.target_repo_kind == TargetRepoKinds.EMPTY:
279+ return graph.EverythingResult(self.source_repo)
280+ else:
281+ # We want everything not already in the target (or target's
282+ # fallbacks).
283+ return graph.EverythingNotInOther(
284+ self.target_repo, self.source_repo).execute()
285+ heads_to_fetch = set(self._explicit_rev_ids)
286+ tags_to_fetch = set()
287+ if self.source_branch is not None:
288+ try:
289+ tags_to_fetch.update(
290+ self.source_branch.tags.get_reverse_tag_dict())
291+ except errors.TagsNotSupported:
292+ pass
293+ if self.source_branch_stop_revision_id is not None:
294+ heads_to_fetch.add(self.source_branch_stop_revision_id)
295+ else:
296+ heads_to_fetch.add(self.source_branch.last_revision())
297+ if self.target_repo_kind == TargetRepoKinds.EMPTY:
298+ # PendingAncestryResult does not raise errors if a requested head
299+ # is absent. Ideally it would support the
300+ # required_ids/if_present_ids distinction, but in practice
301+ # heads_to_fetch will almost certainly be present so this doesn't
302+ # matter much.
303+ all_heads = heads_to_fetch.union(tags_to_fetch)
304+ return graph.PendingAncestryResult(all_heads, self.source_repo)
305+ return graph.NotInOtherForRevs(self.target_repo, self.source_repo,
306+ required_ids=heads_to_fetch, if_present_ids=tags_to_fetch
307+ ).execute()
308+
309+
310
311=== modified file 'bzrlib/graph.py'
312--- bzrlib/graph.py 2011-01-14 17:18:23 +0000
313+++ bzrlib/graph.py 2011-02-07 04:18:21 +0000
314@@ -1576,14 +1576,15 @@
315
316 class AbstractSearch(object):
317
318- def get_search_result(self):
319+ def execute(self):
320 """Construct a network-ready search result from this search description.
321
322 This may take some time to search repositories, etc.
323
324- :return: A search result.
325+ :return: A search result (an object that implements
326+ AbstractSearchResult's API).
327 """
328- raise NotImplementedError(self.get_search_result)
329+ raise NotImplementedError(self.execute)
330
331
332 class SearchResult(AbstractSearchResult):
333@@ -1705,7 +1706,8 @@
334
335 def __repr__(self):
336 if len(self.heads) > 5:
337- heads_repr = repr(list(self.heads)[:5] + ', ...]')
338+ heads_repr = repr(list(self.heads)[:5])[:-1]
339+ heads_repr += ', <%d more>...]' % (len(self.heads) - 5,)
340 else:
341 heads_repr = repr(self.heads)
342 return '<%s heads:%s repo:%r>' % (
343@@ -1813,7 +1815,7 @@
344 self.from_repo = from_repo
345 self.find_ghosts = find_ghosts
346
347- def get_search_result(self):
348+ def execute(self):
349 return self.to_repo.search_missing_revision_ids(
350 self.from_repo, find_ghosts=self.find_ghosts)
351
352@@ -1853,7 +1855,7 @@
353 self.__class__.__name__, self.from_repo, self.to_repo,
354 self.find_ghosts, reqd_revs_repr, ifp_revs_repr)
355
356- def get_search_result(self):
357+ def execute(self):
358 return self.to_repo.search_missing_revision_ids(
359 self.from_repo, revision_ids=self.required_ids,
360 if_present_ids=self.if_present_ids, find_ghosts=self.find_ghosts)
361
362=== modified file 'bzrlib/remote.py'
363--- bzrlib/remote.py 2011-01-14 17:18:23 +0000
364+++ bzrlib/remote.py 2011-02-07 04:18:21 +0000
365@@ -1360,7 +1360,7 @@
366 if symbol_versioning.deprecated_passed(revision_id):
367 symbol_versioning.warn(
368 'search_missing_revision_ids(revision_id=...) was '
369- 'deprecated in 2.3. Use revision_ids=[...] instead.',
370+ 'deprecated in 2.4. Use revision_ids=[...] instead.',
371 DeprecationWarning, stacklevel=2)
372 if revision_ids is not None:
373 raise AssertionError(
374@@ -1991,11 +1991,11 @@
375 if isinstance(search, graph.EverythingResult):
376 error_verb = e.error_from_smart_server.error_verb
377 if error_verb == 'BadSearch':
378- # Pre-2.3 servers don't support this sort of search.
379+ # Pre-2.4 servers don't support this sort of search.
380 # XXX: perhaps falling back to VFS on BadSearch is a
381 # good idea in general? It might provide a little bit
382 # of protection against client-side bugs.
383- medium._remember_remote_is_before((2, 3))
384+ medium._remember_remote_is_before((2, 4))
385 break
386 raise
387 else:
388
389=== modified file 'bzrlib/repofmt/knitrepo.py'
390--- bzrlib/repofmt/knitrepo.py 2010-12-21 06:10:11 +0000
391+++ bzrlib/repofmt/knitrepo.py 2011-02-07 04:18:21 +0000
392@@ -542,7 +542,7 @@
393 if symbol_versioning.deprecated_passed(revision_id):
394 symbol_versioning.warn(
395 'search_missing_revision_ids(revision_id=...) was '
396- 'deprecated in 2.3. Use revision_ids=[...] instead.',
397+ 'deprecated in 2.4. Use revision_ids=[...] instead.',
398 DeprecationWarning, stacklevel=2)
399 if revision_ids is not None:
400 raise AssertionError(
401
402=== modified file 'bzrlib/repofmt/weaverepo.py'
403--- bzrlib/repofmt/weaverepo.py 2011-01-14 17:18:23 +0000
404+++ bzrlib/repofmt/weaverepo.py 2011-02-07 04:18:21 +0000
405@@ -826,7 +826,7 @@
406 if symbol_versioning.deprecated_passed(revision_id):
407 symbol_versioning.warn(
408 'search_missing_revision_ids(revision_id=...) was '
409- 'deprecated in 2.3. Use revision_ids=[...] instead.',
410+ 'deprecated in 2.4. Use revision_ids=[...] instead.',
411 DeprecationWarning, stacklevel=2)
412 if revision_ids is not None:
413 raise AssertionError(
414
415=== modified file 'bzrlib/repository.py'
416--- bzrlib/repository.py 2011-01-19 23:00:12 +0000
417+++ bzrlib/repository.py 2011-02-07 04:18:21 +0000
418@@ -1608,7 +1608,7 @@
419 if symbol_versioning.deprecated_passed(revision_id):
420 symbol_versioning.warn(
421 'search_missing_revision_ids(revision_id=...) was '
422- 'deprecated in 2.3. Use revision_ids=[...] instead.',
423+ 'deprecated in 2.4. Use revision_ids=[...] instead.',
424 DeprecationWarning, stacklevel=3)
425 if revision_ids is not None:
426 raise AssertionError(
427@@ -1785,9 +1785,6 @@
428 not _mod_revision.is_null(revision_id)):
429 self.get_revision(revision_id)
430 return 0, []
431- # if there is no specific appropriate InterRepository, this will get
432- # the InterRepository base class, which raises an
433- # IncompatibleRepositories when asked to fetch.
434 inter = InterRepository.get(source, self)
435 return inter.fetch(revision_id=revision_id, pb=pb,
436 find_ghosts=find_ghosts, fetch_spec=fetch_spec)
437@@ -3482,8 +3479,12 @@
438 # them, make sure that they are present in the target.
439 # We don't care about other ghosts as we can't fetch them and
440 # haven't been asked to.
441+ mutter('reqd: %r if-present: %r -> ghosts: %r', revision_ids,
442+ if_present_ids, ghosts)
443 ghosts_to_check = set(revision_ids.intersection(ghosts))
444 revs_to_get = set(next_revs).union(ghosts_to_check)
445+ mutter('ghosts_to_check: %r revs_to_get: %r searcher_exhausted: %r',
446+ ghosts_to_check, revs_to_get, searcher_exhausted)
447 if revs_to_get:
448 have_revs = set(target_graph.get_parent_map(revs_to_get))
449 # we always have NULL_REVISION present.
450@@ -3526,7 +3527,7 @@
451 if symbol_versioning.deprecated_passed(revision_id):
452 symbol_versioning.warn(
453 'search_missing_revision_ids(revision_id=...) was '
454- 'deprecated in 2.3. Use revision_ids=[...] instead.',
455+ 'deprecated in 2.4. Use revision_ids=[...] instead.',
456 DeprecationWarning, stacklevel=2)
457 if revision_ids is not None:
458 raise AssertionError(
459@@ -3901,10 +3902,9 @@
460 fetch_spec=None):
461 """See InterRepository.fetch()."""
462 if fetch_spec is not None:
463- if (isinstance(fetch_spec, graph.NotInOtherForRevs) and
464- len(fetch_spec.required_ids) == 1 and not
465- fetch_spec.if_present_ids):
466- revision_id = list(fetch_spec.required_ids)[0]
467+ if (isinstance(fetch_spec, graph.SearchResult) and
468+ len(fetch_spec.get_keys()) == 1):
469+ revision_id = list(fetch_spec.get_keys())[0]
470 del fetch_spec
471 else:
472 raise AssertionError("Not implemented yet...")
473
474=== modified file 'bzrlib/tests/blackbox/test_branch.py'
475--- bzrlib/tests/blackbox/test_branch.py 2011-01-10 22:20:12 +0000
476+++ bzrlib/tests/blackbox/test_branch.py 2011-02-07 04:18:21 +0000
477@@ -23,13 +23,11 @@
478 branch,
479 bzrdir,
480 errors,
481- repository,
482 revision as _mod_revision,
483 )
484 from bzrlib.repofmt.knitrepo import RepositoryFormatKnit1
485 from bzrlib.tests import TestCaseWithTransport
486 from bzrlib.tests import (
487- KnownFailure,
488 HardlinkFeature,
489 test_server,
490 )
491@@ -270,6 +268,20 @@
492 self.run_bzr('checkout --lightweight a b')
493 self.assertLength(2, calls)
494
495+ def test_branch_fetches_all_tags(self):
496+ builder = self.make_branch_builder('source')
497+ builder.build_commit(message="Rev 1", rev_id='rev-1')
498+ builder.build_commit(message="Rev 2", rev_id='rev-2')
499+ source = builder.get_branch()
500+ source.tags.set_tag('tag-a', 'rev-2')
501+ source.set_last_revision_info(1, 'rev-1')
502+ # Now source has a tag not in its ancestry. Make a branch from it.
503+ self.run_bzr('branch source new-branch')
504+ new_branch = branch.Branch.open('new-branch')
505+ # The tag is present, and so is its revision.
506+ self.assertEqual('rev-2', new_branch.tags.lookup_tag('tag-a'))
507+ new_branch.repository.get_revision('rev-2')
508+
509
510 class TestBranchStacked(TestCaseWithTransport):
511 """Tests for branch --stacked"""
512@@ -451,6 +463,25 @@
513 # upwards without agreement from bzr's network support maintainers.
514 self.assertLength(14, self.hpss_calls)
515
516+ def test_branch_from_branch_with_tags(self):
517+ self.setup_smart_server_with_call_log()
518+ builder = self.make_branch_builder('source')
519+ builder.build_commit(message="Rev 1", rev_id='rev-1')
520+ builder.build_commit(message="Rev 2", rev_id='rev-2')
521+ source = builder.get_branch()
522+ source.tags.set_tag('tag-a', 'rev-2')
523+ source.tags.set_tag('tag-missing', 'missing-rev')
524+ source.set_last_revision_info(1, 'rev-1')
525+ # Now source has a tag not in its ancestry. Make a branch from it.
526+ self.reset_smart_call_log()
527+ out, err = self.run_bzr(['branch', self.get_url('source'), 'target'])
528+ # This figure represent the amount of work to perform this use case. It
529+ # is entirely ok to reduce this number if a test fails due to rpc_count
530+ # being too low. If rpc_count increases, more network roundtrips have
531+ # become necessary for this use case. Please do not adjust this number
532+ # upwards without agreement from bzr's network support maintainers.
533+ self.assertLength(9, self.hpss_calls)
534+
535
536 class TestRemoteBranch(TestCaseWithSFTPServer):
537
538
539=== modified file 'bzrlib/tests/per_branch/test_sprout.py'
540--- bzrlib/tests/per_branch/test_sprout.py 2011-01-13 05:36:39 +0000
541+++ bzrlib/tests/per_branch/test_sprout.py 2011-02-07 04:18:21 +0000
542@@ -111,6 +111,25 @@
543 self.assertEqual((2, 'rev2-alt'), branch2.last_revision_info())
544 self.assertEqual(['rev1', 'rev2-alt'], branch2.revision_history())
545
546+ def test_sprout_preserves_tags(self):
547+ """Sprout preserves tags, even tags of absent revisions."""
548+ try:
549+ builder = self.make_branch_builder('source')
550+ except errors.UninitializableFormat:
551+ raise tests.TestSkipped('Uninitializable branch format')
552+ builder.build_commit(message="Rev 1", rev_id='rev-1')
553+ source = builder.get_branch()
554+ try:
555+ source.tags.set_tag('tag-a', 'missing-rev')
556+ except errors.TagsNotSupported:
557+ raise tests.TestNotApplicable(
558+ 'Branch format does not support tags.')
559+ # Now source has a tag pointing to an absent revision. Sprout it.
560+ target_bzrdir = self.make_repository('target').bzrdir
561+ new_branch = source.sprout(target_bzrdir)
562+ # The tag is present in the target
563+ self.assertEqual('missing-rev', new_branch.tags.lookup_tag('tag-a'))
564+
565 def test_sprout_from_any_repo_revision(self):
566 """We should be able to sprout from any revision."""
567 wt = self.make_branch_and_tree('source')
568
569=== modified file 'bzrlib/tests/per_controldir/test_controldir.py'
570--- bzrlib/tests/per_controldir/test_controldir.py 2011-01-27 14:27:18 +0000
571+++ bzrlib/tests/per_controldir/test_controldir.py 2011-02-07 04:18:21 +0000
572@@ -503,7 +503,10 @@
573 self.assertNotEqual(dir.transport.base, target.transport.base)
574 self.assertNotEqual(dir.transport.base, shared_repo.bzrdir.transport.base)
575 branch = target.open_branch()
576- self.assertTrue(branch.repository.has_revision('1'))
577+ # The sprouted bzrdir has a branch, so only revisions referenced by
578+ # that branch are copied, rather than the whole repository. It's an
579+ # empty branch, so none are copied.
580+ self.assertEqual([], branch.repository.all_revision_ids())
581 if branch.bzrdir._format.supports_workingtrees:
582 self.assertTrue(branch.repository.make_working_trees())
583 self.assertFalse(branch.repository.is_shared())
584@@ -669,6 +672,103 @@
585 target = dir.sprout(self.get_url('target'), revision_id='1')
586 self.assertEqual('1', target.open_branch().last_revision())
587
588+ def test_sprout_bzrdir_branch_with_tags(self):
589+ # when sprouting a branch all revisions named in the tags are copied
590+ # too.
591+ builder = self.make_branch_builder('source')
592+ builder.build_commit(message="Rev 1", rev_id='rev-1')
593+ builder.build_commit(message="Rev 2", rev_id='rev-2')
594+ source = builder.get_branch()
595+ try:
596+ source.tags.set_tag('tag-a', 'rev-2')
597+ except errors.TagsNotSupported:
598+ raise TestNotApplicable('Branch format does not support tags.')
599+ source.set_last_revision_info(1, 'rev-1')
600+ # Now source has a tag not in its ancestry. Sprout its controldir.
601+ dir = source.bzrdir
602+ target = dir.sprout(self.get_url('target'))
603+ # The tag is present, and so is its revision.
604+ new_branch = target.open_branch()
605+ self.assertEqual('rev-2', new_branch.tags.lookup_tag('tag-a'))
606+ new_branch.repository.get_revision('rev-2')
607+
608+ def test_sprout_bzrdir_branch_with_absent_tag(self):
609+ # tags referencing absent revisions are copied (and those absent
610+ # revisions do not prevent the sprout.)
611+ builder = self.make_branch_builder('source')
612+ builder.build_commit(message="Rev 1", rev_id='rev-1')
613+ source = builder.get_branch()
614+ try:
615+ source.tags.set_tag('tag-a', 'missing-rev')
616+ except errors.TagsNotSupported:
617+ raise TestNotApplicable('Branch format does not support tags.')
618+ # Now source has a tag pointing to an absent revision. Sprout its
619+ # controldir.
620+ dir = source.bzrdir
621+ target = dir.sprout(self.get_url('target'))
622+ # The tag is present in the target
623+ new_branch = target.open_branch()
624+ self.assertEqual('missing-rev', new_branch.tags.lookup_tag('tag-a'))
625+
626+ def test_sprout_bzrdir_passing_source_branch_with_absent_tag(self):
627+ # tags referencing absent revisions are copied (and those absent
628+ # revisions do not prevent the sprout.)
629+ builder = self.make_branch_builder('source')
630+ builder.build_commit(message="Rev 1", rev_id='rev-1')
631+ source = builder.get_branch()
632+ try:
633+ source.tags.set_tag('tag-a', 'missing-rev')
634+ except errors.TagsNotSupported:
635+ raise TestNotApplicable('Branch format does not support tags.')
636+ # Now source has a tag pointing to an absent revision. Sprout its
637+ # controldir.
638+ dir = source.bzrdir
639+ target = dir.sprout(self.get_url('target'), source_branch=source)
640+ # The tag is present in the target
641+ new_branch = target.open_branch()
642+ self.assertEqual('missing-rev', new_branch.tags.lookup_tag('tag-a'))
643+
644+ def test_sprout_bzrdir_passing_rev_not_source_branch_copies_tags(self):
645+ # dir.sprout(..., revision_id='rev1') copies rev1, and all the tags of
646+ # the branch at that bzrdir, the ancestry of all of those, but no other
647+ # revs (not even the tip of the source branch).
648+ builder = self.make_branch_builder('source')
649+ builder.build_commit(message="Base", rev_id='base-rev')
650+ # Make three parallel lines of ancestry off this base.
651+ source = builder.get_branch()
652+ builder.build_commit(message="Rev A1", rev_id='rev-a1')
653+ builder.build_commit(message="Rev A2", rev_id='rev-a2')
654+ builder.build_commit(message="Rev A3", rev_id='rev-a3')
655+ source.set_last_revision_info(1, 'base-rev')
656+ builder.build_commit(message="Rev B1", rev_id='rev-b1')
657+ builder.build_commit(message="Rev B2", rev_id='rev-b2')
658+ builder.build_commit(message="Rev B3", rev_id='rev-b3')
659+ source.set_last_revision_info(1, 'base-rev')
660+ builder.build_commit(message="Rev C1", rev_id='rev-c1')
661+ builder.build_commit(message="Rev C2", rev_id='rev-c2')
662+ builder.build_commit(message="Rev C3", rev_id='rev-c3')
663+ # Set the branch tip to A2
664+ source.set_last_revision_info(3, 'rev-a2')
665+ try:
666+ # Create a tag for B2, and for an absent rev
667+ source.tags.set_tag('tag-non-ancestry', 'rev-b2')
668+ source.tags.set_tag('tag-absent', 'absent-rev')
669+ except errors.TagsNotSupported:
670+ raise TestNotApplicable('Branch format does not support tags.')
671+ # And ask sprout for C2
672+ dir = source.bzrdir
673+ target = dir.sprout(self.get_url('target'), revision_id='rev-c2')
674+ # The tags are present
675+ new_branch = target.open_branch()
676+ self.assertEqual(
677+ {'tag-absent': 'absent-rev', 'tag-non-ancestry': 'rev-b2'},
678+ new_branch.tags.get_tag_dict())
679+ # And the revs for A2, B2 and C2's ancestries are present, but no
680+ # others.
681+ self.assertEqual(
682+ ['base-rev', 'rev-b1', 'rev-b2', 'rev-c1', 'rev-c2'],
683+ sorted(new_branch.repository.all_revision_ids()))
684+
685 def test_sprout_bzrdir_tree_branch_reference(self):
686 # sprouting should create a repository if needed and a sprouted branch.
687 # the tree state should not be copied.
688
689=== modified file 'bzrlib/tests/per_interrepository/test_interrepository.py'
690--- bzrlib/tests/per_interrepository/test_interrepository.py 2011-01-14 17:18:23 +0000
691+++ bzrlib/tests/per_interrepository/test_interrepository.py 2011-02-07 04:18:21 +0000
692@@ -138,7 +138,7 @@
693 find_ghosts=False)
694 self.callDeprecated(
695 ['search_missing_revision_ids(revision_id=...) was deprecated in '
696- '2.3. Use revision_ids=[...] instead.'],
697+ '2.4. Use revision_ids=[...] instead.'],
698 self.assertRaises, errors.NoSuchRevision,
699 repo_b.search_missing_revision_ids, repo_a, revision_id='pizza',
700 find_ghosts=False)
701
702=== modified file 'bzrlib/tests/test_bzrdir.py'
703--- bzrlib/tests/test_bzrdir.py 2011-01-27 14:27:18 +0000
704+++ bzrlib/tests/test_bzrdir.py 2011-02-07 04:18:21 +0000
705@@ -31,6 +31,7 @@
706 help_topics,
707 lock,
708 repository,
709+ revision as _mod_revision,
710 osutils,
711 remote,
712 symbol_versioning,
713@@ -1341,6 +1342,9 @@
714 def copy_content_into(self, destination, revision_id=None):
715 self.calls.append('copy_content_into')
716
717+ def last_revision(self):
718+ return _mod_revision.NULL_REVISION
719+
720 def get_parent(self):
721 return self._parent
722
723
724=== modified file 'bzrlib/tests/test_remote.py'
725--- bzrlib/tests/test_remote.py 2011-01-14 17:18:23 +0000
726+++ bzrlib/tests/test_remote.py 2011-02-07 04:18:21 +0000
727@@ -3211,15 +3211,15 @@
728 override_existing=True)
729
730 def test_fetch_everything_backwards_compat(self):
731- """Can fetch with EverythingResult even with pre 2.3 servers.
732+ """Can fetch with EverythingResult even with pre 2.4 servers.
733
734- Pre-2.3 do not support 'everything' searches with the
735+ Pre-2.4 do not support 'everything' searches with the
736 Repository.get_stream_1.19 verb.
737 """
738 verb_log = []
739 class OldGetStreamVerb(SmartServerRepositoryGetStream_1_19):
740 """A version of the Repository.get_stream_1.19 verb patched to
741- reject 'everything' searches the way 2.2 and earlier do.
742+ reject 'everything' searches the way 2.3 and earlier do.
743 """
744 def recreate_search(self, repository, search_bytes, discard_excess=False):
745 verb_log.append(search_bytes.split('\n', 1)[0])
746
747=== modified file 'doc/en/release-notes/bzr-2.3.txt'
748--- doc/en/release-notes/bzr-2.3.txt 2011-02-03 05:13:46 +0000
749+++ doc/en/release-notes/bzr-2.3.txt 2011-02-07 04:18:21 +0000
750@@ -275,11 +275,6 @@
751 crashes when encountering private bugs (they are just displayed as such).
752 (Vincent Ladeuil, #354985)
753
754-* The ``revision_id`` parameter of
755- ``Repository.search_missing_revision_ids`` and
756- ``InterRepository.search_missing_revision_ids`` is deprecated. It is
757- replaced by the ``revision_ids`` parameter. (Andrew Bennetts)
758-
759 Internals
760 *********
761
762
763=== modified file 'doc/en/release-notes/bzr-2.4.txt'
764--- doc/en/release-notes/bzr-2.4.txt 2011-02-03 05:46:08 +0000
765+++ doc/en/release-notes/bzr-2.4.txt 2011-02-07 04:18:21 +0000
766@@ -94,6 +94,11 @@
767 * Added ``bzrlib.mergetools`` module with helper functions for working with
768 the list of external merge tools. (Gordon Tyler, #489915)
769
770+* The ``revision_id`` parameter of
771+ ``Repository.search_missing_revision_ids`` and
772+ ``InterRepository.search_missing_revision_ids`` is deprecated. It is
773+ replaced by the ``revision_ids`` parameter. (Andrew Bennetts)
774+
775 Internals
776 *********
777