21
from bzrlib.tests import TestCaseWithTransport
20
from bzrlib.selftest import TestCaseInTempDir
22
21
from bzrlib.branch import Branch
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,
22
from bzrlib.commit import Commit
23
from bzrlib.errors import PointlessCommit, BzrError
31
26
# TODO: Test commit with some added, and added-but-missing files
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):
28
class TestCommit(TestCaseInTempDir):
70
30
def test_simple_commit(self):
71
31
"""Commit and check two versions of a single file."""
72
wt = self.make_branch_and_tree('.')
32
b = Branch.initialize('.')
74
33
file('hello', 'w').write('hello world')
76
wt.commit(message='add hello')
77
file_id = wt.path2id('hello')
35
b.commit(message='add hello')
36
file_id = b.working_tree().path2id('hello')
79
38
file('hello', 'w').write('version 2')
80
wt.commit(message='commit 2')
39
b.commit(message='commit 2')
82
41
eq = self.assertEquals
84
43
rh = b.revision_history()
85
rev = b.repository.get_revision(rh[0])
44
rev = b.get_revision(rh[0])
86
45
eq(rev.message, 'add hello')
88
tree1 = b.repository.revision_tree(rh[0])
47
tree1 = b.revision_tree(rh[0])
89
48
text = tree1.get_file_text(file_id)
90
49
eq(text, 'hello world')
92
tree2 = b.repository.revision_tree(rh[1])
51
tree2 = b.revision_tree(rh[1])
93
52
eq(tree2.get_file_text(file_id), 'version 2')
95
55
def test_delete_commit(self):
96
56
"""Test a commit with a deleted file"""
97
wt = self.make_branch_and_tree('.')
57
b = Branch.initialize('.')
99
58
file('hello', 'w').write('hello world')
100
wt.add(['hello'], ['hello-id'])
101
wt.commit(message='add hello')
59
b.add(['hello'], ['hello-id'])
60
b.commit(message='add hello')
103
62
os.remove('hello')
104
wt.commit('removed hello', rev_id='rev2')
63
b.commit('removed hello', rev_id='rev2')
106
tree = b.repository.revision_tree('rev2')
65
tree = b.revision_tree('rev2')
107
66
self.assertFalse(tree.has_id('hello-id'))
109
69
def test_pointless_commit(self):
110
70
"""Commit refuses unless there are changes or it's forced."""
111
wt = self.make_branch_and_tree('.')
71
b = Branch.initialize('.')
113
72
file('hello', 'w').write('hello')
115
wt.commit(message='add hello')
74
b.commit(message='add hello')
116
75
self.assertEquals(b.revno(), 1)
117
76
self.assertRaises(PointlessCommit,
120
79
allow_pointless=False)
121
80
self.assertEquals(b.revno(), 1)
123
84
def test_commit_empty(self):
124
85
"""Commiting an empty tree works."""
125
wt = self.make_branch_and_tree('.')
127
wt.commit(message='empty tree', allow_pointless=True)
86
b = Branch.initialize('.')
87
b.commit(message='empty tree', allow_pointless=True)
128
88
self.assertRaises(PointlessCommit,
130
90
message='empty tree',
131
91
allow_pointless=False)
132
wt.commit(message='empty tree', allow_pointless=True)
92
b.commit(message='empty tree', allow_pointless=True)
133
93
self.assertEquals(b.revno(), 2)
135
96
def test_selective_delete(self):
136
97
"""Selective commit in tree with deletions"""
137
wt = self.make_branch_and_tree('.')
98
b = Branch.initialize('.')
139
99
file('hello', 'w').write('hello')
140
100
file('buongia', 'w').write('buongia')
141
wt.add(['hello', 'buongia'],
101
b.add(['hello', 'buongia'],
142
102
['hello-id', 'buongia-id'])
143
wt.commit(message='add files',
103
b.commit(message='add files',
144
104
rev_id='test@rev-1')
146
106
os.remove('hello')
147
107
file('buongia', 'w').write('new text')
148
wt.commit(message='update text',
108
b.commit(message='update text',
149
109
specific_files=['buongia'],
150
110
allow_pointless=False,
151
111
rev_id='test@rev-2')
153
wt.commit(message='remove hello',
113
b.commit(message='remove hello',
154
114
specific_files=['hello'],
155
115
allow_pointless=False,
156
116
rev_id='test@rev-3')
187
147
ie = tree1.inventory['hello-id']
188
148
eq(ie.revision, 'test@rev-1')
190
tree2 = b.repository.revision_tree('test@rev-2')
150
tree2 = b.revision_tree('test@rev-2')
191
151
eq(tree2.id2path('hello-id'), 'fruity')
192
152
eq(tree2.get_file_text('hello-id'), 'contents of hello\n')
193
153
self.check_inventory_shape(tree2.inventory, ['fruity'])
194
154
ie = tree2.inventory['hello-id']
195
155
eq(ie.revision, 'test@rev-2')
197
158
def test_reused_rev_id(self):
198
159
"""Test that a revision id cannot be reused in a branch"""
199
wt = self.make_branch_and_tree('.')
201
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)
202
162
self.assertRaises(Exception,
204
164
message='reused id',
205
165
rev_id='test@rev-1',
206
166
allow_pointless=True)
208
170
def test_commit_move(self):
209
171
"""Test commit of revisions with moved files and directories"""
210
172
eq = self.assertEquals
211
wt = self.make_branch_and_tree('.')
173
b = Branch.initialize('.')
213
174
r1 = 'test@rev-1'
214
175
self.build_tree(['hello', 'a/', 'b/'])
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')
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')
218
180
r2 = 'test@rev-2'
219
wt.commit('two', rev_id=r2, allow_pointless=False)
220
self.check_inventory_shape(wt.read_working_inventory(),
181
b.commit('two', rev_id=r2, allow_pointless=False)
182
self.check_inventory_shape(b.inventory,
221
183
['a', 'a/hello', 'b'])
224
186
r3 = 'test@rev-3'
225
wt.commit('three', rev_id=r3, allow_pointless=False)
226
self.check_inventory_shape(wt.read_working_inventory(),
187
b.commit('three', rev_id=r3, allow_pointless=False)
188
self.check_inventory_shape(b.inventory,
227
189
['a', 'a/hello', 'a/b'])
228
self.check_inventory_shape(b.repository.get_revision_inventory(r3),
190
self.check_inventory_shape(b.get_revision_inventory(r3),
229
191
['a', 'a/hello', 'a/b'])
231
wt.move(['a/hello'], 'a/b')
193
b.move([os.sep.join(['a', 'hello'])],
194
os.sep.join(['a', 'b']))
232
195
r4 = 'test@rev-4'
233
wt.commit('four', rev_id=r4, allow_pointless=False)
234
self.check_inventory_shape(wt.read_working_inventory(),
196
b.commit('four', rev_id=r4, allow_pointless=False)
197
self.check_inventory_shape(b.inventory,
235
198
['a', 'a/b/hello', 'a/b'])
237
inv = b.repository.get_revision_inventory(r4)
200
inv = b.get_revision_inventory(r4)
238
201
eq(inv['hello-id'].revision, r4)
239
202
eq(inv['a-id'].revision, r1)
240
203
eq(inv['b-id'].revision, r3)
242
206
def test_removed_commit(self):
243
"""Commit with a removed file"""
244
wt = self.make_branch_and_tree('.')
207
"""Test a commit with a removed file"""
208
b = Branch.initialize('.')
246
209
file('hello', 'w').write('hello world')
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')
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')
253
217
self.assertFalse(tree.has_id('hello-id'))
255
220
def test_committed_ancestry(self):
256
221
"""Test commit appends revisions to ancestry."""
257
wt = self.make_branch_and_tree('.')
222
b = Branch.initialize('.')
260
224
for i in range(4):
261
225
file('hello', 'w').write((str(i) * 4) + '\n')
263
wt.add(['hello'], ['hello-id'])
227
b.add(['hello'], ['hello-id'])
264
228
rev_id = 'test@rev-%d' % (i+1)
265
229
rev_ids.append(rev_id)
266
wt.commit(message='rev %d' % (i+1),
230
b.commit(message='rev %d' % (i+1),
268
232
eq = self.assertEquals
269
233
eq(b.revision_history(), rev_ids)
270
234
for i in range(4):
271
anc = b.repository.get_ancestry(rev_ids[i])
235
anc = b.get_ancestry(rev_ids[i])
272
236
eq(anc, [None] + rev_ids[:i+1])
274
def test_commit_new_subdir_child_selective(self):
275
wt = self.make_branch_and_tree('.')
277
self.build_tree(['dir/', 'dir/file1', 'dir/file2'])
278
wt.add(['dir', 'dir/file1', 'dir/file2'],
279
['dirid', 'file1id', 'file2id'])
280
wt.commit('dir/file1', specific_files=['dir/file1'], rev_id='1')
281
inv = b.repository.get_inventory('1')
282
self.assertEqual('1', inv['dirid'].revision)
283
self.assertEqual('1', inv['file1id'].revision)
284
# FIXME: This should raise a KeyError I think, rbc20051006
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
self.merge(other_tree.branch, bound_tree)
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
self.merge(other_tree.branch, this_tree)
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)