Merge lp:~gerard-/bzr/update into lp:bzr

Proposed by Gerard Krol
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
Reviewer Review Type Date Requested Status
Vincent Ladeuil Approve
John A Meinel Approve
Review via email: mp+18464@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Gerard Krol (gerard-) wrote :

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!

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

Thanks for tackling this!

Are there specific bugs that you think this will fix?

Revision history for this message
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

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

just that hey? :-)

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

Revision history for this message
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/HAKCING.txt Coding Style Guidelines) ?

Also, if you disagree with some comments, update them instead
of deleting them (bzrlib/tests/blackbox/test_update.py).

Can you also have a look at http://www.canonical.com/contributors so we can merge your patch
once it is approved ?

You may also add a NEWS entry mentioning the bugs you're addressing.
Make sure you have tests reproducing them.

Revision history for this message
John A Meinel (jameinel) wrote :
Download full text (5.3 KiB)

8 + a_file = file('u1/hosts', 'rt')
9 + text = a_file.read()
10 + a_file.close()
11 + self.assertEqual(text, '<<<<<<< TREE\nfirst offline change in u1=======\naltered in u2>>>>>>> MERGE-SOURCE\n')
12 +

^- I think this is better written as

self.assertFileEqual('u1/hosts', '<<<<<< ...')

