82
86
pb2.update('foo', 0, 1)
85
self.assertEqual([("update", 0, 1)], factory._calls)
89
self.assertEqual([("update", 0, 1, 'foo')], factory._calls)
88
92
class TestCommit(TestCaseWithWorkingTree):
94
def test_autodelete_renamed(self):
95
tree_a = self.make_branch_and_tree('a')
96
self.build_tree(['a/dir/', 'a/dir/f1', 'a/dir/f2'])
97
tree_a.add(['dir', 'dir/f1', 'dir/f2'], ['dir-id', 'f1-id', 'f2-id'])
98
rev_id1 = tree_a.commit('init')
99
# Start off by renaming entries,
100
# but then actually auto delete the whole tree
101
# https://bugs.launchpad.net/bzr/+bug/114615
102
tree_a.rename_one('dir/f1', 'dir/a')
103
tree_a.rename_one('dir/f2', 'dir/z')
104
osutils.rmtree('a/dir')
105
tree_a.commit('autoremoved')
109
root_id = tree_a.get_root_id()
110
paths = [(path, ie.file_id)
111
for path, ie in tree_a.iter_entries_by_dir()]
114
# The only paths left should be the root
115
self.assertEqual([('', root_id)], paths)
117
def test_no_autodelete_renamed_away(self):
118
tree_a = self.make_branch_and_tree('a')
119
self.build_tree(['a/dir/', 'a/dir/f1', 'a/dir/f2', 'a/dir2/'])
120
tree_a.add(['dir', 'dir/f1', 'dir/f2', 'dir2'],
121
['dir-id', 'f1-id', 'f2-id', 'dir2-id'])
122
rev_id1 = tree_a.commit('init')
123
# Rename one entry out of this directory
124
tree_a.rename_one('dir/f1', 'dir2/a')
125
osutils.rmtree('a/dir')
126
tree_a.commit('autoremoved')
130
root_id = tree_a.get_root_id()
131
paths = [(path, ie.file_id)
132
for path, ie in tree_a.iter_entries_by_dir()]
135
# The only paths left should be the root
136
self.assertEqual([('', root_id), ('dir2', 'dir2-id'),
140
def test_no_autodelete_alternate_renamed(self):
141
# Test for bug #114615
142
tree_a = self.make_branch_and_tree('A')
143
self.build_tree(['A/a/', 'A/a/m', 'A/a/n'])
144
tree_a.add(['a', 'a/m', 'a/n'], ['a-id', 'm-id', 'n-id'])
145
tree_a.commit('init')
149
root_id = tree_a.get_root_id()
153
tree_b = tree_a.bzrdir.sprout('B').open_workingtree()
154
self.build_tree(['B/xyz/'])
155
tree_b.add(['xyz'], ['xyz-id'])
156
tree_b.rename_one('a/m', 'xyz/m')
157
osutils.rmtree('B/a')
158
tree_b.commit('delete in B')
160
paths = [(path, ie.file_id)
161
for path, ie in tree_b.iter_entries_by_dir()]
162
self.assertEqual([('', root_id),
167
self.build_tree_contents([('A/a/n', 'new contents for n\n')])
168
tree_a.commit('change n in A')
170
# Merging from A should introduce conflicts because 'n' was modified
171
# and removed, so 'a' needs to be restored.
172
num_conflicts = tree_b.merge_from_branch(tree_a.branch)
173
self.assertEqual(3, num_conflicts)
174
paths = [(path, ie.file_id)
175
for path, ie in tree_b.iter_entries_by_dir()]
176
self.assertEqual([('', root_id),
179
('a/n.OTHER', 'n-id'),
182
osutils.rmtree('B/a')
185
tree_b.set_conflicts(conflicts.ConflictList())
186
except errors.UnsupportedOperation:
187
# On WT2, set_conflicts is unsupported, but the rmtree has the same
190
tree_b.commit('autoremove a, without touching xyz/m')
191
paths = [(path, ie.file_id)
192
for path, ie in tree_b.iter_entries_by_dir()]
193
self.assertEqual([('', root_id),
198
def test_commit_exclude_pending_merge_fails(self):
199
"""Excludes are a form of partial commit."""
200
wt = self.make_branch_and_tree('.')
201
self.build_tree(['foo'])
203
wt.commit('commit one')
204
wt2 = wt.bzrdir.sprout('to').open_workingtree()
205
wt2.commit('change_right')
206
wt.merge_from_branch(wt2.branch)
207
self.assertRaises(errors.CannotCommitSelectedFileMerge,
208
wt.commit, 'test', exclude=['foo'])
210
def test_commit_exclude_exclude_changed_is_pointless(self):
211
tree = self.make_branch_and_tree('.')
212
self.build_tree(['a'])
213
tree.smart_add(['.'])
214
tree.commit('setup test')
215
self.build_tree_contents([('a', 'new contents for "a"\n')])
216
self.assertRaises(errors.PointlessCommit, tree.commit, 'test',
217
exclude=['a'], allow_pointless=False)
219
def test_commit_exclude_excludes_modified_files(self):
220
tree = self.make_branch_and_tree('.')
221
self.build_tree(['a', 'b', 'c'])
222
tree.smart_add(['.'])
223
tree.commit('test', exclude=['b', 'c'])
224
# If b was excluded it will still be 'added' in status.
226
self.addCleanup(tree.unlock)
227
changes = list(tree.iter_changes(tree.basis_tree()))
228
self.assertEqual(2, len(changes))
229
self.assertEqual((None, 'b'), changes[0][1])
230
self.assertEqual((None, 'c'), changes[1][1])
232
def test_commit_exclude_subtree_of_selected(self):
233
tree = self.make_branch_and_tree('.')
234
self.build_tree(['a/', 'a/b'])
235
tree.smart_add(['.'])
236
tree.commit('test', specific_files=['a'], exclude=['a/b'])
237
# If a/b was excluded it will still be 'added' in status.
239
self.addCleanup(tree.unlock)
240
changes = list(tree.iter_changes(tree.basis_tree()))
241
self.assertEqual(1, len(changes))
242
self.assertEqual((None, 'a/b'), changes[0][1])
90
244
def test_commit_sets_last_revision(self):
91
245
tree = self.make_branch_and_tree('tree')
92
tree.commit('foo', rev_id='foo', allow_pointless=True)
93
self.assertEqual('foo', tree.last_revision())
246
committed_id = tree.commit('foo', rev_id='foo')
247
self.assertEqual(['foo'], tree.get_parent_ids())
248
# the commit should have returned the same id we asked for.
249
self.assertEqual('foo', committed_id)
251
def test_commit_returns_revision_id(self):
252
tree = self.make_branch_and_tree('.')
253
committed_id = tree.commit('message')
254
self.assertTrue(tree.branch.repository.has_revision(committed_id))
255
self.assertNotEqual(None, committed_id)
95
257
def test_commit_local_unbound(self):
96
258
# using the library api to do a local commit on unbound branches is
135
314
tree.commit('foo', rev_id='foo', local=True)
136
315
self.failIf(master.repository.has_revision('foo'))
137
self.assertEqual(None, master.last_revision())
316
self.assertEqual(_mod_revision.NULL_REVISION,
317
(_mod_revision.ensure_null(master.last_revision())))
319
def test_record_initial_ghost(self):
320
"""The working tree needs to record ghosts during commit."""
321
wt = self.make_branch_and_tree('.')
322
wt.set_parent_ids(['non:existent@rev--ision--0--2'],
323
allow_leftmost_as_ghost=True)
324
rev_id = wt.commit('commit against a ghost first parent.')
325
rev = wt.branch.repository.get_revision(rev_id)
326
self.assertEqual(rev.parent_ids, ['non:existent@rev--ision--0--2'])
327
# parent_sha1s is not populated now, WTF. rbc 20051003
328
self.assertEqual(len(rev.parent_sha1s), 0)
330
def test_record_two_ghosts(self):
331
"""The working tree should preserve all the parents during commit."""
332
wt = self.make_branch_and_tree('.')
334
'foo@azkhazan-123123-abcabc',
335
'wibble@fofof--20050401--1928390812',
337
allow_leftmost_as_ghost=True)
338
rev_id = wt.commit("commit from ghost base with one merge")
339
# the revision should have been committed with two parents
340
rev = wt.branch.repository.get_revision(rev_id)
341
self.assertEqual(['foo@azkhazan-123123-abcabc',
342
'wibble@fofof--20050401--1928390812'],
345
def test_commit_deleted_subtree_and_files_updates_workingtree(self):
346
"""The working trees inventory may be adjusted by commit."""
347
wt = self.make_branch_and_tree('.')
349
self.build_tree(['a', 'b/', 'b/c', 'd'])
350
wt.add(['a', 'b', 'b/c', 'd'], ['a-id', 'b-id', 'c-id', 'd-id'])
351
this_dir = self.get_transport()
352
this_dir.delete_tree('b')
354
# now we have a tree with a through d in the inventory, but only
355
# a present on disk. After commit b-id, c-id and d-id should be
356
# missing from the inventory, within the same tree transaction.
357
wt.commit('commit stuff')
358
self.assertTrue(wt.has_id('a-id'))
359
self.assertFalse(wt.has_or_had_id('b-id'))
360
self.assertFalse(wt.has_or_had_id('c-id'))
361
self.assertFalse(wt.has_or_had_id('d-id'))
362
self.assertTrue(wt.has_filename('a'))
363
self.assertFalse(wt.has_filename('b'))
364
self.assertFalse(wt.has_filename('b/c'))
365
self.assertFalse(wt.has_filename('d'))
367
# the changes should have persisted to disk - reopen the workingtree
369
wt = wt.bzrdir.open_workingtree()
371
self.assertTrue(wt.has_id('a-id'))
372
self.assertFalse(wt.has_or_had_id('b-id'))
373
self.assertFalse(wt.has_or_had_id('c-id'))
374
self.assertFalse(wt.has_or_had_id('d-id'))
375
self.assertTrue(wt.has_filename('a'))
376
self.assertFalse(wt.has_filename('b'))
377
self.assertFalse(wt.has_filename('b/c'))
378
self.assertFalse(wt.has_filename('d'))
381
def test_commit_deleted_subtree_with_removed(self):
382
wt = self.make_branch_and_tree('.')
383
self.build_tree(['a', 'b/', 'b/c', 'd'])
384
wt.add(['a', 'b', 'b/c'], ['a-id', 'b-id', 'c-id'])
387
this_dir = self.get_transport()
388
this_dir.delete_tree('b')
390
wt.commit('commit deleted rename')
391
self.assertTrue(wt.has_id('a-id'))
392
self.assertFalse(wt.has_or_had_id('b-id'))
393
self.assertFalse(wt.has_or_had_id('c-id'))
394
self.assertTrue(wt.has_filename('a'))
395
self.assertFalse(wt.has_filename('b'))
396
self.assertFalse(wt.has_filename('b/c'))
399
def test_commit_move_new(self):
400
wt = self.make_branch_and_tree('first')
402
wt2 = wt.bzrdir.sprout('second').open_workingtree()
403
self.build_tree(['second/name1'])
404
wt2.add('name1', 'name1-id')
406
wt.merge_from_branch(wt2.branch)
407
wt.rename_one('name1', 'name2')
409
wt.path2id('name1-id')
411
def test_nested_commit(self):
412
"""Commit in multiply-nested trees"""
413
tree = self.make_branch_and_tree('.')
414
if not tree.supports_tree_reference():
417
subtree = self.make_branch_and_tree('subtree')
418
subsubtree = self.make_branch_and_tree('subtree/subtree')
419
subtree.add(['subtree'])
420
tree.add(['subtree'])
421
# use allow_pointless=False to ensure that the deepest tree, which
422
# has no commits made to it, does not get a pointless commit.
423
rev_id = tree.commit('added reference', allow_pointless=False)
425
self.addCleanup(tree.unlock)
426
# the deepest subtree has not changed, so no commit should take place.
427
self.assertEqual('null:', subsubtree.last_revision())
428
# the intermediate tree should have committed a pointer to the current
430
sub_basis = subtree.basis_tree()
431
sub_basis.lock_read()
432
self.addCleanup(sub_basis.unlock)
433
self.assertEqual(subsubtree.last_revision(),
434
sub_basis.get_reference_revision(sub_basis.path2id('subtree')))
435
# the intermediate tree has changed, so should have had a commit
437
self.assertNotEqual(None, subtree.last_revision())
438
# the outer tree should have committed a pointer to the current
440
basis = tree.basis_tree()
442
self.addCleanup(basis.unlock)
443
self.assertEqual(subtree.last_revision(),
444
basis.get_reference_revision(basis.path2id('subtree')))
445
# the outer tree must have have changed too.
446
self.assertNotEqual(None, rev_id)
448
def test_nested_commit_second_commit_detects_changes(self):
449
"""Commit with a nested tree picks up the correct child revid."""
450
tree = self.make_branch_and_tree('.')
451
if not tree.supports_tree_reference():
454
subtree = self.make_branch_and_tree('subtree')
455
tree.add(['subtree'])
456
self.build_tree(['subtree/file'])
457
subtree.add(['file'], ['file-id'])
458
rev_id = tree.commit('added reference', allow_pointless=False)
459
child_revid = subtree.last_revision()
460
# now change the child tree
461
self.build_tree_contents([('subtree/file', 'new-content')])
462
# and commit in the parent should commit the child and grab its revid,
463
# we test with allow_pointless=False here so that we are simulating
464
# what users will see.
465
rev_id2 = tree.commit('changed subtree only', allow_pointless=False)
466
# the child tree has changed, so should have had a commit
468
self.assertNotEqual(None, subtree.last_revision())
469
self.assertNotEqual(child_revid, subtree.last_revision())
470
# the outer tree should have committed a pointer to the current
472
basis = tree.basis_tree()
474
self.addCleanup(basis.unlock)
475
self.assertEqual(subtree.last_revision(),
476
basis.get_reference_revision(basis.path2id('subtree')))
477
self.assertNotEqual(rev_id, rev_id2)
479
def test_nested_pointless_commits_are_pointless(self):
480
tree = self.make_branch_and_tree('.')
481
if not tree.supports_tree_reference():
484
subtree = self.make_branch_and_tree('subtree')
485
tree.add(['subtree'])
486
# record the reference.
487
rev_id = tree.commit('added reference')
488
child_revid = subtree.last_revision()
489
# now do a no-op commit with allow_pointless=False
490
self.assertRaises(errors.PointlessCommit, tree.commit, '',
491
allow_pointless=False)
492
self.assertEqual(child_revid, subtree.last_revision())
493
self.assertEqual(rev_id, tree.last_revision())
140
496
class TestCommitProgress(TestCaseWithWorkingTree):
167
523
# into the factory for this test - just make the test ui factory
168
524
# pun as a reporter. Then we can check the ordering is right.
169
525
tree.commit('second post', specific_files=['b'])
170
# 9 steps: 1 for rev, 2 for inventory, 1 for finishing. 2 for root
171
# and 6 for inventory files.
172
# 2 steps don't trigger an update, as 'a' and 'c' are not
526
# 5 steps, the first of which is reported 2 times, once per dir
528
[('update', 1, 5, 'Collecting changes [Directory 0] - Stage'),
529
('update', 1, 5, 'Collecting changes [Directory 1] - Stage'),
530
('update', 2, 5, 'Saving data locally - Stage'),
531
('update', 3, 5, 'Running pre_commit hooks - Stage'),
532
('update', 4, 5, 'Updating the working tree - Stage'),
533
('update', 5, 5, 'Running post_commit hooks - Stage')],
537
def test_commit_progress_shows_post_hook_names(self):
538
tree = self.make_branch_and_tree('.')
539
# set a progress bar that captures the calls so we can see what is
541
self.old_ui_factory = ui.ui_factory
542
self.addCleanup(self.restoreDefaults)
543
factory = CapturingUIFactory()
544
ui.ui_factory = factory
545
def a_hook(_, _2, _3, _4, _5, _6):
547
branch.Branch.hooks.install_named_hook('post_commit', a_hook,
549
tree.commit('first post')
551
[('update', 1, 5, 'Collecting changes [Directory 0] - Stage'),
552
('update', 1, 5, 'Collecting changes [Directory 1] - Stage'),
553
('update', 2, 5, 'Saving data locally - Stage'),
554
('update', 3, 5, 'Running pre_commit hooks - Stage'),
555
('update', 4, 5, 'Updating the working tree - Stage'),
556
('update', 5, 5, 'Running post_commit hooks - Stage'),
557
('update', 5, 5, 'Running post_commit hooks [hook name] - Stage'),
562
def test_commit_progress_shows_pre_hook_names(self):
563
tree = self.make_branch_and_tree('.')
564
# set a progress bar that captures the calls so we can see what is
566
self.old_ui_factory = ui.ui_factory
567
self.addCleanup(self.restoreDefaults)
568
factory = CapturingUIFactory()
569
ui.ui_factory = factory
570
def a_hook(_, _2, _3, _4, _5, _6, _7, _8):
572
branch.Branch.hooks.install_named_hook('pre_commit', a_hook,
574
tree.commit('first post')
576
[('update', 1, 5, 'Collecting changes [Directory 0] - Stage'),
577
('update', 1, 5, 'Collecting changes [Directory 1] - Stage'),
578
('update', 2, 5, 'Saving data locally - Stage'),
579
('update', 3, 5, 'Running pre_commit hooks - Stage'),
580
('update', 3, 5, 'Running pre_commit hooks [hook name] - Stage'),
581
('update', 4, 5, 'Updating the working tree - Stage'),
582
('update', 5, 5, 'Running post_commit hooks - Stage'),
587
def test_start_commit_hook(self):
588
"""Make sure a start commit hook can modify the tree that is
590
def start_commit_hook_adds_file(tree):
591
open(tree.abspath("newfile"), 'w').write("data")
592
tree.add(["newfile"])
593
def restoreDefaults():
594
mutabletree.MutableTree.hooks['start_commit'] = []
595
self.addCleanup(restoreDefaults)
596
tree = self.make_branch_and_tree('.')
597
mutabletree.MutableTree.hooks.install_named_hook(
599
start_commit_hook_adds_file,
601
revid = tree.commit('first post')
602
committed_tree = tree.basis_tree()
603
self.assertTrue(committed_tree.has_filename("newfile"))