20
from bzrlib.selftest import TestCaseInTempDir
27
21
from bzrlib.branch import Branch
28
from bzrlib.bzrdir import BzrDir, BzrDirMetaFormat1
29
from bzrlib.commit import Commit, NullCommitReporter
30
from bzrlib.config import BranchConfig
31
from bzrlib.errors import (PointlessCommit, BzrError, SigningFailed,
33
from bzrlib.tests import TestCaseWithTransport
34
from bzrlib.workingtree import WorkingTree
22
from bzrlib.commit import Commit
23
from bzrlib.errors import PointlessCommit, BzrError
37
26
# TODO: Test commit with some added, and added-but-missing files
39
class MustSignConfig(BranchConfig):
41
def signature_needed(self):
44
def gpg_signing_command(self):
48
class BranchWithHooks(BranchConfig):
50
def post_commit(self):
51
return "bzrlib.ahook bzrlib.ahook"
54
class CapturingReporter(NullCommitReporter):
55
"""This reporter captures the calls made to it for evaluation later."""
58
# a list of the calls this received
61
def snapshot_change(self, change, path):
62
self.calls.append(('change', change, path))
64
def deleted(self, file_id):
65
self.calls.append(('deleted', file_id))
67
def missing(self, path):
68
self.calls.append(('missing', path))
70
def renamed(self, change, old_path, new_path):
71
self.calls.append(('renamed', change, old_path, new_path))
74
class TestCommit(TestCaseWithTransport):
28
class TestCommit(TestCaseInTempDir):
76
30
def test_simple_commit(self):
77
31
"""Commit and check two versions of a single file."""
78
wt = self.make_branch_and_tree('.')
32
b = Branch.initialize('.')
80
33
file('hello', 'w').write('hello world')
82
wt.commit(message='add hello')
83
file_id = wt.path2id('hello')
35
b.commit(message='add hello')
36
file_id = b.working_tree().path2id('hello')
85
38
file('hello', 'w').write('version 2')
86
wt.commit(message='commit 2')
39
b.commit(message='commit 2')
88
41
eq = self.assertEquals
90
43
rh = b.revision_history()
91
rev = b.repository.get_revision(rh[0])
44
rev = b.get_revision(rh[0])
92
45
eq(rev.message, 'add hello')
94
tree1 = b.repository.revision_tree(rh[0])
47
tree1 = b.revision_tree(rh[0])
95
48
text = tree1.get_file_text(file_id)
96
49
eq(text, 'hello world')
98
tree2 = b.repository.revision_tree(rh[1])
51
tree2 = b.revision_tree(rh[1])
99
52
eq(tree2.get_file_text(file_id), 'version 2')
101
55
def test_delete_commit(self):
102
56
"""Test a commit with a deleted file"""
103
wt = self.make_branch_and_tree('.')
57
b = Branch.initialize('.')
105
58
file('hello', 'w').write('hello world')
106
wt.add(['hello'], ['hello-id'])
107
wt.commit(message='add hello')
59
b.add(['hello'], ['hello-id'])
60
b.commit(message='add hello')
109
62
os.remove('hello')
110
wt.commit('removed hello', rev_id='rev2')
63
b.commit('removed hello', rev_id='rev2')
112
tree = b.repository.revision_tree('rev2')
65
tree = b.revision_tree('rev2')
113
66
self.assertFalse(tree.has_id('hello-id'))
115
69
def test_pointless_commit(self):
116
70
"""Commit refuses unless there are changes or it's forced."""
117
wt = self.make_branch_and_tree('.')
71
b = Branch.initialize('.')
119
72
file('hello', 'w').write('hello')
121
wt.commit(message='add hello')
74
b.commit(message='add hello')
122
75
self.assertEquals(b.revno(), 1)
123
76
self.assertRaises(PointlessCommit,
126
79
allow_pointless=False)
127
80
self.assertEquals(b.revno(), 1)
129
84
def test_commit_empty(self):
130
85
"""Commiting an empty tree works."""
131
wt = self.make_branch_and_tree('.')
133
wt.commit(message='empty tree', allow_pointless=True)
86
b = Branch.initialize('.')
87
b.commit(message='empty tree', allow_pointless=True)
134
88
self.assertRaises(PointlessCommit,
136
90
message='empty tree',
137
91
allow_pointless=False)
138
wt.commit(message='empty tree', allow_pointless=True)
92
b.commit(message='empty tree', allow_pointless=True)
139
93
self.assertEquals(b.revno(), 2)
141
96
def test_selective_delete(self):
142
97
"""Selective commit in tree with deletions"""
143
wt = self.make_branch_and_tree('.')
98
b = Branch.initialize('.')
145
99
file('hello', 'w').write('hello')
146
100
file('buongia', 'w').write('buongia')
147
wt.add(['hello', 'buongia'],
101
b.add(['hello', 'buongia'],
148
102
['hello-id', 'buongia-id'])
149
wt.commit(message='add files',
103
b.commit(message='add files',
150
104
rev_id='test@rev-1')
152
106
os.remove('hello')
153
107
file('buongia', 'w').write('new text')
154
wt.commit(message='update text',
108
b.commit(message='update text',
155
109
specific_files=['buongia'],
156
110
allow_pointless=False,
157
111
rev_id='test@rev-2')
159
wt.commit(message='remove hello',
113
b.commit(message='remove hello',
160
114
specific_files=['hello'],
161
115
allow_pointless=False,
162
116
rev_id='test@rev-3')
193
147
ie = tree1.inventory['hello-id']
194
148
eq(ie.revision, 'test@rev-1')
196
tree2 = b.repository.revision_tree('test@rev-2')
150
tree2 = b.revision_tree('test@rev-2')
197
151
eq(tree2.id2path('hello-id'), 'fruity')
198
152
eq(tree2.get_file_text('hello-id'), 'contents of hello\n')
199
153
self.check_inventory_shape(tree2.inventory, ['fruity'])
200
154
ie = tree2.inventory['hello-id']
201
155
eq(ie.revision, 'test@rev-2')
203
158
def test_reused_rev_id(self):
204
159
"""Test that a revision id cannot be reused in a branch"""
205
wt = self.make_branch_and_tree('.')
207
wt.commit('initial', rev_id='test@rev-1', allow_pointless=True)
160
b = Branch.initialize('.')
161
b.commit('initial', rev_id='test@rev-1', allow_pointless=True)
208
162
self.assertRaises(Exception,
210
164
message='reused id',
211
165
rev_id='test@rev-1',
212
166
allow_pointless=True)
214
170
def test_commit_move(self):
215
171
"""Test commit of revisions with moved files and directories"""
216
172
eq = self.assertEquals
217
wt = self.make_branch_and_tree('.')
173
b = Branch.initialize('.')
219
174
r1 = 'test@rev-1'
220
175
self.build_tree(['hello', 'a/', 'b/'])
221
wt.add(['hello', 'a', 'b'], ['hello-id', 'a-id', 'b-id'])
222
wt.commit('initial', rev_id=r1, allow_pointless=False)
223
wt.move(['hello'], 'a')
176
b.add(['hello', 'a', 'b'], ['hello-id', 'a-id', 'b-id'])
177
b.commit('initial', rev_id=r1, allow_pointless=False)
179
b.move(['hello'], 'a')
224
180
r2 = 'test@rev-2'
225
wt.commit('two', rev_id=r2, allow_pointless=False)
228
self.check_inventory_shape(wt.read_working_inventory(),
229
['a/', 'a/hello', 'b/'])
181
b.commit('two', rev_id=r2, allow_pointless=False)
182
self.check_inventory_shape(b.inventory,
183
['a', 'a/hello', 'b'])
234
186
r3 = 'test@rev-3'
235
wt.commit('three', rev_id=r3, allow_pointless=False)
238
self.check_inventory_shape(wt.read_working_inventory(),
239
['a/', 'a/hello', 'a/b/'])
240
self.check_inventory_shape(b.repository.get_revision_inventory(r3),
241
['a/', 'a/hello', 'a/b/'])
187
b.commit('three', rev_id=r3, allow_pointless=False)
188
self.check_inventory_shape(b.inventory,
189
['a', 'a/hello', 'a/b'])
190
self.check_inventory_shape(b.get_revision_inventory(r3),
191
['a', 'a/hello', 'a/b'])
245
wt.move(['a/hello'], 'a/b')
193
b.move([os.sep.join(['a', 'hello'])],
194
os.sep.join(['a', 'b']))
246
195
r4 = 'test@rev-4'
247
wt.commit('four', rev_id=r4, allow_pointless=False)
250
self.check_inventory_shape(wt.read_working_inventory(),
251
['a/', 'a/b/hello', 'a/b/'])
196
b.commit('four', rev_id=r4, allow_pointless=False)
197
self.check_inventory_shape(b.inventory,
198
['a', 'a/b/hello', 'a/b'])
255
inv = b.repository.get_revision_inventory(r4)
200
inv = b.get_revision_inventory(r4)
256
201
eq(inv['hello-id'].revision, r4)
257
202
eq(inv['a-id'].revision, r1)
258
203
eq(inv['b-id'].revision, r3)
260
206
def test_removed_commit(self):
261
"""Commit with a removed file"""
262
wt = self.make_branch_and_tree('.')
207
"""Test a commit with a removed file"""
208
b = Branch.initialize('.')
264
209
file('hello', 'w').write('hello world')
265
wt.add(['hello'], ['hello-id'])
266
wt.commit(message='add hello')
268
wt.commit('removed hello', rev_id='rev2')
270
tree = b.repository.revision_tree('rev2')
210
b.add(['hello'], ['hello-id'])
211
b.commit(message='add hello')
214
b.commit('removed hello', rev_id='rev2')
216
tree = b.revision_tree('rev2')
271
217
self.assertFalse(tree.has_id('hello-id'))
273
220
def test_committed_ancestry(self):
274
221
"""Test commit appends revisions to ancestry."""
275
wt = self.make_branch_and_tree('.')
222
b = Branch.initialize('.')
278
224
for i in range(4):
279
225
file('hello', 'w').write((str(i) * 4) + '\n')
281
wt.add(['hello'], ['hello-id'])
227
b.add(['hello'], ['hello-id'])
282
228
rev_id = 'test@rev-%d' % (i+1)
283
229
rev_ids.append(rev_id)
284
wt.commit(message='rev %d' % (i+1),
230
b.commit(message='rev %d' % (i+1),
286
232
eq = self.assertEquals
287
233
eq(b.revision_history(), rev_ids)
288
234
for i in range(4):
289
anc = b.repository.get_ancestry(rev_ids[i])
235
anc = b.get_ancestry(rev_ids[i])
290
236
eq(anc, [None] + rev_ids[:i+1])
292
238
def test_commit_new_subdir_child_selective(self):
293
wt = self.make_branch_and_tree('.')
239
b = Branch.initialize('.')
295
240
self.build_tree(['dir/', 'dir/file1', 'dir/file2'])
296
wt.add(['dir', 'dir/file1', 'dir/file2'],
241
b.add(['dir', 'dir/file1', 'dir/file2'],
297
242
['dirid', 'file1id', 'file2id'])
298
wt.commit('dir/file1', specific_files=['dir/file1'], rev_id='1')
299
inv = b.repository.get_inventory('1')
243
b.commit('dir/file1', specific_files=['dir/file1'], rev_id='1')
244
inv = b.get_inventory('1')
300
245
self.assertEqual('1', inv['dirid'].revision)
301
246
self.assertEqual('1', inv['file1id'].revision)
302
247
# FIXME: This should raise a KeyError I think, rbc20051006
303
248
self.assertRaises(BzrError, inv.__getitem__, 'file2id')
305
def test_strict_commit(self):
306
"""Try and commit with unknown files and strict = True, should fail."""
307
from bzrlib.errors import StrictCommitFailed
308
wt = self.make_branch_and_tree('.')
310
file('hello', 'w').write('hello world')
312
file('goodbye', 'w').write('goodbye cruel world!')
313
self.assertRaises(StrictCommitFailed, wt.commit,
314
message='add hello but not goodbye', strict=True)
316
def test_strict_commit_without_unknowns(self):
317
"""Try and commit with no unknown files and strict = True,
319
from bzrlib.errors import StrictCommitFailed
320
wt = self.make_branch_and_tree('.')
322
file('hello', 'w').write('hello world')
324
wt.commit(message='add hello', strict=True)
326
def test_nonstrict_commit(self):
327
"""Try and commit with unknown files and strict = False, should work."""
328
wt = self.make_branch_and_tree('.')
330
file('hello', 'w').write('hello world')
332
file('goodbye', 'w').write('goodbye cruel world!')
333
wt.commit(message='add hello but not goodbye', strict=False)
335
def test_nonstrict_commit_without_unknowns(self):
336
"""Try and commit with no unknown files and strict = False,
338
wt = self.make_branch_and_tree('.')
340
file('hello', 'w').write('hello world')
342
wt.commit(message='add hello', strict=False)
344
def test_signed_commit(self):
346
import bzrlib.commit as commit
347
oldstrategy = bzrlib.gpg.GPGStrategy
348
wt = self.make_branch_and_tree('.')
350
wt.commit("base", allow_pointless=True, rev_id='A')
351
self.failIf(branch.repository.has_signature_for_revision_id('A'))
353
from bzrlib.testament import Testament
354
# monkey patch gpg signing mechanism
355
bzrlib.gpg.GPGStrategy = bzrlib.gpg.LoopbackGPGStrategy
356
commit.Commit(config=MustSignConfig(branch)).commit(message="base",
357
allow_pointless=True,
361
return bzrlib.gpg.LoopbackGPGStrategy(None).sign(text)
362
self.assertEqual(sign(Testament.from_revision(branch.repository,
363
'B').as_short_text()),
364
branch.repository.get_signature_text('B'))
366
bzrlib.gpg.GPGStrategy = oldstrategy
368
def test_commit_failed_signature(self):
370
import bzrlib.commit as commit
371
oldstrategy = bzrlib.gpg.GPGStrategy
372
wt = self.make_branch_and_tree('.')
374
wt.commit("base", allow_pointless=True, rev_id='A')
375
self.failIf(branch.repository.has_signature_for_revision_id('A'))
377
from bzrlib.testament import Testament
378
# monkey patch gpg signing mechanism
379
bzrlib.gpg.GPGStrategy = bzrlib.gpg.DisabledGPGStrategy
380
config = MustSignConfig(branch)
381
self.assertRaises(SigningFailed,
382
commit.Commit(config=config).commit,
384
allow_pointless=True,
387
branch = Branch.open(self.get_url('.'))
388
self.assertEqual(branch.revision_history(), ['A'])
389
self.failIf(branch.repository.has_revision('B'))
391
bzrlib.gpg.GPGStrategy = oldstrategy
393
def test_commit_invokes_hooks(self):
394
import bzrlib.commit as commit
395
wt = self.make_branch_and_tree('.')
398
def called(branch, rev_id):
399
calls.append('called')
400
bzrlib.ahook = called
402
config = BranchWithHooks(branch)
403
commit.Commit(config=config).commit(
405
allow_pointless=True,
406
rev_id='A', working_tree = wt)
407
self.assertEqual(['called', 'called'], calls)
411
def test_commit_object_doesnt_set_nick(self):
412
# using the Commit object directly does not set the branch nick.
413
wt = self.make_branch_and_tree('.')
415
c.commit(working_tree=wt, message='empty tree', allow_pointless=True)
416
self.assertEquals(wt.branch.revno(), 1)
418
wt.branch.repository.get_revision(
419
wt.branch.last_revision()).properties)
421
def test_safe_master_lock(self):
423
master = BzrDirMetaFormat1().initialize('master')
424
master.create_repository()
425
master_branch = master.create_branch()
426
master.create_workingtree()
427
bound = master.sprout('bound')
428
wt = bound.open_workingtree()
429
wt.branch.set_bound_location(os.path.realpath('master'))
431
orig_default = lockdir._DEFAULT_TIMEOUT_SECONDS
432
master_branch.lock_write()
434
lockdir._DEFAULT_TIMEOUT_SECONDS = 1
435
self.assertRaises(LockContention, wt.commit, 'silly')
437
lockdir._DEFAULT_TIMEOUT_SECONDS = orig_default
438
master_branch.unlock()
440
def test_commit_bound_merge(self):
441
# see bug #43959; commit of a merge in a bound branch fails to push
442
# the new commit into the master
443
master_branch = self.make_branch('master')
444
bound_tree = self.make_branch_and_tree('bound')
445
bound_tree.branch.bind(master_branch)
447
self.build_tree_contents([('bound/content_file', 'initial contents\n')])
448
bound_tree.add(['content_file'])
449
bound_tree.commit(message='woo!')
451
other_bzrdir = master_branch.bzrdir.sprout('other')
452
other_tree = other_bzrdir.open_workingtree()
454
# do a commit to the the other branch changing the content file so
455
# that our commit after merging will have a merged revision in the
456
# content file history.
457
self.build_tree_contents([('other/content_file', 'change in other\n')])
458
other_tree.commit('change in other')
460
# do a merge into the bound branch from other, and then change the
461
# content file locally to force a new revision (rather than using the
462
# revision from other). This forces extra processing in commit.
463
bound_tree.merge_from_branch(other_tree.branch)
464
self.build_tree_contents([('bound/content_file', 'change in bound\n')])
466
# before #34959 was fixed, this failed with 'revision not present in
467
# weave' when trying to implicitly push from the bound branch to the master
468
bound_tree.commit(message='commit of merge in bound tree')
470
def test_commit_reporting_after_merge(self):
471
# when doing a commit of a merge, the reporter needs to still
472
# be called for each item that is added/removed/deleted.
473
this_tree = self.make_branch_and_tree('this')
474
# we need a bunch of files and dirs, to perform one action on each.
477
'this/dirtoreparent/',
480
'this/filetoreparent',
497
this_tree.commit('create_files')
498
other_dir = this_tree.bzrdir.sprout('other')
499
other_tree = other_dir.open_workingtree()
500
other_tree.lock_write()
501
# perform the needed actions on the files and dirs.
503
other_tree.rename_one('dirtorename', 'renameddir')
504
other_tree.rename_one('dirtoreparent', 'renameddir/reparenteddir')
505
other_tree.rename_one('filetorename', 'renamedfile')
506
other_tree.rename_one('filetoreparent', 'renameddir/reparentedfile')
507
other_tree.remove(['dirtoremove', 'filetoremove'])
508
self.build_tree_contents([
510
('other/filetomodify', 'new content'),
511
('other/newfile', 'new file content')])
512
other_tree.add('newfile')
513
other_tree.add('newdir/')
514
other_tree.commit('modify all sample files and dirs.')
517
this_tree.merge_from_branch(other_tree.branch)
518
reporter = CapturingReporter()
519
this_tree.commit('do the commit', reporter=reporter)
521
('change', 'unchanged', ''),
522
('change', 'unchanged', 'dirtoleave'),
523
('change', 'unchanged', 'filetoleave'),
524
('change', 'modified', 'filetomodify'),
525
('change', 'added', 'newdir'),
526
('change', 'added', 'newfile'),
527
('renamed', 'renamed', 'dirtorename', 'renameddir'),
528
('renamed', 'renamed', 'dirtoreparent', 'renameddir/reparenteddir'),
529
('renamed', 'renamed', 'filetoreparent', 'renameddir/reparentedfile'),
530
('renamed', 'renamed', 'filetorename', 'renamedfile'),
531
('deleted', 'dirtoremove'),
532
('deleted', 'filetoremove'),
536
def test_commit_removals_respects_filespec(self):
537
"""Commit respects the specified_files for removals."""
538
tree = self.make_branch_and_tree('.')
539
self.build_tree(['a', 'b'])
541
tree.commit('added a, b')
542
tree.remove(['a', 'b'])
543
tree.commit('removed a', specific_files='a')
544
basis = tree.basis_tree()
547
self.assertIs(None, basis.path2id('a'))
548
self.assertFalse(basis.path2id('b') is None)
552
def test_commit_saves_1ms_timestamp(self):
553
"""Passing in a timestamp is saved with 1ms resolution"""
554
tree = self.make_branch_and_tree('.')
555
self.build_tree(['a'])
557
tree.commit('added a', timestamp=1153248633.4186721, timezone=0,
560
rev = tree.branch.repository.get_revision('a1')
561
self.assertEqual(1153248633.419, rev.timestamp)
563
def test_commit_has_1ms_resolution(self):
564
"""Allowing commit to generate the timestamp also has 1ms resolution"""
565
tree = self.make_branch_and_tree('.')
566
self.build_tree(['a'])
568
tree.commit('added a', rev_id='a1')
570
rev = tree.branch.repository.get_revision('a1')
571
timestamp = rev.timestamp
572
timestamp_1ms = round(timestamp, 3)
573
self.assertEqual(timestamp_1ms, timestamp)
575
def assertBasisTreeKind(self, kind, tree, file_id):
576
basis = tree.basis_tree()
579
self.assertEqual(kind, basis.kind(file_id))
583
def test_commit_kind_changes(self):
584
if not osutils.has_symlinks():
585
raise tests.TestSkipped('Test requires symlink support')
586
tree = self.make_branch_and_tree('.')
587
os.symlink('target', 'name')
588
tree.add('name', 'a-file-id')
589
tree.commit('Added a symlink')
590
self.assertBasisTreeKind('symlink', tree, 'a-file-id')
593
self.build_tree(['name'])
594
tree.commit('Changed symlink to file')
595
self.assertBasisTreeKind('file', tree, 'a-file-id')
598
os.symlink('target', 'name')
599
tree.commit('file to symlink')
600
self.assertBasisTreeKind('symlink', tree, 'a-file-id')
604
tree.commit('symlink to directory')
605
self.assertBasisTreeKind('directory', tree, 'a-file-id')
608
os.symlink('target', 'name')
609
tree.commit('directory to symlink')
610
self.assertBasisTreeKind('symlink', tree, 'a-file-id')
612
# prepare for directory <-> file tests
615
tree.commit('symlink to directory')
616
self.assertBasisTreeKind('directory', tree, 'a-file-id')
619
self.build_tree(['name'])
620
tree.commit('Changed directory to file')
621
self.assertBasisTreeKind('file', tree, 'a-file-id')
625
tree.commit('file to directory')
626
self.assertBasisTreeKind('directory', tree, 'a-file-id')
628
def test_commit_unversioned_specified(self):
629
"""Commit should raise if specified files isn't in basis or worktree"""
630
tree = self.make_branch_and_tree('.')
631
self.assertRaises(errors.PathsNotVersionedError, tree.commit,
632
'message', specific_files=['bogus'])
634
class Callback(object):
636
def __init__(self, message, testcase):
638
self.message = message
639
self.testcase = testcase
641
def __call__(self, commit_obj):
643
self.testcase.assertTrue(isinstance(commit_obj, Commit))
646
def test_commit_callback(self):
647
"""Commit should invoke a callback to get the message"""
649
tree = self.make_branch_and_tree('.')
653
self.assertTrue(isinstance(e, BzrError))
654
self.assertEqual('The message or message_callback keyword'
655
' parameter is required for commit().', str(e))
657
self.fail('exception not raised')
658
cb = self.Callback(u'commit 1', self)
659
tree.commit(message_callback=cb)
660
self.assertTrue(cb.called)
661
repository = tree.branch.repository
662
message = repository.get_revision(tree.last_revision()).message
663
self.assertEqual('commit 1', message)
665
def test_no_callback_pointless(self):
666
"""Callback should not be invoked for pointless commit"""
667
tree = self.make_branch_and_tree('.')
668
cb = self.Callback(u'commit 2', self)
669
self.assertRaises(PointlessCommit, tree.commit, message_callback=cb,
670
allow_pointless=False)
671
self.assertFalse(cb.called)
673
def test_no_callback_netfailure(self):
674
"""Callback should not be invoked if connectivity fails"""
675
tree = self.make_branch_and_tree('.')
676
cb = self.Callback(u'commit 2', self)
677
repository = tree.branch.repository
678
# simulate network failure
679
def raise_(self, arg, arg2):
680
raise errors.NoSuchFile('foo')
681
repository.add_inventory = raise_
682
self.assertRaises(errors.NoSuchFile, tree.commit, message_callback=cb)
683
self.assertFalse(cb.called)
685
def test_selected_file_merge_commit(self):
686
"""Ensure the correct error is raised"""
687
tree = self.make_branch_and_tree('foo')
688
# pending merge would turn into a left parent
689
tree.commit('commit 1')
690
tree.add_parent_tree_id('example')
691
self.build_tree(['foo/bar', 'foo/baz'])
692
tree.add(['bar', 'baz'])
693
err = self.assertRaises(errors.CannotCommitSelectedFileMerge,
694
tree.commit, 'commit 2', specific_files=['bar', 'baz'])
695
self.assertEqual(['bar', 'baz'], err.files)
696
self.assertEqual('Selected-file commit of merges is not supported'
697
' yet: files bar, baz', str(err))