Merge lp:~mbp/bzr/391411-reconfigure-stacked into lp:~bzr/bzr/trunk-old

Proposed by Martin Pool
Status: Merged
Merged at revision: not available
Proposed branch: lp:~mbp/bzr/391411-reconfigure-stacked
Merge into: lp:~bzr/bzr/trunk-old
Diff against target: 656 lines
To merge this branch: bzr merge lp:~mbp/bzr/391411-reconfigure-stacked
Reviewer Review Type Date Requested Status
Ian Clatworthy Needs Fixing
Review via email: mp+8527@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Martin Pool (mbp) wrote :

This adds reconfigure --stacked-on and --unstacked options, and fixes a bug in fetching data when unstacking that made it break in all but trivial cases of empty branches.

I've included here part of the -Dunlock code because it made debugging this much easier.

It also adds a RepositoryBase so that we can have at least some common code with RemoteRepository.

Revision history for this message
Ian Clatworthy (ian-clatworthy) wrote :

Martin Pool wrote:
> Martin Pool has proposed merging lp:~mbp/bzr/391411-reconfigure-stacked into lp:bzr.
>
> Requested reviews:
> bzr-core (bzr-core)
>
> This adds reconfigure --stacked-on and --unstacked options, and fixes a bug in fetching data when unstacking that made it break in all but trivial cases of empty branches.
>
> I've included here part of the -Dunlock code because it made debugging this much easier.
>
> It also adds a RepositoryBase so that we can have at least some common code with RemoteRepository.
>
Thanks for working on this. It certainly fills in an important hole. I
suspect it needs another round of polish though before it's good enough
to land.

> + Option('unstacked',
> + help='Reconfigure a branch to be unstacked. This '
> + 'may requiring copying substantial substantial data into it.',
>

s/requiring/require/
substantial x 2

> + stacked_on=None,
> + unstacked=None):
> directory = bzrdir.BzrDir.open(location)
> + if stacked_on is not None:
>

I think we ought to guard against both --stacked-on and --unstacked
being both passed. (At the moment, we assume stacked-on is what the user
meant. If we want to keep that assumption, we need to document it in the
help at a minimum.) A blackbox test is required for this case too.

> -Dmerge Emit information for debugging merges.
> +-Dunlock Some errors during unlock are treated as warnings.
> -Dpack Emit information about pack operations.
>

Looks like a tabbing or whitespace issue here.

> + # block, so it's useful to have tho option not to generate a new error
>

s/tho/the/

> @@ -18,6 +18,7 @@
> # across to run on the server.
>
> import bz2
> +import warnings
>
> from bzrlib import (
> bencode,
> @@ -27,11 +28,13 @@
> debug,
> errors,
> graph,
> + lock,
> lockdir,
> repository,
> revision,
> revision as _mod_revision,
> symbol_versioning,
> + trace,
>

The new imports of warnings and trace are redundant here.

> + def has_same_fallbacks(self, other_repo):
> + my_fb = self._fallback_repositories
> + other_fb = other_repo._fallback_repositories
> + if len(my_fb) != len(other_fb):
> + return False
> + for f, g in zip(my_fb, other_fb):
> + if not f.has_same_location(g):
> + return False
> + return True
>

This needs a docstring. It's also a public API (currently) so it needs
API tests written for it and/or needs to be made private. (BTW, I don't
think _fallback_repositories can ever be None. If it can, the above code
needs to be slightly more defensive as well.)

> +Be aware that when ``bzr reconfigure --unstacked`` is used, bzr will
> +to copy all the referenced data from the stacked-on repository into the
s/to copy/copy/

In summary, it's mainly small tweaks above but I suspect you want a few
more tests as well.

Ian C.

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

2009/7/14 Ian Clatworthy <email address hidden>:
>> +                'may requiring copying substantial substantial data into it.',

Well, it's _really_ substantial :)

Thanks for the review, I'll do those tweaks.

--
Martin <http://launchpad.net/~mbp/>

Revision history for this message
Robert Collins (lifeless) wrote :

On Fri, 2009-07-10 at 08:55 +0000, Martin Pool wrote:

> @@ -662,6 +663,9 @@
> """
> if not self._format.supports_stacking():
> raise errors.UnstackableBranchFormat(self._format,
> self.base)
> + # XXX: Changing from one fallback repository to another does
> not check
> + # that all the data you need is present in the new fallback.
> + # Possibly it should.

I think this would be better as a bug 'reconfigure --stacked-on-url does
not check new location has sufficient data'.

> - # in.
> - source_repository =
> self._get_fallback_repository(old_url)
> - for revision_id in chain([self.last_revision()],
> - self.tags.get_reverse_tag_dict()):
> - self.repository.fetch(source_repository, revision_id,
> - find_ghosts=True)

...
I think it would be better to /try/ to copy the tags. That should be
easy to do (fetch the tip, then for each tag try and catch
NoSuchRevision).

> + self.repository.fetch(old_repository,
> + revision_id=self.last_revision(),
> + find_ghosts=True)
> + finally:
> + old_repository.unlock()
> + finally:
> + pb.finished()
>
> def _set_tags_bytes(self, bytes):
> """Mirror method for _get_tags_bytes.
...
> - def run(self, location=None, target_type=None, bind_to=None,
> force=False):
> + def run(self, location=None, target_type=None, bind_to=None,
> force=False,
> +... 'specified')
> elif target_type == 'branch':
> reconfiguration =
> reconfigure.Reconfigure.to_branch(directory)
> elif target_type == 'tree':

Your changes to the reconfigure command add quite a few lines of logic,
but most of reconfigure is meant to be in the reconfigure module where
it is accessible to GUI's and so on. I think that this should be moved
there before landing, as otherwise the branch 'reduces clarity of
layering'.

> === modified file 'bzrlib/fetch.py'
> --- bzrlib/fetch.py 2009-06-17 17:57:15 +0000
> +++ bzrlib/fetch.py 2009-07-09 08:59:51 +0000
> @@ -59,11 +59,8 @@
> symbol_versioning.deprecated_in((1, 14, 0))
> % "pb parameter to RepoFetcher.__init__")
> # and for simplicity it is in fact ignored
> - if to_repository.has_same_location(from_repository):
> - # repository.fetch should be taking care of this case.
> - raise errors.BzrError('RepoFetcher run '
> - 'between two objects at the same location: '
> - '%r and %r' % (to_repository, from_repository))
> + # repository.fetch has the responsibility for
> short-circuiting
> + # attempts to copy between a repository and itself.

