Status: | Merged | ||||
---|---|---|---|---|---|
Approved by: | Vincent Ladeuil | ||||
Approved revision: | not available | ||||
Merged at revision: | not available | ||||
Proposed branch: | lp:~gerard-/bzr/update | ||||
Merge into: | lp:bzr | ||||
Diff against target: |
312 lines (+180/-49) 4 files modified
NEWS (+4/-0) bzrlib/tests/blackbox/test_commit.py (+7/-0) bzrlib/tests/blackbox/test_update.py (+131/-8) bzrlib/workingtree.py (+38/-41) |
||||
To merge this branch: | bzr merge lp:~gerard-/bzr/update | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Vincent Ladeuil | Approve | ||
John A Meinel | Approve | ||
Review via email: mp+18464@code.launchpad.net |
Commit message
Description of the change
Gerard Krol (gerard-) wrote : | # |
Martin Pool (mbp) wrote : | # |
Thanks for tackling this!
Are there specific bugs that you think this will fix?
Gerard Krol (gerard-) wrote : | # |
Just bug #113809 (update performs two merges) + the 6 duplicates bug #116655, bug #175589, bug #228506, bug #236724, bug #270665 and bug #395514
Martin Pool (mbp) wrote : | # |
just that hey? :-)
--
Martin <http://
Vincent Ladeuil (vila) wrote : | # |
I let John review this as he's more familiar that code.
Just a couple of comments on some details.
While you're tweaking, can you fix the lines too long
(see doc/developers/
Also, if you disagree with some comments, update them instead
of deleting them (bzrlib/
Can you also have a look at http://
once it is approved ?
You may also add a NEWS entry mentioning the bugs you're addressing.
Make sure you have tests reproducing them.
John A Meinel (jameinel) wrote : | # |
8 + a_file = file('u1/hosts', 'rt')
9 + text = a_file.read()
10 + a_file.close()
11 + self.assertEqua
12 +
^- I think this is better written as
self.assertFile
If that doesn't work because of line endings (it may be done in 'b' instead of
't' mode), then at least use:
self.assertEqu
first offline change in u1...
""", text)
We generally put 'expected' first, and assertEqualDiff gives nice output when
they aren't equal.
You have this a few times, and the lines are longer than 80 chars. (And some
other comments, etc are also >80 chars.)
I think this would do well with a clear statement of steps. Probably in a
comment somewhere. At least, it helped me sort it out. The old code did:
1) old_tip = self.branch.
master_
2) last_rev = basis_revision for current working tree
3) revision = new master_
4) merge_inner(
5) set_parent_
6) base_rev_id = unique_
7) merge_inner(
Which thinking about it, does seem a bit backwards. In the above analysis we
have these revisions at play
W wt.last_revision()
B wt.branch.
M wt.branch.
P wt.get_
already
The goal is to get from W to M in the most logical procession, but then also
handle that some of W (or B) may not be in M, so they have to be re-introduced.
Actually, I think it is that revisions in B that are not in M need to be
re-introduced, but I'm not sure if we care about revisions in W that aren't
in B or M. (though we certainly do care about P.)
So the current code does:
merge_inner(W => M)
Change the workingtree directly into the new master revision
merge_inner(
And re-merge the changes in the Branch that were not present in the Master
The new code does:
1) [same] old_tip = self.branch.
2) [same] last_rev = basis_revision for current working tree
3) [same] revision = master_
4) if old_tip != revision
merge_
if conflicts => stop (mark old_tip as merged into current tree)
5) if not merged:
merge_
else:
merge_
So I think this is:
merge_inner(
merge_inner(
# Note: I think one thing that neither code handles is when some of the
# revisions in M are already part of P, and thus probably shouldn't be
# re-introduced
So what does that do with a real graph
1
|\
2 3
| |
B-4 5-M
|
W
The old code would do:
apply the difference from W to M (revert 2,4, apply 3,5)
merge (1 => B) to M (apply 2,4)
Which should change the graph to look like:
1
|\
3 2
| |
MB-5 4
|/
W
Which is ~ what I would expect. It probably suffers from the probl...
John A Meinel (jameinel) wrote : | # |
Just a thought....
Maybe what we want is:
if wt.get_
merge_
merge_inner(
Gerard Krol (gerard-) wrote : | # |
> Also, if you disagree with some comments, update them instead
> of deleting them (bzrlib/
Those comments were identical to each other and to the comments on the test they were copied from. The best I can do at this point is remove them, as inventing my own will not add any information beyond what is already visible from the code. I might even misunderstand the code, and wrong comments are even worse than none.
Gerard Krol (gerard-) wrote : | # |
> While you're tweaking, can you fix the lines too long
Fixed
Gerard Krol (gerard-) wrote : | # |
> 8 + a_file = file('u1/hosts', 'rt')
> 9 + text = a_file.read()
> 10 + a_file.close()
> 11 + self.assertEqua
> u1=======\naltered in u2>>>>>>> MERGE-SOURCE\n')
> 12 +
>
>
> ^- I think this is better written as
>
> self.assertFile
Fixed
Gerard Krol (gerard-) wrote : | # |
I FINALLY understand the issue (thanks John!)
I think that this is ready for the merge now.
Gerard Krol (gerard-) wrote : | # |
I'll talk with my boss about signing the "Canonical Contributor Agreement" this Monday.
If there are any additional changes needed to the patch or actions to take please let me know.
Gerard Krol (gerard-) wrote : | # |
> I'll talk with my boss about signing the "Canonical Contributor Agreement"
> this Monday.
Signed it.
John A Meinel (jameinel) wrote : | # |
This fits my expectations. If one of the merging steps creates conflicts, it stops as expected. It also looks like it should not spuriously remove committed changes, causing conflicts in uncommitted changes when bringing in the updates from the master branch.
We should still get a second review, I think.
Vincent Ladeuil (vila) wrote : | # |
I've cleanup the tests a bit to be real whitebox ones based on branchbuilder.
I still the feeling that we don't have enough tests here but that's good enough for now.
I'll merge.
Preview Diff
1 | === modified file 'NEWS' |
2 | --- NEWS 2010-02-05 10:57:26 +0000 |
3 | +++ NEWS 2010-02-06 21:29:15 +0000 |
4 | @@ -67,6 +67,10 @@ |
5 | the same value to avoid confusing ``make`` and other date-based build |
6 | systems. (Robert Collins, #515631) |
7 | |
8 | +* ``bzr update`` performs the two merges in a more logical order and will stop |
9 | + when it encounters conflicts. |
10 | + (Gerard Krol, #113809) |
11 | + |
12 | Improvements |
13 | ************ |
14 | |
15 | |
16 | === modified file 'bzrlib/tests/blackbox/test_commit.py' |
17 | --- bzrlib/tests/blackbox/test_commit.py 2010-01-25 17:48:22 +0000 |
18 | +++ bzrlib/tests/blackbox/test_commit.py 2010-02-06 21:29:15 +0000 |
19 | @@ -359,6 +359,13 @@ |
20 | # work as a merge |
21 | # retcode 1 as we expect a text conflict |
22 | self.run_bzr('update u1', retcode=1) |
23 | + self.assertFileEqual('<<<<<<< TREE\n' |
24 | + 'first offline change in u1' |
25 | + '=======\n' |
26 | + 'altered in u2' |
27 | + '>>>>>>> MERGE-SOURCE\n', |
28 | + 'u1/hosts') |
29 | + |
30 | self.run_bzr('resolved u1/hosts') |
31 | # add a text change here to represent resolving the merge conflicts in |
32 | # favour of a new version of the file not identical to either the u1 |
33 | |
34 | === modified file 'bzrlib/tests/blackbox/test_update.py' |
35 | --- bzrlib/tests/blackbox/test_update.py 2009-12-23 06:31:19 +0000 |
36 | +++ bzrlib/tests/blackbox/test_update.py 2010-02-06 21:29:15 +0000 |
37 | @@ -171,9 +171,9 @@ |
38 | # get all three files and a pending merge. |
39 | out, err = self.run_bzr('update checkout') |
40 | self.assertEqual('', out) |
41 | - self.assertEqualDiff("""+N file |
42 | + self.assertEqualDiff("""+N file_b |
43 | All changes applied successfully. |
44 | -+N file_b |
45 | ++N file |
46 | All changes applied successfully. |
47 | Updated to revision 1 of branch %s |
48 | Your local commits will now show as pending merges with 'bzr status', and can be committed with 'bzr commit'. |
49 | @@ -242,9 +242,6 @@ |
50 | self.run_bzr('update checkout') |
51 | |
52 | def test_update_dash_r(self): |
53 | - # Test that 'bzr update' works correctly when you have |
54 | - # an update in the master tree, and a lightweight checkout |
55 | - # which has merged another branch |
56 | master = self.make_branch_and_tree('master') |
57 | os.chdir('master') |
58 | self.build_tree(['./file1']) |
59 | @@ -266,9 +263,6 @@ |
60 | self.assertEquals(['m1'], master.get_parent_ids()) |
61 | |
62 | def test_update_dash_r_outside_history(self): |
63 | - # Test that 'bzr update' works correctly when you have |
64 | - # an update in the master tree, and a lightweight checkout |
65 | - # which has merged another branch |
66 | master = self.make_branch_and_tree('master') |
67 | self.build_tree(['master/file1']) |
68 | master.add(['file1']) |
69 | @@ -315,3 +309,132 @@ |
70 | 2>All changes applied successfully. |
71 | 2>Updated to revision 2 of branch .../master |
72 | ''') |
73 | + |
74 | + def test_update_checkout_prevent_double_merge(self): |
75 | + """"Launchpad bug 113809 in bzr "update performs two merges" |
76 | + https://launchpad.net/bugs/113809""" |
77 | + master = self.make_branch_and_tree('master') |
78 | + self.build_tree_contents([('master/file', 'initial contents')]) |
79 | + master.add(['file']) |
80 | + master.commit('one', rev_id='m1') |
81 | + |
82 | + checkout = master.branch.create_checkout('checkout') |
83 | + lightweight = checkout.branch.create_checkout('lightweight', |
84 | + lightweight=True) |
85 | + |
86 | + # time to create a mess |
87 | + # add a commit to the master |
88 | + self.build_tree_contents([('master/file', 'master')]) |
89 | + master.commit('two', rev_id='m2') |
90 | + self.build_tree_contents([('master/file', 'master local changes')]) |
91 | + |
92 | + # local commit on the checkout |
93 | + self.build_tree_contents([('checkout/file', 'checkout')]) |
94 | + checkout.commit('tree', rev_id='c2', local=True) |
95 | + self.build_tree_contents([('checkout/file', 'checkout local changes')]) |
96 | + |
97 | + # lightweight |
98 | + self.build_tree_contents([('lightweight/file', |
99 | + 'lightweight local changes')]) |
100 | + |
101 | + # now update (and get conflicts) |
102 | + out, err = self.run_bzr('update lightweight', retcode=1) |
103 | + self.assertEqual('', out) |
104 | + self.assertFileEqual('<<<<<<< TREE\n' |
105 | + 'lightweight local changes' |
106 | + '=======\n' |
107 | + 'checkout' |
108 | + '>>>>>>> MERGE-SOURCE\n', |
109 | + 'lightweight/file') |
110 | + |
111 | + # resolve it |
112 | + self.build_tree_contents([('lightweight/file', |
113 | + 'lightweight+checkout')]) |
114 | + self.run_bzr('resolved lightweight/file') |
115 | + |
116 | + # check we get the second conflict |
117 | + out, err = self.run_bzr('update lightweight', retcode=1) |
118 | + self.assertEqual('', out) |
119 | + self.assertFileEqual('<<<<<<< TREE\n' |
120 | + 'lightweight+checkout' |
121 | + '=======\n' |
122 | + 'master' |
123 | + '>>>>>>> MERGE-SOURCE\n', |
124 | + 'lightweight/file') |
125 | + |
126 | + def test_update_remove_commit(self): |
127 | + """Update should remove revisions when the branch has removed some commits |
128 | + 1 |
129 | + |\ |
130 | + B-2 3 |
131 | + | | |
132 | + 4 5-M |
133 | + | |
134 | + W |
135 | + |
136 | + What you want is to revert 4, as in this graph the final result should |
137 | + be equivalent to: |
138 | + |
139 | + 1 |
140 | + |\ |
141 | + 3 2 |
142 | + | |\ |
143 | + MB-5 | 4 |
144 | + |/ |
145 | + W |
146 | + |
147 | + And the changes in 4 have been removed from the WT. |
148 | + """ |
149 | + |
150 | + # building the picture |
151 | + M = self.make_branch_and_tree('M') |
152 | + self.build_tree_contents([('M/file1', '1')]) |
153 | + M.add(['file1']) |
154 | + M.commit('one', rev_id='c1') |
155 | + |
156 | + B = M.branch.create_checkout('B') |
157 | + self.build_tree_contents([('B/file2', '2')]) |
158 | + B.add(['file2']) |
159 | + B.commit('two', rev_id='c2', local=True) |
160 | + self.build_tree_contents([('B/file4', '4')]) |
161 | + B.add(['file4']) |
162 | + B.commit('four', rev_id='c4', local=True) |
163 | + |
164 | + W = B.branch.create_checkout('W', lightweight=True) |
165 | + # move B back to 2 |
166 | + B.pull(B.branch, local=True, stop_revision='c2', overwrite=True) |
167 | + |
168 | + self.build_tree_contents([('M/file3', '3')]) |
169 | + M.add(['file3']) |
170 | + M.commit('three', rev_id='c3') |
171 | + self.build_tree_contents([('M/file5', '5')]) |
172 | + M.add(['file5']) |
173 | + M.commit('five', rev_id='c5') |
174 | + |
175 | + # now check if everything is as we expect it to be (test the test) |
176 | + # M has 1,3,5 |
177 | + self.failUnlessExists('M/file1') |
178 | + self.failIfExists( 'M/file2') |
179 | + self.failUnlessExists('M/file3') |
180 | + self.failIfExists( 'M/file4') |
181 | + self.failUnlessExists('M/file5') |
182 | + # B has 1,2 |
183 | + self.failUnlessExists('B/file1') |
184 | + self.failUnlessExists('B/file2') |
185 | + self.failIfExists( 'B/file3') |
186 | + self.failIfExists( 'B/file4') |
187 | + self.failIfExists( 'B/file5') |
188 | + # W has 1,2,4 |
189 | + self.failUnlessExists('W/file1') |
190 | + self.failUnlessExists('W/file2') |
191 | + self.failIfExists( 'W/file3') |
192 | + self.failUnlessExists('W/file4') |
193 | + self.failIfExists( 'W/file5') |
194 | + |
195 | + out, err = self.run_bzr('update W', retcode=0) |
196 | + self.assertEqual('', out) |
197 | + self.failUnlessExists('W/file1') |
198 | + self.failUnlessExists('W/file2') |
199 | + self.failUnlessExists('W/file3') |
200 | + self.failIfExists('W/file4') |
201 | + self.failUnlessExists('W/file5') |
202 | |
203 | === modified file 'bzrlib/workingtree.py' |
204 | --- bzrlib/workingtree.py 2010-02-03 07:16:18 +0000 |
205 | +++ bzrlib/workingtree.py 2010-02-06 21:29:15 +0000 |
206 | @@ -2274,26 +2274,53 @@ |
207 | else: |
208 | if revision not in self.branch.revision_history(): |
209 | raise errors.NoSuchRevision(self.branch, revision) |
210 | + |
211 | + old_tip = old_tip or _mod_revision.NULL_REVISION |
212 | + |
213 | + if not _mod_revision.is_null(old_tip) and old_tip != last_rev: |
214 | + # the branch we are bound to was updated |
215 | + # merge those changes in first |
216 | + base_tree = self.basis_tree() |
217 | + other_tree = self.branch.repository.revision_tree(old_tip) |
218 | + result = merge.merge_inner( |
219 | + self.branch, |
220 | + other_tree, |
221 | + base_tree, |
222 | + this_tree=self, |
223 | + change_reporter=change_reporter) |
224 | + if result > 0: |
225 | + self.add_parent_tree((old_tip, other_tree)) |
226 | + trace.note('Rerun update after fixing the conflicts.') |
227 | + return result |
228 | + |
229 | if last_rev != _mod_revision.ensure_null(revision): |
230 | - # merge tree state up to specified revision. |
231 | + # the working tree is up to date with the branch |
232 | + # we can merge the specified revision from master |
233 | + to_tree = self.branch.repository.revision_tree(revision) |
234 | + to_root_id = to_tree.get_root_id() |
235 | + |
236 | basis = self.basis_tree() |
237 | basis.lock_read() |
238 | try: |
239 | - to_tree = self.branch.repository.revision_tree(revision) |
240 | - to_root_id = to_tree.get_root_id() |
241 | if (basis.inventory.root is None |
242 | or basis.inventory.root.file_id != to_root_id): |
243 | self.set_root_id(to_root_id) |
244 | self.flush() |
245 | - result += merge.merge_inner( |
246 | - self.branch, |
247 | - to_tree, |
248 | - basis, |
249 | - this_tree=self, |
250 | - change_reporter=change_reporter) |
251 | - self.set_last_revision(revision) |
252 | finally: |
253 | basis.unlock() |
254 | + |
255 | + # determine the branch point |
256 | + graph = self.branch.repository.get_graph() |
257 | + base_rev_id = graph.find_unique_lca(self.branch.last_revision(), last_rev) |
258 | + base_tree = self.branch.repository.revision_tree(base_rev_id) |
259 | + |
260 | + result = merge.merge_inner( |
261 | + self.branch, |
262 | + to_tree, |
263 | + base_tree, |
264 | + this_tree=self, |
265 | + change_reporter=change_reporter) |
266 | + self.set_last_revision(revision) |
267 | # TODO - dedup parents list with things merged by pull ? |
268 | # reuse the tree we've updated to to set the basis: |
269 | parent_trees = [(revision, to_tree)] |
270 | @@ -2306,41 +2333,11 @@ |
271 | for parent in merges: |
272 | parent_trees.append( |
273 | (parent, self.branch.repository.revision_tree(parent))) |
274 | - if (old_tip is not None and not _mod_revision.is_null(old_tip)): |
275 | + if not _mod_revision.is_null(old_tip): |
276 | parent_trees.append( |
277 | (old_tip, self.branch.repository.revision_tree(old_tip))) |
278 | self.set_parent_trees(parent_trees) |
279 | last_rev = parent_trees[0][0] |
280 | - else: |
281 | - # the working tree had the same last-revision as the master |
282 | - # branch did. We may still have pivot local work from the local |
283 | - # branch into old_tip: |
284 | - if (old_tip is not None and not _mod_revision.is_null(old_tip)): |
285 | - self.add_parent_tree_id(old_tip) |
286 | - if (old_tip is not None and not _mod_revision.is_null(old_tip) |
287 | - and old_tip != last_rev): |
288 | - # our last revision was not the prior branch last revision |
289 | - # and we have converted that last revision to a pending merge. |
290 | - # base is somewhere between the branch tip now |
291 | - # and the now pending merge |
292 | - |
293 | - # Since we just modified the working tree and inventory, flush out |
294 | - # the current state, before we modify it again. |
295 | - # TODO: jam 20070214 WorkingTree3 doesn't require this, dirstate |
296 | - # requires it only because TreeTransform directly munges the |
297 | - # inventory and calls tree._write_inventory(). Ultimately we |
298 | - # should be able to remove this extra flush. |
299 | - self.flush() |
300 | - graph = self.branch.repository.get_graph() |
301 | - base_rev_id = graph.find_unique_lca(revision, old_tip) |
302 | - base_tree = self.branch.repository.revision_tree(base_rev_id) |
303 | - other_tree = self.branch.repository.revision_tree(old_tip) |
304 | - result += merge.merge_inner( |
305 | - self.branch, |
306 | - other_tree, |
307 | - base_tree, |
308 | - this_tree=self, |
309 | - change_reporter=change_reporter) |
310 | return result |
311 | |
312 | def _write_hashcache_if_dirty(self): |
I don't really proposing to merge this as-is, but I think I solved the double merge problem update had for a long time. It will now perform the merges in a order that will minimize conflicts (first the local branch, then the master) and also stop after it encounters a conflict for the first merge. The only problem is really that I don't understand how it works...
I also added some tests for the new behaviour.
Please comment!