20
from bzrlib.selftest import TestCaseInTempDir
25
21
from bzrlib.branch import Branch
26
from bzrlib.bzrdir import BzrDir, BzrDirMetaFormat1
27
from bzrlib.commit import Commit, NullCommitReporter
28
from bzrlib.config import BranchConfig
29
from bzrlib.errors import (PointlessCommit, BzrError, SigningFailed,
31
from bzrlib.tests import TestCaseWithTransport
32
from bzrlib.workingtree import WorkingTree
22
from bzrlib.commit import Commit
23
from bzrlib.errors import PointlessCommit, BzrError
35
26
# TODO: Test commit with some added, and added-but-missing files
37
class MustSignConfig(BranchConfig):
39
def signature_needed(self):
42
def gpg_signing_command(self):
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):
28
class TestCommit(TestCaseInTempDir):
74
30
def test_simple_commit(self):
75
31
"""Commit and check two versions of a single file."""
76
wt = self.make_branch_and_tree('.')
32
b = Branch.initialize('.')
78
33
file('hello', 'w').write('hello world')
80
wt.commit(message='add hello')
81
file_id = wt.path2id('hello')
35
b.commit(message='add hello')
36
file_id = b.working_tree().path2id('hello')
83
38
file('hello', 'w').write('version 2')
84
wt.commit(message='commit 2')
39
b.commit(message='commit 2')
86
41
eq = self.assertEquals
88
43
rh = b.revision_history()
89
rev = b.repository.get_revision(rh[0])
44
rev = b.get_revision(rh[0])
90
45
eq(rev.message, 'add hello')
92
tree1 = b.repository.revision_tree(rh[0])
47
tree1 = b.revision_tree(rh[0])
93
48
text = tree1.get_file_text(file_id)
94
49
eq(text, 'hello world')
96
tree2 = b.repository.revision_tree(rh[1])
51
tree2 = b.revision_tree(rh[1])
97
52
eq(tree2.get_file_text(file_id), 'version 2')
99
55
def test_delete_commit(self):
100
56
"""Test a commit with a deleted file"""
101
wt = self.make_branch_and_tree('.')
57
b = Branch.initialize('.')
103
58
file('hello', 'w').write('hello world')
104
wt.add(['hello'], ['hello-id'])
105
wt.commit(message='add hello')
59
b.add(['hello'], ['hello-id'])
60
b.commit(message='add hello')
107
62
os.remove('hello')
108
wt.commit('removed hello', rev_id='rev2')
63
b.commit('removed hello', rev_id='rev2')
110
tree = b.repository.revision_tree('rev2')
65
tree = b.revision_tree('rev2')
111
66
self.assertFalse(tree.has_id('hello-id'))
113
69
def test_pointless_commit(self):
114
70
"""Commit refuses unless there are changes or it's forced."""
115
wt = self.make_branch_and_tree('.')
71
b = Branch.initialize('.')
117
72
file('hello', 'w').write('hello')
119
wt.commit(message='add hello')
74
b.commit(message='add hello')
120
75
self.assertEquals(b.revno(), 1)
121
76
self.assertRaises(PointlessCommit,
124
79
allow_pointless=False)
125
80
self.assertEquals(b.revno(), 1)
127
84
def test_commit_empty(self):
128
85
"""Commiting an empty tree works."""
129
wt = self.make_branch_and_tree('.')
131
wt.commit(message='empty tree', allow_pointless=True)
86
b = Branch.initialize('.')
87
b.commit(message='empty tree', allow_pointless=True)
132
88
self.assertRaises(PointlessCommit,
134
90
message='empty tree',
135
91
allow_pointless=False)
136
wt.commit(message='empty tree', allow_pointless=True)
92
b.commit(message='empty tree', allow_pointless=True)
137
93
self.assertEquals(b.revno(), 2)
139
96
def test_selective_delete(self):
140
97
"""Selective commit in tree with deletions"""
141
wt = self.make_branch_and_tree('.')
98
b = Branch.initialize('.')
143
99
file('hello', 'w').write('hello')
144
100
file('buongia', 'w').write('buongia')
145
wt.add(['hello', 'buongia'],
101
b.add(['hello', 'buongia'],
146
102
['hello-id', 'buongia-id'])
147
wt.commit(message='add files',
103
b.commit(message='add files',
148
104
rev_id='test@rev-1')
150
106
os.remove('hello')
151
107
file('buongia', 'w').write('new text')
152
wt.commit(message='update text',
108
b.commit(message='update text',
153
109
specific_files=['buongia'],
154
110
allow_pointless=False,
155
111
rev_id='test@rev-2')
157
wt.commit(message='remove hello',
113
b.commit(message='remove hello',
158
114
specific_files=['hello'],
159
115
allow_pointless=False,
160
116
rev_id='test@rev-3')
191
147
ie = tree1.inventory['hello-id']
192
148
eq(ie.revision, 'test@rev-1')
194
tree2 = b.repository.revision_tree('test@rev-2')
150
tree2 = b.revision_tree('test@rev-2')
195
151
eq(tree2.id2path('hello-id'), 'fruity')
196
152
eq(tree2.get_file_text('hello-id'), 'contents of hello\n')
197
153
self.check_inventory_shape(tree2.inventory, ['fruity'])
198
154
ie = tree2.inventory['hello-id']
199
155
eq(ie.revision, 'test@rev-2')
201
158
def test_reused_rev_id(self):
202
159
"""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)
160
b = Branch.initialize('.')
161
b.commit('initial', rev_id='test@rev-1', allow_pointless=True)
206
162
self.assertRaises(Exception,
208
164
message='reused id',
209
165
rev_id='test@rev-1',
210
166
allow_pointless=True)
212
170
def test_commit_move(self):
213
171
"""Test commit of revisions with moved files and directories"""
214
172
eq = self.assertEquals
215
wt = self.make_branch_and_tree('.')
173
b = Branch.initialize('.')
217
174
r1 = 'test@rev-1'
218
175
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')
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')
222
180
r2 = 'test@rev-2'
223
wt.commit('two', rev_id=r2, allow_pointless=False)
224
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,
225
183
['a', 'a/hello', 'b'])
228
186
r3 = 'test@rev-3'
229
wt.commit('three', rev_id=r3, allow_pointless=False)
230
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,
231
189
['a', 'a/hello', 'a/b'])
232
self.check_inventory_shape(b.repository.get_revision_inventory(r3),
190
self.check_inventory_shape(b.get_revision_inventory(r3),
233
191
['a', 'a/hello', 'a/b'])
235
wt.move(['a/hello'], 'a/b')
193
b.move([os.sep.join(['a', 'hello'])],
194
os.sep.join(['a', 'b']))
236
195
r4 = 'test@rev-4'
237
wt.commit('four', rev_id=r4, allow_pointless=False)
238
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,
239
198
['a', 'a/b/hello', 'a/b'])
241
inv = b.repository.get_revision_inventory(r4)
200
inv = b.get_revision_inventory(r4)
242
201
eq(inv['hello-id'].revision, r4)
243
202
eq(inv['a-id'].revision, r1)
244
203
eq(inv['b-id'].revision, r3)
246
206
def test_removed_commit(self):
247
"""Commit with a removed file"""
248
wt = self.make_branch_and_tree('.')
207
"""Test a commit with a removed file"""
208
b = Branch.initialize('.')
250
209
file('hello', 'w').write('hello world')
251
wt.add(['hello'], ['hello-id'])
252
wt.commit(message='add hello')
254
wt.commit('removed hello', rev_id='rev2')
256
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')
257
217
self.assertFalse(tree.has_id('hello-id'))
259
220
def test_committed_ancestry(self):
260
221
"""Test commit appends revisions to ancestry."""
261
wt = self.make_branch_and_tree('.')
222
b = Branch.initialize('.')
264
224
for i in range(4):
265
225
file('hello', 'w').write((str(i) * 4) + '\n')
267
wt.add(['hello'], ['hello-id'])
227
b.add(['hello'], ['hello-id'])
268
228
rev_id = 'test@rev-%d' % (i+1)
269
229
rev_ids.append(rev_id)
270
wt.commit(message='rev %d' % (i+1),
230
b.commit(message='rev %d' % (i+1),
272
232
eq = self.assertEquals
273
233
eq(b.revision_history(), rev_ids)
274
234
for i in range(4):
275
anc = b.repository.get_ancestry(rev_ids[i])
235
anc = b.get_ancestry(rev_ids[i])
276
236
eq(anc, [None] + rev_ids[:i+1])
278
238
def test_commit_new_subdir_child_selective(self):
279
wt = self.make_branch_and_tree('.')
239
b = Branch.initialize('.')
281
240
self.build_tree(['dir/', 'dir/file1', 'dir/file2'])
282
wt.add(['dir', 'dir/file1', 'dir/file2'],
241
b.add(['dir', 'dir/file1', 'dir/file2'],
283
242
['dirid', 'file1id', 'file2id'])
284
wt.commit('dir/file1', specific_files=['dir/file1'], rev_id='1')
285
inv = b.repository.get_inventory('1')
243
b.commit('dir/file1', specific_files=['dir/file1'], rev_id='1')
244
inv = b.get_inventory('1')
286
245
self.assertEqual('1', inv['dirid'].revision)
287
246
self.assertEqual('1', inv['file1id'].revision)
288
247
# FIXME: This should raise a KeyError I think, rbc20051006
289
248
self.assertRaises(BzrError, inv.__getitem__, 'file2id')
291
def test_strict_commit(self):
292
"""Try and commit with unknown files and strict = True, should fail."""
293
from bzrlib.errors import StrictCommitFailed
294
wt = self.make_branch_and_tree('.')
296
file('hello', 'w').write('hello world')
298
file('goodbye', 'w').write('goodbye cruel world!')
299
self.assertRaises(StrictCommitFailed, wt.commit,
300
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
def test_nonstrict_commit(self):
313
"""Try and commit with unknown files and strict = False, should work."""
314
wt = self.make_branch_and_tree('.')
316
file('hello', 'w').write('hello world')
318
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)
330
def test_signed_commit(self):
332
import bzrlib.commit as commit
333
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'))
339
from bzrlib.testament import Testament
340
# monkey patch gpg signing mechanism
341
bzrlib.gpg.GPGStrategy = bzrlib.gpg.LoopbackGPGStrategy
342
commit.Commit(config=MustSignConfig(branch)).commit(message="base",
343
allow_pointless=True,
346
self.assertEqual(Testament.from_revision(branch.repository,
347
'B').as_short_text(),
348
branch.repository.get_signature_text('B'))
350
bzrlib.gpg.GPGStrategy = oldstrategy
352
def test_commit_failed_signature(self):
354
import bzrlib.commit as commit
355
oldstrategy = bzrlib.gpg.GPGStrategy
356
wt = self.make_branch_and_tree('.')
358
wt.commit("base", allow_pointless=True, rev_id='A')
359
self.failIf(branch.repository.has_signature_for_revision_id('A'))
361
from bzrlib.testament import Testament
362
# monkey patch gpg signing mechanism
363
bzrlib.gpg.GPGStrategy = bzrlib.gpg.DisabledGPGStrategy
364
config = MustSignConfig(branch)
365
self.assertRaises(SigningFailed,
366
commit.Commit(config=config).commit,
368
allow_pointless=True,
371
branch = Branch.open(self.get_url('.'))
372
self.assertEqual(branch.revision_history(), ['A'])
373
self.failIf(branch.repository.has_revision('B'))
375
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'])