Merge lp:~abentley/bzr/branch-store into lp:bzr

Proposed by Aaron Bentley on 2012-07-18
Status: Merged
Merged at revision: 6540
Proposed branch: lp:~abentley/bzr/branch-store
Merge into: lp:bzr
Diff against target: 847 lines (+496/-35)
16 files modified
bzrlib/branch.py (+57/-0)
bzrlib/builtins.py (+6/-2)
bzrlib/errors.py (+21/-0)
bzrlib/lock.py (+8/-0)
bzrlib/remote.py (+8/-0)
bzrlib/shelf.py (+23/-9)
bzrlib/switch.py (+30/-11)
bzrlib/tests/blackbox/test_switch.py (+38/-1)
bzrlib/tests/per_branch/test_branch.py (+89/-0)
bzrlib/tests/test_switch.py (+58/-10)
bzrlib/tests/test_workingtree.py (+32/-1)
bzrlib/workingtree.py (+29/-0)
doc/en/release-notes/bzr-2.6.txt (+2/-1)
doc/en/user-guide/index-plain.txt (+1/-0)
doc/en/user-guide/index.txt (+1/-0)
doc/en/user-guide/switch_store.txt (+93/-0)
To merge this branch: bzr merge lp:~abentley/bzr/branch-store
Reviewer Review Type Date Requested Status
Martin Packman (community) Approve on 2012-07-23
Jelmer Vernooij (community) 2012-07-18 Approve on 2012-07-20
Review via email: mp+115618@code.launchpad.net

Commit Message

(abentley) Switch stores uncommitted changes in branch (Aaron Bentley)

Description of the Change

This branch adds one of my favourite pipeline features to core Bazaar: storing and restoring uncommitted changes when switching between branches.

With colocated workflows now supported by core Bazaar, we should make them work smoothly. Storing uncommitted changes makes it convenient to switch between branches as needed.

The effect is similar to using "shelve --all" and "unshelve", but since the changes are associated with the branch instead of the working tree, bzr is able to select the correct changes automatically during the switch operation.

I have made this new mode the default because I believe it is the common case, and the least-likely to cause problems for users. Specifically, merging changes into existing branches often leads to conflicts. So for users who do not intend to apply the changes from their current branch to the new branch, there are two problems: 1. the changes are applied to the new branch, 2. The changes acquire conflicts, which makes it difficult to restore them to the original branch. By contrast, the only case where the new behaviour leads to conflicts is when the new branch has stored uncommitted changes, and the new branch's last_revision has changed since. This is possible, but not common.

There are two classes of users who may be surprised when switch merges their uncommitted changes into the new branch: users unfamiliar with switch, and users who did not realize they had uncommitted changes in their tree.

To post a comment you must log in.
Jelmer Vernooij (jelmer) wrote :

On Wed, Jul 18, 2012 at 08:47:19PM -0000, Aaron Bentley wrote:
> This branch adds one of my favourite pipeline features to core
> Bazaar: storing and restoring uncommitted changes when switching
> between branches.

> With colocated workflows now supported by core Bazaar, we should
> make them work smoothly. Storing uncommitted changes makes it
> convenient to switch between branches as needed.

> The effect is similar to using "shelve --all" and "unshelve", but
> since the changes are associated with the branch instead of the
> working tree, bzr is able to select the correct changes
> automatically during the switch operation.

> I have made this new mode the default because I believe it is the
> common case, and the least-likely to cause problems for users.
> Specifically, merging changes into existing branches often leads to
> conflicts. So for users who do not intend to apply the changes from
> their current branch to the new branch, there are two problems: 1.
> the changes are applied to the new branch, 2. The changes acquire
> conflicts, which makes it difficult to restore them to the original
> branch. By contrast, the only case where the new behaviour leads to
> conflicts is when the new branch has stored uncommitted changes, and
> the new branch's last_revision has changed since. This is possible,
> but not common.

