20
from bzrlib.selftest import TestCaseInTempDir
21
27
from bzrlib.branch import Branch
22
from bzrlib.commit import Commit
23
from bzrlib.errors import PointlessCommit, BzrError
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
26
37
# TODO: Test commit with some added, and added-but-missing files
28
class TestCommit(TestCaseInTempDir):
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):
30
76
def test_simple_commit(self):
31
77
"""Commit and check two versions of a single file."""
32
b = Branch.initialize('.')
78
wt = self.make_branch_and_tree('.')
33
80
file('hello', 'w').write('hello world')
35
b.commit(message='add hello')
36
file_id = b.working_tree().path2id('hello')
82
wt.commit(message='add hello')
83
file_id = wt.path2id('hello')
38
85
file('hello', 'w').write('version 2')
39
b.commit(message='commit 2')
86
wt.commit(message='commit 2')
41
88
eq = self.assertEquals
43
90
rh = b.revision_history()
44
rev = b.get_revision(rh[0])
91
rev = b.repository.get_revision(rh[0])
45
92
eq(rev.message, 'add hello')
47
tree1 = b.revision_tree(rh[0])
94
tree1 = b.repository.revision_tree(rh[0])
48
95
text = tree1.get_file_text(file_id)
49
96
eq(text, 'hello world')
51
tree2 = b.revision_tree(rh[1])
98
tree2 = b.repository.revision_tree(rh[1])
52
99
eq(tree2.get_file_text(file_id), 'version 2')
55
101
def test_delete_commit(self):
56
102
"""Test a commit with a deleted file"""
57
b = Branch.initialize('.')
103
wt = self.make_branch_and_tree('.')
58
105
file('hello', 'w').write('hello world')
59
b.add(['hello'], ['hello-id'])
60
b.commit(message='add hello')
106
wt.add(['hello'], ['hello-id'])
107
wt.commit(message='add hello')
62
109
os.remove('hello')
63
b.commit('removed hello', rev_id='rev2')
110
wt.commit('removed hello', rev_id='rev2')
65
tree = b.revision_tree('rev2')
112
tree = b.repository.revision_tree('rev2')
66
113
self.assertFalse(tree.has_id('hello-id'))
69
115
def test_pointless_commit(self):
70
116
"""Commit refuses unless there are changes or it's forced."""
71
b = Branch.initialize('.')
117
wt = self.make_branch_and_tree('.')
72
119
file('hello', 'w').write('hello')
74
b.commit(message='add hello')
121
wt.commit(message='add hello')
75
122
self.assertEquals(b.revno(), 1)
76
123
self.assertRaises(PointlessCommit,
79
126
allow_pointless=False)
80
127
self.assertEquals(b.revno(), 1)
84
129
def test_commit_empty(self):
85
130
"""Commiting an empty tree works."""
86
b = Branch.initialize('.')
87
b.commit(message='empty tree', allow_pointless=True)
131
wt = self.make_branch_and_tree('.')
133
wt.commit(message='empty tree', allow_pointless=True)
88
134
self.assertRaises(PointlessCommit,
90
136
message='empty tree',
91
137
allow_pointless=False)
92
b.commit(message='empty tree', allow_pointless=True)
138
wt.commit(message='empty tree', allow_pointless=True)
93
139
self.assertEquals(b.revno(), 2)
96
141
def test_selective_delete(self):
97
142
"""Selective commit in tree with deletions"""
98
b = Branch.initialize('.')
143
wt = self.make_branch_and_tree('.')
99
145
file('hello', 'w').write('hello')
100
146
file('buongia', 'w').write('buongia')
101
b.add(['hello', 'buongia'],
147
wt.add(['hello', 'buongia'],
102
148
['hello-id', 'buongia-id'])
103
b.commit(message='add files',
149
wt.commit(message='add files',
104
150
rev_id='test@rev-1')
106
152
os.remove('hello')
107
153
file('buongia', 'w').write('new text')
108
b.commit(message='update text',
154
wt.commit(message='update text',
109
155
specific_files=['buongia'],
110
156
allow_pointless=False,
111
157
rev_id='test@rev-2')
113
b.commit(message='remove hello',
159
wt.commit(message='remove hello',
114
160
specific_files=['hello'],
115
161
allow_pointless=False,
116
162
rev_id='test@rev-3')
147
193
ie = tree1.inventory['hello-id']
148
194
eq(ie.revision, 'test@rev-1')
150
tree2 = b.revision_tree('test@rev-2')
196
tree2 = b.repository.revision_tree('test@rev-2')
151
197
eq(tree2.id2path('hello-id'), 'fruity')
152
198
eq(tree2.get_file_text('hello-id'), 'contents of hello\n')
153
199
self.check_inventory_shape(tree2.inventory, ['fruity'])
154
200
ie = tree2.inventory['hello-id']
155
201
eq(ie.revision, 'test@rev-2')
158
203
def test_reused_rev_id(self):
159
204
"""Test that a revision id cannot be reused in a branch"""
160
b = Branch.initialize('.')
161
b.commit('initial', rev_id='test@rev-1', allow_pointless=True)
205
wt = self.make_branch_and_tree('.')
207
wt.commit('initial', rev_id='test@rev-1', allow_pointless=True)
162
208
self.assertRaises(Exception,
164
210
message='reused id',
165
211
rev_id='test@rev-1',
166
212
allow_pointless=True)
170
214
def test_commit_move(self):
171
215
"""Test commit of revisions with moved files and directories"""
172
216
eq = self.assertEquals
173
b = Branch.initialize('.')
217
wt = self.make_branch_and_tree('.')
174
219
r1 = 'test@rev-1'
175
220
self.build_tree(['hello', 'a/', 'b/'])
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')
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')
180
224
r2 = 'test@rev-2'
181
b.commit('two', rev_id=r2, allow_pointless=False)
182
self.check_inventory_shape(b.inventory,
183
['a', 'a/hello', 'b'])
225
wt.commit('two', rev_id=r2, allow_pointless=False)
228
self.check_inventory_shape(wt.read_working_inventory(),
229
['a', 'a/hello', 'b'])
186
234
r3 = 'test@rev-3'
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'])
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'])
193
b.move([os.sep.join(['a', 'hello'])],
194
os.sep.join(['a', 'b']))
245
wt.move(['a/hello'], 'a/b')
195
246
r4 = 'test@rev-4'
196
b.commit('four', rev_id=r4, allow_pointless=False)
197
self.check_inventory_shape(b.inventory,
198
['a', 'a/b/hello', 'a/b'])
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'])
200
inv = b.get_revision_inventory(r4)
255
inv = b.repository.get_revision_inventory(r4)
201
256
eq(inv['hello-id'].revision, r4)
202
257
eq(inv['a-id'].revision, r1)
203
258
eq(inv['b-id'].revision, r3)
206
260
def test_removed_commit(self):
207
"""Test a commit with a removed file"""
208
b = Branch.initialize('.')
261
"""Commit with a removed file"""
262
wt = self.make_branch_and_tree('.')
209
264
file('hello', 'w').write('hello world')
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')
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')
217
271
self.assertFalse(tree.has_id('hello-id'))
220
273
def test_committed_ancestry(self):
221
274
"""Test commit appends revisions to ancestry."""
222
b = Branch.initialize('.')
275
wt = self.make_branch_and_tree('.')
224
278
for i in range(4):
225
279
file('hello', 'w').write((str(i) * 4) + '\n')
227
b.add(['hello'], ['hello-id'])
281
wt.add(['hello'], ['hello-id'])
228
282
rev_id = 'test@rev-%d' % (i+1)
229
283
rev_ids.append(rev_id)
230
b.commit(message='rev %d' % (i+1),
284
wt.commit(message='rev %d' % (i+1),
232
286
eq = self.assertEquals
233
287
eq(b.revision_history(), rev_ids)
234
288
for i in range(4):
235
anc = b.get_ancestry(rev_ids[i])
289
anc = b.repository.get_ancestry(rev_ids[i])
236
290
eq(anc, [None] + rev_ids[:i+1])
292
def test_commit_new_subdir_child_selective(self):
293
wt = self.make_branch_and_tree('.')
295
self.build_tree(['dir/', 'dir/file1', 'dir/file2'])
296
wt.add(['dir', 'dir/file1', 'dir/file2'],
297
['dirid', 'file1id', 'file2id'])
298
wt.commit('dir/file1', specific_files=['dir/file1'], rev_id='1')
299
inv = b.repository.get_inventory('1')
300
self.assertEqual('1', inv['dirid'].revision)
301
self.assertEqual('1', inv['file1id'].revision)
302
# FIXME: This should raise a KeyError I think, rbc20051006
303
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,
360
self.assertEqual(Testament.from_revision(branch.repository,
361
'B').as_short_text(),
362
branch.repository.get_signature_text('B'))
364
bzrlib.gpg.GPGStrategy = oldstrategy
366
def test_commit_failed_signature(self):
368
import bzrlib.commit as commit
369
oldstrategy = bzrlib.gpg.GPGStrategy
370
wt = self.make_branch_and_tree('.')
372
wt.commit("base", allow_pointless=True, rev_id='A')
373
self.failIf(branch.repository.has_signature_for_revision_id('A'))
375
from bzrlib.testament import Testament
376
# monkey patch gpg signing mechanism
377
bzrlib.gpg.GPGStrategy = bzrlib.gpg.DisabledGPGStrategy
378
config = MustSignConfig(branch)
379
self.assertRaises(SigningFailed,
380
commit.Commit(config=config).commit,
382
allow_pointless=True,
385
branch = Branch.open(self.get_url('.'))
386
self.assertEqual(branch.revision_history(), ['A'])
387
self.failIf(branch.repository.has_revision('B'))
389
bzrlib.gpg.GPGStrategy = oldstrategy
391
def test_commit_invokes_hooks(self):
392
import bzrlib.commit as commit
393
wt = self.make_branch_and_tree('.')
396
def called(branch, rev_id):
397
calls.append('called')
398
bzrlib.ahook = called
400
config = BranchWithHooks(branch)
401
commit.Commit(config=config).commit(
403
allow_pointless=True,
404
rev_id='A', working_tree = wt)
405
self.assertEqual(['called', 'called'], calls)
409
def test_commit_object_doesnt_set_nick(self):
410
# using the Commit object directly does not set the branch nick.
411
wt = self.make_branch_and_tree('.')
413
c.commit(working_tree=wt, message='empty tree', allow_pointless=True)
414
self.assertEquals(wt.branch.revno(), 1)
416
wt.branch.repository.get_revision(
417
wt.branch.last_revision()).properties)
419
def test_safe_master_lock(self):
421
master = BzrDirMetaFormat1().initialize('master')
422
master.create_repository()
423
master_branch = master.create_branch()
424
master.create_workingtree()
425
bound = master.sprout('bound')
426
wt = bound.open_workingtree()
427
wt.branch.set_bound_location(os.path.realpath('master'))
429
orig_default = lockdir._DEFAULT_TIMEOUT_SECONDS
430
master_branch.lock_write()
432
lockdir._DEFAULT_TIMEOUT_SECONDS = 1
433
self.assertRaises(LockContention, wt.commit, 'silly')
435
lockdir._DEFAULT_TIMEOUT_SECONDS = orig_default
436
master_branch.unlock()
438
def test_commit_bound_merge(self):
439
# see bug #43959; commit of a merge in a bound branch fails to push
440
# the new commit into the master
441
master_branch = self.make_branch('master')
442
bound_tree = self.make_branch_and_tree('bound')
443
bound_tree.branch.bind(master_branch)
445
self.build_tree_contents([('bound/content_file', 'initial contents\n')])
446
bound_tree.add(['content_file'])
447
bound_tree.commit(message='woo!')
449
other_bzrdir = master_branch.bzrdir.sprout('other')
450
other_tree = other_bzrdir.open_workingtree()
452
# do a commit to the the other branch changing the content file so
453
# that our commit after merging will have a merged revision in the
454
# content file history.
455
self.build_tree_contents([('other/content_file', 'change in other\n')])
456
other_tree.commit('change in other')
458
# do a merge into the bound branch from other, and then change the
459
# content file locally to force a new revision (rather than using the
460
# revision from other). This forces extra processing in commit.
461
bound_tree.merge_from_branch(other_tree.branch)
462
self.build_tree_contents([('bound/content_file', 'change in bound\n')])
464
# before #34959 was fixed, this failed with 'revision not present in
465
# weave' when trying to implicitly push from the bound branch to the master
466
bound_tree.commit(message='commit of merge in bound tree')
468
def test_commit_reporting_after_merge(self):
469
# when doing a commit of a merge, the reporter needs to still
470
# be called for each item that is added/removed/deleted.
471
this_tree = self.make_branch_and_tree('this')
472
# we need a bunch of files and dirs, to perform one action on each.
475
'this/dirtoreparent/',
478
'this/filetoreparent',
495
this_tree.commit('create_files')
496
other_dir = this_tree.bzrdir.sprout('other')
497
other_tree = other_dir.open_workingtree()
498
other_tree.lock_write()
499
# perform the needed actions on the files and dirs.
501
other_tree.rename_one('dirtorename', 'renameddir')
502
other_tree.rename_one('dirtoreparent', 'renameddir/reparenteddir')
503
other_tree.rename_one('filetorename', 'renamedfile')
504
other_tree.rename_one('filetoreparent', 'renameddir/reparentedfile')
505
other_tree.remove(['dirtoremove', 'filetoremove'])
506
self.build_tree_contents([
508
('other/filetomodify', 'new content'),
509
('other/newfile', 'new file content')])
510
other_tree.add('newfile')
511
other_tree.add('newdir/')
512
other_tree.commit('modify all sample files and dirs.')
515
this_tree.merge_from_branch(other_tree.branch)
516
reporter = CapturingReporter()
517
this_tree.commit('do the commit', reporter=reporter)
519
('change', 'unchanged', ''),
520
('change', 'unchanged', 'dirtoleave'),
521
('change', 'unchanged', 'filetoleave'),
522
('change', 'modified', 'filetomodify'),
523
('change', 'added', 'newdir'),
524
('change', 'added', 'newfile'),
525
('renamed', 'renamed', 'dirtorename', 'renameddir'),
526
('renamed', 'renamed', 'dirtoreparent', 'renameddir/reparenteddir'),
527
('renamed', 'renamed', 'filetoreparent', 'renameddir/reparentedfile'),
528
('renamed', 'renamed', 'filetorename', 'renamedfile'),
529
('deleted', 'dirtoremove'),
530
('deleted', 'filetoremove'),
534
def test_commit_removals_respects_filespec(self):
535
"""Commit respects the specified_files for removals."""
536
tree = self.make_branch_and_tree('.')
537
self.build_tree(['a', 'b'])
539
tree.commit('added a, b')
540
tree.remove(['a', 'b'])
541
tree.commit('removed a', specific_files='a')
542
basis = tree.basis_tree()
545
self.assertIs(None, basis.path2id('a'))
546
self.assertFalse(basis.path2id('b') is None)
550
def test_commit_saves_1ms_timestamp(self):
551
"""Passing in a timestamp is saved with 1ms resolution"""
552
tree = self.make_branch_and_tree('.')
553
self.build_tree(['a'])
555
tree.commit('added a', timestamp=1153248633.4186721, timezone=0,
558
rev = tree.branch.repository.get_revision('a1')
559
self.assertEqual(1153248633.419, rev.timestamp)
561
def test_commit_has_1ms_resolution(self):
562
"""Allowing commit to generate the timestamp also has 1ms resolution"""
563
tree = self.make_branch_and_tree('.')
564
self.build_tree(['a'])
566
tree.commit('added a', rev_id='a1')
568
rev = tree.branch.repository.get_revision('a1')
569
timestamp = rev.timestamp
570
timestamp_1ms = round(timestamp, 3)
571
self.assertEqual(timestamp_1ms, timestamp)
573
def assertBasisTreeKind(self, kind, tree, file_id):
574
basis = tree.basis_tree()
577
self.assertEqual(kind, basis.kind(file_id))
581
def test_commit_kind_changes(self):
582
if not osutils.has_symlinks():
583
raise tests.TestSkipped('Test requires symlink support')
584
tree = self.make_branch_and_tree('.')
585
os.symlink('target', 'name')
586
tree.add('name', 'a-file-id')
587
tree.commit('Added a symlink')
588
self.assertBasisTreeKind('symlink', tree, 'a-file-id')
591
self.build_tree(['name'])
592
tree.commit('Changed symlink to file')
593
self.assertBasisTreeKind('file', tree, 'a-file-id')
596
os.symlink('target', 'name')
597
tree.commit('file to symlink')
598
self.assertBasisTreeKind('symlink', tree, 'a-file-id')
602
tree.commit('symlink to directory')
603
self.assertBasisTreeKind('directory', tree, 'a-file-id')
606
os.symlink('target', 'name')
607
tree.commit('directory to symlink')
608
self.assertBasisTreeKind('symlink', tree, 'a-file-id')
610
# prepare for directory <-> file tests
613
tree.commit('symlink to directory')
614
self.assertBasisTreeKind('directory', tree, 'a-file-id')
617
self.build_tree(['name'])
618
tree.commit('Changed directory to file')
619
self.assertBasisTreeKind('file', tree, 'a-file-id')
623
tree.commit('file to directory')
624
self.assertBasisTreeKind('directory', tree, 'a-file-id')
626
def test_commit_unversioned_specified(self):
627
"""Commit should raise if specified files isn't in basis or worktree"""
628
tree = self.make_branch_and_tree('.')
629
self.assertRaises(errors.PathsNotVersionedError, tree.commit,
630
'message', specific_files=['bogus'])
632
class Callback(object):
634
def __init__(self, message, testcase):
636
self.message = message
637
self.testcase = testcase
639
def __call__(self, commit_obj):
641
self.testcase.assertTrue(isinstance(commit_obj, Commit))
644
def test_commit_callback(self):
645
"""Commit should invoke a callback to get the message"""
647
tree = self.make_branch_and_tree('.')
651
self.assertTrue(isinstance(e, BzrError))
652
self.assertEqual('The message or message_callback keyword'
653
' parameter is required for commit().', str(e))
655
self.fail('exception not raised')
656
cb = self.Callback(u'commit 1', self)
657
tree.commit(message_callback=cb)
658
self.assertTrue(cb.called)
659
repository = tree.branch.repository
660
message = repository.get_revision(tree.last_revision()).message
661
self.assertEqual('commit 1', message)
663
def test_no_callback_pointless(self):
664
"""Callback should not be invoked for pointless commit"""
665
tree = self.make_branch_and_tree('.')
666
cb = self.Callback(u'commit 2', self)
667
self.assertRaises(PointlessCommit, tree.commit, message_callback=cb,
668
allow_pointless=False)
669
self.assertFalse(cb.called)
671
def test_no_callback_netfailure(self):
672
"""Callback should not be invoked if connectivity fails"""
673
tree = self.make_branch_and_tree('.')
674
cb = self.Callback(u'commit 2', self)
675
repository = tree.branch.repository
676
# simulate network failure
677
def raise_(self, arg, arg2):
678
raise errors.NoSuchFile('foo')
679
repository.add_inventory = raise_
680
self.assertRaises(errors.NoSuchFile, tree.commit, message_callback=cb)
681
self.assertFalse(cb.called)