Rev 4566: (mbp) add reconfigure --stacked-on and --unstacked in file:///home/pqm/archives/thelove/bzr/%2Btrunk/
Canonical.com Patch Queue Manager
pqm at pqm.ubuntu.com
Fri Jul 24 05:22:21 BST 2009
At file:///home/pqm/archives/thelove/bzr/%2Btrunk/
------------------------------------------------------------
revno: 4566 [merge]
revision-id: pqm at pqm.ubuntu.com-20090724042219-cpfjr65il0k0tdet
parent: pqm at pqm.ubuntu.com-20090724000654-q6gwe0bpgk1buemd
parent: mbp at sourcefrog.net-20090724031556-5zyef6f1ixtn6r3z
committer: Canonical.com Patch Queue Manager <pqm at pqm.ubuntu.com>
branch nick: +trunk
timestamp: Fri 2009-07-24 05:22:19 +0100
message:
(mbp) add reconfigure --stacked-on and --unstacked
modified:
NEWS NEWS-20050323055033-4e00b5db738777ff
bzrlib/branch.py branch.py-20050309040759-e4baf4e0d046576e
bzrlib/builtins.py builtins.py-20050830033751-fc01482b9ca23183
bzrlib/fetch.py fetch.py-20050818234941-26fea6105696365d
bzrlib/help_topics/en/debug-flags.txt debugflags.txt-20090312050229-rdspqbqq4fzbjtpe-1
bzrlib/lock.py lock.py-20050527050856-ec090bb51bc03349
bzrlib/lockable_files.py control_files.py-20051111201905-bb88546e799d669f
bzrlib/lockdir.py lockdir.py-20060220222025-98258adf27fbdda3
bzrlib/reconfigure.py reconfigure.py-20070908040425-6ykgo7escxhyrg9p-1
bzrlib/remote.py remote.py-20060720103555-yeeg2x51vn0rbtdp-1
bzrlib/repository.py rev_storage.py-20051111201905-119e9401e46257e3
bzrlib/tests/blackbox/test_reconfigure.py test_reconfigure.py-20070908173426-khfo5fi2rgzgtwj3-1
bzrlib/tests/per_branch/test_stacking.py test_stacking.py-20080214020755-msjlkb7urobwly0f-1
doc/en/user-guide/stacked.txt stacked.txt-20080711023247-4uh9oovoka0sze8b-1
=== modified file 'NEWS'
--- a/NEWS 2009-07-23 17:00:27 +0000
+++ b/NEWS 2009-07-24 03:15:56 +0000
@@ -16,8 +16,12 @@
New Features
************
-* ``merge --interactive`` applies a user-selected portion of the merge. The UI
- is similar to ``shelve``. (Aaron Bentley)
+* ``bzr merge --interactive`` applies a user-selected portion of the
+ merge. The UI is similar to ``shelve``. (Aaron Bentley)
+
+* ``bzr reconfigure`` now takes options ``--stacked-on URL`` and
+ ``--unstacked`` to change stacking of a branch.
+ (Martin Pool, #391411)
Bug Fixes
*********
@@ -172,7 +176,7 @@
* ``bzr push`` now aborts if uncommitted changes (including pending merges)
are present in the working tree (if one is present) and no revision is
- speified. The configuration option ``push_strict`` can be used to set the
+ specified. The configuration option ``push_strict`` can be used to set the
default for this behavior. (Vincent Ladeuil, #284038, #322808, #65286)
* ``bzr revno`` and ``bzr revision-info`` now have a ``--tree`` option to
=== modified file 'bzrlib/branch.py'
--- a/bzrlib/branch.py 2009-07-01 10:30:41 +0000
+++ b/bzrlib/branch.py 2009-07-10 08:42:35 +0000
@@ -35,6 +35,7 @@
symbol_versioning,
transport,
tsort,
+ ui,
urlutils,
)
from bzrlib.config import BranchConfig, TransportConfig
@@ -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.
self._check_stackable_repo()
if not url:
try:
@@ -669,27 +673,67 @@
except (errors.NotStacked, errors.UnstackableBranchFormat,
errors.UnstackableRepositoryFormat):
return
- url = ''
- # XXX: Lock correctness - should unlock our old repo if we were
- # locked.
- # repositories don't offer an interface to remove fallback
- # repositories today; take the conceptually simpler option and just
- # reopen it.
- self.repository = self.bzrdir.find_repository()
- self.repository.lock_write()
- # for every revision reference the branch has, ensure it is pulled
- # 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)
+ self._unstack()
else:
self._activate_fallback_location(url)
# write this out after the repository is stacked to avoid setting a
# stacked config that doesn't work.
self._set_config_location('stacked_on_location', url)
+ def _unstack(self):
+ """Change a branch to be unstacked, copying data as needed.
+
+ Don't call this directly, use set_stacked_on_url(None).
+ """
+ pb = ui.ui_factory.nested_progress_bar()
+ try:
+ pb.update("Unstacking")
+ # The basic approach here is to fetch the tip of the branch,
+ # including all available ghosts, from the existing stacked
+ # repository into a new repository object without the fallbacks.
+ #
+ # XXX: See <https://launchpad.net/bugs/397286> - this may not be
+ # correct for CHKMap repostiories
+ old_repository = self.repository
+ if len(old_repository._fallback_repositories) != 1:
+ raise AssertionError("can't cope with fallback repositories "
+ "of %r" % (self.repository,))
+ # unlock it, including unlocking the fallback
+ old_repository.unlock()
+ old_repository.lock_read()
+ try:
+ # Repositories don't offer an interface to remove fallback
+ # repositories today; take the conceptually simpler option and just
+ # reopen it. We reopen it starting from the URL so that we
+ # get a separate connection for RemoteRepositories and can
+ # stream from one of them to the other. This does mean doing
+ # separate SSH connection setup, but unstacking is not a
+ # common operation so it's tolerable.
+ new_bzrdir = bzrdir.BzrDir.open(self.bzrdir.root_transport.base)
+ new_repository = new_bzrdir.find_repository()
+ self.repository = new_repository
+ if self.repository._fallback_repositories:
+ raise AssertionError("didn't expect %r to have "
+ "fallback_repositories"
+ % (self.repository,))
+ # this is not paired with an unlock because it's just restoring
+ # the previous state; the lock's released when set_stacked_on_url
+ # returns
+ self.repository.lock_write()
+ # XXX: If you unstack a branch while it has a working tree
+ # with a pending merge, the pending-merged revisions will no
+ # longer be present. You can (probably) revert and remerge.
+ #
+ # XXX: This only fetches up to the tip of the repository; it
+ # doesn't bring across any tags. That's fairly consistent
+ # with how branch works, but perhaps not ideal.
+ 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.
=== modified file 'bzrlib/builtins.py'
--- a/bzrlib/builtins.py 2009-07-19 04:47:49 +0000
+++ b/bzrlib/builtins.py 2009-07-24 03:15:56 +0000
@@ -5259,14 +5259,37 @@
),
Option('bind-to', help='Branch to bind checkout to.', type=str),
Option('force',
- help='Perform reconfiguration even if local changes'
- ' will be lost.')
+ help='Perform reconfiguration even if local changes'
+ ' will be lost.'),
+ Option('stacked-on',
+ help='Reconfigure a branch to be stacked on another branch.',
+ type=unicode,
+ ),
+ Option('unstacked',
+ help='Reconfigure a branch to be unstacked. This '
+ 'may require copying substantial data into it.',
+ ),
]
- 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,
+ stacked_on=None,
+ unstacked=None):
directory = bzrdir.BzrDir.open(location)
+ if stacked_on and unstacked:
+ raise BzrCommandError("Can't use both --stacked-on and --unstacked")
+ elif stacked_on is not None:
+ reconfigure.ReconfigureStackedOn().apply(directory, stacked_on)
+ elif unstacked:
+ reconfigure.ReconfigureUnstacked().apply(directory)
+ # At the moment you can use --stacked-on and a different
+ # reconfiguration shape at the same time; there seems no good reason
+ # to ban it.
if target_type is None:
- raise errors.BzrCommandError('No target configuration specified')
+ if stacked_on or unstacked:
+ return
+ else:
+ raise errors.BzrCommandError('No target configuration '
+ 'specified')
elif target_type == 'branch':
reconfiguration = reconfigure.Reconfigure.to_branch(directory)
elif target_type == 'tree':
=== modified file 'bzrlib/fetch.py'
--- a/bzrlib/fetch.py 2009-06-17 17:57:15 +0000
+++ b/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.
self.to_repository = to_repository
self.from_repository = from_repository
self.sink = to_repository._get_sink()
=== modified file 'bzrlib/help_topics/en/debug-flags.txt'
--- a/bzrlib/help_topics/en/debug-flags.txt 2009-07-16 00:01:17 +0000
+++ b/bzrlib/help_topics/en/debug-flags.txt 2009-07-24 03:15:56 +0000
@@ -23,5 +23,6 @@
-Dknit Trace knit operations.
-Dlock Trace when lockdir locks are taken or released.
-Dmerge Emit information for debugging merges.
+-Dunlock Some errors during unlock are treated as warnings.
-Dpack Emit information about pack operations.
-Dsftp Trace SFTP internals.
=== modified file 'bzrlib/lock.py'
--- a/bzrlib/lock.py 2009-07-07 09:00:59 +0000
+++ b/bzrlib/lock.py 2009-07-20 08:47:58 +0000
@@ -37,8 +37,10 @@
import errno
import os
import sys
+import warnings
from bzrlib import (
+ debug,
errors,
osutils,
trace,
@@ -86,6 +88,23 @@
self.lock_url, self.details)
+def cant_unlock_not_held(locked_object):
+ """An attempt to unlock failed because the object was not locked.
+
+ This provides a policy point from which we can generate either a warning
+ or an exception.
+ """
+ # This is typically masking some other error and called from a finally
+ # block, so it's useful to have the option not to generate a new error
+ # here. You can use -Werror to make it fatal. It should possibly also
+ # raise LockNotHeld.
+ if 'unlock' in debug.debug_flags:
+ warnings.warn("%r is already unlocked" % (locked_object,),
+ stacklevel=3)
+ else:
+ raise errors.LockNotHeld(locked_object)
+
+
try:
import fcntl
have_fcntl = True
=== modified file 'bzrlib/lockable_files.py'
--- a/bzrlib/lockable_files.py 2009-03-25 04:20:12 +0000
+++ b/bzrlib/lockable_files.py 2009-07-10 07:47:21 +0000
@@ -24,6 +24,7 @@
from bzrlib import (
counted_lock,
errors,
+ lock,
osutils,
transactions,
urlutils,
@@ -308,7 +309,7 @@
def unlock(self):
if not self._lock_mode:
- raise errors.LockNotHeld(self)
+ return lock.cant_unlock_not_held(self)
if self._lock_warner.lock_count > 1:
self._lock_warner.lock_count -= 1
else:
=== modified file 'bzrlib/lockdir.py'
--- a/bzrlib/lockdir.py 2009-05-08 15:39:11 +0000
+++ b/bzrlib/lockdir.py 2009-07-10 07:47:21 +0000
@@ -293,7 +293,7 @@
self._fake_read_lock = False
return
if not self._lock_held:
- raise LockNotHeld(self)
+ return lock.cant_unlock_not_held(self)
if self._locked_via_token:
self._locked_via_token = False
self._lock_held = False
=== modified file 'bzrlib/reconfigure.py'
--- a/bzrlib/reconfigure.py 2009-07-10 08:33:11 +0000
+++ b/bzrlib/reconfigure.py 2009-07-24 03:15:56 +0000
@@ -1,4 +1,4 @@
-# Copyright (C) 2007 Canonical Ltd
+# Copyright (C) 2007, 2009 Canonical Ltd
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -14,14 +14,62 @@
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
-"""Reconfigure a bzrdir into a new tree/branch/repository layout"""
+"""Reconfigure a bzrdir into a new tree/branch/repository layout.
+
+Various types of reconfiguration operation are available either by
+constructing a class or using a factory method on Reconfigure.
+"""
+
from bzrlib import (
branch,
bzrdir,
errors,
+ trace,
+ ui,
+ urlutils,
)
+
+# TODO: common base class for all reconfigure operations, making no
+# assumptions about what kind of change will be done.
+
+
+class ReconfigureStackedOn(object):
+ """Reconfigures a branch to be stacked on another branch."""
+
+ def apply(self, bzrdir, stacked_on_url):
+ branch = bzrdir.open_branch()
+ # it may be a path relative to the cwd or a url; the branch wants
+ # a path relative to itself...
+ on_url = urlutils.relative_url(branch.base,
+ urlutils.normalize_url(stacked_on_url))
+ branch.lock_write()
+ try:
+ branch.set_stacked_on_url(on_url)
+ if not trace.is_quiet():
+ ui.ui_factory.note(
+ "%s is now stacked on %s\n"
+ % (branch.base, branch.get_stacked_on_url()))
+ finally:
+ branch.unlock()
+
+
+class ReconfigureUnstacked(object):
+
+ def apply(self, bzrdir):
+ branch = bzrdir.open_branch()
+ branch.lock_write()
+ try:
+ branch.set_stacked_on_url(None)
+ if not trace.is_quiet():
+ ui.ui_factory.note(
+ "%s is now not stacked\n"
+ % (branch.base,))
+ finally:
+ branch.unlock()
+
+
class Reconfigure(object):
def __init__(self, bzrdir, new_bound_location=None):
=== modified file 'bzrlib/remote.py'
--- a/bzrlib/remote.py 2009-07-06 09:47:35 +0000
+++ b/bzrlib/remote.py 2009-07-20 10:14:42 +0000
@@ -1,4 +1,4 @@
-# Copyright (C) 2006, 2007, 2008 Canonical Ltd
+# Copyright (C) 2006, 2007, 2008, 2009 Canonical Ltd
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -14,9 +14,6 @@
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
-# TODO: At some point, handle upgrades by just passing the whole request
-# across to run on the server.
-
import bz2
from bzrlib import (
@@ -27,6 +24,7 @@
debug,
errors,
graph,
+ lock,
lockdir,
repository,
revision,
@@ -813,7 +811,23 @@
result.add(_mod_revision.NULL_REVISION)
return result
+ def _has_same_fallbacks(self, other_repo):
+ """Returns true if the repositories have the same fallbacks."""
+ # XXX: copied from Repository; it should be unified into a base class
+ # <https://bugs.edge.launchpad.net/bzr/+bug/401622>
+ 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
+
def has_same_location(self, other):
+ # TODO: Move to RepositoryBase and unify with the regular Repository
+ # one; unfortunately the tests rely on slightly different behaviour at
+ # present -- mbp 20090710
return (self.__class__ is other.__class__ and
self.bzrdir.transport.base == other.bzrdir.transport.base)
@@ -1025,7 +1039,7 @@
def unlock(self):
if not self._lock_count:
- raise errors.LockNotHeld(self)
+ return lock.cant_unlock_not_held(self)
self._lock_count -= 1
if self._lock_count > 0:
return
@@ -1230,7 +1244,9 @@
raise errors.InternalBzrError(
"May not fetch while in a write group.")
# fast path same-url fetch operations
- if self.has_same_location(source) and fetch_spec is None:
+ if (self.has_same_location(source)
+ and fetch_spec is None
+ and self._has_same_fallbacks(source)):
# check that last_revision is in 'from' and then return a
# no-operation.
if (revision_id is not None and
=== modified file 'bzrlib/repository.py'
--- a/bzrlib/repository.py 2009-07-02 23:10:53 +0000
+++ b/bzrlib/repository.py 2009-07-20 10:14:42 +0000
@@ -848,6 +848,7 @@
######################################################################
# Repositories
+
class Repository(object):
"""Repository holding history for one or more branches.
@@ -1183,8 +1184,25 @@
self._inventory_entry_cache = fifo_cache.FIFOCache(10*1024)
def __repr__(self):
- return '%s(%r)' % (self.__class__.__name__,
- self.base)
+ if self._fallback_repositories:
+ return '%s(%r, fallback_repositories=%r)' % (
+ self.__class__.__name__,
+ self.base,
+ self._fallback_repositories)
+ else:
+ return '%s(%r)' % (self.__class__.__name__,
+ self.base)
+
+ def _has_same_fallbacks(self, other_repo):
+ """Returns true if the repositories have the same fallbacks."""
+ 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
def has_same_location(self, other):
"""Returns a boolean indicating if this repository is at the same
@@ -1529,7 +1547,11 @@
raise errors.InternalBzrError(
"May not fetch while in a write group.")
# fast path same-url fetch operations
- if self.has_same_location(source) and fetch_spec is None:
+ # TODO: lift out to somewhere common with RemoteRepository
+ # <https://bugs.edge.launchpad.net/bzr/+bug/401646>
+ if (self.has_same_location(source)
+ and fetch_spec is None
+ and self._has_same_fallbacks(source)):
# check that last_revision is in 'from' and then return a
# no-operation.
if (revision_id is not None and
=== modified file 'bzrlib/tests/blackbox/test_reconfigure.py'
--- a/bzrlib/tests/blackbox/test_reconfigure.py 2009-05-07 05:08:46 +0000
+++ b/bzrlib/tests/blackbox/test_reconfigure.py 2009-07-10 08:18:28 +0000
@@ -1,4 +1,4 @@
-# Copyright (C) 2007 Canonical Ltd
+# Copyright (C) 2007, 2009 Canonical Ltd
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -20,6 +20,7 @@
tests,
workingtree,
)
+from bzrlib.branchbuilder import BranchBuilder
class TestReconfigure(tests.TestCaseWithTransport):
@@ -182,3 +183,59 @@
def test_lightweight_rich_root_pack_checkout_to_tree(self, format=None):
self.test_lightweight_format_checkout_to_tree('rich-root-pack')
+
+
+class TestReconfigureStacking(tests.TestCaseWithTransport):
+
+ def test_reconfigure_stacking(self):
+ """Test a fairly realistic scenario for stacking:
+
+ * make a branch with some history
+ * branch it
+ * make the second branch stacked on the first
+ * commit in the second
+ * then make the second unstacked, so it has to fill in history from
+ the original fallback lying underneath its original content
+
+ See discussion in <https://bugs.edge.launchpad.net/bzr/+bug/391411>
+ """
+ # there are also per_branch tests that exercise remote operation etc
+ tree_1 = self.make_branch_and_tree('b1', format='2a')
+ self.build_tree(['b1/foo'])
+ tree_1.add(['foo'])
+ tree_1.commit('add foo')
+ branch_1 = tree_1.branch
+ # now branch and commit again
+ bzrdir_2 = tree_1.bzrdir.sprout('b2')
+ tree_2 = bzrdir_2.open_workingtree()
+ branch_2 = tree_2.branch
+ # now reconfigure to be stacked
+ out, err = self.run_bzr('reconfigure --stacked-on b1 b2')
+ self.assertContainsRe(out,
+ '^.*/b2/ is now stacked on ../b1\n$')
+ self.assertEquals('', err)
+ # can also give the absolute URL of the branch, and it gets stored
+ # as a relative path if possible
+ out, err = self.run_bzr('reconfigure --stacked-on %s b2'
+ % (self.get_url('b1'),))
+ self.assertContainsRe(out,
+ '^.*/b2/ is now stacked on ../b1\n$')
+ self.assertEquals('', err)
+ # It should be given a relative URL to the destination, if possible,
+ # because that's most likely to work across different transports
+ self.assertEquals(branch_2.get_stacked_on_url(),
+ '../b1')
+ # commit, and it should be stored into b2's repo
+ self.build_tree_contents([('foo', 'new foo')])
+ tree_2.commit('update foo')
+ # Now turn it off again
+ out, err = self.run_bzr('reconfigure --unstacked b2')
+ self.assertContainsRe(out,
+ '^.*/b2/ is now not stacked\n$')
+ self.assertEquals('', err)
+ self.assertRaises(errors.NotStacked,
+ branch_2.get_stacked_on_url)
+
+ # XXX: Needs a test for reconfiguring stacking and shape at the same time;
+ # no branch at location; stacked-on is not a branch; quiet mode.
+ # -- mbp 20090706
=== modified file 'bzrlib/tests/per_branch/test_stacking.py'
--- a/bzrlib/tests/per_branch/test_stacking.py 2009-07-10 05:49:34 +0000
+++ b/bzrlib/tests/per_branch/test_stacking.py 2009-07-24 03:15:56 +0000
@@ -184,11 +184,17 @@
trunk_revid = trunk_tree.commit('revision on mainline')
# and make branch from it which is stacked
try:
- new_dir = trunk_tree.bzrdir.sprout('newbranch', stacked=True)
+ new_dir = trunk_tree.bzrdir.sprout(self.get_url('newbranch'),
+ stacked=True)
except unstackable_format_errors, e:
raise TestNotApplicable(e)
# stacked repository
self.assertRevisionNotInRepository('newbranch', trunk_revid)
+ # TODO: we'd like to commit in the stacked repository; that requires
+ # some care (maybe a BranchBuilder) if it's remote and has no
+ # workingtree
+ ##newbranch_revid = new_dir.open_workingtree().commit('revision in '
+ ##'newbranch')
# now when we unstack that should implicitly fetch, to make sure that
# the branch will still work
new_branch = new_dir.open_branch()
=== modified file 'doc/en/user-guide/stacked.txt'
--- a/doc/en/user-guide/stacked.txt 2008-07-17 06:00:08 +0000
+++ b/doc/en/user-guide/stacked.txt 2009-07-20 08:47:58 +0000
@@ -83,3 +83,15 @@
stacked-on branch needs to be available for almost all operations. This is
not an issue when both branches are local or both branches are on the
same server.
+
+
+Changing branch stacking
+------------------------
+
+Stacking of existing branches can be changed using the ``bzr reconfigure``
+command to either stack on an existing branch, or to turn off stacking.
+Be aware that when ``bzr reconfigure --unstacked`` is used, bzr will
+copy all the referenced data from the stacked-on repository into the
+previously stacked repository. For large repositories this may take
+considerable time and may substantially increase the size of the
+repository.
More information about the bazaar-commits
mailing list