Is the above change needed? We added it when we refactored - if you're
removing this because you noticed it, thats fine. If you're removing it
because it was firing, thats a problem.

I don't think RepositoryBase is needed. Thats what Repository is.

-Rob

Revision history for this message
Martin Pool (mbp) wrote :
Download full text (4.0 KiB)

2009/7/15 Robert Collins <email address hidden>:
> On Fri, 2009-07-10 at 08:55 +0000, Martin Pool wrote:
>
>> @@ -662,6 +663,9 @@
>>          """
>>          if not self._format.supports_stacking():
>>              raise errors.UnstackableBranchFormat(self._format,
>> self.base)
>> +        # XXX: Changing from one fallback repository to another does
>> not check
>> +        # that all the data you need is present in the new fallback.
>> +        # Possibly it should.
>
> I think this would be better as a bug 'reconfigure --stacked-on-url does
> not check new location has sufficient data'.

Agree.

>> -            # in.
>> -            source_repository =
>> self._get_fallback_repository(old_url)
>> -            for revision_id in chain([self.last_revision()],
>> -                self.tags.get_reverse_tag_dict()):
>> -                self.repository.fetch(source_repository, revision_id,
>> -                    find_ghosts=True)
>
> ...
> I think it would be better to /try/ to copy the tags. That should be
> easy to do (fetch the tip, then for each tag try and catch
> NoSuchRevision).

I was a bit concerned this would make reconfigure be O(n_tags). I
suppose the common case is they will already have been copied and
we'll have enough of the graph cached to tell this quickly. It might
be nice if fetch could be give multiple revids and just tell you which
ones failed to be copied.

>
>> +                self.repository.fetch(old_repository,
>> +                    revision_id=self.last_revision(),
>> +                    find_ghosts=True)
>> +            finally:
>> +                old_repository.unlock()
>> +        finally:
>> +            pb.finished()
>>
>>      def _set_tags_bytes(self, bytes):
>>          """Mirror method for _get_tags_bytes.
> ...
>> -    def run(self, location=None, target_type=None, bind_to=None,
>> force=False):
>> +    def run(self, location=None, target_type=None, bind_to=None,
>> force=False,
>> +...                    'specified')
>>          elif target_type == 'branch':
>>              reconfiguration =
>> reconfigure.Reconfigure.to_branch(directory)
>>          elif target_type == 'tree':
>
> Your changes to the reconfigure command add quite a few lines of logic,
> but most of reconfigure is meant to be in the reconfigure module where
> it is accessible to GUI's and so on. I think that this should be moved
> there before landing, as otherwise the branch 'reduces clarity of
> layering'.

OK

>
>> === modified file 'bzrlib/fetch.py'
>> --- bzrlib/fetch.py     2009-06-17 17:57:15 +0000
>> +++ bzrlib/fetch.py     2009-07-09 08:59:51 +0000
>> @@ -59,11 +59,8 @@
>>                  symbol_versioning.deprecated_in((1, 14, 0))
>>                  % "pb parameter to RepoFetcher.__init__")
>>              # and for simplicity it is in fact ignored
>> -        if to_repository.has_same_location(from_repository):
>> -            # repository.fetch should be taking care of this case.
>> -            raise errors.BzrError('RepoFetcher run '
>> -                    'between two objects at the same location: '
>> -                    '%r and %r' % (to_repository, from_repository))
>> +        # repository.fetch has the r...

Read more...

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

2009/7/15 Martin Pool <email address hidden>:

>> I don't think RepositoryBase is needed. Thats what Repository is.
>
> It would be nice if Repository was actually the base.  At the moment
> RemoteRepository does not inherit from it, and there seems to be some
> amount of copy-and-paste between them because of this.  Perhaps that
> should be a separate bug and I should continue the copy/paste for now.

