Merge lp:~abentley/bzr/merge-into-empty into lp:bzr

Proposed by Aaron Bentley
Status: Merged
Approved by: Vincent Ladeuil
Approved revision: no longer in the source branch.
Merged at revision: 5968
Proposed branch: lp:~abentley/bzr/merge-into-empty
Merge into: lp:bzr
Prerequisite: lp:~abentley/bzr/merge-into-empty-fixups
Diff against target: 256 lines (+98/-24)
9 files modified
bzrlib/merge.py (+17/-6)
bzrlib/tests/blackbox/test_merge.py (+3/-2)
bzrlib/tests/per_workingtree/test_merge_from_branch.py (+2/-0)
bzrlib/tests/test_merge.py (+34/-0)
bzrlib/tests/test_shelf.py (+8/-9)
bzrlib/tests/test_shelf_ui.py (+9/-4)
bzrlib/tests/test_transform.py (+17/-0)
bzrlib/transform.py (+6/-3)
doc/en/release-notes/bzr-2.4.txt (+2/-0)
To merge this branch: bzr merge lp:~abentley/bzr/merge-into-empty
Reviewer Review Type Date Requested Status
Vincent Ladeuil Approve
John A Meinel Needs Fixing
Review via email: mp+64078@code.launchpad.net

Commit message

Support merge into empty tree.

Description of the change

This branch enables bzrlib to merge into the empty tree, i.e. the tree associated with the null revision. This tree is special because it has no root.

