1# Copyright (C) 2005-2012, 2016 Canonical Ltd 2# 3# This program is free software; you can redistribute it and/or modify 4# it under the terms of the GNU General Public License as published by 5# the Free Software Foundation; either version 2 of the License, or 6# (at your option) any later version. 7# 8# This program is distributed in the hope that it will be useful, 9# but WITHOUT ANY WARRANTY; without even the implied warranty of 10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11# GNU General Public License for more details. 12# 13# You should have received a copy of the GNU General Public License 14# along with this program; if not, write to the Free Software 15# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 16 17import os 18import sys 19 20import breezy 21from .. import ( 22 controldir, 23 errors, 24 merge_directive, 25 osutils, 26 ) 27from ..bzr.conflicts import ( 28 ContentsConflict, 29 TextConflict, 30 PathConflict, 31 ) 32from ..merge import ( 33 Merge3Merger, 34 Diff3Merger, 35 WeaveMerger, 36 Merger, 37 ) 38from ..bzr import ( 39 generate_ids, 40 ) 41from ..osutils import getcwd, pathjoin 42from . import TestCaseWithTransport, TestSkipped 43from ..workingtree import WorkingTree 44 45 46class MergeBuilder(object): 47 48 def __init__(self, dir=None): 49 self.dir = osutils.mkdtemp(prefix="merge-test", dir=dir) 50 self.tree_root = generate_ids.gen_root_id() 51 52 def wt(name): 53 path = pathjoin(self.dir, name) 54 os.mkdir(path) 55 wt = controldir.ControlDir.create_standalone_workingtree(path) 56 # the tests perform pulls, so need a branch that is writeable. 57 wt.lock_write() 58 wt.set_root_id(self.tree_root) 59 wt.flush() 60 tt = wt.transform() 61 return wt, tt 62 self.base, self.base_tt = wt('base') 63 self.this, self.this_tt = wt('this') 64 self.other, self.other_tt = wt('other') 65 66 def get_cset_path(self, parent, name): 67 if name is None: 68 if parent is not None: 69 raise AssertionError() 70 return None 71 return pathjoin(self.cset.entries[parent].path, name) 72 73 def add_file(self, id, parent, name, contents, executable, this=True, 74 base=True, other=True): 75 def new_file(tt): 76 parent_id = tt.trans_id_file_id(parent) 77 tt.new_file(name, parent_id, [contents], id, executable) 78 for option, tt in self.selected_transforms(this, base, other): 79 if option is True: 80 new_file(tt) 81 82 def merge(self, merge_type=Merge3Merger, interesting_files=None, **kwargs): 83 merger = self.make_merger(merge_type, interesting_files, **kwargs) 84 merger.do_merge() 85 return merger.cooked_conflicts 86 87 def make_preview_transform(self): 88 merger = self.make_merger(Merge3Merger, None, this_revision_tree=True) 89 return merger.make_preview_transform() 90 91 def make_merger(self, merge_type, interesting_files, 92 this_revision_tree=False, **kwargs): 93 self.base_tt.apply() 94 self.base.commit('base commit') 95 for tt, wt in ((self.this_tt, self.this), (self.other_tt, self.other)): 96 # why does this not do wt.pull() ? 97 wt.branch.pull(self.base.branch) 98 wt.set_parent_ids([wt.branch.last_revision()]) 99 wt.flush() 100 # We maintain a write lock, so make sure changes are flushed to 101 # disk first 102 tt.apply() 103 wt.commit('branch commit') 104 wt.flush() 105 if wt.branch.last_revision_info()[0] != 2: 106 raise AssertionError() 107 self.this.branch.fetch(self.other.branch) 108 other_basis = self.other.branch.basis_tree() 109 if this_revision_tree: 110 self.this.commit('message') 111 this_tree = self.this.basis_tree() 112 else: 113 this_tree = self.this 114 merger = merge_type(this_tree, self.this, self.base, other_basis, 115 interesting_files=interesting_files, do_merge=False, 116 this_branch=self.this.branch, **kwargs) 117 return merger 118 119 def list_transforms(self): 120 return [self.this_tt, self.base_tt, self.other_tt] 121 122 def selected_transforms(self, this, base, other): 123 pairs = [(this, self.this_tt), (base, self.base_tt), 124 (other, self.other_tt)] 125 return [(v, tt) for (v, tt) in pairs if v is not None] 126 127 def add_symlink(self, id, parent, name, contents): 128 for tt in self.list_transforms(): 129 parent_id = tt.trans_id_file_id(parent) 130 tt.new_symlink(name, parent_id, contents, id) 131 132 def remove_file(self, file_id, base=False, this=False, other=False): 133 for option, tt in self.selected_transforms(this, base, other): 134 if option is True: 135 trans_id = tt.trans_id_file_id(file_id) 136 tt.cancel_creation(trans_id) 137 tt.cancel_versioning(trans_id) 138 tt.set_executability(None, trans_id) 139 140 def add_dir(self, file_id, parent, name, this=True, base=True, other=True): 141 for option, tt in self.selected_transforms(this, base, other): 142 if option is True: 143 parent_id = tt.trans_id_file_id(parent) 144 tt.new_directory(name, parent_id, file_id) 145 146 def change_name(self, id, base=None, this=None, other=None): 147 for val, tt in ((base, self.base_tt), (this, self.this_tt), 148 (other, self.other_tt)): 149 if val is None: 150 continue 151 trans_id = tt.trans_id_file_id(id) 152 parent_id = tt.final_parent(trans_id) 153 tt.adjust_path(val, parent_id, trans_id) 154 155 def change_parent(self, file_id, base=None, this=None, other=None): 156 for parent, tt in self.selected_transforms(this, base, other): 157 trans_id = tt.trans_id_file_id(file_id) 158 parent_id = tt.trans_id_file_id(parent) 159 tt.adjust_path(tt.final_name(trans_id), parent_id, trans_id) 160 161 def change_contents(self, file_id, base=None, this=None, other=None): 162 for contents, tt in self.selected_transforms(this, base, other): 163 trans_id = tt.trans_id_file_id(file_id) 164 tt.cancel_creation(trans_id) 165 tt.create_file([contents], trans_id) 166 167 def change_target(self, id, base=None, this=None, other=None): 168 for target, tt in self.selected_transforms(this, base, other): 169 trans_id = tt.trans_id_file_id(id) 170 tt.cancel_creation(trans_id) 171 tt.create_symlink(target, trans_id) 172 173 def change_perms(self, id, base=None, this=None, other=None): 174 for executability, tt in self.selected_transforms(this, base, other): 175 trans_id = tt.trans_id_file_id(id) 176 tt.set_executability(None, trans_id) 177 tt.set_executability(executability, trans_id) 178 179 def change_perms_tree(self, id, tree, mode): 180 os.chmod(tree.full_path(id), mode) 181 182 def apply_inv_change(self, inventory_change, orig_inventory): 183 orig_inventory_by_path = {} 184 for file_id, path in orig_inventory.items(): 185 orig_inventory_by_path[path] = file_id 186 187 def parent_id(file_id): 188 try: 189 parent_dir = os.path.dirname(orig_inventory[file_id]) 190 except: 191 print(file_id) 192 raise 193 if parent_dir == "": 194 return None 195 return orig_inventory_by_path[parent_dir] 196 197 def new_path(file_id): 198 if fild_id in inventory_change: 199 return inventory_change[file_id] 200 else: 201 parent = parent_id(file_id) 202 if parent is None: 203 return orig_inventory[file_id] 204 dirname = new_path(parent) 205 return pathjoin(dirname, os.path.basename(orig_inventory[file_id])) 206 207 new_inventory = {} 208 for file_id in orig_inventory: 209 path = new_path(file_id) 210 if path is None: 211 continue 212 new_inventory[file_id] = path 213 214 for file_id, path in inventory_change.items(): 215 if file_id in orig_inventory: 216 continue 217 new_inventory[file_id] = path 218 return new_inventory 219 220 def unlock(self): 221 self.base.unlock() 222 self.this.unlock() 223 self.other.unlock() 224 225 def cleanup(self): 226 self.unlock() 227 osutils.rmtree(self.dir) 228 229 230class MergeTest(TestCaseWithTransport): 231 232 def test_change_name(self): 233 """Test renames""" 234 builder = MergeBuilder(getcwd()) 235 builder.add_file(b"1", builder.tree_root, "name1", b"hello1", True) 236 builder.change_name(b"1", other="name2") 237 builder.add_file(b"2", builder.tree_root, "name3", b"hello2", True) 238 builder.change_name(b"2", base="name4") 239 builder.add_file(b"3", builder.tree_root, "name5", b"hello3", True) 240 builder.change_name(b"3", this="name6") 241 builder.merge() 242 builder.cleanup() 243 builder = MergeBuilder(getcwd()) 244 builder.add_file(b"1", builder.tree_root, "name1", b"hello1", False) 245 builder.change_name(b"1", other="name2", this="name3") 246 conflicts = builder.merge() 247 self.assertEqual(conflicts, [PathConflict('name3', 'name2', b'1')]) 248 builder.cleanup() 249 250 def test_merge_one(self): 251 builder = MergeBuilder(getcwd()) 252 builder.add_file(b"1", builder.tree_root, "name1", b"hello1", True) 253 builder.change_contents(b"1", other=b"text4") 254 builder.add_file(b"2", builder.tree_root, "name2", b"hello1", True) 255 builder.change_contents(b"2", other=b"text4") 256 builder.merge(interesting_files=["name1"]) 257 self.assertEqual(builder.this.get_file("name1").read(), b"text4") 258 self.assertEqual(builder.this.get_file("name2").read(), b"hello1") 259 builder.cleanup() 260 261 def test_file_moves(self): 262 """Test moves""" 263 builder = MergeBuilder(getcwd()) 264 builder.add_dir(b"1", builder.tree_root, "dir1") 265 builder.add_dir(b"2", builder.tree_root, "dir2") 266 builder.add_file(b"3", b"1", "file1", b"hello1", True) 267 builder.add_file(b"4", b"1", "file2", b"hello2", True) 268 builder.add_file(b"5", b"1", "file3", b"hello3", True) 269 builder.change_parent(b"3", other=b"2") 270 builder.change_parent(b"4", this=b"2") 271 builder.change_parent(b"5", base=b"2") 272 builder.merge() 273 builder.cleanup() 274 275 builder = MergeBuilder(getcwd()) 276 builder.add_dir(b"1", builder.tree_root, "dir1") 277 builder.add_dir(b"2", builder.tree_root, "dir2") 278 builder.add_dir(b"3", builder.tree_root, "dir3") 279 builder.add_file(b"4", b"1", "file1", b"hello1", False) 280 builder.change_parent(b"4", other=b"2", this=b"3") 281 conflicts = builder.merge() 282 path2 = pathjoin('dir2', 'file1') 283 path3 = pathjoin('dir3', 'file1') 284 self.assertEqual(conflicts, [PathConflict(path3, path2, b'4')]) 285 builder.cleanup() 286 287 def test_contents_merge(self): 288 """Test merge3 merging""" 289 self.do_contents_test(Merge3Merger) 290 291 def test_contents_merge2(self): 292 """Test diff3 merging""" 293 if sys.platform == 'win32': 294 raise TestSkipped("diff3 does not have --binary flag" 295 " and therefore always fails on win32") 296 try: 297 self.do_contents_test(Diff3Merger) 298 except errors.NoDiff3: 299 raise TestSkipped("diff3 not available") 300 301 def test_contents_merge3(self): 302 """Test diff3 merging""" 303 self.do_contents_test(WeaveMerger) 304 305 def test_reprocess_weave(self): 306 # Reprocess works on weaves, and behaves as expected 307 builder = MergeBuilder(getcwd()) 308 builder.add_file(b'a', builder.tree_root, 'blah', b'a', False) 309 builder.change_contents( 310 b'a', this=b'b\nc\nd\ne\n', other=b'z\nc\nd\ny\n') 311 builder.merge(WeaveMerger, reprocess=True) 312 expected = b"""<<<<<<< TREE 313b 314======= 315z 316>>>>>>> MERGE-SOURCE 317c 318d 319<<<<<<< TREE 320e 321======= 322y 323>>>>>>> MERGE-SOURCE 324""" 325 self.assertEqualDiff( 326 builder.this.get_file(builder.this.id2path(b"a")).read(), 327 expected) 328 builder.cleanup() 329 330 def do_contents_test(self, merge_factory): 331 """Test merging with specified ContentsChange factory""" 332 builder = self.contents_test_success(merge_factory) 333 builder.cleanup() 334 self.contents_test_conflicts(merge_factory) 335 336 def contents_test_success(self, merge_factory): 337 builder = MergeBuilder(getcwd()) 338 builder.add_file(b"1", builder.tree_root, "name1", b"text1", True) 339 builder.change_contents(b"1", other=b"text4") 340 builder.add_file(b"2", builder.tree_root, "name3", b"text2", False) 341 builder.change_contents(b"2", base=b"text5") 342 builder.add_file(b"3", builder.tree_root, "name5", b"text3", True) 343 builder.add_file(b"4", builder.tree_root, "name6", b"text4", True) 344 builder.remove_file(b"4", base=True) 345 builder.add_file(b"5", builder.tree_root, "name7", b"a\nb\nc\nd\ne\nf\n", 346 True) 347 builder.change_contents(b"5", other=b"a\nz\nc\nd\ne\nf\n", 348 this=b"a\nb\nc\nd\ne\nz\n") 349 conflicts = builder.merge(merge_factory) 350 try: 351 self.assertEqual([], conflicts) 352 self.assertEqual(b"text4", builder.this.get_file("name1").read()) 353 self.assertEqual(b"text2", builder.this.get_file("name3").read()) 354 self.assertEqual(b"a\nz\nc\nd\ne\nz\n", 355 builder.this.get_file("name7").read()) 356 self.assertTrue(builder.this.is_executable("name1")) 357 self.assertFalse(builder.this.is_executable("name3")) 358 self.assertTrue(builder.this.is_executable("name5")) 359 except: 360 builder.unlock() 361 raise 362 return builder 363 364 def contents_test_conflicts(self, merge_factory): 365 builder = MergeBuilder(getcwd()) 366 builder.add_file(b"1", builder.tree_root, "name1", b"text1", True) 367 builder.change_contents(b"1", other=b"text4", this=b"text3") 368 builder.add_file(b"2", builder.tree_root, "name2", b"text1", True) 369 builder.change_contents(b"2", other=b"\x00", this=b"text3") 370 builder.add_file(b"3", builder.tree_root, "name3", b"text5", False) 371 builder.change_perms(b"3", this=True) 372 builder.change_contents(b'3', this=b'moretext') 373 builder.remove_file(b'3', other=True) 374 conflicts = builder.merge(merge_factory) 375 self.assertEqual(conflicts, [TextConflict('name1', file_id=b'1'), 376 ContentsConflict('name2', file_id=b'2'), 377 ContentsConflict('name3', file_id=b'3')]) 378 with builder.this.get_file(builder.this.id2path(b'2')) as f: 379 self.assertEqual(f.read(), b'\x00') 380 builder.cleanup() 381 382 def test_symlink_conflicts(self): 383 if sys.platform != "win32": 384 builder = MergeBuilder(getcwd()) 385 builder.add_symlink(b"2", builder.tree_root, "name2", "target1") 386 builder.change_target(b"2", other="target4", base="text3") 387 conflicts = builder.merge() 388 self.assertEqual(conflicts, [ContentsConflict('name2', 389 file_id=b'2')]) 390 builder.cleanup() 391 392 def test_symlink_merge(self): 393 if sys.platform != "win32": 394 builder = MergeBuilder(getcwd()) 395 builder.add_symlink(b"1", builder.tree_root, "name1", "target1") 396 builder.add_symlink(b"2", builder.tree_root, "name2", "target1") 397 builder.add_symlink(b"3", builder.tree_root, "name3", "target1") 398 builder.change_target(b"1", this=b"target2") 399 builder.change_target(b"2", base=b"target2") 400 builder.change_target(b"3", other=b"target2") 401 builder.merge() 402 self.assertEqual( 403 builder.this.get_symlink_target("name1"), "target2") 404 self.assertEqual( 405 builder.this.get_symlink_target("name2"), "target1") 406 self.assertEqual( 407 builder.this.get_symlink_target("name3"), "target2") 408 builder.cleanup() 409 410 def test_no_passive_add(self): 411 builder = MergeBuilder(getcwd()) 412 builder.add_file(b"1", builder.tree_root, "name1", b"text1", True) 413 builder.remove_file(b"1", this=True) 414 builder.merge() 415 builder.cleanup() 416 417 def test_perms_merge(self): 418 builder = MergeBuilder(getcwd()) 419 builder.add_file(b"1", builder.tree_root, "name1", b"text1", True) 420 builder.change_perms(b"1", other=False) 421 builder.add_file(b"2", builder.tree_root, "name2", b"text2", True) 422 builder.change_perms(b"2", base=False) 423 builder.add_file(b"3", builder.tree_root, "name3", b"text3", True) 424 builder.change_perms(b"3", this=False) 425 builder.add_file(b'4', builder.tree_root, 'name4', b'text4', False) 426 builder.change_perms(b'4', this=True) 427 builder.remove_file(b'4', base=True) 428 builder.merge() 429 self.assertIs(builder.this.is_executable("name1"), False) 430 self.assertIs(builder.this.is_executable("name2"), True) 431 self.assertIs(builder.this.is_executable("name3"), False) 432 builder.cleanup() 433 434 def test_new_suffix(self): 435 builder = MergeBuilder(getcwd()) 436 builder.add_file(b"1", builder.tree_root, "name1", b"text1", True) 437 builder.change_contents(b"1", other=b"text3") 438 builder.add_file(b"2", builder.tree_root, "name1.new", b"text2", True) 439 builder.merge() 440 os.lstat(builder.this.abspath(builder.this.id2path(b"2"))) 441 builder.cleanup() 442 443 def test_spurious_conflict(self): 444 builder = MergeBuilder(getcwd()) 445 builder.add_file(b"1", builder.tree_root, "name1", b"text1", False) 446 builder.remove_file(b"1", other=True) 447 builder.add_file(b"2", builder.tree_root, "name1", b"text1", False, 448 this=False, base=False) 449 conflicts = builder.merge() 450 self.assertEqual(conflicts, []) 451 builder.cleanup() 452 453 def test_merge_one_renamed(self): 454 builder = MergeBuilder(getcwd()) 455 builder.add_file(b'1', builder.tree_root, 'name1', b'text1a', False) 456 builder.change_name(b'1', this='name2') 457 builder.change_contents(b'1', other=b'text2') 458 builder.merge(interesting_files=['name2']) 459 self.assertEqual(b'text2', builder.this.get_file('name2').read()) 460 builder.cleanup() 461 462 463class FunctionalMergeTest(TestCaseWithTransport): 464 465 def test_trivial_star_merge(self): 466 """Test that merges in a star shape Just Work.""" 467 # John starts a branch 468 self.build_tree(("original/", "original/file1", "original/file2")) 469 tree = self.make_branch_and_tree('original') 470 branch = tree.branch 471 tree.smart_add(["original"]) 472 tree.commit("start branch.", verbose=False) 473 # Mary branches it. 474 self.build_tree(("mary/",)) 475 branch.controldir.clone("mary") 476 # Now John commits a change 477 with open("original/file1", "wt") as f: 478 f.write("John\n") 479 tree.commit("change file1") 480 # Mary does too 481 mary_tree = WorkingTree.open('mary') 482 mary_branch = mary_tree.branch 483 with open("mary/file2", "wt") as f: 484 f.write("Mary\n") 485 mary_tree.commit("change file2") 486 # john should be able to merge with no conflicts. 487 base = [None, None] 488 other = ("mary", -1) 489 tree.merge_from_branch(mary_tree.branch) 490 with open("original/file1", "rt") as f: 491 self.assertEqual("John\n", f.read()) 492 with open("original/file2", "rt") as f: 493 self.assertEqual("Mary\n", f.read()) 494 495 def test_conflicts(self): 496 wta = self.make_branch_and_tree('a') 497 self.build_tree_contents([('a/file', b'contents\n')]) 498 wta.add('file') 499 wta.commit('base revision', allow_pointless=False) 500 d_b = wta.branch.controldir.clone('b') 501 self.build_tree_contents([('a/file', b'other contents\n')]) 502 wta.commit('other revision', allow_pointless=False) 503 self.build_tree_contents([('b/file', b'this contents contents\n')]) 504 wtb = d_b.open_workingtree() 505 wtb.commit('this revision', allow_pointless=False) 506 self.assertEqual(1, len(wtb.merge_from_branch(wta.branch))) 507 self.assertPathExists('b/file.THIS') 508 self.assertPathExists('b/file.BASE') 509 self.assertPathExists('b/file.OTHER') 510 wtb.revert() 511 self.assertEqual(1, len(wtb.merge_from_branch(wta.branch, 512 merge_type=WeaveMerger))) 513 self.assertPathExists('b/file') 514 self.assertPathExists('b/file.THIS') 515 self.assertPathExists('b/file.BASE') 516 self.assertPathExists('b/file.OTHER') 517 518 def test_weave_conflicts_not_in_base(self): 519 builder = self.make_branch_builder('source') 520 builder.start_series() 521 # See bug #494197 522 # A base revision (before criss-cross) 523 # |\ 524 # B C B does nothing, C adds 'foo' 525 # |X| 526 # D E D and E modify foo in incompatible ways 527 # 528 # Merging will conflict, with C as a clean base text. However, the 529 # current code uses A as the global base and 'foo' doesn't exist there. 530 # It isn't trivial to create foo.BASE because it tries to look up 531 # attributes like 'executable' in A. 532 builder.build_snapshot(None, [ 533 ('add', ('', b'TREE_ROOT', 'directory', None))], 534 revision_id=b'A-id') 535 builder.build_snapshot([b'A-id'], [], revision_id=b'B-id') 536 builder.build_snapshot([b'A-id'], [ 537 ('add', ('foo', b'foo-id', 'file', b'orig\ncontents\n'))], 538 revision_id=b'C-id') 539 builder.build_snapshot([b'B-id', b'C-id'], [ 540 ('add', ('foo', b'foo-id', 'file', b'orig\ncontents\nand D\n'))], 541 revision_id=b'D-id') 542 builder.build_snapshot([b'C-id', b'B-id'], [ 543 ('modify', ('foo', b'orig\ncontents\nand E\n'))], 544 revision_id=b'E-id') 545 builder.finish_series() 546 tree = builder.get_branch().create_checkout('tree', lightweight=True) 547 self.assertEqual(1, len(tree.merge_from_branch(tree.branch, 548 to_revision=b'D-id', 549 merge_type=WeaveMerger))) 550 self.assertPathExists('tree/foo.THIS') 551 self.assertPathExists('tree/foo.OTHER') 552 self.expectFailure('fail to create .BASE in some criss-cross merges', 553 self.assertPathExists, 'tree/foo.BASE') 554 self.assertPathExists('tree/foo.BASE') 555 556 def test_merge_unrelated(self): 557 """Sucessfully merges unrelated branches with no common names""" 558 wta = self.make_branch_and_tree('a') 559 a = wta.branch 560 with open('a/a_file', 'wb') as f: 561 f.write(b'contents\n') 562 wta.add('a_file') 563 wta.commit('a_revision', allow_pointless=False) 564 wtb = self.make_branch_and_tree('b') 565 b = wtb.branch 566 with open('b/b_file', 'wb') as f: 567 f.write(b'contents\n') 568 wtb.add('b_file') 569 b_rev = wtb.commit('b_revision', allow_pointless=False) 570 wta.merge_from_branch(wtb.branch, b_rev, b'null:') 571 self.assertTrue(os.path.lexists('a/b_file')) 572 self.assertEqual([b_rev], wta.get_parent_ids()[1:]) 573 574 def test_merge_unrelated_conflicting(self): 575 """Sucessfully merges unrelated branches with common names""" 576 wta = self.make_branch_and_tree('a') 577 a = wta.branch 578 with open('a/file', 'wb') as f: 579 f.write(b'contents\n') 580 wta.add('file') 581 wta.commit('a_revision', allow_pointless=False) 582 wtb = self.make_branch_and_tree('b') 583 b = wtb.branch 584 with open('b/file', 'wb') as f: 585 f.write(b'contents\n') 586 wtb.add('file') 587 b_rev = wtb.commit('b_revision', allow_pointless=False) 588 wta.merge_from_branch(wtb.branch, b_rev, b'null:') 589 self.assertTrue(os.path.lexists('a/file')) 590 self.assertTrue(os.path.lexists('a/file.moved')) 591 self.assertEqual([b_rev], wta.get_parent_ids()[1:]) 592 593 def test_merge_deleted_conflicts(self): 594 wta = self.make_branch_and_tree('a') 595 with open('a/file', 'wb') as f: 596 f.write(b'contents\n') 597 wta.add('file') 598 wta.commit('a_revision', allow_pointless=False) 599 self.run_bzr('branch a b') 600 os.remove('a/file') 601 wta.commit('removed file', allow_pointless=False) 602 with open('b/file', 'wb') as f: 603 f.write(b'changed contents\n') 604 wtb = WorkingTree.open('b') 605 wtb.commit('changed file', allow_pointless=False) 606 wtb.merge_from_branch(wta.branch, wta.branch.last_revision(), 607 wta.branch.get_rev_id(1)) 608 self.assertFalse(os.path.lexists('b/file')) 609 610 def test_merge_metadata_vs_deletion(self): 611 """Conflict deletion vs metadata change""" 612 a_wt = self.make_branch_and_tree('a') 613 with open('a/file', 'wb') as f: 614 f.write(b'contents\n') 615 a_wt.add('file') 616 a_wt.commit('r0') 617 self.run_bzr('branch a b') 618 b_wt = WorkingTree.open('b') 619 os.chmod('b/file', 0o755) 620 os.remove('a/file') 621 a_wt.commit('removed a') 622 self.assertEqual(a_wt.branch.revno(), 2) 623 self.assertFalse(os.path.exists('a/file')) 624 b_wt.commit('exec a') 625 a_wt.merge_from_branch(b_wt.branch, b_wt.last_revision(), b'null:') 626 self.assertTrue(os.path.exists('a/file')) 627 628 def test_merge_swapping_renames(self): 629 a_wt = self.make_branch_and_tree('a') 630 with open('a/un', 'wb') as f: 631 f.write(b'UN') 632 with open('a/deux', 'wb') as f: 633 f.write(b'DEUX') 634 a_wt.add('un', b'un-id') 635 a_wt.add('deux', b'deux-id') 636 a_wt.commit('r0', rev_id=b'r0') 637 self.run_bzr('branch a b') 638 b_wt = WorkingTree.open('b') 639 b_wt.rename_one('un', 'tmp') 640 b_wt.rename_one('deux', 'un') 641 b_wt.rename_one('tmp', 'deux') 642 b_wt.commit('r1', rev_id=b'r1') 643 self.assertEqual( 644 0, len(a_wt.merge_from_branch( 645 b_wt.branch, b_wt.branch.last_revision(), 646 b_wt.branch.get_rev_id(1)))) 647 self.assertPathExists('a/un') 648 self.assertTrue('a/deux') 649 self.assertFalse(os.path.exists('a/tmp')) 650 with open('a/un', 'r') as f: 651 self.assertEqual(f.read(), 'DEUX') 652 with open('a/deux', 'r') as f: 653 self.assertEqual(f.read(), 'UN') 654 655 def test_merge_delete_and_add_same(self): 656 a_wt = self.make_branch_and_tree('a') 657 with open('a/file', 'wb') as f: 658 f.write(b'THIS') 659 a_wt.add('file') 660 a_wt.commit('r0') 661 self.run_bzr('branch a b') 662 b_wt = WorkingTree.open('b') 663 os.remove('b/file') 664 b_wt.commit('r1') 665 with open('b/file', 'wb') as f: 666 f.write(b'THAT') 667 b_wt.add('file') 668 b_wt.commit('r2') 669 a_wt.merge_from_branch(b_wt.branch, b_wt.branch.last_revision(), 670 b_wt.branch.get_rev_id(1)) 671 self.assertTrue(os.path.exists('a/file')) 672 with open('a/file', 'r') as f: 673 self.assertEqual(f.read(), 'THAT') 674 675 def test_merge_rename_before_create(self): 676 """rename before create 677 678 This case requires that you must not do creates 679 before move-into-place: 680 681 $ touch foo 682 $ bzr add foo 683 $ bzr commit 684 $ bzr mv foo bar 685 $ touch foo 686 $ bzr add foo 687 $ bzr commit 688 """ 689 a_wt = self.make_branch_and_tree('a') 690 with open('a/foo', 'wb') as f: 691 f.write(b'A/FOO') 692 a_wt.add('foo') 693 a_wt.commit('added foo') 694 self.run_bzr('branch a b') 695 b_wt = WorkingTree.open('b') 696 b_wt.rename_one('foo', 'bar') 697 with open('b/foo', 'wb') as f: 698 f.write(b'B/FOO') 699 b_wt.add('foo') 700 b_wt.commit('moved foo to bar, added new foo') 701 a_wt.merge_from_branch(b_wt.branch, b_wt.branch.last_revision(), 702 b_wt.branch.get_rev_id(1)) 703 704 def test_merge_create_before_rename(self): 705 """create before rename, target parents before children 706 707 This case requires that you must not do move-into-place 708 before creates, and that you must not do children after 709 parents: 710 711 $ touch foo 712 $ bzr add foo 713 $ bzr commit 714 $ bzr mkdir bar 715 $ bzr add bar 716 $ bzr mv foo bar/foo 717 $ bzr commit 718 """ 719 os.mkdir('a') 720 a_wt = self.make_branch_and_tree('a') 721 with open('a/foo', 'wb') as f: 722 f.write(b'A/FOO') 723 a_wt.add('foo') 724 a_wt.commit('added foo') 725 self.run_bzr('branch a b') 726 b_wt = WorkingTree.open('b') 727 os.mkdir('b/bar') 728 b_wt.add('bar') 729 b_wt.rename_one('foo', 'bar/foo') 730 b_wt.commit('created bar dir, moved foo into bar') 731 a_wt.merge_from_branch(b_wt.branch, b_wt.branch.last_revision(), 732 b_wt.branch.get_rev_id(1)) 733 734 def test_merge_rename_to_temp_before_delete(self): 735 """rename to temp before delete, source children before parents 736 737 This case requires that you must not do deletes before 738 move-out-of-the-way, and that you must not do children 739 after parents: 740 741 $ mkdir foo 742 $ touch foo/bar 743 $ bzr add foo/bar 744 $ bzr commit 745 $ bzr mv foo/bar bar 746 $ rmdir foo 747 $ bzr commit 748 """ 749 a_wt = self.make_branch_and_tree('a') 750 os.mkdir('a/foo') 751 with open('a/foo/bar', 'wb') as f: 752 f.write(b'A/FOO/BAR') 753 a_wt.add('foo') 754 a_wt.add('foo/bar') 755 a_wt.commit('added foo/bar') 756 self.run_bzr('branch a b') 757 b_wt = WorkingTree.open('b') 758 b_wt.rename_one('foo/bar', 'bar') 759 os.rmdir('b/foo') 760 b_wt.remove('foo') 761 b_wt.commit('moved foo/bar to bar, deleted foo') 762 a_wt.merge_from_branch(b_wt.branch, b_wt.branch.last_revision(), 763 b_wt.branch.get_rev_id(1)) 764 765 def test_merge_delete_before_rename_to_temp(self): 766 """delete before rename to temp 767 768 This case requires that you must not do 769 move-out-of-the-way before deletes: 770 771 $ touch foo 772 $ touch bar 773 $ bzr add foo bar 774 $ bzr commit 775 $ rm foo 776 $ bzr rm foo 777 $ bzr mv bar foo 778 $ bzr commit 779 """ 780 a_wt = self.make_branch_and_tree('a') 781 with open('a/foo', 'wb') as f: 782 f.write(b'A/FOO') 783 with open('a/bar', 'wb') as f: 784 f.write(b'A/BAR') 785 a_wt.add('foo') 786 a_wt.add('bar') 787 a_wt.commit('added foo and bar') 788 self.run_bzr('branch a b') 789 b_wt = WorkingTree.open('b') 790 os.unlink('b/foo') 791 b_wt.remove('foo') 792 b_wt.rename_one('bar', 'foo') 793 b_wt.commit('deleted foo, renamed bar to foo') 794 a_wt.merge_from_branch(b_wt.branch, b_wt.branch.last_revision(), 795 b_wt.branch.get_rev_id(1)) 796 797 798class TestMerger(TestCaseWithTransport): 799 800 def set_up_trees(self): 801 this = self.make_branch_and_tree('this') 802 this.commit('rev1', rev_id=b'rev1') 803 other = this.controldir.sprout('other').open_workingtree() 804 this.commit('rev2a', rev_id=b'rev2a') 805 other.commit('rev2b', rev_id=b'rev2b') 806 return this, other 807 808 def test_from_revision_ids(self): 809 this, other = self.set_up_trees() 810 self.assertRaises(errors.NoSuchRevision, Merger.from_revision_ids, 811 this, b'rev2b') 812 this.lock_write() 813 self.addCleanup(this.unlock) 814 merger = Merger.from_revision_ids(this, 815 b'rev2b', other_branch=other.branch) 816 self.assertEqual(b'rev2b', merger.other_rev_id) 817 self.assertEqual(b'rev1', merger.base_rev_id) 818 merger = Merger.from_revision_ids(this, 819 b'rev2b', b'rev2a', other_branch=other.branch) 820 self.assertEqual(b'rev2a', merger.base_rev_id) 821 822 def test_from_uncommitted(self): 823 this, other = self.set_up_trees() 824 merger = Merger.from_uncommitted(this, other, None) 825 self.assertIs(other, merger.other_tree) 826 self.assertIs(None, merger.other_rev_id) 827 self.assertEqual(b'rev2b', merger.base_rev_id) 828 829 def prepare_for_merging(self): 830 this, other = self.set_up_trees() 831 other.commit('rev3', rev_id=b'rev3') 832 this.lock_write() 833 self.addCleanup(this.unlock) 834 return this, other 835 836 def test_from_mergeable(self): 837 this, other = self.prepare_for_merging() 838 md = merge_directive.MergeDirective2.from_objects( 839 other.branch.repository, b'rev3', 0, 0, 'this') 840 other.lock_read() 841 self.addCleanup(other.unlock) 842 merger, verified = Merger.from_mergeable(this, md) 843 md.patch = None 844 merger, verified = Merger.from_mergeable(this, md) 845 self.assertEqual('inapplicable', verified) 846 self.assertEqual(b'rev3', merger.other_rev_id) 847 self.assertEqual(b'rev1', merger.base_rev_id) 848 md.base_revision_id = b'rev2b' 849 merger, verified = Merger.from_mergeable(this, md) 850 self.assertEqual(b'rev2b', merger.base_rev_id) 851 852 def test_from_mergeable_old_merge_directive(self): 853 this, other = self.prepare_for_merging() 854 other.lock_write() 855 self.addCleanup(other.unlock) 856 md = merge_directive.MergeDirective.from_objects( 857 other.branch.repository, b'rev3', 0, 0, 'this') 858 merger, verified = Merger.from_mergeable(this, md) 859 self.assertEqual(b'rev3', merger.other_rev_id) 860 self.assertEqual(b'rev1', merger.base_rev_id) 861