I may be wrong but I anticipate some trouble if I change this; I
recall Spiv talking for a while about whether it should be a subclass
or not...

--
Martin <http://launchpad.net/~mbp/>

Revision history for this message
Robert Collins (lifeless) wrote :

On Tue, 2009-07-14 at 23:48 +0000, Martin Pool wrote:
>
> > It would be nice if Repository was actually the base. At the moment
> > RemoteRepository does not inherit from it, and there seems to be
> some
> > amount of copy-and-paste between them because of this. Perhaps that
> > should be a separate bug and I should continue the copy/paste for
> now.
>
> I may be wrong but I anticipate some trouble if I change this; I
> recall Spiv talking for a while about whether it should be a subclass
> or not...

Spiv and I have repeatedly lamented them not being subclasses; however
we haven't had time to make it so. It may be easy.

-Rob

Revision history for this message
Ian Clatworthy (ian-clatworthy) wrote :

See comments above from Robert and I.

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

> Spiv and I have repeatedly lamented them not being subclasses; however
> we haven't had time to make it so. It may be easy.

https://bugs.edge.launchpad.net/bzr/+bug/401622 for doing this

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

Also bug 401642 for checking when reconfiguring to a different fallback, and bug 401646 for copying data referenced by tags.

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

> On Fri, 2009-07-10 at 08:55 +0000, Martin Pool wrote:
>
> > @@ -662,6 +663,9 @@
> > """
> > if not self._format.supports_stacking():
> > raise errors.UnstackableBranchFormat(self._format,
> > self.base)
> > + # XXX: Changing from one fallback repository to another does
> > not check
> > + # that all the data you need is present in the new fallback.
> > + # Possibly it should.
>
> I think this would be better as a bug 'reconfigure --stacked-on-url does
> not check new location has sufficient data'.
>
> > - # in.
> > - source_repository =
> > self._get_fallback_repository(old_url)
> > - for revision_id in chain([self.last_revision()],
> > - self.tags.get_reverse_tag_dict()):
> > - self.repository.fetch(source_repository, revision_id,
> > - find_ghosts=True)
>
> ...
> I think it would be better to /try/ to copy the tags. That should be
> easy to do (fetch the tip, then for each tag try and catch
> NoSuchRevision).
>
> > + self.repository.fetch(old_repository,
> > + revision_id=self.last_revision(),
> > + find_ghosts=True)
> > + finally:
> > + old_repository.unlock()
> > + finally:
> > + pb.finished()
> >
> > def _set_tags_bytes(self, bytes):
> > """Mirror method for _get_tags_bytes.
> ...
> > - def run(self, location=None, target_type=None, bind_to=None,
> > force=False):
> > + def run(self, location=None, target_type=None, bind_to=None,
> > force=False,
> > +... 'specified')
> > elif target_type == 'branch':
> > reconfiguration =
> > reconfigure.Reconfigure.to_branch(directory)
> > elif target_type == 'tree':
>

> Your changes to the reconfigure command add quite a few lines of logic,
> but most of reconfigure is meant to be in the reconfigure module where
> it is accessible to GUI's and so on. I think that this should be moved
> there before landing, as otherwise the branch 'reduces clarity of
> layering'.

That's a very good point about putting this outside of builtins.py where it can be reused.

I have some qualms about putting it into class Reconfigure though; this seems like the kind of thing we don't have an entirely satisfactory style for. Perhaps in this case the different types of reconfiguration should be different subclasses, rather than a bunch of variables on the instance...

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

> I have some qualms about putting it into class Reconfigure though; this seems
> like the kind of thing we don't have an entirely satisfactory style for.
> Perhaps in this case the different types of reconfiguration should be
> different subclasses, rather than a bunch of variables on the instance...

