20
from bzrlib.selftest import TestCaseInTempDir
21
from bzrlib.tests import TestCaseWithTransport
21
22
from bzrlib.branch import Branch
22
from bzrlib.commit import Commit
23
from bzrlib.errors import PointlessCommit, BzrError
23
from bzrlib.bzrdir import BzrDir, BzrDirMetaFormat1
24
from bzrlib.workingtree import WorkingTree
25
from bzrlib.commit import Commit, NullCommitReporter
26
from bzrlib.config import BranchConfig
27
from bzrlib.errors import (PointlessCommit, BzrError, SigningFailed,
26
31
# TODO: Test commit with some added, and added-but-missing files
28
class TestCommit(TestCaseInTempDir):
33
class MustSignConfig(BranchConfig):
35
def signature_needed(self):
38
def gpg_signing_command(self):
42
class BranchWithHooks(BranchConfig):
44
def post_commit(self):
45
return "bzrlib.ahook bzrlib.ahook"
48
class CapturingReporter(NullCommitReporter):
49
"""This reporter captures the calls made to it for evaluation later."""
52
# a list of the calls this received
55
def snapshot_change(self, change, path):
56
self.calls.append(('change', change, path))
58
def deleted(self, file_id):
59
self.calls.append(('deleted', file_id))
61
def missing(self, path):
62
self.calls.append(('missing', path))
64
def renamed(self, change, old_path, new_path):
65
self.calls.append(('renamed', change, old_path, new_path))
68
class TestCommit(TestCaseWithTransport):
30
70
def test_simple_commit(self):
31
71
"""Commit and check two versions of a single file."""
32
b = Branch.initialize('.')
72
wt = self.make_branch_and_tree('.')
33
74
file('hello', 'w').write('hello world')
35
b.commit(message='add hello')
36
file_id = b.working_tree().path2id('hello')
76
wt.commit(message='add hello')
77
file_id = wt.path2id('hello')
38
79
file('hello', 'w').write('version 2')
39
b.commit(message='commit 2')
80
wt.commit(message='commit 2')
41
82
eq = self.assertEquals
43
84
rh = b.revision_history()
44
rev = b.get_revision(rh[0])
85
rev = b.repository.get_revision(rh[0])
45
86
eq(rev.message, 'add hello')
47
tree1 = b.revision_tree(rh[0])
88
tree1 = b.repository.revision_tree(rh[0])
48
89
text = tree1.get_file_text(file_id)
49
90
eq(text, 'hello world')
51
tree2 = b.revision_tree(rh[1])
92
tree2 = b.repository.revision_tree(rh[1])
52
93
eq(tree2.get_file_text(file_id), 'version 2')
55
95
def test_delete_commit(self):
56
96
"""Test a commit with a deleted file"""
57
b = Branch.initialize('.')
97
wt = self.make_branch_and_tree('.')
58
99
file('hello', 'w').write('hello world')
59
b.add(['hello'], ['hello-id'])
60
b.commit(message='add hello')
100
wt.add(['hello'], ['hello-id'])
101
wt.commit(message='add hello')
62
103
os.remove('hello')
63
b.commit('removed hello', rev_id='rev2')
104
wt.commit('removed hello', rev_id='rev2')
65
tree = b.revision_tree('rev2')
106
tree = b.repository.revision_tree('rev2')
66
107
self.assertFalse(tree.has_id('hello-id'))
69
109
def test_pointless_commit(self):
70
110
"""Commit refuses unless there are changes or it's forced."""
71
b = Branch.initialize('.')
111
wt = self.make_branch_and_tree('.')
72
113
file('hello', 'w').write('hello')
74
b.commit(message='add hello')
115
wt.commit(message='add hello')
75
116
self.assertEquals(b.revno(), 1)
76
117
self.assertRaises(PointlessCommit,
79
120
allow_pointless=False)
80
121
self.assertEquals(b.revno(), 1)
84
123
def test_commit_empty(self):
85
124
"""Commiting an empty tree works."""
86
b = Branch.initialize('.')
87
b.commit(message='empty tree', allow_pointless=True)
125
wt = self.make_branch_and_tree('.')
127
wt.commit(message='empty tree', allow_pointless=True)
88
128
self.assertRaises(PointlessCommit,
90
130
message='empty tree',
91
131
allow_pointless=False)
92
b.commit(message='empty tree', allow_pointless=True)
132
wt.commit(message='empty tree', allow_pointless=True)
93
133
self.assertEquals(b.revno(), 2)
96
135
def test_selective_delete(self):
97
136
"""Selective commit in tree with deletions"""
98
b = Branch.initialize('.')
137
wt = self.make_branch_and_tree('.')
99
139
file('hello', 'w').write('hello')
100
140
file('buongia', 'w').write('buongia')
101
b.add(['hello', 'buongia'],
141
wt.add(['hello', 'buongia'],
102
142
['hello-id', 'buongia-id'])
103
b.commit(message='add files',
143
wt.commit(message='add files',
104
144
rev_id='test@rev-1')
106
146
os.remove('hello')
107
147
file('buongia', 'w').write('new text')
108
b.commit(message='update text',
148
wt.commit(message='update text',
109
149
specific_files=['buongia'],
110
150
allow_pointless=False,
111
151
rev_id='test@rev-2')
113
b.commit(message='remove hello',
153
wt.commit(message='remove hello',
114
154
specific_files=['hello'],
115
155
allow_pointless=False,
116
156
rev_id='test@rev-3')
147
187
ie = tree1.inventory['hello-id']
148
188
eq(ie.revision, 'test@rev-1')
150
tree2 = b.revision_tree('test@rev-2')
190
tree2 = b.repository.revision_tree('test@rev-2')
151
191
eq(tree2.id2path('hello-id'), 'fruity')
152
192
eq(tree2.get_file_text('hello-id'), 'contents of hello\n')
153
193
self.check_inventory_shape(tree2.inventory, ['fruity'])
154
194
ie = tree2.inventory['hello-id']
155
195
eq(ie.revision, 'test@rev-2')
158
197
def test_reused_rev_id(self):
159
198
"""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)
199
wt = self.make_branch_and_tree('.')
201
wt.commit('initial', rev_id='test@rev-1', allow_pointless=True)
162
202
self.assertRaises(Exception,
164
204
message='reused id',
165
205
rev_id='test@rev-1',
166
206
allow_pointless=True)
170
208
def test_commit_move(self):
171
209
"""Test commit of revisions with moved files and directories"""
172
210
eq = self.assertEquals
173
b = Branch.initialize('.')
211
wt = self.make_branch_and_tree('.')
174
213
r1 = 'test@rev-1'
175
214
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')
215
wt.add(['hello', 'a', 'b'], ['hello-id', 'a-id', 'b-id'])
216
wt.commit('initial', rev_id=r1, allow_pointless=False)
217
wt.move(['hello'], 'a')
180
218
r2 = 'test@rev-2'
181
b.commit('two', rev_id=r2, allow_pointless=False)
182
self.check_inventory_shape(b.inventory,
219
wt.commit('two', rev_id=r2, allow_pointless=False)
220
self.check_inventory_shape(wt.read_working_inventory(),
183
221
['a', 'a/hello', 'b'])
186
224
r3 = 'test@rev-3'
187
b.commit('three', rev_id=r3, allow_pointless=False)
188
self.check_inventory_shape(b.inventory,
225
wt.commit('three', rev_id=r3, allow_pointless=False)
226
self.check_inventory_shape(wt.read_working_inventory(),
189
227
['a', 'a/hello', 'a/b'])
190
self.check_inventory_shape(b.get_revision_inventory(r3),
228
self.check_inventory_shape(b.repository.get_revision_inventory(r3),
191
229
['a', 'a/hello', 'a/b'])
193
b.move([os.sep.join(['a', 'hello'])],
194
os.sep.join(['a', 'b']))
231
wt.move(['a/hello'], 'a/b')
195
232
r4 = 'test@rev-4'
196
b.commit('four', rev_id=r4, allow_pointless=False)
197
self.check_inventory_shape(b.inventory,
233
wt.commit('four', rev_id=r4, allow_pointless=False)
234
self.check_inventory_shape(wt.read_working_inventory(),
198
235
['a', 'a/b/hello', 'a/b'])
200
inv = b.get_revision_inventory(r4)
237
inv = b.repository.get_revision_inventory(r4)
201
238
eq(inv['hello-id'].revision, r4)
202
239
eq(inv['a-id'].revision, r1)
203
240
eq(inv['b-id'].revision, r3)
206
242
def test_removed_commit(self):
207
"""Test a commit with a removed file"""
208
b = Branch.initialize('.')
243
"""Commit with a removed file"""
244
wt = self.make_branch_and_tree('.')
209
246
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')
247
wt.add(['hello'], ['hello-id'])
248
wt.commit(message='add hello')
250
wt.commit('removed hello', rev_id='rev2')
252
tree = b.repository.revision_tree('rev2')
217
253
self.assertFalse(tree.has_id('hello-id'))
220
255
def test_committed_ancestry(self):
221
256
"""Test commit appends revisions to ancestry."""
222
b = Branch.initialize('.')
257
wt = self.make_branch_and_tree('.')
224
260
for i in range(4):
225
261
file('hello', 'w').write((str(i) * 4) + '\n')
227
b.add(['hello'], ['hello-id'])
263
wt.add(['hello'], ['hello-id'])
228
264
rev_id = 'test@rev-%d' % (i+1)
229
265
rev_ids.append(rev_id)
230
b.commit(message='rev %d' % (i+1),
266
wt.commit(message='rev %d' % (i+1),
232
268
eq = self.assertEquals
233
269
eq(b.revision_history(), rev_ids)
234
270
for i in range(4):
235
anc = b.get_ancestry(rev_ids[i])
271
anc = b.repository.get_ancestry(rev_ids[i])
236
272
eq(anc, [None] + rev_ids[:i+1])
238
274
def test_commit_new_subdir_child_selective(self):
239
b = Branch.initialize('.')
275
wt = self.make_branch_and_tree('.')
240
277
self.build_tree(['dir/', 'dir/file1', 'dir/file2'])
241
b.add(['dir', 'dir/file1', 'dir/file2'],
278
wt.add(['dir', 'dir/file1', 'dir/file2'],
242
279
['dirid', 'file1id', 'file2id'])
243
b.commit('dir/file1', specific_files=['dir/file1'], rev_id='1')
244
inv = b.get_inventory('1')
280
wt.commit('dir/file1', specific_files=['dir/file1'], rev_id='1')
281
inv = b.repository.get_inventory('1')
245
282
self.assertEqual('1', inv['dirid'].revision)
246
283
self.assertEqual('1', inv['file1id'].revision)
247
284
# FIXME: This should raise a KeyError I think, rbc20051006
248
285
self.assertRaises(BzrError, inv.__getitem__, 'file2id')
287
def test_strict_commit(self):
288
"""Try and commit with unknown files and strict = True, should fail."""
289
from bzrlib.errors import StrictCommitFailed
290
wt = self.make_branch_and_tree('.')
292
file('hello', 'w').write('hello world')
294
file('goodbye', 'w').write('goodbye cruel world!')
295
self.assertRaises(StrictCommitFailed, wt.commit,
296
message='add hello but not goodbye', strict=True)
298
def test_strict_commit_without_unknowns(self):
299
"""Try and commit with no unknown files and strict = True,
301
from bzrlib.errors import StrictCommitFailed
302
wt = self.make_branch_and_tree('.')
304
file('hello', 'w').write('hello world')
306
wt.commit(message='add hello', strict=True)
308
def test_nonstrict_commit(self):
309
"""Try and commit with unknown files and strict = False, should work."""
310
wt = self.make_branch_and_tree('.')
312
file('hello', 'w').write('hello world')
314
file('goodbye', 'w').write('goodbye cruel world!')
315
wt.commit(message='add hello but not goodbye', strict=False)
317
def test_nonstrict_commit_without_unknowns(self):
318
"""Try and commit with no unknown files and strict = False,
320
wt = self.make_branch_and_tree('.')
322
file('hello', 'w').write('hello world')
324
wt.commit(message='add hello', strict=False)
326
def test_signed_commit(self):
328
import bzrlib.commit as commit
329
oldstrategy = bzrlib.gpg.GPGStrategy
330
wt = self.make_branch_and_tree('.')
332
wt.commit("base", allow_pointless=True, rev_id='A')
333
self.failIf(branch.repository.has_signature_for_revision_id('A'))
335
from bzrlib.testament import Testament
336
# monkey patch gpg signing mechanism
337
bzrlib.gpg.GPGStrategy = bzrlib.gpg.LoopbackGPGStrategy
338
commit.Commit(config=MustSignConfig(branch)).commit(message="base",
339
allow_pointless=True,
342
self.assertEqual(Testament.from_revision(branch.repository,
343
'B').as_short_text(),
344
branch.repository.get_signature_text('B'))
346
bzrlib.gpg.GPGStrategy = oldstrategy
348
def test_commit_failed_signature(self):
350
import bzrlib.commit as commit
351
oldstrategy = bzrlib.gpg.GPGStrategy
352
wt = self.make_branch_and_tree('.')
354
wt.commit("base", allow_pointless=True, rev_id='A')
355
self.failIf(branch.repository.has_signature_for_revision_id('A'))
357
from bzrlib.testament import Testament
358
# monkey patch gpg signing mechanism
359
bzrlib.gpg.GPGStrategy = bzrlib.gpg.DisabledGPGStrategy
360
config = MustSignConfig(branch)
361
self.assertRaises(SigningFailed,
362
commit.Commit(config=config).commit,
364
allow_pointless=True,
367
branch = Branch.open(self.get_url('.'))
368
self.assertEqual(branch.revision_history(), ['A'])
369
self.failIf(branch.repository.has_revision('B'))
371
bzrlib.gpg.GPGStrategy = oldstrategy
373
def test_commit_invokes_hooks(self):
374
import bzrlib.commit as commit
375
wt = self.make_branch_and_tree('.')
378
def called(branch, rev_id):
379
calls.append('called')
380
bzrlib.ahook = called
382
config = BranchWithHooks(branch)
383
commit.Commit(config=config).commit(
385
allow_pointless=True,
386
rev_id='A', working_tree = wt)
387
self.assertEqual(['called', 'called'], calls)
391
def test_commit_object_doesnt_set_nick(self):
392
# using the Commit object directly does not set the branch nick.
393
wt = self.make_branch_and_tree('.')
395
c.commit(working_tree=wt, message='empty tree', allow_pointless=True)
396
self.assertEquals(wt.branch.revno(), 1)
398
wt.branch.repository.get_revision(
399
wt.branch.last_revision()).properties)
401
def test_safe_master_lock(self):
403
master = BzrDirMetaFormat1().initialize('master')
404
master.create_repository()
405
master_branch = master.create_branch()
406
master.create_workingtree()
407
bound = master.sprout('bound')
408
wt = bound.open_workingtree()
409
wt.branch.set_bound_location(os.path.realpath('master'))
410
master_branch.lock_write()
412
self.assertRaises(LockContention, wt.commit, 'silly')
414
master_branch.unlock()
416
def test_commit_bound_merge(self):
417
# see bug #43959; commit of a merge in a bound branch fails to push
418
# the new commit into the master
419
master_branch = self.make_branch('master')
420
bound_tree = self.make_branch_and_tree('bound')
421
bound_tree.branch.bind(master_branch)
423
self.build_tree_contents([('bound/content_file', 'initial contents\n')])
424
bound_tree.add(['content_file'])
425
bound_tree.commit(message='woo!')
427
other_bzrdir = master_branch.bzrdir.sprout('other')
428
other_tree = other_bzrdir.open_workingtree()
430
# do a commit to the the other branch changing the content file so
431
# that our commit after merging will have a merged revision in the
432
# content file history.
433
self.build_tree_contents([('other/content_file', 'change in other\n')])
434
other_tree.commit('change in other')
436
# do a merge into the bound branch from other, and then change the
437
# content file locally to force a new revision (rather than using the
438
# revision from other). This forces extra processing in commit.
439
bound_tree.merge_from_branch(other_tree.branch)
440
self.build_tree_contents([('bound/content_file', 'change in bound\n')])
442
# before #34959 was fixed, this failed with 'revision not present in
443
# weave' when trying to implicitly push from the bound branch to the master
444
bound_tree.commit(message='commit of merge in bound tree')
446
def test_commit_reporting_after_merge(self):
447
# when doing a commit of a merge, the reporter needs to still
448
# be called for each item that is added/removed/deleted.
449
this_tree = self.make_branch_and_tree('this')
450
# we need a bunch of files and dirs, to perform one action on each.
453
'this/dirtoreparent/',
456
'this/filetoreparent',
473
this_tree.commit('create_files')
474
other_dir = this_tree.bzrdir.sprout('other')
475
other_tree = other_dir.open_workingtree()
476
other_tree.lock_write()
477
# perform the needed actions on the files and dirs.
479
other_tree.rename_one('dirtorename', 'renameddir')
480
other_tree.rename_one('dirtoreparent', 'renameddir/reparenteddir')
481
other_tree.rename_one('filetorename', 'renamedfile')
482
other_tree.rename_one('filetoreparent', 'renameddir/reparentedfile')
483
other_tree.remove(['dirtoremove', 'filetoremove'])
484
self.build_tree_contents([
486
('other/filetomodify', 'new content'),
487
('other/newfile', 'new file content')])
488
other_tree.add('newfile')
489
other_tree.add('newdir/')
490
other_tree.commit('modify all sample files and dirs.')
493
this_tree.merge_from_branch(other_tree.branch)
494
reporter = CapturingReporter()
495
this_tree.commit('do the commit', reporter=reporter)
497
('change', 'unchanged', ''),
498
('change', 'unchanged', 'dirtoleave'),
499
('change', 'unchanged', 'filetoleave'),
500
('change', 'modified', 'filetomodify'),
501
('change', 'added', 'newdir'),
502
('change', 'added', 'newfile'),
503
('renamed', 'renamed', 'dirtorename', 'renameddir'),
504
('renamed', 'renamed', 'dirtoreparent', 'renameddir/reparenteddir'),
505
('renamed', 'renamed', 'filetoreparent', 'renameddir/reparentedfile'),
506
('renamed', 'renamed', 'filetorename', 'renamedfile'),
507
('deleted', 'dirtoremove'),
508
('deleted', 'filetoremove'),
512
def test_commit_removals_respects_filespec(self):
513
"""Commit respects the specified_files for removals."""
514
tree = self.make_branch_and_tree('.')
515
self.build_tree(['a', 'b'])
517
tree.commit('added a, b')
518
tree.remove(['a', 'b'])
519
tree.commit('removed a', specific_files='a')
520
basis = tree.basis_tree().inventory
521
self.assertIs(None, basis.path2id('a'))
522
self.assertFalse(basis.path2id('b') is None)
524
def test_commit_saves_1ms_timestamp(self):
525
"""Passing in a timestamp is saved with 1ms resolution"""
526
tree = self.make_branch_and_tree('.')
527
self.build_tree(['a'])
529
tree.commit('added a', timestamp=1153248633.4186721, timezone=0,
532
rev = tree.branch.repository.get_revision('a1')
533
self.assertEqual(1153248633.419, rev.timestamp)
535
def test_commit_has_1ms_resolution(self):
536
"""Allowing commit to generate the timestamp also has 1ms resolution"""
537
tree = self.make_branch_and_tree('.')
538
self.build_tree(['a'])
540
tree.commit('added a', rev_id='a1')
542
rev = tree.branch.repository.get_revision('a1')
543
timestamp = rev.timestamp
544
timestamp_1ms = round(timestamp, 3)
545
self.assertEqual(timestamp_1ms, timestamp)