Such merges are not typically useful for bzr itself, but they are attempted when someone proposes a merge in Launchpad that merges a branch with commits into a branch that has no commits. (See bug #595328.) I would rather fix the root cause in bzrlib than special-case the diff generation code.

The code in TreeTransform.fixup_new_roots seemed more suitable than the code in Merger.fix_root, so I've switched to fixup_new_roots and deprecated fix_root.

There is some behavioural difference between the two. fixup_new_roots does not generate a conflict if a new root is added and the old root is not deleted. It happily merges the contents of the two roots. In such cases, the new root wins, whereas fix_root would have the old root win. Still, the new behaviour is in keeping with Merge's tendency of letting OTHER win in the case of ambiguity. And reducing duplication between fixup_new_roots and fix_root is also a win.

I've also changed Merge to ensure that root entries are processed.

I've had a bit of a revelation that TreeTransforms that delete the root aren't *invalid*, they simply can't be applied to a working tree. (They could be used to generate a _PreviewTree, for example) With that in mind, I've updated apply_removals to skip deletions of the root directory.

As a bonus, test_shelve_old_root_deleted no longer fails. I've taken the opportunity to rewrite it slightly using ExpectedException, which I find is clearer than assertRaises.

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

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

On 6/9/2011 9:51 PM, Aaron Bentley wrote:
> Aaron Bentley has proposed merging lp:~abentley/bzr/merge-into-empty into lp:bzr with lp:~abentley/bzr/merge-into-empty-fixups as a prerequisite.
>
> Requested reviews:
> bzr-core (bzr-core)
> Related bugs:
> Bug #595328 in Bazaar: "merge-proposal-jobs OOPS-1628MPJ1 in cancel_deletion when merging into empty tree"
> https://bugs.launchpad.net/bzr/+bug/595328
>
> For more details, see:
> https://code.launchpad.net/~abentley/bzr/merge-into-empty/+merge/64078
>
> This branch enables bzrlib to merge into the empty tree, i.e. the tree associated with the null revision. This tree is special because it has no root.
>
> Such merges are not typically useful for bzr itself, but they are attempted when someone proposes a merge in Launchpad that merges a branch with commits into a branch that has no commits. (See bug #595328.) I would rather fix the root cause in bzrlib than special-case the diff generation code.
>
> The code in TreeTransform.fixup_new_roots seemed more suitable than the code in Merger.fix_root, so I've switched to fixup_new_roots and deprecated fix_root.
>
> There is some behavioural difference between the two. fixup_new_roots does not generate a conflict if a new root is added and the old root is not deleted. It happily merges the contents of the two roots. In such cases, the new root wins, whereas fix_root would have the old root win. Still, the new behaviour is in keeping with Merge's tendency of letting OTHER win in the case of ambiguity. And reducing duplication between fixup_new_roots and fix_root is also a win.
>
> I've also changed Merge to ensure that root entries are processed.
>
> I've had a bit of a revelation that TreeTransforms that delete the root aren't *invalid*, they simply can't be applied to a working tree. (They could be used to generate a _PeviewTree, for example) With that in mind, I've updated apply_removals to skip deletions of the root directory.
>
> As a bonus, test_shelve_old_root_deleted no longer fails. I've taken the opportunity to rewrite it slightly using ExpectedException, which I find is clearer than assertRaises.

+ name = names[self.winner_idx[name_winner]]
+ if parent_id is not None or name is not None:
             # if we get here, name_winner and parent_winner are set to safe
             # values.
- - self.tt.adjust_path(names[self.winner_idx[name_winner]],
- - self.tt.trans_id_file_id(parent_id),
+ if parent_id is None and name is not None:
+ # if parent_id is None and name is non-None, current
file is
+ # the tree root.
+ if names[self.winner_idx[parent_id_winner]] != '':
+ raise AssertionError(
+ 'File looks like a root, but named %s' %
+ names[self.winner_idx[parent_id_winner]])

^- Why are you doing the lookup with the parent_id_winner rather than
the name_winner that you just used?

+ parent_trans_id = transform.ROOT_PARENT
+ else:
+ parent_trans_id = self.tt.tr...

Read more...

review: Needs Fixing
Revision history for this message
Vincent Ladeuil (vila) wrote :
Download full text (5.3 KiB)

OK, so we're talking about brain surgery here... Thanks a lot for
tackling this one which looks indeed a bit far from your original
problem.

Great job (again), I'd like a few tweaks before landing, let's
discuss more if you disagree.

Given the area, I think it also deserves a second review but I
don't have particular hints for the other reviewer, in fact, I think
it's even better if he starts with a fresh mind (spoiler alert ;)

29 + # 'conflict'-- if there's no 'other', or it's not versioned, we
30 + # leave it alone.

[needsinfo] This comment puzzled me a bit so I dug and ended up with lp:~vila/bzr/merge-into-empty . Does the corresponding commit makes sense ?

- # it doesn't matter whether the result was 'other' or
- # 'conflict'-- if there's no 'other', or it's not versioned, we
- # leave it alone.
+ # it doesn't matter whether the result was 'other' or 'conflict'--
+ # if there's no 'other', or it's not versioned (unversioned being
+ # one such case), we leave it alone.

Additionally, do we still need:

43 + if names[self.winner_idx[parent_id_winner]] != '':
44 + raise AssertionError(
45 + 'File looks like a root, but named %s' %
46 + names[self.winner_idx[parent_id_winner]])

or do you feel better keeping it ? Hmm. I think *I*'d feel better keeping it but comments welcome.

65 + retcode = 1 if context == '.' or context == '' else 0

[needsfixing] At first I thought this wasn't really readable, but
digging a bit, I feel you're masking the fact that what really
happens there is that a conflict is created involving root. While
I realize this is out of scope for your proposal, that's not the
best way to indicate that this test is just... bad. Not only does
it rely on a side-effect: the merge with context == '' cannot
succeed at all if the previous context == '.' does not remember
the merge location but the root conflict itself deserves a better
test by itself.

In the context of this proposal, I think it's better to just get
rid of it or even better skip it and mentions bug #795456. I
started rewriting it but filed the bug instead when I realized
the implications.

78 - self.assertEqual(4, nb_conflicts)
79 + self.assertEqual(1, nb_conflicts)

Great, these conflicts were really ugly.

87 - # file4 could not be added to its original root, so it gets added to
88 - # the new root with a conflict.
89 - self.assertEqual(1, nb_conflicts)

I'm glad the conflict is gone, but can you fix the comment
instead of deleting it ? Especially since it looks like this is
as side-effect of your change that is not described in the cover
letter. As I understand it (imperfectly remembering the issues
this test evokes), file4 "should" not end up in the tree root but
instead should go into whatever the user decided to call the
directory he first merged. So not emitting a conflict here gives
no hint to the user that file4 is not where it should and that
could easily be missed in big merges (which is related to the fix
this test is associated with).

142 + def test_merge_reverse_revision_range(self):
143 + tree = self.make_branch_and_tree(".")

Oooh, here it is again :)

159 + tree.revert()
160 + se...

Read more...

review: Needs Fixing
Revision history for this message
Vincent Ladeuil (vila) wrote :

For the record: I discovered jam's review after posting mine.

Revision history for this message
Vincent Ladeuil (vila) wrote :

> In the context of this proposal, I think it's better to just get
> rid of it or even better skip it and mentions bug #795456. I
> started rewriting it but filed the bug instead when I realized
> the implications.

Not that much after all but better tracked outside of this proposal IMHO, so:
https://code.launchpad.net/~vila/bzr/795456-eager-test/+merge/64164

In a nutshell: you can leave the test as-is in *this* proposal and review *that* proposal.

Adding a test that both create the conflict for the tree root and shows how to resolve it will still be a nice addition in *this* proposal.

Revision history for this message
Aaron Bentley (abentley) wrote :

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

On 11-06-10 06:04 AM, John A Meinel wrote:
> + if names[self.winner_idx[parent_id_winner]] != '':
> + raise AssertionError(
> + 'File looks like a root, but named %s' %
> + names[self.winner_idx[parent_id_winner]])
>
> ^- Why are you doing the lookup with the parent_id_winner rather than
> the name_winner that you just used?

The rule is that parent_id = None means "root entry" only if the
corresponding name is ''. If I use name_winner, I am not checking the
corresponding entry.

> ^- I think even though the syntax is supported by python2.6 we don't
> actually use inline if-else syntax.

Okay.

> I know it takes more lines, but *I* certainly find it easier to read.

But if you never read it, you'll never get used to it. :-)

> ^- Can you assert "0, nb_conflicts" instead of nothing?

Actually, these tests should actually be generating conflicts. They are
broken because make_outer_tree is merging from null:, and that behaviour
has changed slightly.

> @@ -310,11 +312,11 @@
> tree1.merge_from_branch(tree2.branch,
> from_revision=revision.NULL_REVISION)
> tree1.commit('Replaced root entry')
> + self.shelve_all(tree1, rev2)
> # This is essentially assertNotRaises(InconsistentDelta)
> - self.expectFailure('Cannot shelve replacing a root entry',
> - self.assertRaises, AssertionError,
> - self.assertRaises, errors.InconsistentDelta,
> - self.shelve_all, tree1, rev2)
> + with ExpectedException(AssertionError, ''):
> + with ExpectedException(errors.InconsistentDelta, ''):
> + self.shelve_all(tree1, rev2)
>
> ^- If 'self.shelve_all(tree1, rev2)' now passes, why would we have
> ExpectedException at all?

That first shelve_all was test code, and I shouldn't have committed it.
 I've fixed it.

> It seems like this is artificial and should
> just be removed completely. (It is an InconsistentDelta because you've
> already shelved the changes.)

People (e.g. vila, about test_apply_retains_root_directory) often
complain when a test has no assertions. Here we're asserting that
shelve_all will not raise an InconsistentDelta.

Aaron
-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1.4.11 (GNU/Linux)
Comment: Using GnuPG with Mozilla - http://enigmail.mozdev.org/

iEYEARECAAYFAk3yY/YACgkQ0F+nu1YWqI3PJwCggPX6kA62ZRys1Xq2+IpTzTBc
X/EAn2uxtbmyHhyUTkq9Y4cSpua8aHWC
=miel
-----END PGP SIGNATURE-----

Revision history for this message
Aaron Bentley (abentley) wrote :
Download full text (5.0 KiB)

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

On 11-06-10 06:56 AM, Vincent Ladeuil wrote:
> 29 + # 'conflict'-- if there's no 'other', or it's not versioned, we
> 30 + # leave it alone.

Per our IRC discussion, I've changed this to:
            # it doesn't matter whether the result was 'other' or
            # 'conflict'-- if it has no file id, we leave it alone.

> Additionally, do we still need:
>
> 43 + if names[self.winner_idx[parent_id_winner]] != '':
> 44 + raise AssertionError(
> 45 + 'File looks like a root, but named %s' %
> 46 + names[self.winner_idx[parent_id_winner]])
>
> or do you feel better keeping it ? Hmm. I think *I*'d feel better keeping it but comments welcome.

I feel better keeping it. If we get a file with no parent_id and
accidentally treat it as a root entry, the errors we get will be really
confusing.

> In the context of this proposal, I think it's better to just get
> rid of it or even better skip it and mentions bug #795456. I
> started rewriting it but filed the bug instead when I realized
> the implications.

Okay.

>
> 78 - self.assertEqual(4, nb_conflicts)
> 79 + self.assertEqual(1, nb_conflicts)
>
> Great, these conflicts were really ugly.
>
> 87 - # file4 could not be added to its original root, so it gets added to
> 88 - # the new root with a conflict.
> 89 - self.assertEqual(1, nb_conflicts)
>
> I'm glad the conflict is gone, but can you fix the comment
> instead of deleting it ? Especially since it looks like this is
> as side-effect of your change that is not described in the cover
> letter. As I understand it (imperfectly remembering the issues
> this test evokes), file4 "should" not end up in the tree root but
> instead should go into whatever the user decided to call the
> directory he first merged. So not emitting a conflict here gives
> no hint to the user that file4 is not where it should and that
> could easily be missed in big merges (which is related to the fix
> this test is associated with).

As discussed on IRC, my changes actually broke these tests, which were
testing something different. We'll have to chat further about the right
way to fix them.

> How about:
> - # This is essentially assertNotRaises(InconsistentDelta)
> - with ExpectedException(AssertionError, ''):
> + with ExpectedException(AssertionError, 'InconsistentDelta not raised'):
> + # We expect errors.InconsistentDelta to not be raised.
> + # ExpectedException will raise an AssertionError that we'll catch
> + # in the enclosing 'with'

Too long-winded, but I've added 'InconsistentDelta not raised'.

> 202 + def test_apply_retains_root_directory(self):
> 203 + # Do not attempt to delete the physical root directory, because that
> 204 + # is impossible.
> 205 + transform, root = self.get_transform()
> 206 + with transform:
> 207 + transform.delete_contents(root)
> 208 + transform.apply()
>
> No checks here ? This is... surprising. Can you either add one

Done. It asserts that TransformRenameFailed is not raised.

> 221 - self.version_file(file_id, self._new_root)
> 222 + if file_id is not None:
> 223 + self.version_file(file_id, self._new_root)
>
> If I get rid o...

Read more...

Revision history for this message
John A Meinel (jameinel) wrote :

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

On 06/10/2011 08:37 PM, Aaron Bentley wrote:
...

...

> That first shelve_all was test code, and I shouldn't have committed it.
> I've fixed it.
>
>> It seems like this is artificial and should
>> just be removed completely. (It is an InconsistentDelta because you've
>> already shelved the changes.)
>
> People (e.g. vila, about test_apply_retains_root_directory) often
> complain when a test has no assertions. Here we're asserting that
> shelve_all will not raise an InconsistentDelta.
>
> Aaron

2 double negatives isn't a great way to assert a positive. A simple
comment is probably much more obvious.

# If something is broken, this will raise InconsistentDelta
self.shelve_all(tree1, rev2)

I agree that 'with ExpectedException' is a clearer way of *doing* the
double negative, but it is still a really ugly way of asserting that
something doesn't raise an exception.

John
=:->
-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1.4.11 (GNU/Linux)
Comment: Using GnuPG with Mozilla - http://enigmail.mozdev.org/

iEYEARECAAYFAk3yp38ACgkQJdeBCYSNAAOSkwCfbngIikjAXdVm2cQtWEfNc/CD
JPcAoMyCwcFHTSdZFP1CdwfBIMSPWoEx
=Zdgd
-----END PGP SIGNATURE-----

Revision history for this message
Vincent Ladeuil (vila) :
review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'bzrlib/merge.py'
2--- bzrlib/merge.py 2011-05-21 16:43:19 +0000
3+++ bzrlib/merge.py 2011-06-13 15:41:42 +0000
4@@ -868,7 +868,7 @@
5 executable3, file_status, resolver=resolver)
6 finally:
7 child_pb.finished()
8- self.fix_root()
9+ self.tt.fixup_new_roots()
10 self._finish_computing_transform()
11
12 def _finish_computing_transform(self):
13@@ -1093,6 +1093,7 @@
14 ))
15 return result
16
17+ @deprecated_method(deprecated_in((2, 4, 0)))
18 def fix_root(self):
19 if self.tt.final_kind(self.tt.root) is None:
20 self.tt.cancel_deletion(self.tt.root)
21@@ -1316,16 +1317,26 @@
22 self._raw_conflicts.append(('path conflict', trans_id, file_id,
23 this_parent, this_name,
24 other_parent, other_name))
25- if other_name is None:
26+ if not self.other_tree.has_id(file_id):
27 # it doesn't matter whether the result was 'other' or
28- # 'conflict'-- if there's no 'other', we leave it alone.
29+ # 'conflict'-- if it has no file id, we leave it alone.
30 return
31 parent_id = parents[self.winner_idx[parent_id_winner]]
32- if parent_id is not None:
33+ name = names[self.winner_idx[name_winner]]
34+ if parent_id is not None or name is not None:
35 # if we get here, name_winner and parent_winner are set to safe
36 # values.
37- self.tt.adjust_path(names[self.winner_idx[name_winner]],
38- self.tt.trans_id_file_id(parent_id),
39+ if parent_id is None and name is not None:
40+ # if parent_id is None and name is non-None, current file is
41+ # the tree root.
42+ if names[self.winner_idx[parent_id_winner]] != '':
43+ raise AssertionError(
44+ 'File looks like a root, but named %s' %
45+ names[self.winner_idx[parent_id_winner]])
46+ parent_trans_id = transform.ROOT_PARENT
47+ else:
48+ parent_trans_id = self.tt.trans_id_file_id(parent_id)
49+ self.tt.adjust_path(name, parent_trans_id,
50 self.tt.trans_id_file_id(file_id))
51
52 def _do_merge_contents(self, file_id):
53
54=== modified file 'bzrlib/tests/blackbox/test_merge.py'
55--- bzrlib/tests/blackbox/test_merge.py 2011-05-27 09:45:44 +0000
56+++ bzrlib/tests/blackbox/test_merge.py 2011-06-13 15:41:42 +0000
57@@ -668,8 +668,9 @@
58 self.build_tree([f])
59 tree.add(f)
60 tree.commit("added "+f)
61- for context in (".", "", "a"):
62- self.run_bzr("merge -r 1..0 " + context)
63+ for context in (".", "", 'a'):
64+ retcode = 1 if context == '.' or context == '' else 0
65+ self.run_bzr("merge -r 1..0 " + context, retcode=retcode)
66 self.assertPathDoesNotExist("a")
67 tree.revert()
68 self.assertPathExists("a")
69
70=== modified file 'bzrlib/tests/per_workingtree/test_merge_from_branch.py'
71--- bzrlib/tests/per_workingtree/test_merge_from_branch.py 2011-04-13 14:33:56 +0000
72+++ bzrlib/tests/per_workingtree/test_merge_from_branch.py 2011-06-13 15:41:42 +0000
73@@ -162,6 +162,8 @@
74 outer.commit('added foo')
75 inner = self.make_inner_branch()
76 outer.merge_from_branch(inner, to_revision='1', from_revision='null:')
77+ #retain original root id.
78+ outer.set_root_id(outer.basis_tree().get_root_id())
79 outer.commit('merge inner branch')
80 outer.mkdir('dir-outer', 'dir-outer-id')
81 outer.move(['dir', 'file3'], to_dir='dir-outer')
82
83=== modified file 'bzrlib/tests/test_merge.py'
84--- bzrlib/tests/test_merge.py 2011-05-26 08:05:45 +0000
85+++ bzrlib/tests/test_merge.py 2011-06-13 15:41:42 +0000
86@@ -121,6 +121,20 @@
87 finally:
88 wt1.unlock()
89
90+ def test_merge_into_null_tree(self):
91+ wt = self.make_branch_and_tree('tree')
92+ null_tree = wt.basis_tree()
93+ self.build_tree(['tree/file'])
94+ wt.add('file')
95+ wt.commit('tree with root')
96+ merger = _mod_merge.Merge3Merger(null_tree, null_tree, null_tree, wt,
97+ this_branch=wt.branch,
98+ do_merge=False)
99+ with merger.make_preview_transform() as tt:
100+ self.assertEqual([], tt.find_conflicts())
101+ preview = tt.get_preview_tree()
102+ self.assertEqual(wt.get_root_id(), preview.get_root_id())
103+
104 def test_create_rename(self):
105 """Rename an inventory entry while creating the file"""
106 tree =self.make_branch_and_tree('.')
107@@ -387,6 +401,26 @@
108 '>>>>>>> MERGE-SOURCE\n',
109 'this/file')
110
111+ def test_merge_reverse_revision_range(self):
112+ tree = self.make_branch_and_tree(".")
113+ tree.lock_write()
114+ self.addCleanup(tree.unlock)
115+ self.build_tree(['a'])
116+ tree.add('a')
117+ tree.commit("added a")
118+ first_rev = tree.branch.revision_history()[0]
119+ merger = _mod_merge.Merger.from_revision_ids(None, tree,
120+ _mod_revision.NULL_REVISION,
121+ first_rev)
122+ merger.merge_type = _mod_merge.Merge3Merger
123+ merger.interesting_files = 'a'
124+ conflict_count = merger.do_merge()
125+ self.assertEqual(0, conflict_count)
126+
127+ self.assertPathDoesNotExist("a")
128+ tree.revert()
129+ self.assertPathExists("a")
130+
131 def test_make_merger(self):
132 this_tree = self.make_branch_and_tree('this')
133 this_tree.commit('rev1', rev_id='rev1')
134
135=== modified file 'bzrlib/tests/test_shelf.py'
136--- bzrlib/tests/test_shelf.py 2011-05-13 12:51:05 +0000
137+++ bzrlib/tests/test_shelf.py 2011-06-13 15:41:42 +0000
138@@ -568,19 +568,18 @@
139 list(creator.iter_shelvable())
140 creator.shelve_deletion('foo-id')
141 creator.shelve_deletion('bar-id')
142- shelf_file = open('shelf', 'w+b')
143- self.addCleanup(shelf_file.close)
144- creator.write_shelf(shelf_file)
145- creator.transform()
146- creator.finalize()
147+ with open('shelf', 'w+b') as shelf_file:
148+ creator.write_shelf(shelf_file)
149+ creator.transform()
150+ creator.finalize()
151 # validate the test setup
152 self.assertTrue('foo-id' in tree)
153 self.assertTrue('bar-id' in tree)
154 self.assertFileEqual('baz', 'tree/foo/bar')
155- shelf_file.seek(0)
156- unshelver = shelf.Unshelver.from_tree_and_shelf(tree, shelf_file)
157- self.addCleanup(unshelver.finalize)
158- unshelver.make_merger().do_merge()
159+ with open('shelf', 'r+b') as shelf_file:
160+ unshelver = shelf.Unshelver.from_tree_and_shelf(tree, shelf_file)
161+ self.addCleanup(unshelver.finalize)
162+ unshelver.make_merger().do_merge()
163 self.assertFalse('foo-id' in tree)
164 self.assertFalse('bar-id' in tree)
165
166
167=== modified file 'bzrlib/tests/test_shelf_ui.py'
168--- bzrlib/tests/test_shelf_ui.py 2011-06-09 14:40:22 +0000
169+++ bzrlib/tests/test_shelf_ui.py 2011-06-13 15:41:42 +0000
170@@ -311,10 +311,15 @@
171 from_revision=revision.NULL_REVISION)
172 tree1.commit('Replaced root entry')
173 # This is essentially assertNotRaises(InconsistentDelta)
174- self.expectFailure('Cannot shelve replacing a root entry',
175- self.assertRaises, AssertionError,
176- self.assertRaises, errors.InconsistentDelta,
177- self.shelve_all, tree1, rev2)
178+ # With testtools 0.99, it can be rewritten as:
179+ # with ExpectedException(AssertionError,
180+ # 'InconsistentDelta not raised'):
181+ # with ExpectedException(errors.InconsistentDelta, ''):
182+ # self.shelve_all(tree1, rev2)
183+ e = self.assertRaises(AssertionError, self.assertRaises,
184+ errors.InconsistentDelta, self.shelve_all, tree1,
185+ rev2)
186+ self.assertContainsRe('InconsistentDelta not raised', str(e))
187
188 def test_shelve_split(self):
189 outer_tree = self.make_branch_and_tree('outer')
190
191=== modified file 'bzrlib/tests/test_transform.py'
192--- bzrlib/tests/test_transform.py 2011-06-10 16:19:57 +0000
193+++ bzrlib/tests/test_transform.py 2011-06-13 15:41:42 +0000
194@@ -285,6 +285,23 @@
195 new_trans_id = transform.new_directory('', ROOT_PARENT, 'alt-root-id')
196 self.assertRaises(ValueError, transform.fixup_new_roots)
197
198+ def test_add_unversioned_root(self):
199+ transform, root = self.get_transform()
200+ new_trans_id = transform.new_directory('', ROOT_PARENT, None)
201+ transform.fixup_new_roots()
202+ self.assertNotIn(transform.root, transform._new_id)
203+
204+ def test_apply_retains_root_directory(self):
205+ # Do not attempt to delete the physical root directory, because that
206+ # is impossible.
207+ transform, root = self.get_transform()
208+ with transform:
209+ transform.delete_contents(root)
210+ e = self.assertRaises(AssertionError, self.assertRaises,
211+ errors.TransformRenameFailed,
212+ transform.apply)
213+ self.assertContainsRe('TransformRenameFailed not raised', str(e))
214+
215 def test_hardlink(self):
216 self.requireFeature(HardlinkFeature)
217 transform, root = self.get_transform()
218
219=== modified file 'bzrlib/transform.py'
220--- bzrlib/transform.py 2011-06-10 16:19:57 +0000
221+++ bzrlib/transform.py 2011-06-13 15:41:42 +0000
222@@ -252,7 +252,8 @@
223 if (self.tree_file_id(self._new_root) is not None and
224 self._new_root not in self._removed_id):
225 self.unversion_file(self._new_root)
226- self.version_file(file_id, self._new_root)
227+ if file_id is not None:
228+ self.version_file(file_id, self._new_root)
229
230 # Now move children of new root into old root directory.
231 # Ensure all children are registered with the transaction, but don't
232@@ -1826,8 +1827,10 @@
233 tree_paths.sort(reverse=True)
234 child_pb = ui.ui_factory.nested_progress_bar()
235 try:
236- for num, data in enumerate(tree_paths):
237- path, trans_id = data
238+ for num, (path, trans_id) in enumerate(tree_paths):
239+ # do not attempt to move root into a subdirectory of itself.
240+ if path == '':
241+ continue
242 child_pb.update('removing file', num, len(tree_paths))
243 full_path = self._tree.abspath(path)
244 if trans_id in self._removed_contents:
245
246=== modified file 'doc/en/release-notes/bzr-2.4.txt'
247--- doc/en/release-notes/bzr-2.4.txt 2011-06-09 11:59:08 +0000
248+++ doc/en/release-notes/bzr-2.4.txt 2011-06-13 15:41:42 +0000
249@@ -73,6 +73,8 @@
250 ``UIFactory.get_password`` and ``UIFactory.get_boolean`` now require a
251 unicode prompt to be passed in. (Jelmer Vernooij, #592083)
252
253+* Support merging into the empty tree. (Aaron Bentley, #595328)
254+
255 Documentation
256 *************
257