I've put it into new Reconfigure-like classes to do this, and having done the other tweaks will submit this.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'NEWS'
2--- NEWS 2009-07-23 17:00:27 +0000
3+++ NEWS 2009-07-24 03:35:23 +0000
4@@ -16,8 +16,12 @@
5 New Features
6 ************
7
8-* ``merge --interactive`` applies a user-selected portion of the merge. The UI
9- is similar to ``shelve``. (Aaron Bentley)
10+* ``bzr merge --interactive`` applies a user-selected portion of the
11+ merge. The UI is similar to ``shelve``. (Aaron Bentley)
12+
13+* ``bzr reconfigure`` now takes options ``--stacked-on URL`` and
14+ ``--unstacked`` to change stacking of a branch.
15+ (Martin Pool, #391411)
16
17 Bug Fixes
18 *********
19@@ -172,7 +176,7 @@
20
21 * ``bzr push`` now aborts if uncommitted changes (including pending merges)
22 are present in the working tree (if one is present) and no revision is
23- speified. The configuration option ``push_strict`` can be used to set the
24+ specified. The configuration option ``push_strict`` can be used to set the
25 default for this behavior. (Vincent Ladeuil, #284038, #322808, #65286)
26
27 * ``bzr revno`` and ``bzr revision-info`` now have a ``--tree`` option to
28
29=== modified file 'bzrlib/branch.py'
30--- bzrlib/branch.py 2009-07-01 10:30:41 +0000
31+++ bzrlib/branch.py 2009-07-24 03:35:24 +0000
32@@ -35,6 +35,7 @@
33 symbol_versioning,
34 transport,
35 tsort,
36+ ui,
37 urlutils,
38 )
39 from bzrlib.config import BranchConfig, TransportConfig
40@@ -662,6 +663,9 @@
41 """
42 if not self._format.supports_stacking():
43 raise errors.UnstackableBranchFormat(self._format, self.base)
44+ # XXX: Changing from one fallback repository to another does not check
45+ # that all the data you need is present in the new fallback.
46+ # Possibly it should.
47 self._check_stackable_repo()
48 if not url:
49 try:
50@@ -669,27 +673,67 @@
51 except (errors.NotStacked, errors.UnstackableBranchFormat,
52 errors.UnstackableRepositoryFormat):
53 return
54- url = ''
55- # XXX: Lock correctness - should unlock our old repo if we were
56- # locked.
57- # repositories don't offer an interface to remove fallback
58- # repositories today; take the conceptually simpler option and just
59- # reopen it.
60- self.repository = self.bzrdir.find_repository()
61- self.repository.lock_write()
62- # for every revision reference the branch has, ensure it is pulled
63- # in.
64- source_repository = self._get_fallback_repository(old_url)
65- for revision_id in chain([self.last_revision()],
66- self.tags.get_reverse_tag_dict()):
67- self.repository.fetch(source_repository, revision_id,
68- find_ghosts=True)
69+ self._unstack()
70 else:
71 self._activate_fallback_location(url)
72 # write this out after the repository is stacked to avoid setting a
73 # stacked config that doesn't work.
74 self._set_config_location('stacked_on_location', url)
75
76+ def _unstack(self):
77+ """Change a branch to be unstacked, copying data as needed.
78+
79+ Don't call this directly, use set_stacked_on_url(None).
80+ """
81+ pb = ui.ui_factory.nested_progress_bar()
82+ try:
83+ pb.update("Unstacking")
84+ # The basic approach here is to fetch the tip of the branch,
85+ # including all available ghosts, from the existing stacked
86+ # repository into a new repository object without the fallbacks.
87+ #
88+ # XXX: See <https://launchpad.net/bugs/397286> - this may not be
89+ # correct for CHKMap repostiories
90+ old_repository = self.repository
91+ if len(old_repository._fallback_repositories) != 1:
92+ raise AssertionError("can't cope with fallback repositories "
93+ "of %r" % (self.repository,))
94+ # unlock it, including unlocking the fallback
95+ old_repository.unlock()
96+ old_repository.lock_read()
97+ try:
98+ # Repositories don't offer an interface to remove fallback
99+ # repositories today; take the conceptually simpler option and just
100+ # reopen it. We reopen it starting from the URL so that we
101+ # get a separate connection for RemoteRepositories and can
102+ # stream from one of them to the other. This does mean doing
103+ # separate SSH connection setup, but unstacking is not a
104+ # common operation so it's tolerable.
105+ new_bzrdir = bzrdir.BzrDir.open(self.bzrdir.root_transport.base)
106+ new_repository = new_bzrdir.find_repository()
107+ self.repository = new_repository
108+ if self.repository._fallback_repositories:
109+ raise AssertionError("didn't expect %r to have "
110+ "fallback_repositories"
111+ % (self.repository,))
112+ # this is not paired with an unlock because it's just restoring
113+ # the previous state; the lock's released when set_stacked_on_url
114+ # returns
115+ self.repository.lock_write()
116+ # XXX: If you unstack a branch while it has a working tree
117+ # with a pending merge, the pending-merged revisions will no
118+ # longer be present. You can (probably) revert and remerge.
119+ #
120+ # XXX: This only fetches up to the tip of the repository; it
121+ # doesn't bring across any tags. That's fairly consistent
122+ # with how branch works, but perhaps not ideal.
123+ self.repository.fetch(old_repository,
124+ revision_id=self.last_revision(),
125+ find_ghosts=True)
126+ finally:
127+ old_repository.unlock()
128+ finally:
129+ pb.finished()
130
131 def _set_tags_bytes(self, bytes):
132 """Mirror method for _get_tags_bytes.
133
134=== modified file 'bzrlib/builtins.py'
135--- bzrlib/builtins.py 2009-07-19 04:47:49 +0000
136+++ bzrlib/builtins.py 2009-07-24 03:35:24 +0000
137@@ -5259,14 +5259,37 @@
138 ),
139 Option('bind-to', help='Branch to bind checkout to.', type=str),
140 Option('force',
141- help='Perform reconfiguration even if local changes'
142- ' will be lost.')
143+ help='Perform reconfiguration even if local changes'
144+ ' will be lost.'),
145+ Option('stacked-on',
146+ help='Reconfigure a branch to be stacked on another branch.',
147+ type=unicode,
148+ ),
149+ Option('unstacked',
150+ help='Reconfigure a branch to be unstacked. This '
151+ 'may require copying substantial data into it.',
152+ ),
153 ]
154
155- def run(self, location=None, target_type=None, bind_to=None, force=False):
156+ def run(self, location=None, target_type=None, bind_to=None, force=False,
157+ stacked_on=None,
158+ unstacked=None):
159 directory = bzrdir.BzrDir.open(location)
160+ if stacked_on and unstacked:
161+ raise BzrCommandError("Can't use both --stacked-on and --unstacked")
162+ elif stacked_on is not None:
163+ reconfigure.ReconfigureStackedOn().apply(directory, stacked_on)
164+ elif unstacked:
165+ reconfigure.ReconfigureUnstacked().apply(directory)
166+ # At the moment you can use --stacked-on and a different
167+ # reconfiguration shape at the same time; there seems no good reason
168+ # to ban it.
169 if target_type is None:
170- raise errors.BzrCommandError('No target configuration specified')
171+ if stacked_on or unstacked:
172+ return
173+ else:
174+ raise errors.BzrCommandError('No target configuration '
175+ 'specified')
176 elif target_type == 'branch':
177 reconfiguration = reconfigure.Reconfigure.to_branch(directory)
178 elif target_type == 'tree':
179
180=== modified file 'bzrlib/bundle/serializer/v08.py'
181--- bzrlib/bundle/serializer/v08.py 2009-05-06 05:36:28 +0000
182+++ bzrlib/bundle/serializer/v08.py 2009-07-24 03:35:25 +0000
183@@ -1,4 +1,4 @@
184-# Copyright (C) 2005, 2006 Canonical Ltd
185+# Copyright (C) 2005, 2006, 2009 Canonical Ltd
186 #
187 # This program is free software; you can redistribute it and/or modify
188 # it under the terms of the GNU General Public License as published by
189@@ -19,7 +19,10 @@
190
191 import os
192
193-from bzrlib import errors
194+from bzrlib import (
195+ errors,
196+ ui,
197+ )
198 from bzrlib.bundle.serializer import (BundleSerializer,
199 _get_bundle_header,
200 )
201@@ -27,9 +30,7 @@
202 from bzrlib.bundle.bundle_data import (RevisionInfo, BundleInfo, BundleTree)
203 from bzrlib.diff import internal_diff
204 from bzrlib.osutils import pathjoin
205-from bzrlib.progress import DummyProgress
206 from bzrlib.revision import NULL_REVISION
207-import bzrlib.ui
208 from bzrlib.testament import StrictTestament
209 from bzrlib.timestamp import (
210 format_highres_date,
211@@ -119,12 +120,11 @@
212 source.lock_read()
213 try:
214 self._write_main_header()
215- pb = DummyProgress()
216+ pb = ui.ui_factory.nested_progress_bar()
217 try:
218 self._write_revisions(pb)
219 finally:
220- pass
221- #pb.finished()
222+ pb.finished()
223 finally:
224 source.unlock()
225
226@@ -183,7 +183,7 @@
227
228 i_max = len(self.revision_ids)
229 for i, rev_id in enumerate(self.revision_ids):
230- pb.update("Generating revsion data", i, i_max)
231+ pb.update("Generating revision data", i, i_max)
232 rev = self.source.get_revision(rev_id)
233 if rev_id == last_rev_id:
234 rev_tree = last_rev_tree
235
236=== modified file 'bzrlib/fetch.py'
237--- bzrlib/fetch.py 2009-06-17 17:57:15 +0000
238+++ bzrlib/fetch.py 2009-07-24 03:35:24 +0000
239@@ -59,11 +59,8 @@
240 symbol_versioning.deprecated_in((1, 14, 0))
241 % "pb parameter to RepoFetcher.__init__")
242 # and for simplicity it is in fact ignored
243- if to_repository.has_same_location(from_repository):
244- # repository.fetch should be taking care of this case.
245- raise errors.BzrError('RepoFetcher run '
246- 'between two objects at the same location: '
247- '%r and %r' % (to_repository, from_repository))
248+ # repository.fetch has the responsibility for short-circuiting
249+ # attempts to copy between a repository and itself.
250 self.to_repository = to_repository
251 self.from_repository = from_repository
252 self.sink = to_repository._get_sink()
253
254=== modified file 'bzrlib/help_topics/en/debug-flags.txt'
255--- bzrlib/help_topics/en/debug-flags.txt 2009-07-16 00:01:17 +0000
256+++ bzrlib/help_topics/en/debug-flags.txt 2009-07-24 03:35:25 +0000
257@@ -23,5 +23,6 @@
258 -Dknit Trace knit operations.
259 -Dlock Trace when lockdir locks are taken or released.
260 -Dmerge Emit information for debugging merges.
261+-Dunlock Some errors during unlock are treated as warnings.
262 -Dpack Emit information about pack operations.
263 -Dsftp Trace SFTP internals.
264
265=== modified file 'bzrlib/lock.py'
266--- bzrlib/lock.py 2009-07-07 09:00:59 +0000
267+++ bzrlib/lock.py 2009-07-24 03:35:24 +0000
268@@ -37,8 +37,10 @@
269 import errno
270 import os
271 import sys
272+import warnings
273
274 from bzrlib import (
275+ debug,
276 errors,
277 osutils,
278 trace,
279@@ -86,6 +88,23 @@
280 self.lock_url, self.details)
281
282
283+def cant_unlock_not_held(locked_object):
284+ """An attempt to unlock failed because the object was not locked.
285+
286+ This provides a policy point from which we can generate either a warning
287+ or an exception.
288+ """
289+ # This is typically masking some other error and called from a finally
290+ # block, so it's useful to have the option not to generate a new error
291+ # here. You can use -Werror to make it fatal. It should possibly also
292+ # raise LockNotHeld.
293+ if 'unlock' in debug.debug_flags:
294+ warnings.warn("%r is already unlocked" % (locked_object,),
295+ stacklevel=3)
296+ else:
297+ raise errors.LockNotHeld(locked_object)
298+
299+
300 try:
301 import fcntl
302 have_fcntl = True
303
304=== modified file 'bzrlib/lockable_files.py'
305--- bzrlib/lockable_files.py 2009-03-25 04:20:12 +0000
306+++ bzrlib/lockable_files.py 2009-07-24 03:35:24 +0000
307@@ -24,6 +24,7 @@
308 from bzrlib import (
309 counted_lock,
310 errors,
311+ lock,
312 osutils,
313 transactions,
314 urlutils,
315@@ -308,7 +309,7 @@
316
317 def unlock(self):
318 if not self._lock_mode:
319- raise errors.LockNotHeld(self)
320+ return lock.cant_unlock_not_held(self)
321 if self._lock_warner.lock_count > 1:
322 self._lock_warner.lock_count -= 1
323 else:
324
325=== modified file 'bzrlib/lockdir.py'
326--- bzrlib/lockdir.py 2009-05-08 15:39:11 +0000
327+++ bzrlib/lockdir.py 2009-07-24 03:35:24 +0000
328@@ -293,7 +293,7 @@
329 self._fake_read_lock = False
330 return
331 if not self._lock_held:
332- raise LockNotHeld(self)
333+ return lock.cant_unlock_not_held(self)
334 if self._locked_via_token:
335 self._locked_via_token = False
336 self._lock_held = False
337
338=== modified file 'bzrlib/reconfigure.py'
339--- bzrlib/reconfigure.py 2009-07-10 08:33:11 +0000
340+++ bzrlib/reconfigure.py 2009-07-24 03:35:24 +0000
341@@ -1,4 +1,4 @@
342-# Copyright (C) 2007 Canonical Ltd
343+# Copyright (C) 2007, 2009 Canonical Ltd
344 #
345 # This program is free software; you can redistribute it and/or modify
346 # it under the terms of the GNU General Public License as published by
347@@ -14,14 +14,62 @@
348 # along with this program; if not, write to the Free Software
349 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
350
351-"""Reconfigure a bzrdir into a new tree/branch/repository layout"""
352+"""Reconfigure a bzrdir into a new tree/branch/repository layout.
353+
354+Various types of reconfiguration operation are available either by
355+constructing a class or using a factory method on Reconfigure.
356+"""
357+
358
359 from bzrlib import (
360 branch,
361 bzrdir,
362 errors,
363+ trace,
364+ ui,
365+ urlutils,
366 )
367
368+
369+# TODO: common base class for all reconfigure operations, making no
370+# assumptions about what kind of change will be done.
371+
372+
373+class ReconfigureStackedOn(object):
374+ """Reconfigures a branch to be stacked on another branch."""
375+
376+ def apply(self, bzrdir, stacked_on_url):
377+ branch = bzrdir.open_branch()
378+ # it may be a path relative to the cwd or a url; the branch wants
379+ # a path relative to itself...
380+ on_url = urlutils.relative_url(branch.base,
381+ urlutils.normalize_url(stacked_on_url))
382+ branch.lock_write()
383+ try:
384+ branch.set_stacked_on_url(on_url)
385+ if not trace.is_quiet():
386+ ui.ui_factory.note(
387+ "%s is now stacked on %s\n"
388+ % (branch.base, branch.get_stacked_on_url()))
389+ finally:
390+ branch.unlock()
391+
392+
393+class ReconfigureUnstacked(object):
394+
395+ def apply(self, bzrdir):
396+ branch = bzrdir.open_branch()
397+ branch.lock_write()
398+ try:
399+ branch.set_stacked_on_url(None)
400+ if not trace.is_quiet():
401+ ui.ui_factory.note(
402+ "%s is now not stacked\n"
403+ % (branch.base,))
404+ finally:
405+ branch.unlock()
406+
407+
408 class Reconfigure(object):
409
410 def __init__(self, bzrdir, new_bound_location=None):
411
412=== modified file 'bzrlib/remote.py'
413--- bzrlib/remote.py 2009-07-06 09:47:35 +0000
414+++ bzrlib/remote.py 2009-07-24 03:35:24 +0000
415@@ -1,4 +1,4 @@
416-# Copyright (C) 2006, 2007, 2008 Canonical Ltd
417+# Copyright (C) 2006, 2007, 2008, 2009 Canonical Ltd
418 #
419 # This program is free software; you can redistribute it and/or modify
420 # it under the terms of the GNU General Public License as published by
421@@ -14,9 +14,6 @@
422 # along with this program; if not, write to the Free Software
423 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
424
425-# TODO: At some point, handle upgrades by just passing the whole request
426-# across to run on the server.
427-
428 import bz2
429
430 from bzrlib import (
431@@ -27,6 +24,7 @@
432 debug,
433 errors,
434 graph,
435+ lock,
436 lockdir,
437 repository,
438 revision,
439@@ -813,7 +811,23 @@
440 result.add(_mod_revision.NULL_REVISION)
441 return result
442
443+ def _has_same_fallbacks(self, other_repo):
444+ """Returns true if the repositories have the same fallbacks."""
445+ # XXX: copied from Repository; it should be unified into a base class
446+ # <https://bugs.edge.launchpad.net/bzr/+bug/401622>
447+ my_fb = self._fallback_repositories
448+ other_fb = other_repo._fallback_repositories
449+ if len(my_fb) != len(other_fb):
450+ return False
451+ for f, g in zip(my_fb, other_fb):
452+ if not f.has_same_location(g):
453+ return False
454+ return True
455+
456 def has_same_location(self, other):
457+ # TODO: Move to RepositoryBase and unify with the regular Repository
458+ # one; unfortunately the tests rely on slightly different behaviour at
459+ # present -- mbp 20090710
460 return (self.__class__ is other.__class__ and
461 self.bzrdir.transport.base == other.bzrdir.transport.base)
462
463@@ -1025,7 +1039,7 @@
464
465 def unlock(self):
466 if not self._lock_count:
467- raise errors.LockNotHeld(self)
468+ return lock.cant_unlock_not_held(self)
469 self._lock_count -= 1
470 if self._lock_count > 0:
471 return
472@@ -1230,7 +1244,9 @@
473 raise errors.InternalBzrError(
474 "May not fetch while in a write group.")
475 # fast path same-url fetch operations
476- if self.has_same_location(source) and fetch_spec is None:
477+ if (self.has_same_location(source)
478+ and fetch_spec is None
479+ and self._has_same_fallbacks(source)):
480 # check that last_revision is in 'from' and then return a
481 # no-operation.
482 if (revision_id is not None and
483
484=== modified file 'bzrlib/repository.py'
485--- bzrlib/repository.py 2009-07-02 23:10:53 +0000
486+++ bzrlib/repository.py 2009-07-24 03:35:25 +0000
487@@ -848,6 +848,7 @@
488 ######################################################################
489 # Repositories
490
491+
492 class Repository(object):
493 """Repository holding history for one or more branches.
494
495@@ -1183,8 +1184,25 @@
496 self._inventory_entry_cache = fifo_cache.FIFOCache(10*1024)
497
498 def __repr__(self):
499- return '%s(%r)' % (self.__class__.__name__,
500- self.base)
501+ if self._fallback_repositories:
502+ return '%s(%r, fallback_repositories=%r)' % (
503+ self.__class__.__name__,
504+ self.base,
505+ self._fallback_repositories)
506+ else:
507+ return '%s(%r)' % (self.__class__.__name__,
508+ self.base)
509+
510+ def _has_same_fallbacks(self, other_repo):
511+ """Returns true if the repositories have the same fallbacks."""
512+ my_fb = self._fallback_repositories
513+ other_fb = other_repo._fallback_repositories
514+ if len(my_fb) != len(other_fb):
515+ return False
516+ for f, g in zip(my_fb, other_fb):
517+ if not f.has_same_location(g):
518+ return False
519+ return True
520
521 def has_same_location(self, other):
522 """Returns a boolean indicating if this repository is at the same
523@@ -1529,7 +1547,11 @@
524 raise errors.InternalBzrError(
525 "May not fetch while in a write group.")
526 # fast path same-url fetch operations
527- if self.has_same_location(source) and fetch_spec is None:
528+ # TODO: lift out to somewhere common with RemoteRepository
529+ # <https://bugs.edge.launchpad.net/bzr/+bug/401646>
530+ if (self.has_same_location(source)
531+ and fetch_spec is None
532+ and self._has_same_fallbacks(source)):
533 # check that last_revision is in 'from' and then return a
534 # no-operation.
535 if (revision_id is not None and
536
537=== modified file 'bzrlib/tests/blackbox/test_reconfigure.py'
538--- bzrlib/tests/blackbox/test_reconfigure.py 2009-05-07 05:08:46 +0000
539+++ bzrlib/tests/blackbox/test_reconfigure.py 2009-07-24 03:35:25 +0000
540@@ -1,4 +1,4 @@
541-# Copyright (C) 2007 Canonical Ltd
542+# Copyright (C) 2007, 2009 Canonical Ltd
543 #
544 # This program is free software; you can redistribute it and/or modify
545 # it under the terms of the GNU General Public License as published by
546@@ -20,6 +20,7 @@
547 tests,
548 workingtree,
549 )
550+from bzrlib.branchbuilder import BranchBuilder
551
552
553 class TestReconfigure(tests.TestCaseWithTransport):
554@@ -182,3 +183,59 @@
555
556 def test_lightweight_rich_root_pack_checkout_to_tree(self, format=None):
557 self.test_lightweight_format_checkout_to_tree('rich-root-pack')
558+
559+
560+class TestReconfigureStacking(tests.TestCaseWithTransport):
561+
562+ def test_reconfigure_stacking(self):
563+ """Test a fairly realistic scenario for stacking:
564+
565+ * make a branch with some history
566+ * branch it
567+ * make the second branch stacked on the first
568+ * commit in the second
569+ * then make the second unstacked, so it has to fill in history from
570+ the original fallback lying underneath its original content
571+
572+ See discussion in <https://bugs.edge.launchpad.net/bzr/+bug/391411>
573+ """
574+ # there are also per_branch tests that exercise remote operation etc
575+ tree_1 = self.make_branch_and_tree('b1', format='2a')
576+ self.build_tree(['b1/foo'])
577+ tree_1.add(['foo'])
578+ tree_1.commit('add foo')
579+ branch_1 = tree_1.branch
580+ # now branch and commit again
581+ bzrdir_2 = tree_1.bzrdir.sprout('b2')
582+ tree_2 = bzrdir_2.open_workingtree()
583+ branch_2 = tree_2.branch
584+ # now reconfigure to be stacked
585+ out, err = self.run_bzr('reconfigure --stacked-on b1 b2')
586+ self.assertContainsRe(out,
587+ '^.*/b2/ is now stacked on ../b1\n$')
588+ self.assertEquals('', err)
589+ # can also give the absolute URL of the branch, and it gets stored
590+ # as a relative path if possible
591+ out, err = self.run_bzr('reconfigure --stacked-on %s b2'
592+ % (self.get_url('b1'),))
593+ self.assertContainsRe(out,
594+ '^.*/b2/ is now stacked on ../b1\n$')
595+ self.assertEquals('', err)
596+ # It should be given a relative URL to the destination, if possible,
597+ # because that's most likely to work across different transports
598+ self.assertEquals(branch_2.get_stacked_on_url(),
599+ '../b1')
600+ # commit, and it should be stored into b2's repo
601+ self.build_tree_contents([('foo', 'new foo')])
602+ tree_2.commit('update foo')
603+ # Now turn it off again
604+ out, err = self.run_bzr('reconfigure --unstacked b2')
605+ self.assertContainsRe(out,
606+ '^.*/b2/ is now not stacked\n$')
607+ self.assertEquals('', err)
608+ self.assertRaises(errors.NotStacked,
609+ branch_2.get_stacked_on_url)
610+
611+ # XXX: Needs a test for reconfiguring stacking and shape at the same time;
612+ # no branch at location; stacked-on is not a branch; quiet mode.
613+ # -- mbp 20090706
614
615=== modified file 'bzrlib/tests/per_branch/test_stacking.py'
616--- bzrlib/tests/per_branch/test_stacking.py 2009-07-10 05:49:34 +0000
617+++ bzrlib/tests/per_branch/test_stacking.py 2009-07-24 03:35:25 +0000
618@@ -184,11 +184,17 @@
619 trunk_revid = trunk_tree.commit('revision on mainline')
620 # and make branch from it which is stacked
621 try:
622- new_dir = trunk_tree.bzrdir.sprout('newbranch', stacked=True)
623+ new_dir = trunk_tree.bzrdir.sprout(self.get_url('newbranch'),
624+ stacked=True)
625 except unstackable_format_errors, e:
626 raise TestNotApplicable(e)
627 # stacked repository
628 self.assertRevisionNotInRepository('newbranch', trunk_revid)
629+ # TODO: we'd like to commit in the stacked repository; that requires
630+ # some care (maybe a BranchBuilder) if it's remote and has no
631+ # workingtree
632+ ##newbranch_revid = new_dir.open_workingtree().commit('revision in '
633+ ##'newbranch')
634 # now when we unstack that should implicitly fetch, to make sure that
635 # the branch will still work
636 new_branch = new_dir.open_branch()
637
638=== modified file 'doc/en/user-guide/stacked.txt'
639--- doc/en/user-guide/stacked.txt 2008-07-17 06:00:08 +0000
640+++ doc/en/user-guide/stacked.txt 2009-07-24 03:35:25 +0000
641@@ -83,3 +83,15 @@
642 stacked-on branch needs to be available for almost all operations. This is
643 not an issue when both branches are local or both branches are on the
644 same server.
645+
646+
647+Changing branch stacking
648+------------------------
649+
650+Stacking of existing branches can be changed using the ``bzr reconfigure``
651+command to either stack on an existing branch, or to turn off stacking.
652+Be aware that when ``bzr reconfigure --unstacked`` is used, bzr will
653+copy all the referenced data from the stacked-on repository into the
654+previously stacked repository. For large repositories this may take
655+considerable time and may substantially increase the size of the
656+repository.