43
36
return ['cat', '-']
46
class BranchWithHooks(BranchConfig):
48
def post_commit(self):
49
return "bzrlib.ahook bzrlib.ahook"
52
class CapturingReporter(NullCommitReporter):
53
"""This reporter captures the calls made to it for evaluation later."""
56
# a list of the calls this received
59
def snapshot_change(self, change, path):
60
self.calls.append(('change', change, path))
62
def deleted(self, file_id):
63
self.calls.append(('deleted', file_id))
65
def missing(self, path):
66
self.calls.append(('missing', path))
68
def renamed(self, change, old_path, new_path):
69
self.calls.append(('renamed', change, old_path, new_path))
72
class TestCommit(TestCaseWithTransport):
39
class TestCommit(TestCaseInTempDir):
74
41
def test_simple_commit(self):
75
42
"""Commit and check two versions of a single file."""
76
wt = self.make_branch_and_tree('.')
43
b = Branch.initialize('.')
78
44
file('hello', 'w').write('hello world')
80
wt.commit(message='add hello')
81
file_id = wt.path2id('hello')
46
b.commit(message='add hello')
47
file_id = b.working_tree().path2id('hello')
83
49
file('hello', 'w').write('version 2')
84
wt.commit(message='commit 2')
50
b.commit(message='commit 2')
86
52
eq = self.assertEquals
88
54
rh = b.revision_history()
89
rev = b.repository.get_revision(rh[0])
55
rev = b.get_revision(rh[0])
90
56
eq(rev.message, 'add hello')
92
tree1 = b.repository.revision_tree(rh[0])
58
tree1 = b.revision_tree(rh[0])
93
59
text = tree1.get_file_text(file_id)
94
60
eq(text, 'hello world')
96
tree2 = b.repository.revision_tree(rh[1])
62
tree2 = b.revision_tree(rh[1])
97
63
eq(tree2.get_file_text(file_id), 'version 2')
99
65
def test_delete_commit(self):
100
66
"""Test a commit with a deleted file"""
101
wt = self.make_branch_and_tree('.')
67
b = Branch.initialize('.')
103
68
file('hello', 'w').write('hello world')
104
wt.add(['hello'], ['hello-id'])
105
wt.commit(message='add hello')
69
b.add(['hello'], ['hello-id'])
70
b.commit(message='add hello')
107
72
os.remove('hello')
108
wt.commit('removed hello', rev_id='rev2')
73
b.commit('removed hello', rev_id='rev2')
110
tree = b.repository.revision_tree('rev2')
75
tree = b.revision_tree('rev2')
111
76
self.assertFalse(tree.has_id('hello-id'))
113
79
def test_pointless_commit(self):
114
80
"""Commit refuses unless there are changes or it's forced."""
115
wt = self.make_branch_and_tree('.')
81
b = Branch.initialize('.')
117
82
file('hello', 'w').write('hello')
119
wt.commit(message='add hello')
84
b.commit(message='add hello')
120
85
self.assertEquals(b.revno(), 1)
121
86
self.assertRaises(PointlessCommit,
124
89
allow_pointless=False)
125
90
self.assertEquals(b.revno(), 1)
127
94
def test_commit_empty(self):
128
95
"""Commiting an empty tree works."""
129
wt = self.make_branch_and_tree('.')
131
wt.commit(message='empty tree', allow_pointless=True)
96
b = Branch.initialize('.')
97
b.commit(message='empty tree', allow_pointless=True)
132
98
self.assertRaises(PointlessCommit,
134
100
message='empty tree',
135
101
allow_pointless=False)
136
wt.commit(message='empty tree', allow_pointless=True)
102
b.commit(message='empty tree', allow_pointless=True)
137
103
self.assertEquals(b.revno(), 2)
139
106
def test_selective_delete(self):
140
107
"""Selective commit in tree with deletions"""
141
wt = self.make_branch_and_tree('.')
108
b = Branch.initialize('.')
143
109
file('hello', 'w').write('hello')
144
110
file('buongia', 'w').write('buongia')
145
wt.add(['hello', 'buongia'],
111
b.add(['hello', 'buongia'],
146
112
['hello-id', 'buongia-id'])
147
wt.commit(message='add files',
113
b.commit(message='add files',
148
114
rev_id='test@rev-1')
150
116
os.remove('hello')
151
117
file('buongia', 'w').write('new text')
152
wt.commit(message='update text',
118
b.commit(message='update text',
153
119
specific_files=['buongia'],
154
120
allow_pointless=False,
155
121
rev_id='test@rev-2')
157
wt.commit(message='remove hello',
123
b.commit(message='remove hello',
158
124
specific_files=['hello'],
159
125
allow_pointless=False,
160
126
rev_id='test@rev-3')
191
157
ie = tree1.inventory['hello-id']
192
158
eq(ie.revision, 'test@rev-1')
194
tree2 = b.repository.revision_tree('test@rev-2')
160
tree2 = b.revision_tree('test@rev-2')
195
161
eq(tree2.id2path('hello-id'), 'fruity')
196
162
eq(tree2.get_file_text('hello-id'), 'contents of hello\n')
197
163
self.check_inventory_shape(tree2.inventory, ['fruity'])
198
164
ie = tree2.inventory['hello-id']
199
165
eq(ie.revision, 'test@rev-2')
201
168
def test_reused_rev_id(self):
202
169
"""Test that a revision id cannot be reused in a branch"""
203
wt = self.make_branch_and_tree('.')
205
wt.commit('initial', rev_id='test@rev-1', allow_pointless=True)
170
b = Branch.initialize('.')
171
b.commit('initial', rev_id='test@rev-1', allow_pointless=True)
206
172
self.assertRaises(Exception,
208
174
message='reused id',
209
175
rev_id='test@rev-1',
210
176
allow_pointless=True)
212
180
def test_commit_move(self):
213
181
"""Test commit of revisions with moved files and directories"""
214
182
eq = self.assertEquals
215
wt = self.make_branch_and_tree('.')
183
b = Branch.initialize('.')
217
184
r1 = 'test@rev-1'
218
185
self.build_tree(['hello', 'a/', 'b/'])
219
wt.add(['hello', 'a', 'b'], ['hello-id', 'a-id', 'b-id'])
220
wt.commit('initial', rev_id=r1, allow_pointless=False)
221
wt.move(['hello'], 'a')
186
b.add(['hello', 'a', 'b'], ['hello-id', 'a-id', 'b-id'])
187
b.commit('initial', rev_id=r1, allow_pointless=False)
189
b.move(['hello'], 'a')
222
190
r2 = 'test@rev-2'
223
wt.commit('two', rev_id=r2, allow_pointless=False)
224
self.check_inventory_shape(wt.read_working_inventory(),
191
b.commit('two', rev_id=r2, allow_pointless=False)
192
self.check_inventory_shape(b.inventory,
225
193
['a', 'a/hello', 'b'])
228
196
r3 = 'test@rev-3'
229
wt.commit('three', rev_id=r3, allow_pointless=False)
230
self.check_inventory_shape(wt.read_working_inventory(),
197
b.commit('three', rev_id=r3, allow_pointless=False)
198
self.check_inventory_shape(b.inventory,
231
199
['a', 'a/hello', 'a/b'])
232
self.check_inventory_shape(b.repository.get_revision_inventory(r3),
200
self.check_inventory_shape(b.get_revision_inventory(r3),
233
201
['a', 'a/hello', 'a/b'])
235
wt.move(['a/hello'], 'a/b')
203
b.move([os.sep.join(['a', 'hello'])],
204
os.sep.join(['a', 'b']))
236
205
r4 = 'test@rev-4'
237
wt.commit('four', rev_id=r4, allow_pointless=False)
238
self.check_inventory_shape(wt.read_working_inventory(),
206
b.commit('four', rev_id=r4, allow_pointless=False)
207
self.check_inventory_shape(b.inventory,
239
208
['a', 'a/b/hello', 'a/b'])
241
inv = b.repository.get_revision_inventory(r4)
210
inv = b.get_revision_inventory(r4)
242
211
eq(inv['hello-id'].revision, r4)
243
212
eq(inv['a-id'].revision, r1)
244
213
eq(inv['b-id'].revision, r3)
246
216
def test_removed_commit(self):
247
217
"""Commit with a removed file"""
248
wt = self.make_branch_and_tree('.')
218
b = Branch.initialize('.')
219
wt = b.working_tree()
250
220
file('hello', 'w').write('hello world')
251
wt.add(['hello'], ['hello-id'])
252
wt.commit(message='add hello')
221
b.add(['hello'], ['hello-id'])
222
b.commit(message='add hello')
224
wt = b.working_tree() # FIXME: kludge for aliasing of working inventory
253
225
wt.remove('hello')
254
wt.commit('removed hello', rev_id='rev2')
226
b.commit('removed hello', rev_id='rev2')
256
tree = b.repository.revision_tree('rev2')
228
tree = b.revision_tree('rev2')
257
229
self.assertFalse(tree.has_id('hello-id'))
259
232
def test_committed_ancestry(self):
260
233
"""Test commit appends revisions to ancestry."""
261
wt = self.make_branch_and_tree('.')
234
b = Branch.initialize('.')
264
236
for i in range(4):
265
237
file('hello', 'w').write((str(i) * 4) + '\n')
267
wt.add(['hello'], ['hello-id'])
239
b.add(['hello'], ['hello-id'])
268
240
rev_id = 'test@rev-%d' % (i+1)
269
241
rev_ids.append(rev_id)
270
wt.commit(message='rev %d' % (i+1),
242
b.commit(message='rev %d' % (i+1),
272
244
eq = self.assertEquals
273
245
eq(b.revision_history(), rev_ids)
274
246
for i in range(4):
275
anc = b.repository.get_ancestry(rev_ids[i])
247
anc = b.get_ancestry(rev_ids[i])
276
248
eq(anc, [None] + rev_ids[:i+1])
278
250
def test_commit_new_subdir_child_selective(self):
279
wt = self.make_branch_and_tree('.')
251
b = Branch.initialize('.')
281
252
self.build_tree(['dir/', 'dir/file1', 'dir/file2'])
282
wt.add(['dir', 'dir/file1', 'dir/file2'],
253
b.add(['dir', 'dir/file1', 'dir/file2'],
283
254
['dirid', 'file1id', 'file2id'])
284
wt.commit('dir/file1', specific_files=['dir/file1'], rev_id='1')
285
inv = b.repository.get_inventory('1')
255
b.commit('dir/file1', specific_files=['dir/file1'], rev_id='1')
256
inv = b.get_inventory('1')
286
257
self.assertEqual('1', inv['dirid'].revision)
287
258
self.assertEqual('1', inv['file1id'].revision)
288
259
# FIXME: This should raise a KeyError I think, rbc20051006
291
262
def test_strict_commit(self):
292
263
"""Try and commit with unknown files and strict = True, should fail."""
293
264
from bzrlib.errors import StrictCommitFailed
294
wt = self.make_branch_and_tree('.')
265
b = Branch.initialize('.')
296
266
file('hello', 'w').write('hello world')
298
268
file('goodbye', 'w').write('goodbye cruel world!')
299
self.assertRaises(StrictCommitFailed, wt.commit,
269
self.assertRaises(StrictCommitFailed, b.commit,
300
270
message='add hello but not goodbye', strict=True)
302
def test_strict_commit_without_unknowns(self):
303
"""Try and commit with no unknown files and strict = True,
305
from bzrlib.errors import StrictCommitFailed
306
wt = self.make_branch_and_tree('.')
308
file('hello', 'w').write('hello world')
310
wt.commit(message='add hello', strict=True)
312
272
def test_nonstrict_commit(self):
313
273
"""Try and commit with unknown files and strict = False, should work."""
314
wt = self.make_branch_and_tree('.')
274
b = Branch.initialize('.')
316
275
file('hello', 'w').write('hello world')
318
277
file('goodbye', 'w').write('goodbye cruel world!')
319
wt.commit(message='add hello but not goodbye', strict=False)
321
def test_nonstrict_commit_without_unknowns(self):
322
"""Try and commit with no unknown files and strict = False,
324
wt = self.make_branch_and_tree('.')
326
file('hello', 'w').write('hello world')
328
wt.commit(message='add hello', strict=False)
278
b.commit(message='add hello but not goodbye', strict=False)
330
280
def test_signed_commit(self):
331
281
import bzrlib.gpg
332
282
import bzrlib.commit as commit
333
283
oldstrategy = bzrlib.gpg.GPGStrategy
334
wt = self.make_branch_and_tree('.')
336
wt.commit("base", allow_pointless=True, rev_id='A')
337
self.failIf(branch.repository.has_signature_for_revision_id('A'))
284
branch = Branch.initialize('.')
285
branch.commit("base", allow_pointless=True, rev_id='A')
286
self.failIf(branch.revision_store.has_id('A', 'sig'))
339
288
from bzrlib.testament import Testament
340
289
# monkey patch gpg signing mechanism
341
290
bzrlib.gpg.GPGStrategy = bzrlib.gpg.LoopbackGPGStrategy
342
commit.Commit(config=MustSignConfig(branch)).commit(message="base",
291
commit.Commit(config=MustSignConfig(branch)).commit(branch, "base",
343
292
allow_pointless=True,
346
self.assertEqual(Testament.from_revision(branch.repository,
347
'B').as_short_text(),
348
branch.repository.get_signature_text('B'))
294
self.assertEqual(Testament.from_revision(branch,'B').as_short_text(),
295
branch.revision_store.get('B', 'sig').read())
350
297
bzrlib.gpg.GPGStrategy = oldstrategy
364
310
config = MustSignConfig(branch)
365
311
self.assertRaises(SigningFailed,
366
312
commit.Commit(config=config).commit,
368
314
allow_pointless=True,
371
branch = Branch.open(self.get_url('.'))
316
branch = Branch.open('.')
372
317
self.assertEqual(branch.revision_history(), ['A'])
373
self.failIf(branch.repository.has_revision('B'))
318
self.failIf(branch.revision_store.has_id('B'))
375
320
bzrlib.gpg.GPGStrategy = oldstrategy
377
def test_commit_invokes_hooks(self):
378
import bzrlib.commit as commit
379
wt = self.make_branch_and_tree('.')
382
def called(branch, rev_id):
383
calls.append('called')
384
bzrlib.ahook = called
386
config = BranchWithHooks(branch)
387
commit.Commit(config=config).commit(
389
allow_pointless=True,
390
rev_id='A', working_tree = wt)
391
self.assertEqual(['called', 'called'], calls)
395
def test_commit_object_doesnt_set_nick(self):
396
# using the Commit object directly does not set the branch nick.
397
wt = self.make_branch_and_tree('.')
399
c.commit(working_tree=wt, message='empty tree', allow_pointless=True)
400
self.assertEquals(wt.branch.revno(), 1)
402
wt.branch.repository.get_revision(
403
wt.branch.last_revision()).properties)
405
def test_safe_master_lock(self):
407
master = BzrDirMetaFormat1().initialize('master')
408
master.create_repository()
409
master_branch = master.create_branch()
410
master.create_workingtree()
411
bound = master.sprout('bound')
412
wt = bound.open_workingtree()
413
wt.branch.set_bound_location(os.path.realpath('master'))
415
orig_default = lockdir._DEFAULT_TIMEOUT_SECONDS
416
master_branch.lock_write()
418
lockdir._DEFAULT_TIMEOUT_SECONDS = 1
419
self.assertRaises(LockContention, wt.commit, 'silly')
421
lockdir._DEFAULT_TIMEOUT_SECONDS = orig_default
422
master_branch.unlock()
424
def test_commit_bound_merge(self):
425
# see bug #43959; commit of a merge in a bound branch fails to push
426
# the new commit into the master
427
master_branch = self.make_branch('master')
428
bound_tree = self.make_branch_and_tree('bound')
429
bound_tree.branch.bind(master_branch)
431
self.build_tree_contents([('bound/content_file', 'initial contents\n')])
432
bound_tree.add(['content_file'])
433
bound_tree.commit(message='woo!')
435
other_bzrdir = master_branch.bzrdir.sprout('other')
436
other_tree = other_bzrdir.open_workingtree()
438
# do a commit to the the other branch changing the content file so
439
# that our commit after merging will have a merged revision in the
440
# content file history.
441
self.build_tree_contents([('other/content_file', 'change in other\n')])
442
other_tree.commit('change in other')
444
# do a merge into the bound branch from other, and then change the
445
# content file locally to force a new revision (rather than using the
446
# revision from other). This forces extra processing in commit.
447
bound_tree.merge_from_branch(other_tree.branch)
448
self.build_tree_contents([('bound/content_file', 'change in bound\n')])
450
# before #34959 was fixed, this failed with 'revision not present in
451
# weave' when trying to implicitly push from the bound branch to the master
452
bound_tree.commit(message='commit of merge in bound tree')
454
def test_commit_reporting_after_merge(self):
455
# when doing a commit of a merge, the reporter needs to still
456
# be called for each item that is added/removed/deleted.
457
this_tree = self.make_branch_and_tree('this')
458
# we need a bunch of files and dirs, to perform one action on each.
461
'this/dirtoreparent/',
464
'this/filetoreparent',
481
this_tree.commit('create_files')
482
other_dir = this_tree.bzrdir.sprout('other')
483
other_tree = other_dir.open_workingtree()
484
other_tree.lock_write()
485
# perform the needed actions on the files and dirs.
487
other_tree.rename_one('dirtorename', 'renameddir')
488
other_tree.rename_one('dirtoreparent', 'renameddir/reparenteddir')
489
other_tree.rename_one('filetorename', 'renamedfile')
490
other_tree.rename_one('filetoreparent', 'renameddir/reparentedfile')
491
other_tree.remove(['dirtoremove', 'filetoremove'])
492
self.build_tree_contents([
494
('other/filetomodify', 'new content'),
495
('other/newfile', 'new file content')])
496
other_tree.add('newfile')
497
other_tree.add('newdir/')
498
other_tree.commit('modify all sample files and dirs.')
501
this_tree.merge_from_branch(other_tree.branch)
502
reporter = CapturingReporter()
503
this_tree.commit('do the commit', reporter=reporter)
505
('change', 'unchanged', ''),
506
('change', 'unchanged', 'dirtoleave'),
507
('change', 'unchanged', 'filetoleave'),
508
('change', 'modified', 'filetomodify'),
509
('change', 'added', 'newdir'),
510
('change', 'added', 'newfile'),
511
('renamed', 'renamed', 'dirtorename', 'renameddir'),
512
('renamed', 'renamed', 'dirtoreparent', 'renameddir/reparenteddir'),
513
('renamed', 'renamed', 'filetoreparent', 'renameddir/reparentedfile'),
514
('renamed', 'renamed', 'filetorename', 'renamedfile'),
515
('deleted', 'dirtoremove'),
516
('deleted', 'filetoremove'),
520
def test_commit_removals_respects_filespec(self):
521
"""Commit respects the specified_files for removals."""
522
tree = self.make_branch_and_tree('.')
523
self.build_tree(['a', 'b'])
525
tree.commit('added a, b')
526
tree.remove(['a', 'b'])
527
tree.commit('removed a', specific_files='a')
528
basis = tree.basis_tree().inventory
529
self.assertIs(None, basis.path2id('a'))
530
self.assertFalse(basis.path2id('b') is None)
532
def test_commit_saves_1ms_timestamp(self):
533
"""Passing in a timestamp is saved with 1ms resolution"""
534
tree = self.make_branch_and_tree('.')
535
self.build_tree(['a'])
537
tree.commit('added a', timestamp=1153248633.4186721, timezone=0,
540
rev = tree.branch.repository.get_revision('a1')
541
self.assertEqual(1153248633.419, rev.timestamp)
543
def test_commit_has_1ms_resolution(self):
544
"""Allowing commit to generate the timestamp also has 1ms resolution"""
545
tree = self.make_branch_and_tree('.')
546
self.build_tree(['a'])
548
tree.commit('added a', rev_id='a1')
550
rev = tree.branch.repository.get_revision('a1')
551
timestamp = rev.timestamp
552
timestamp_1ms = round(timestamp, 3)
553
self.assertEqual(timestamp_1ms, timestamp)
555
def test_commit_unversioned_specified(self):
556
"""Commit should raise if specified files isn't in basis or worktree"""
557
tree = self.make_branch_and_tree('.')
558
self.assertRaises(errors.PathsNotVersionedError, tree.commit,
559
'message', specific_files=['bogus'])
561
class Callback(object):
563
def __init__(self, message, testcase):
565
self.message = message
566
self.testcase = testcase
568
def __call__(self, commit_obj):
570
self.testcase.assertTrue(isinstance(commit_obj, Commit))
573
def test_commit_callback(self):
574
"""Commit should invoke a callback to get the message"""
576
tree = self.make_branch_and_tree('.')
580
self.assertTrue(isinstance(e, BzrError))
581
self.assertEqual('The message or message_callback keyword'
582
' parameter is required for commit().', str(e))
584
self.fail('exception not raised')
585
cb = self.Callback(u'commit 1', self)
586
tree.commit(message_callback=cb)
587
self.assertTrue(cb.called)
588
repository = tree.branch.repository
589
message = repository.get_revision(tree.last_revision()).message
590
self.assertEqual('commit 1', message)
592
def test_no_callback_pointless(self):
593
"""Callback should not be invoked for pointless commit"""
594
tree = self.make_branch_and_tree('.')
595
cb = self.Callback(u'commit 2', self)
596
self.assertRaises(PointlessCommit, tree.commit, message_callback=cb,
597
allow_pointless=False)
598
self.assertFalse(cb.called)
600
def test_no_callback_netfailure(self):
601
"""Callback should not be invoked if connectivity fails"""
602
tree = self.make_branch_and_tree('.')
603
cb = self.Callback(u'commit 2', self)
604
repository = tree.branch.repository
605
# simulate network failure
606
def raise_(self, arg, arg2):
607
raise errors.NoSuchFile('foo')
608
repository.add_inventory = raise_
609
self.assertRaises(errors.NoSuchFile, tree.commit, message_callback=cb)
610
self.assertFalse(cb.called)