> There are two classes of users who may be surprised when switch
> merges their uncommitted changes into the new branch: users
> unfamiliar with switch, and users who did not realize they had
> uncommitted changes in their tree.
Personally, I prefer the current behaviour; it means it's possible to
switch over to a new branch easily if you happen to change a few
things that really deserve their own branch ("bzr switch -b foo && bzr
ci"). The current behaviour is also consistent with git, for what
that is worth.

If switch is going to rely on them, then the new methods on Branch
should really be public. Foreign branch support will break with this
merge proposal as-is.

Cheers,

Jelmer

Aaron Bentley (abentley) wrote :

> On Wed, Jul 18, 2012 at 08:47:19PM -0000, Aaron Bentley wrote:
> Personally, I prefer the current behaviour; it means it's possible to
> switch over to a new branch easily if you happen to change a few
> things that really deserve their own branch ("bzr switch -b foo && bzr
> ci").

I'm willing to leave the current behaviour as the default. I think this behaviour is safer and more convenient in most cases. Please try it out for a few weeks, and I think you'll find you want it as the default, too.

> The current behaviour is also consistent with git, for what
> that is worth.

Okay. I was under the impression git didn't automatically merge, but said "error: You have local changes to "X"; cannot switch branches." like this: http://stackoverflow.com/questions/1304626/git-switch-branch-and-ignore-any-changes-without-committing

In fact, one of my co-workers who uses git was surprised that bzr didn't behave like the above, and that was one of the reasons I worked on this.

> If switch is going to rely on them, then the new methods on Branch
> should really be public. Foreign branch support will break with this
> merge proposal as-is.

Per our IRC discussion, I'll turn get_unshelver/store_uncommitted/has_stored_uncommitted into BzrBranch methods, since implementing _put_uncommitted and _get_uncommitted on foreign branches would not be desirable (if it could be implemented at all, it would store bzr-specific data in foreign branches).

Aaron Bentley (abentley) wrote :

Further research suggests git only errors if the uncommitted changes *conflict*: http://www.gitguys.com/topics/switching-branches-without-committing/

Aaron Bentley (abentley) wrote :

Updated per our discussion. I've removed _get/_put_uncommitted and has_stored_uncommitted. The remaining functions are implemented on BzrBranch/RemoteBranch, and an exception is available for foreign branches that don't support it (I've tested it locally with bzr-svn). The behaviour is disabled by default, enabled with --store.

Jelmer Vernooij (jelmer) wrote :

Thanks; this looks good to land to me now. It would be good if somebody else (mgz?) could also have a quick look at it.

Would --store perhaps be better named as --shelve, since that's a term people are already familiar with?

review: Approve
Aaron Bentley (abentley) wrote :

> Would --store perhaps be better named as --shelve, since that's a term people
> are already familiar with?

I thought it would be confusing to use "shelf", because of the UI differences between these and shelves:

- they cannot be manipulated with "bzr shelve/unshelve"
- they don't have the same interactive UI as "bzr shelve"
- there is one store per branch, but many shelves per working tree

What do you think?

Aaron

Jelmer Vernooij (jelmer) wrote :

> > Would --store perhaps be better named as --shelve, since that's a term
> people
> > are already familiar with?
>
> I thought it would be confusing to use "shelf", because of the UI differences
> between these and shelves:
>
> - they cannot be manipulated with "bzr shelve/unshelve"
> - they don't have the same interactive UI as "bzr shelve"
> - there is one store per branch, but many shelves per working tree
>
> What do you think?
Hmm, I hadn't considered it that way - I guess it could indeed be confusing if looks like a shelve but isn't quite a shelve. Let me retract that suggestion.

Martin Packman (gz) wrote :

Code all looks good, tests are helpful in explaining the intended behaviour.

Documentation is rather on the light side, a help string for a command switch and an entry in what's new isn't going to be enough for people who want this to find it.

Using --store to mean both stash the changes to the current branch and unstash the changes to the target branch is a slight stretch of english.

I'd like to see a blackbox test where both branches have stored changes.

Overall, this seems more like an alternative behaviour for switch that some people might want to configure on all the time, rather than a command line switch for situational use.

How would it work if say, you accidentally switched back to a branch with stored changes without passing --store, then commited something, then wanted to get your pending work again? Shelves are hard to recover manually, we don't want to introduce a new workflow that's likely to confuse people.

Configuring stuff off by default can be a bad compromise, we already have a bunch of good changes stuck behind an obscure option or command due to disagreements or not being finished.

Apart from question of whether this should really be a switch param rather than a config option, don't think anything should block this landing for now though.

review: Approve
lp:~abentley/bzr/branch-store updated on 2012-07-23
6570. By Aaron Bentley on 2012-07-23

Add docs on switch --store.

6571. By Aaron Bentley on 2012-07-23

Tweak docs.

6572. By Aaron Bentley on 2012-07-23

Update blackbox test to show storing and restoring in one switch.

Aaron Bentley (abentley) wrote :

> Documentation is rather on the light side, a help string for a command switch
> and an entry in what's new isn't going to be enough for people who want this
> to find it.

I've added switch_store.txt

> Using --store to mean both stash the changes to the current branch and unstash
> the changes to the target branch is a slight stretch of english.

Yes, but I can't think of anything better. It's also a bit of a cheat in that "store" and "restore" aren't really antonyms.

> I'd like to see a blackbox test where both branches have stored changes.

Per our IRC discussion, I've changed test_store_and_restore to include the case where the target branch has stored changes and the working tree has uncommitted changes.

> Overall, this seems more like an alternative behaviour for switch that some
> people might want to configure on all the time, rather than a command line
> switch for situational use.

Everyone will want the current default behaviour sometimes. If they alias "switch=switch --store", they can get it back with "switch --no-store". With a config option, it would be much more painful.

> How would it work if say, you accidentally switched back to a branch with
> stored changes without passing --store, then commited something, then wanted
> to get your pending work again?

You would use "bzr switch . --store" to get it back. I've added it to the docs.

> Configuring stuff off by default can be a bad compromise, we already have a
> bunch of good changes stuck behind an obscure option or command due to
> disagreements or not being finished.

Hopefully, we can revisit the default in the future. For now, this gets me an approval from jelmer.

Aaron

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'bzrlib/branch.py'
2--- bzrlib/branch.py 2012-07-06 11:27:16 +0000
3+++ bzrlib/branch.py 2012-07-23 17:13:23 +0000
4@@ -39,6 +39,7 @@
5 repository,
6 revision as _mod_revision,
7 rio,
8+ shelf,
9 tag as _mod_tag,
10 transport,
11 ui,
12@@ -251,6 +252,23 @@
13 """
14 raise NotImplementedError(self._get_config)
15
16+ def store_uncommitted(self, creator):
17+ """Store uncommitted changes from a ShelfCreator.
18+
19+ :param creator: The ShelfCreator containing uncommitted changes, or
20+ None to delete any stored changes.
21+ :raises: ChangesAlreadyStored if the branch already has changes.
22+ """
23+ raise NotImplementedError(self.store_uncommitted)
24+
25+ def get_unshelver(self, tree):
26+ """Return a shelf.Unshelver for this branch and tree.
27+
28+ :param tree: The tree to use to construct the Unshelver.
29+ :return: an Unshelver or None if no changes are stored.
30+ """
31+ raise NotImplementedError(self.get_unshelver)
32+
33 def _get_fallback_repository(self, url, possible_transports):
34 """Get the repository we fallback to at url."""
35 url = urlutils.join(self.base, url)
36@@ -2386,6 +2404,45 @@
37 self.conf_store = _mod_config.BranchStore(self)
38 return self.conf_store
39
40+ def _uncommitted_branch(self):
41+ """Return the branch that may contain uncommitted changes."""
42+ master = self.get_master_branch()
43+ if master is not None:
44+ return master
45+ else:
46+ return self
47+
48+ def store_uncommitted(self, creator):
49+ """Store uncommitted changes from a ShelfCreator.
50+
51+ :param creator: The ShelfCreator containing uncommitted changes, or
52+ None to delete any stored changes.
53+ :raises: ChangesAlreadyStored if the branch already has changes.
54+ """
55+ branch = self._uncommitted_branch()
56+ if creator is None:
57+ branch._transport.delete('stored-transform')
58+ return
59+ if branch._transport.has('stored-transform'):
60+ raise errors.ChangesAlreadyStored
61+ transform = StringIO()
62+ creator.write_shelf(transform)
63+ transform.seek(0)
64+ branch._transport.put_file('stored-transform', transform)
65+
66+ def get_unshelver(self, tree):
67+ """Return a shelf.Unshelver for this branch and tree.
68+
69+ :param tree: The tree to use to construct the Unshelver.
70+ :return: an Unshelver or None if no changes are stored.
71+ """
72+ branch = self._uncommitted_branch()
73+ try:
74+ transform = branch._transport.get('stored-transform')
75+ except errors.NoSuchFile:
76+ return None
77+ return shelf.Unshelver.from_tree_and_shelf(tree, transform)
78+
79 def is_locked(self):
80 return self.control_files.is_locked()
81
82
83=== modified file 'bzrlib/builtins.py'
84--- bzrlib/builtins.py 2012-07-20 13:23:32 +0000
85+++ bzrlib/builtins.py 2012-07-23 17:13:23 +0000
86@@ -6233,10 +6233,13 @@
87 Option('create-branch', short_name='b',
88 help='Create the target branch from this one before'
89 ' switching to it.'),
90+ Option('store',
91+ help='Store and restore uncommitted changes in the'
92+ ' branch.'),
93 ]
94
95 def run(self, to_location=None, force=False, create_branch=False,
96- revision=None, directory=u'.'):
97+ revision=None, directory=u'.', store=False):
98 from bzrlib import switch
99 tree_location = directory
100 revision = _get_one_revision('switch', revision)
101@@ -6273,7 +6276,8 @@
102 possible_transports=possible_transports)
103 if revision is not None:
104 revision = revision.as_revision_id(to_branch)
105- switch.switch(control_dir, to_branch, force, revision_id=revision)
106+ switch.switch(control_dir, to_branch, force, revision_id=revision,
107+ store_uncommitted=store)
108 if had_explicit_nick:
109 branch = control_dir.open_branch() #get the new branch!
110 branch.nick = to_branch.nick
111
112=== modified file 'bzrlib/errors.py'
113--- bzrlib/errors.py 2012-06-18 11:43:07 +0000
114+++ bzrlib/errors.py 2012-07-23 17:13:23 +0000
115@@ -2893,6 +2893,21 @@
116 BzrError.__init__(self, tree=tree, display_url=display_url, more=more)
117
118
119+class StoringUncommittedNotSupported(BzrError):
120+
121+ _fmt = ('Branch "%(display_url)s" does not support storing uncommitted'
122+ ' changes.')
123+
124+ def __init__(self, branch):
125+ import bzrlib.urlutils as urlutils
126+ user_url = getattr(branch, "user_url", None)
127+ if user_url is None:
128+ display_url = str(branch)
129+ else:
130+ display_url = urlutils.unescape_for_display(user_url, 'ascii')
131+ BzrError.__init__(self, branch=branch, display_url=display_url)
132+
133+
134 class ShelvedChanges(UncommittedChanges):
135
136 _fmt = ('Working tree "%(display_url)s" has shelved changes'
137@@ -3340,3 +3355,9 @@
138
139 def __init__(self, feature):
140 self.feature = feature
141+
142+
143+class ChangesAlreadyStored(BzrCommandError):
144+
145+ _fmt = ('Cannot store uncommitted changes because this branch already'
146+ ' stores uncommitted changes.')
147
148=== modified file 'bzrlib/lock.py'
149--- bzrlib/lock.py 2011-12-19 13:23:58 +0000
150+++ bzrlib/lock.py 2012-07-23 17:13:23 +0000
151@@ -35,6 +35,7 @@
152
153 from __future__ import absolute_import
154
155+import contextlib
156 import errno
157 import os
158 import sys
159@@ -548,3 +549,10 @@
160 trace.note(gettext('{0!r} was {1} locked again'), self, type_name)
161 self._prev_lock = lock_type
162
163+@contextlib.contextmanager
164+def write_locked(lockable):
165+ lockable.lock_write()
166+ try:
167+ yield lockable
168+ finally:
169+ lockable.unlock()
170
171=== modified file 'bzrlib/remote.py'
172--- bzrlib/remote.py 2012-06-28 16:17:34 +0000
173+++ bzrlib/remote.py 2012-07-23 17:13:23 +0000
174@@ -3393,6 +3393,14 @@
175 self.conf_store = RemoteBranchStore(self)
176 return self.conf_store
177
178+ def store_uncommitted(self, creator):
179+ self._ensure_real()
180+ return self._real_branch.store_uncommitted(creator)
181+
182+ def get_unshelver(self, tree):
183+ self._ensure_real()
184+ return self._real_branch.get_unshelver(tree)
185+
186 def _get_real_transport(self):
187 # if we try vfs access, return the real branch's vfs transport
188 self._ensure_real()
189
190=== modified file 'bzrlib/shelf.py'
191--- bzrlib/shelf.py 2011-12-18 15:28:38 +0000
192+++ bzrlib/shelf.py 2012-07-23 17:13:23 +0000
193@@ -125,9 +125,14 @@
194 raise ValueError('Unknown change kind: "%s"' % change[0])
195
196 def shelve_all(self):
197- """Shelve all changes."""
198+ """Shelve all changes.
199+
200+ :return: True if changes were shelved, False if there were no changes.
201+ """
202+ change = None
203 for change in self.iter_shelvable():
204 self.shelve_change(change)
205+ return change is not None
206
207 def shelve_rename(self, file_id):
208 """Shelve a file rename.
209@@ -255,6 +260,14 @@
210 """Shelve changes from working tree."""
211 self.work_transform.apply()
212
213+ @staticmethod
214+ def metadata_record(serializer, revision_id, message=None):
215+ metadata = {'revision_id': revision_id}
216+ if message is not None:
217+ metadata['message'] = message.encode('utf-8')
218+ return serializer.bytes_record(
219+ bencode.bencode(metadata), (('metadata',),))
220+
221 def write_shelf(self, shelf_file, message=None):
222 """Serialize the shelved changes to a file.
223
224@@ -263,16 +276,17 @@
225 :return: the filename of the written file.
226 """
227 transform.resolve_conflicts(self.shelf_transform)
228+ revision_id = self.target_tree.get_revision_id()
229+ return self._write_shelf(shelf_file, self.shelf_transform, revision_id,
230+ message)
231+
232+ @classmethod
233+ def _write_shelf(cls, shelf_file, transform, revision_id, message=None):
234 serializer = pack.ContainerSerialiser()
235 shelf_file.write(serializer.begin())
236- metadata = {
237- 'revision_id': self.target_tree.get_revision_id(),
238- }
239- if message is not None:
240- metadata['message'] = message.encode('utf-8')
241- shelf_file.write(serializer.bytes_record(
242- bencode.bencode(metadata), (('metadata',),)))
243- for bytes in self.shelf_transform.serialize(serializer):
244+ metadata = cls.metadata_record(serializer, revision_id, message)
245+ shelf_file.write(metadata)
246+ for bytes in transform.serialize(serializer):
247 shelf_file.write(bytes)
248 shelf_file.write(serializer.end())
249
250
251=== modified file 'bzrlib/switch.py'
252--- bzrlib/switch.py 2012-02-14 17:22:37 +0000
253+++ bzrlib/switch.py 2012-07-23 17:13:23 +0000
254@@ -18,7 +18,12 @@
255
256 # Original author: David Allouche
257
258-from bzrlib import errors, merge, revision
259+from bzrlib import (
260+ errors,
261+ lock,
262+ merge,
263+ revision
264+ )
265 from bzrlib.branch import Branch
266 from bzrlib.i18n import gettext
267 from bzrlib.trace import note
268@@ -32,26 +37,32 @@
269 for hook in hooks:
270 hook(params)
271
272-def switch(control_dir, to_branch, force=False, quiet=False, revision_id=None):
273+def switch(control_dir, to_branch, force=False, quiet=False, revision_id=None,
274+ store_uncommitted=False):
275 """Switch the branch associated with a checkout.
276
277 :param control_dir: ControlDir of the checkout to change
278 :param to_branch: branch that the checkout is to reference
279 :param force: skip the check for local commits in a heavy checkout
280 :param revision_id: revision ID to switch to.
281+ :param store_uncommitted: If True, store uncommitted changes in the
282+ branch.
283 """
284 _check_pending_merges(control_dir, force)
285 try:
286 source_repository = control_dir.open_branch().repository
287 except errors.NotBranchError:
288 source_repository = to_branch.repository
289+ if store_uncommitted:
290+ with lock.write_locked(control_dir.open_workingtree()) as tree:
291+ tree.store_uncommitted()
292 to_branch.lock_read()
293 try:
294 _set_branch_location(control_dir, to_branch, force)
295 finally:
296 to_branch.unlock()
297 tree = control_dir.open_workingtree()
298- _update(tree, source_repository, quiet, revision_id)
299+ _update(tree, source_repository, quiet, revision_id, store_uncommitted)
300 _run_post_switch_hooks(control_dir, to_branch, force, revision_id)
301
302 def _check_pending_merges(control, force=False):
303@@ -151,13 +162,18 @@
304 return False
305
306
307-def _update(tree, source_repository, quiet=False, revision_id=None):
308+def _update(tree, source_repository, quiet=False, revision_id=None,
309+ restore_uncommitted=False):
310 """Update a working tree to the latest revision of its branch.
311
312 :param tree: the working tree
313 :param source_repository: repository holding the revisions
314+ :param restore_uncommitted: restore any uncommitted changes in the branch.
315 """
316- tree.lock_tree_write()
317+ if restore_uncommitted:
318+ tree.lock_write()
319+ else:
320+ tree.lock_tree_write()
321 try:
322 to_branch = tree.branch
323 if revision_id is None:
324@@ -165,11 +181,14 @@
325 if tree.last_revision() == revision_id:
326 if not quiet:
327 note(gettext("Tree is up to date at revision %d."), to_branch.revno())
328- return
329- base_tree = source_repository.revision_tree(tree.last_revision())
330- merge.Merge3Merger(tree, tree, base_tree, to_branch.repository.revision_tree(revision_id))
331- tree.set_last_revision(to_branch.last_revision())
332- if not quiet:
333- note(gettext('Updated to revision %d.') % to_branch.revno())
334+ else:
335+ base_tree = source_repository.revision_tree(tree.last_revision())
336+ merge.Merge3Merger(tree, tree, base_tree,
337+ to_branch.repository.revision_tree(revision_id))
338+ tree.set_last_revision(to_branch.last_revision())
339+ if not quiet:
340+ note(gettext('Updated to revision %d.') % to_branch.revno())
341+ if restore_uncommitted:
342+ tree.restore_uncommitted()
343 finally:
344 tree.unlock()
345
346=== modified file 'bzrlib/tests/blackbox/test_switch.py'
347--- bzrlib/tests/blackbox/test_switch.py 2012-06-18 11:43:07 +0000
348+++ bzrlib/tests/blackbox/test_switch.py 2012-07-23 17:13:23 +0000
349@@ -304,7 +304,8 @@
350
351 def test_switch_lightweight_after_branch_moved_relative(self):
352 self.prepare_lightweight_switch()
353- self.run_bzr('switch --force branch1', working_dir='tree')
354+ self.run_bzr('switch --force branch1',
355+ working_dir='tree')
356 branch_location = WorkingTree.open('tree').branch.base
357 self.assertEndsWith(branch_location, 'branch1/')
358
359@@ -492,3 +493,39 @@
360 self.assertLength(24, self.hpss_calls)
361 self.assertLength(4, self.hpss_connections)
362 self.assertThat(self.hpss_calls, ContainsNoVfsCalls)
363+
364+
365+class TestSwitchUncommitted(TestCaseWithTransport):
366+
367+ def prepare(self):
368+ tree = self.make_branch_and_tree('orig')
369+ tree.commit('')
370+ tree.branch.bzrdir.sprout('new')
371+ checkout = tree.branch.create_checkout('checkout', lightweight=True)
372+ self.build_tree(['checkout/a'])
373+ self.assertPathExists('checkout/a')
374+ checkout.add('a')
375+ return checkout
376+
377+ def test_store_and_restore_uncommitted(self):
378+ checkout = self.prepare()
379+ self.run_bzr(['switch', '--store', '-d', 'checkout', 'new'])
380+ self.build_tree(['checkout/b'])
381+ checkout.add('b')
382+ self.assertPathDoesNotExist('checkout/a')
383+ self.assertPathExists('checkout/b')
384+ self.run_bzr(['switch', '--store', '-d', 'checkout', 'orig'])
385+ self.assertPathExists('checkout/a')
386+ self.assertPathDoesNotExist('checkout/b')
387+
388+ def test_does_not_store(self):
389+ self.prepare()
390+ self.run_bzr(['switch', '-d', 'checkout', 'new'])
391+ self.assertPathExists('checkout/a')
392+
393+ def test_does_not_restore_changes(self):
394+ self.prepare()
395+ self.run_bzr(['switch', '--store', '-d', 'checkout', 'new'])
396+ self.assertPathDoesNotExist('checkout/a')
397+ self.run_bzr(['switch', '-d', 'checkout', 'orig'])
398+ self.assertPathDoesNotExist('checkout/a')
399
400=== modified file 'bzrlib/tests/per_branch/test_branch.py'
401--- bzrlib/tests/per_branch/test_branch.py 2012-03-14 11:49:29 +0000
402+++ bzrlib/tests/per_branch/test_branch.py 2012-07-23 17:13:23 +0000
403@@ -16,6 +16,8 @@
404
405 """Tests for branch implementations - tests a branch format."""
406
407+import contextlib
408+
409 from bzrlib import (
410 branch as _mod_branch,
411 controldir,
412@@ -25,10 +27,12 @@
413 merge,
414 osutils,
415 urlutils,
416+ transform,
417 transport,
418 remote,
419 repository,
420 revision,
421+ shelf,
422 symbol_versioning,
423 tests,
424 )
425@@ -1057,3 +1061,88 @@
426 # above the control dir but we might need to relax that?
427 self.assertEqual(br.control_url.find(br.user_url), 0)
428 self.assertEqual(br.control_url, br.control_transport.base)
429+
430+
431+class FakeShelfCreator(object):
432+
433+ def __init__(self, branch):
434+ self.branch = branch
435+
436+ def write_shelf(self, shelf_file, message=None):
437+ tree = self.branch.repository.revision_tree(revision.NULL_REVISION)
438+ with transform.TransformPreview(tree) as tt:
439+ shelf.ShelfCreator._write_shelf(
440+ shelf_file, tt, revision.NULL_REVISION)
441+
442+
443+@contextlib.contextmanager
444+def skip_if_storing_uncommitted_unsupported():
445+ try:
446+ yield
447+ except errors.StoringUncommittedNotSupported:
448+ raise tests.TestNotApplicable('Cannot store uncommitted changes.')
449+
450+
451+class TestUncommittedChanges(per_branch.TestCaseWithBranch):
452+
453+ def bind(self, branch, master):
454+ try:
455+ branch.bind(master)
456+ except errors.UpgradeRequired:
457+ raise tests.TestNotApplicable('Branch cannot be bound.')
458+
459+ def test_store_uncommitted(self):
460+ tree = self.make_branch_and_tree('b')
461+ branch = tree.branch
462+ creator = FakeShelfCreator(branch)
463+ with skip_if_storing_uncommitted_unsupported():
464+ self.assertIs(None, branch.get_unshelver(tree))
465+ branch.store_uncommitted(creator)
466+ self.assertIsNot(None, branch.get_unshelver(tree))
467+
468+ def test_store_uncommitted_bound(self):
469+ tree = self.make_branch_and_tree('b')
470+ branch = tree.branch
471+ master = self.make_branch('master')
472+ self.bind(branch, master)
473+ creator = FakeShelfCreator(tree.branch)
474+ self.assertIs(None, tree.branch.get_unshelver(tree))
475+ self.assertIs(None, master.get_unshelver(tree))
476+ tree.branch.store_uncommitted(creator)
477+ self.assertIsNot(None, master.get_unshelver(tree))
478+
479+ def test_store_uncommitted_already_stored(self):
480+ branch = self.make_branch('b')
481+ with skip_if_storing_uncommitted_unsupported():
482+ branch.store_uncommitted(FakeShelfCreator(branch))
483+ self.assertRaises(errors.ChangesAlreadyStored,
484+ branch.store_uncommitted, FakeShelfCreator(branch))
485+
486+ def test_store_uncommitted_none(self):
487+ branch = self.make_branch('b')
488+ with skip_if_storing_uncommitted_unsupported():
489+ branch.store_uncommitted(FakeShelfCreator(branch))
490+ branch.store_uncommitted(None)
491+ self.assertIs(None, branch.get_unshelver(None))
492+
493+ def test_get_unshelver(self):
494+ tree = self.make_branch_and_tree('tree')
495+ tree.commit('')
496+ self.build_tree_contents([('tree/file', 'contents1')])
497+ tree.add('file')
498+ with skip_if_storing_uncommitted_unsupported():
499+ tree.store_uncommitted()
500+ unshelver = tree.branch.get_unshelver(tree)
501+ self.assertIsNot(None, unshelver)
502+
503+ def test_get_unshelver_bound(self):
504+ tree = self.make_branch_and_tree('tree')
505+ tree.commit('')
506+ self.build_tree_contents([('tree/file', 'contents1')])
507+ tree.add('file')
508+ with skip_if_storing_uncommitted_unsupported():
509+ tree.store_uncommitted()
510+ branch = self.make_branch('branch')
511+ self.bind(branch, tree.branch)
512+ unshelver = branch.get_unshelver(tree)
513+ self.assertIsNot(None, unshelver)
514
515=== modified file 'bzrlib/tests/test_switch.py'
516--- bzrlib/tests/test_switch.py 2011-05-13 12:51:05 +0000
517+++ bzrlib/tests/test_switch.py 2012-07-23 17:13:23 +0000
518@@ -22,9 +22,11 @@
519 from bzrlib import (
520 branch,
521 errors,
522+ lock,
523 merge as _mod_merge,
524 switch,
525 tests,
526+ workingtree,
527 )
528
529
530@@ -34,6 +36,14 @@
531 super(TestSwitch, self).setUp()
532 self.lightweight = True
533
534+ @staticmethod
535+ def _master_if_present(branch):
536+ master = branch.get_master_branch()
537+ if master:
538+ return master
539+ else:
540+ return branch
541+
542 def _setup_tree(self):
543 tree = self.make_branch_and_tree('branch-1')
544 self.build_tree(['branch-1/file-1'])
545@@ -41,18 +51,56 @@
546 tree.commit('rev1')
547 return tree
548
549+ def _setup_uncommitted(self, same_revision=False):
550+ tree = self._setup_tree()
551+ to_branch = tree.bzrdir.sprout('branch-2').open_branch()
552+ self.build_tree(['branch-1/file-2'])
553+ if not same_revision:
554+ tree.add('file-2')
555+ tree.remove('file-1')
556+ tree.commit('rev2')
557+ checkout = tree.branch.create_checkout('checkout',
558+ lightweight=self.lightweight)
559+ self.build_tree(['checkout/file-3'])
560+ checkout.add('file-3')
561+ return checkout, to_branch
562+
563+ def test_switch_store_uncommitted(self):
564+ """Test switch updates tree and stores uncommitted changes."""
565+ checkout, to_branch = self._setup_uncommitted()
566+ self.assertPathDoesNotExist('checkout/file-1')
567+ self.assertPathExists('checkout/file-2')
568+ switch.switch(checkout.bzrdir, to_branch, store_uncommitted=True)
569+ self.assertPathExists('checkout/file-1')
570+ self.assertPathDoesNotExist('checkout/file-2')
571+ self.assertPathDoesNotExist('checkout/file-3')
572+
573+ def test_switch_restore_uncommitted(self):
574+ """Test switch updates tree and restores uncommitted changes."""
575+ checkout, to_branch = self._setup_uncommitted()
576+ old_branch = self._master_if_present(checkout.branch)
577+ self.assertPathDoesNotExist('checkout/file-1')
578+ self.assertPathExists('checkout/file-2')
579+ self.assertPathExists('checkout/file-3')
580+ switch.switch(checkout.bzrdir, to_branch, store_uncommitted=True)
581+ checkout = workingtree.WorkingTree.open('checkout')
582+ switch.switch(checkout.bzrdir, old_branch, store_uncommitted=True)
583+ self.assertPathDoesNotExist('checkout/file-1')
584+ self.assertPathExists('checkout/file-2')
585+ self.assertPathExists('checkout/file-3')
586+
587+ def test_switch_restore_uncommitted_same_revision(self):
588+ """Test switch updates tree and restores uncommitted changes."""
589+ checkout, to_branch = self._setup_uncommitted(same_revision=True)
590+ old_branch = self._master_if_present(checkout.branch)
591+ switch.switch(checkout.bzrdir, to_branch, store_uncommitted=True)
592+ checkout = workingtree.WorkingTree.open('checkout')
593+ switch.switch(checkout.bzrdir, old_branch, store_uncommitted=True)
594+ self.assertPathExists('checkout/file-3')
595+
596 def test_switch_updates(self):
597 """Test switch updates tree and keeps uncommitted changes."""
598- tree = self._setup_tree()
599- to_branch = tree.bzrdir.sprout('branch-2').open_branch()
600- self.build_tree(['branch-1/file-2'])
601- tree.add('file-2')
602- tree.remove('file-1')
603- tree.commit('rev2')
604- checkout = tree.branch.create_checkout('checkout',
605- lightweight=self.lightweight)
606- self.build_tree(['checkout/file-3'])
607- checkout.add('file-3')
608+ checkout, to_branch = self._setup_uncommitted()
609 self.assertPathDoesNotExist('checkout/file-1')
610 self.assertPathExists('checkout/file-2')
611 switch.switch(checkout.bzrdir, to_branch)
612
613=== modified file 'bzrlib/tests/test_workingtree.py'
614--- bzrlib/tests/test_workingtree.py 2012-03-14 08:34:10 +0000
615+++ bzrlib/tests/test_workingtree.py 2012-07-23 17:13:23 +0000
616@@ -19,12 +19,12 @@
617 bzrdir,
618 conflicts,
619 errors,
620- symbol_versioning,
621 transport,
622 workingtree,
623 workingtree_3,
624 workingtree_4,
625 )
626+from bzrlib.lock import write_locked
627 from bzrlib.lockdir import LockDir
628 from bzrlib.mutabletree import needs_tree_write_lock
629 from bzrlib.tests import TestCase, TestCaseWithTransport, TestSkipped
630@@ -495,3 +495,34 @@
631 self.make_branch('qux')
632 trees = workingtree.WorkingTree.find_trees('.')
633 self.assertEqual(2, len(list(trees)))
634+
635+
636+class TestStoredUncommitted(TestCaseWithTransport):
637+
638+ def store_uncommitted(self):
639+ tree = self.make_branch_and_tree('tree')
640+ tree.commit('get root in there')
641+ self.build_tree_contents([('tree/file', 'content')])
642+ tree.add('file', 'file-id')
643+ tree.store_uncommitted()
644+ return tree
645+
646+ def test_store_uncommitted(self):
647+ self.store_uncommitted()
648+ self.assertPathDoesNotExist('tree/file')
649+
650+ def test_store_uncommitted_no_change(self):
651+ tree = self.make_branch_and_tree('tree')
652+ tree.commit('get root in there')
653+ tree.store_uncommitted()
654+ self.assertIs(None, tree.branch.get_unshelver(tree))
655+
656+ def test_restore_uncommitted(self):
657+ with write_locked(self.store_uncommitted()) as tree:
658+ tree.restore_uncommitted()
659+ self.assertPathExists('tree/file')
660+ self.assertIs(None, tree.branch.get_unshelver(tree))
661+
662+ def test_restore_uncommitted_none(self):
663+ tree = self.make_branch_and_tree('tree')
664+ tree.restore_uncommitted()
665
666=== modified file 'bzrlib/workingtree.py'
667--- bzrlib/workingtree.py 2012-06-18 11:43:07 +0000
668+++ bzrlib/workingtree.py 2012-07-23 17:13:23 +0000
669@@ -60,6 +60,7 @@
670 revision as _mod_revision,
671 revisiontree,
672 rio as _mod_rio,
673+ shelf,
674 transform,
675 transport,
676 ui,
677@@ -1351,6 +1352,34 @@
678 basis_tree.unlock()
679 return conflicts
680
681+ @needs_write_lock
682+ def store_uncommitted(self):
683+ """Store uncommitted changes from the tree in the branch."""
684+ target_tree = self.basis_tree()
685+ shelf_creator = shelf.ShelfCreator(self, target_tree)
686+ try:
687+ if not shelf_creator.shelve_all():
688+ return
689+ self.branch.store_uncommitted(shelf_creator)
690+ shelf_creator.transform()
691+ finally:
692+ shelf_creator.finalize()
693+ note('Uncommitted changes stored in branch "%s".', self.branch.nick)
694+
695+ @needs_write_lock
696+ def restore_uncommitted(self):
697+ """Restore uncommitted changes from the branch into the tree."""
698+ unshelver = self.branch.get_unshelver(self)
699+ if unshelver is None:
700+ return
701+ try:
702+ merger = unshelver.make_merger()
703+ merger.ignore_zero = True
704+ merger.do_merge()
705+ self.branch.store_uncommitted(None)
706+ finally:
707+ unshelver.finalize()
708+
709 def revision_tree(self, revision_id):
710 """See Tree.revision_tree.
711
712
713=== modified file 'doc/en/release-notes/bzr-2.6.txt'
714--- doc/en/release-notes/bzr-2.6.txt 2012-07-10 12:19:23 +0000
715+++ doc/en/release-notes/bzr-2.6.txt 2012-07-23 17:13:23 +0000
716@@ -18,7 +18,8 @@
717 New Features
718 ************
719
720-.. New commands, options, etc that users may wish to try out.
721+* ``bzr switch --store`` now stores uncommitted changes in the branch, and
722+ restores them when switching back to the branch. (Aaron Bentley)
723
724 Improvements
725 ************
726
727=== modified file 'doc/en/user-guide/index-plain.txt'
728--- doc/en/user-guide/index-plain.txt 2011-05-16 10:22:06 +0000
729+++ doc/en/user-guide/index-plain.txt 2012-07-23 17:13:23 +0000
730@@ -79,6 +79,7 @@
731 .. include:: part2_intro.txt
732 .. include:: adv_merging.txt
733 .. include:: shelving_changes.txt
734+.. include:: switch_store.txt
735 .. include:: filtered_views.txt
736 .. include:: stacked.txt
737 .. include:: server.txt
738
739=== modified file 'doc/en/user-guide/index.txt'
740--- doc/en/user-guide/index.txt 2011-06-13 14:08:28 +0000
741+++ doc/en/user-guide/index.txt 2012-07-23 17:13:23 +0000
742@@ -97,6 +97,7 @@
743
744 part2_intro
745 adv_merging
746+ switch_store
747 shelving_changes
748 filtered_views
749 stacked
750
751=== added file 'doc/en/user-guide/switch_store.txt'
752--- doc/en/user-guide/switch_store.txt 1970-01-01 00:00:00 +0000
753+++ doc/en/user-guide/switch_store.txt 2012-07-23 17:13:23 +0000
754@@ -0,0 +1,93 @@
755+Switch --store
756+==============
757+
758+In workflows that a single working tree, like co-located branches, sometimes
759+you want to switch while you have uncommitted changes. By default, ``switch``
760+will apply your uncommitted changes to the new branch that you switch to. But
761+often you don't want that. You just want to do some work in the other branch,
762+and eventually return to this branch and work some more.
763+
764+You could run ``bzr shelve --all`` before switching, to store the changes
765+safely. So you have to know that there are uncommitted changes present, and
766+you have to remember to run ``bzr shelve --all``. Then when you switch back to
767+the branch, you need to remember to unshelve the changes, and you need to know
768+what their shelf-id was.
769+
770+Using ``switch --store`` takes care of all of this for you. If there are any
771+uncommitted changes in your tree, it stores them in your branch. It then
772+restores any uncommitted changes that were stored in the branch of your target
773+tree. It's almost like having two working trees and using ``cd`` to switch
774+between them.
775+
776+To take an example, first we'd set up a co-located branch::
777+
778+ $ bzr init foo
779+ Created a standalone tree (format: 2a)
780+ $ cd foo
781+ $ bzr switch -b foo
782+
783+Now create committed and uncommitted changes::
784+
785+ $ touch committed
786+ $ bzr add
787+ adding committed
788+ $ bzr commit -m "Add committed"
789+ Committing to: /home/abentley/sandbox/foo/
790+ added committed
791+ Committed revision 1.
792+ $ touch uncommitted
793+ $ bzr add
794+ adding uncommitted
795+ $ ls
796+ committed uncommitted
797+
798+Now create a new branch using ``--store``. The uncommitted changes are stored
799+in "foo", but the committed changes are retained.
800+::
801+
802+ $ bzr switch -b --store bar
803+ Uncommitted changes stored in branch "foo".
804+ Tree is up to date at revision 1.
805+ Switched to branch: /home/abentley/sandbox/foo/
806+ abentley@speedy:~/sandbox/foo$ ls
807+ committed
808+
809+Now, create uncommitted changes in "bar"::
810+
811+ $ touch uncommitted-bar
812+ $ bzr add
813+ adding uncommitted-bar
814+
815+Finally, switch back to "foo"::
816+
817+ $ bzr switch --store foo
818+ Uncommitted changes stored in branch "bar".
819+ Tree is up to date at revision 1.
820+ Switched to branch: /home/abentley/sandbox/foo/
821+ $ ls
822+ committed uncommitted
823+
824+Each branch holds only one set of stored changes. If you try to store a second
825+set, you get an error. If you use ``--store`` all the time, this can't happen.
826+But if you use plain switch, then it won't restore the uncommitted changes
827+already present::
828+
829+ $ bzr switch bar
830+ Tree is up to date at revision 1.
831+ Switched to branch: /home/abentley/sandbox/foo/
832+ $ bzr switch --store foo
833+ bzr: ERROR: Cannot store uncommitted changes because this branch already
834+ stores uncommitted changes.
835+
836+If you're working in a branch that already has stored changes, you can restore
837+them with ``bzr switch . --store``::
838+
839+ $ bzr shelve --all -m "Uncommitted changes from foo"
840+ Selected changes:
841+ -D uncommitted
842+ Changes shelved with id "1".
843+ $ bzr switch . --store
844+ Tree is up to date at revision 1.
845+ Switched to branch: /home/abentley/sandbox/foo/
846+ $ ls
847+ committed uncommitted-bar