If that doesn't work because of line endings (it may be done in 'b' instead of
't' mode), then at least use:

 self.assertEqualDiff("""<<<<<<< TREE
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.update() (go from branch.last_revision() =>
   master_branch.last_revision(), return old last_revision())
2) last_rev = basis_revision for current working tree
3) revision = new master_branch.last_revision()
4) merge_inner(last_rev => revision)
5) set_parent_trees(revision + existing_merges)
6) base_rev_id = unique_lca(revision, old_tip)
7) merge_inner(base_rev_id => old_tip)

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.last_revision()
M wt.branch.get_master_branch().last_revision()
P wt.get_parent_ids()[1:], things that claim to be merged into the WT
    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(ancestor(W,B) => W)
    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.update()
2) [same] last_rev = basis_revision for current working tree
3) [same] revision = master_branch.last_revision()
4) if old_tip != revision
     merge_inner(ancestor(revision, old_tip) => old_tip)
   if conflicts => stop (mark old_tip as merged into current tree)
5) if not merged:
    merge_inner(last_rev => revision)
   else:
    merge_inner(ancestor(revision, last_rev) => revision)

So I think this is:
merge_inner(ancestor(M,B) => B)
merge_inner(ancestor(M,W) => M)

# 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...

Read more...

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

Just a thought....

Maybe what we want is:

if wt.get_parent_ids()[0] != wt.branch.last_revision():
  merge_inner(wt.get_parent_ids()[0] => wt.branch.last_revision())
merge_inner(ancestor(wt.branch.last_revision(), revision), revision)

Revision history for this message
Gerard Krol (gerard-) wrote :

> Also, if you disagree with some comments, update them instead
> of deleting them (bzrlib/tests/blackbox/test_update.py).

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.

Revision history for this message
Gerard Krol (gerard-) wrote :

> While you're tweaking, can you fix the lines too long

Fixed

Revision history for this message
Gerard Krol (gerard-) wrote :

> 8 + a_file = file('u1/hosts', 'rt')
> 9 + text = a_file.read()
> 10 + a_file.close()
> 11 + self.assertEqual(text, '<<<<<<< TREE\nfirst offline change in
> u1=======\naltered in u2>>>>>>> MERGE-SOURCE\n')
> 12 +
>
>
> ^- I think this is better written as
>
> self.assertFileEqual('u1/hosts', '<<<<<< ...')

Fixed

Revision history for this message
Gerard Krol (gerard-) wrote :

I FINALLY understand the issue (thanks John!)

I think that this is ready for the merge now.

Revision history for this message
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.

Revision history for this message
Gerard Krol (gerard-) wrote :

> I'll talk with my boss about signing the "Canonical Contributor Agreement"
> this Monday.

Signed it.

Revision history for this message
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.

review: Approve
Revision history for this message
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.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'NEWS'
--- NEWS 2010-02-05 10:57:26 +0000
+++ NEWS 2010-02-06 21:29:15 +0000
@@ -67,6 +67,10 @@
67 the same value to avoid confusing ``make`` and other date-based build67 the same value to avoid confusing ``make`` and other date-based build
68 systems. (Robert Collins, #515631)68 systems. (Robert Collins, #515631)
6969
70* ``bzr update`` performs the two merges in a more logical order and will stop
71 when it encounters conflicts.
72 (Gerard Krol, #113809)
73
70Improvements74Improvements
71************75************
7276
7377
=== modified file 'bzrlib/tests/blackbox/test_commit.py'
--- bzrlib/tests/blackbox/test_commit.py 2010-01-25 17:48:22 +0000
+++ bzrlib/tests/blackbox/test_commit.py 2010-02-06 21:29:15 +0000
@@ -359,6 +359,13 @@
359 # work as a merge359 # work as a merge
360 # retcode 1 as we expect a text conflict360 # retcode 1 as we expect a text conflict
361 self.run_bzr('update u1', retcode=1)361 self.run_bzr('update u1', retcode=1)
362 self.assertFileEqual('<<<<<<< TREE\n'
363 'first offline change in u1'
364 '=======\n'
365 'altered in u2'
366 '>>>>>>> MERGE-SOURCE\n',
367 'u1/hosts')
368
362 self.run_bzr('resolved u1/hosts')369 self.run_bzr('resolved u1/hosts')
363 # add a text change here to represent resolving the merge conflicts in370 # add a text change here to represent resolving the merge conflicts in
364 # favour of a new version of the file not identical to either the u1371 # favour of a new version of the file not identical to either the u1
365372
=== modified file 'bzrlib/tests/blackbox/test_update.py'
--- bzrlib/tests/blackbox/test_update.py 2009-12-23 06:31:19 +0000
+++ bzrlib/tests/blackbox/test_update.py 2010-02-06 21:29:15 +0000
@@ -171,9 +171,9 @@
171 # get all three files and a pending merge.171 # get all three files and a pending merge.
172 out, err = self.run_bzr('update checkout')172 out, err = self.run_bzr('update checkout')
173 self.assertEqual('', out)173 self.assertEqual('', out)
174 self.assertEqualDiff("""+N file174 self.assertEqualDiff("""+N file_b
175All changes applied successfully.175All changes applied successfully.
176+N file_b176+N file
177All changes applied successfully.177All changes applied successfully.
178Updated to revision 1 of branch %s178Updated to revision 1 of branch %s
179Your local commits will now show as pending merges with 'bzr status', and can be committed with 'bzr commit'.179Your local commits will now show as pending merges with 'bzr status', and can be committed with 'bzr commit'.
@@ -242,9 +242,6 @@
242 self.run_bzr('update checkout')242 self.run_bzr('update checkout')
243243
244 def test_update_dash_r(self):244 def test_update_dash_r(self):
245 # Test that 'bzr update' works correctly when you have
246 # an update in the master tree, and a lightweight checkout
247 # which has merged another branch
248 master = self.make_branch_and_tree('master')245 master = self.make_branch_and_tree('master')
249 os.chdir('master')246 os.chdir('master')
250 self.build_tree(['./file1'])247 self.build_tree(['./file1'])
@@ -266,9 +263,6 @@
266 self.assertEquals(['m1'], master.get_parent_ids())263 self.assertEquals(['m1'], master.get_parent_ids())
267264
268 def test_update_dash_r_outside_history(self):265 def test_update_dash_r_outside_history(self):
269 # Test that 'bzr update' works correctly when you have
270 # an update in the master tree, and a lightweight checkout
271 # which has merged another branch
272 master = self.make_branch_and_tree('master')266 master = self.make_branch_and_tree('master')
273 self.build_tree(['master/file1'])267 self.build_tree(['master/file1'])
274 master.add(['file1'])268 master.add(['file1'])
@@ -315,3 +309,132 @@
3152>All changes applied successfully.3092>All changes applied successfully.
3162>Updated to revision 2 of branch .../master3102>Updated to revision 2 of branch .../master
317''')311''')
312
313 def test_update_checkout_prevent_double_merge(self):
314 """"Launchpad bug 113809 in bzr "update performs two merges"
315 https://launchpad.net/bugs/113809"""
316 master = self.make_branch_and_tree('master')
317 self.build_tree_contents([('master/file', 'initial contents')])
318 master.add(['file'])
319 master.commit('one', rev_id='m1')
320
321 checkout = master.branch.create_checkout('checkout')
322 lightweight = checkout.branch.create_checkout('lightweight',
323 lightweight=True)
324
325 # time to create a mess
326 # add a commit to the master
327 self.build_tree_contents([('master/file', 'master')])
328 master.commit('two', rev_id='m2')
329 self.build_tree_contents([('master/file', 'master local changes')])
330
331 # local commit on the checkout
332 self.build_tree_contents([('checkout/file', 'checkout')])
333 checkout.commit('tree', rev_id='c2', local=True)
334 self.build_tree_contents([('checkout/file', 'checkout local changes')])
335
336 # lightweight
337 self.build_tree_contents([('lightweight/file',
338 'lightweight local changes')])
339
340 # now update (and get conflicts)
341 out, err = self.run_bzr('update lightweight', retcode=1)
342 self.assertEqual('', out)
343 self.assertFileEqual('<<<<<<< TREE\n'
344 'lightweight local changes'
345 '=======\n'
346 'checkout'
347 '>>>>>>> MERGE-SOURCE\n',
348 'lightweight/file')
349
350 # resolve it
351 self.build_tree_contents([('lightweight/file',
352 'lightweight+checkout')])
353 self.run_bzr('resolved lightweight/file')
354
355 # check we get the second conflict
356 out, err = self.run_bzr('update lightweight', retcode=1)
357 self.assertEqual('', out)
358 self.assertFileEqual('<<<<<<< TREE\n'
359 'lightweight+checkout'
360 '=======\n'
361 'master'
362 '>>>>>>> MERGE-SOURCE\n',
363 'lightweight/file')
364
365 def test_update_remove_commit(self):
366 """Update should remove revisions when the branch has removed some commits
367 1
368 |\
369 B-2 3
370 | |
371 4 5-M
372 |
373 W
374
375 What you want is to revert 4, as in this graph the final result should
376 be equivalent to:
377
378 1
379 |\
380 3 2
381 | |\
382 MB-5 | 4
383 |/
384 W
385
386 And the changes in 4 have been removed from the WT.
387 """
388
389 # building the picture
390 M = self.make_branch_and_tree('M')
391 self.build_tree_contents([('M/file1', '1')])
392 M.add(['file1'])
393 M.commit('one', rev_id='c1')
394
395 B = M.branch.create_checkout('B')
396 self.build_tree_contents([('B/file2', '2')])
397 B.add(['file2'])
398 B.commit('two', rev_id='c2', local=True)
399 self.build_tree_contents([('B/file4', '4')])
400 B.add(['file4'])
401 B.commit('four', rev_id='c4', local=True)
402
403 W = B.branch.create_checkout('W', lightweight=True)
404 # move B back to 2
405 B.pull(B.branch, local=True, stop_revision='c2', overwrite=True)
406
407 self.build_tree_contents([('M/file3', '3')])
408 M.add(['file3'])
409 M.commit('three', rev_id='c3')
410 self.build_tree_contents([('M/file5', '5')])
411 M.add(['file5'])
412 M.commit('five', rev_id='c5')
413
414 # now check if everything is as we expect it to be (test the test)
415 # M has 1,3,5
416 self.failUnlessExists('M/file1')
417 self.failIfExists( 'M/file2')
418 self.failUnlessExists('M/file3')
419 self.failIfExists( 'M/file4')
420 self.failUnlessExists('M/file5')
421 # B has 1,2
422 self.failUnlessExists('B/file1')
423 self.failUnlessExists('B/file2')
424 self.failIfExists( 'B/file3')
425 self.failIfExists( 'B/file4')
426 self.failIfExists( 'B/file5')
427 # W has 1,2,4
428 self.failUnlessExists('W/file1')
429 self.failUnlessExists('W/file2')
430 self.failIfExists( 'W/file3')
431 self.failUnlessExists('W/file4')
432 self.failIfExists( 'W/file5')
433
434 out, err = self.run_bzr('update W', retcode=0)
435 self.assertEqual('', out)
436 self.failUnlessExists('W/file1')
437 self.failUnlessExists('W/file2')
438 self.failUnlessExists('W/file3')
439 self.failIfExists('W/file4')
440 self.failUnlessExists('W/file5')
318441
=== modified file 'bzrlib/workingtree.py'
--- bzrlib/workingtree.py 2010-02-03 07:16:18 +0000
+++ bzrlib/workingtree.py 2010-02-06 21:29:15 +0000
@@ -2274,26 +2274,53 @@
2274 else:2274 else:
2275 if revision not in self.branch.revision_history():2275 if revision not in self.branch.revision_history():
2276 raise errors.NoSuchRevision(self.branch, revision)2276 raise errors.NoSuchRevision(self.branch, revision)
2277
2278 old_tip = old_tip or _mod_revision.NULL_REVISION
2279
2280 if not _mod_revision.is_null(old_tip) and old_tip != last_rev:
2281 # the branch we are bound to was updated
2282 # merge those changes in first
2283 base_tree = self.basis_tree()
2284 other_tree = self.branch.repository.revision_tree(old_tip)
2285 result = merge.merge_inner(
2286 self.branch,
2287 other_tree,
2288 base_tree,
2289 this_tree=self,
2290 change_reporter=change_reporter)
2291 if result > 0:
2292 self.add_parent_tree((old_tip, other_tree))
2293 trace.note('Rerun update after fixing the conflicts.')
2294 return result
2295
2277 if last_rev != _mod_revision.ensure_null(revision):2296 if last_rev != _mod_revision.ensure_null(revision):
2278 # merge tree state up to specified revision.2297 # the working tree is up to date with the branch
2298 # we can merge the specified revision from master
2299 to_tree = self.branch.repository.revision_tree(revision)
2300 to_root_id = to_tree.get_root_id()
2301
2279 basis = self.basis_tree()2302 basis = self.basis_tree()
2280 basis.lock_read()2303 basis.lock_read()
2281 try:2304 try:
2282 to_tree = self.branch.repository.revision_tree(revision)
2283 to_root_id = to_tree.get_root_id()
2284 if (basis.inventory.root is None2305 if (basis.inventory.root is None
2285 or basis.inventory.root.file_id != to_root_id):2306 or basis.inventory.root.file_id != to_root_id):
2286 self.set_root_id(to_root_id)2307 self.set_root_id(to_root_id)
2287 self.flush()2308 self.flush()
2288 result += merge.merge_inner(
2289 self.branch,
2290 to_tree,
2291 basis,
2292 this_tree=self,
2293 change_reporter=change_reporter)
2294 self.set_last_revision(revision)
2295 finally:2309 finally:
2296 basis.unlock()2310 basis.unlock()
2311
2312 # determine the branch point
2313 graph = self.branch.repository.get_graph()
2314 base_rev_id = graph.find_unique_lca(self.branch.last_revision(), last_rev)
2315 base_tree = self.branch.repository.revision_tree(base_rev_id)
2316
2317 result = merge.merge_inner(
2318 self.branch,
2319 to_tree,
2320 base_tree,
2321 this_tree=self,
2322 change_reporter=change_reporter)
2323 self.set_last_revision(revision)
2297 # TODO - dedup parents list with things merged by pull ?2324 # TODO - dedup parents list with things merged by pull ?
2298 # reuse the tree we've updated to to set the basis:2325 # reuse the tree we've updated to to set the basis:
2299 parent_trees = [(revision, to_tree)]2326 parent_trees = [(revision, to_tree)]
@@ -2306,41 +2333,11 @@
2306 for parent in merges:2333 for parent in merges:
2307 parent_trees.append(2334 parent_trees.append(
2308 (parent, self.branch.repository.revision_tree(parent)))2335 (parent, self.branch.repository.revision_tree(parent)))
2309 if (old_tip is not None and not _mod_revision.is_null(old_tip)):2336 if not _mod_revision.is_null(old_tip):
2310 parent_trees.append(2337 parent_trees.append(
2311 (old_tip, self.branch.repository.revision_tree(old_tip)))2338 (old_tip, self.branch.repository.revision_tree(old_tip)))
2312 self.set_parent_trees(parent_trees)2339 self.set_parent_trees(parent_trees)
2313 last_rev = parent_trees[0][0]2340 last_rev = parent_trees[0][0]
2314 else:
2315 # the working tree had the same last-revision as the master
2316 # branch did. We may still have pivot local work from the local
2317 # branch into old_tip:
2318 if (old_tip is not None and not _mod_revision.is_null(old_tip)):
2319 self.add_parent_tree_id(old_tip)
2320 if (old_tip is not None and not _mod_revision.is_null(old_tip)
2321 and old_tip != last_rev):
2322 # our last revision was not the prior branch last revision
2323 # and we have converted that last revision to a pending merge.
2324 # base is somewhere between the branch tip now
2325 # and the now pending merge
2326
2327 # Since we just modified the working tree and inventory, flush out
2328 # the current state, before we modify it again.
2329 # TODO: jam 20070214 WorkingTree3 doesn't require this, dirstate
2330 # requires it only because TreeTransform directly munges the
2331 # inventory and calls tree._write_inventory(). Ultimately we
2332 # should be able to remove this extra flush.
2333 self.flush()
2334 graph = self.branch.repository.get_graph()
2335 base_rev_id = graph.find_unique_lca(revision, old_tip)
2336 base_tree = self.branch.repository.revision_tree(base_rev_id)
2337 other_tree = self.branch.repository.revision_tree(old_tip)
2338 result += merge.merge_inner(
2339 self.branch,
2340 other_tree,
2341 base_tree,
2342 this_tree=self,
2343 change_reporter=change_reporter)
2344 return result2341 return result
23452342
2346 def _write_hashcache_if_dirty(self):2343 def _write_hashcache_if_dirty(self):