1
# Copyright (C) 2005, 2006 Canonical Ltd
3
# This program is free software; you can redistribute it and/or modify
4
# it under the terms of the GNU General Public License as published by
5
# the Free Software Foundation; either version 2 of the License, or
6
# (at your option) any later version.
8
# This program is distributed in the hope that it will be useful,
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
# GNU General Public License for more details.
13
# You should have received a copy of the GNU General Public License
14
# along with this program; if not, write to the Free Software
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
27
from bzrlib.branch import Branch
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
37
# TODO: Test commit with some added, and added-but-missing files
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):
76
def test_simple_commit(self):
77
"""Commit and check two versions of a single file."""
78
wt = self.make_branch_and_tree('.')
80
file('hello', 'w').write('hello world')
82
wt.commit(message='add hello')
83
file_id = wt.path2id('hello')
85
file('hello', 'w').write('version 2')
86
wt.commit(message='commit 2')
88
eq = self.assertEquals
90
rh = b.revision_history()
91
rev = b.repository.get_revision(rh[0])
92
eq(rev.message, 'add hello')
94
tree1 = b.repository.revision_tree(rh[0])
95
text = tree1.get_file_text(file_id)
96
eq(text, 'hello world')
98
tree2 = b.repository.revision_tree(rh[1])
99
eq(tree2.get_file_text(file_id), 'version 2')
101
def test_delete_commit(self):
102
"""Test a commit with a deleted file"""
103
wt = self.make_branch_and_tree('.')
105
file('hello', 'w').write('hello world')
106
wt.add(['hello'], ['hello-id'])
107
wt.commit(message='add hello')
110
wt.commit('removed hello', rev_id='rev2')
112
tree = b.repository.revision_tree('rev2')
113
self.assertFalse(tree.has_id('hello-id'))
115
def test_pointless_commit(self):
116
"""Commit refuses unless there are changes or it's forced."""
117
wt = self.make_branch_and_tree('.')
119
file('hello', 'w').write('hello')
121
wt.commit(message='add hello')
122
self.assertEquals(b.revno(), 1)
123
self.assertRaises(PointlessCommit,
126
allow_pointless=False)
127
self.assertEquals(b.revno(), 1)
129
def test_commit_empty(self):
130
"""Commiting an empty tree works."""
131
wt = self.make_branch_and_tree('.')
133
wt.commit(message='empty tree', allow_pointless=True)
134
self.assertRaises(PointlessCommit,
136
message='empty tree',
137
allow_pointless=False)
138
wt.commit(message='empty tree', allow_pointless=True)
139
self.assertEquals(b.revno(), 2)
141
def test_selective_delete(self):
142
"""Selective commit in tree with deletions"""
143
wt = self.make_branch_and_tree('.')
145
file('hello', 'w').write('hello')
146
file('buongia', 'w').write('buongia')
147
wt.add(['hello', 'buongia'],
148
['hello-id', 'buongia-id'])
149
wt.commit(message='add files',
153
file('buongia', 'w').write('new text')
154
wt.commit(message='update text',
155
specific_files=['buongia'],
156
allow_pointless=False,
159
wt.commit(message='remove hello',
160
specific_files=['hello'],
161
allow_pointless=False,
164
eq = self.assertEquals
167
tree2 = b.repository.revision_tree('test@rev-2')
168
self.assertTrue(tree2.has_filename('hello'))
169
self.assertEquals(tree2.get_file_text('hello-id'), 'hello')
170
self.assertEquals(tree2.get_file_text('buongia-id'), 'new text')
172
tree3 = b.repository.revision_tree('test@rev-3')
173
self.assertFalse(tree3.has_filename('hello'))
174
self.assertEquals(tree3.get_file_text('buongia-id'), 'new text')
176
def test_commit_rename(self):
177
"""Test commit of a revision where a file is renamed."""
178
tree = self.make_branch_and_tree('.')
180
self.build_tree(['hello'], line_endings='binary')
181
tree.add(['hello'], ['hello-id'])
182
tree.commit(message='one', rev_id='test@rev-1', allow_pointless=False)
184
tree.rename_one('hello', 'fruity')
185
tree.commit(message='renamed', rev_id='test@rev-2', allow_pointless=False)
187
eq = self.assertEquals
188
tree1 = b.repository.revision_tree('test@rev-1')
189
eq(tree1.id2path('hello-id'), 'hello')
190
eq(tree1.get_file_text('hello-id'), 'contents of hello\n')
191
self.assertFalse(tree1.has_filename('fruity'))
192
self.check_inventory_shape(tree1.inventory, ['hello'])
193
ie = tree1.inventory['hello-id']
194
eq(ie.revision, 'test@rev-1')
196
tree2 = b.repository.revision_tree('test@rev-2')
197
eq(tree2.id2path('hello-id'), 'fruity')
198
eq(tree2.get_file_text('hello-id'), 'contents of hello\n')
199
self.check_inventory_shape(tree2.inventory, ['fruity'])
200
ie = tree2.inventory['hello-id']
201
eq(ie.revision, 'test@rev-2')
203
def test_reused_rev_id(self):
204
"""Test that a revision id cannot be reused in a branch"""
205
wt = self.make_branch_and_tree('.')
207
wt.commit('initial', rev_id='test@rev-1', allow_pointless=True)
208
self.assertRaises(Exception,
212
allow_pointless=True)
214
def test_commit_move(self):
215
"""Test commit of revisions with moved files and directories"""
216
eq = self.assertEquals
217
wt = self.make_branch_and_tree('.')
220
self.build_tree(['hello', 'a/', 'b/'])
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')
225
wt.commit('two', rev_id=r2, allow_pointless=False)
226
self.check_inventory_shape(wt.read_working_inventory(),
227
['a', 'a/hello', 'b'])
231
wt.commit('three', rev_id=r3, allow_pointless=False)
232
self.check_inventory_shape(wt.read_working_inventory(),
233
['a', 'a/hello', 'a/b'])
234
self.check_inventory_shape(b.repository.get_revision_inventory(r3),
235
['a', 'a/hello', 'a/b'])
237
wt.move(['a/hello'], 'a/b')
239
wt.commit('four', rev_id=r4, allow_pointless=False)
240
self.check_inventory_shape(wt.read_working_inventory(),
241
['a', 'a/b/hello', 'a/b'])
243
inv = b.repository.get_revision_inventory(r4)
244
eq(inv['hello-id'].revision, r4)
245
eq(inv['a-id'].revision, r1)
246
eq(inv['b-id'].revision, r3)
248
def test_removed_commit(self):
249
"""Commit with a removed file"""
250
wt = self.make_branch_and_tree('.')
252
file('hello', 'w').write('hello world')
253
wt.add(['hello'], ['hello-id'])
254
wt.commit(message='add hello')
256
wt.commit('removed hello', rev_id='rev2')
258
tree = b.repository.revision_tree('rev2')
259
self.assertFalse(tree.has_id('hello-id'))
261
def test_committed_ancestry(self):
262
"""Test commit appends revisions to ancestry."""
263
wt = self.make_branch_and_tree('.')
267
file('hello', 'w').write((str(i) * 4) + '\n')
269
wt.add(['hello'], ['hello-id'])
270
rev_id = 'test@rev-%d' % (i+1)
271
rev_ids.append(rev_id)
272
wt.commit(message='rev %d' % (i+1),
274
eq = self.assertEquals
275
eq(b.revision_history(), rev_ids)
277
anc = b.repository.get_ancestry(rev_ids[i])
278
eq(anc, [None] + rev_ids[:i+1])
280
def test_commit_new_subdir_child_selective(self):
281
wt = self.make_branch_and_tree('.')
283
self.build_tree(['dir/', 'dir/file1', 'dir/file2'])
284
wt.add(['dir', 'dir/file1', 'dir/file2'],
285
['dirid', 'file1id', 'file2id'])
286
wt.commit('dir/file1', specific_files=['dir/file1'], rev_id='1')
287
inv = b.repository.get_inventory('1')
288
self.assertEqual('1', inv['dirid'].revision)
289
self.assertEqual('1', inv['file1id'].revision)
290
# FIXME: This should raise a KeyError I think, rbc20051006
291
self.assertRaises(BzrError, inv.__getitem__, 'file2id')
293
def test_strict_commit(self):
294
"""Try and commit with unknown files and strict = True, should fail."""
295
from bzrlib.errors import StrictCommitFailed
296
wt = self.make_branch_and_tree('.')
298
file('hello', 'w').write('hello world')
300
file('goodbye', 'w').write('goodbye cruel world!')
301
self.assertRaises(StrictCommitFailed, wt.commit,
302
message='add hello but not goodbye', strict=True)
304
def test_strict_commit_without_unknowns(self):
305
"""Try and commit with no unknown files and strict = True,
307
from bzrlib.errors import StrictCommitFailed
308
wt = self.make_branch_and_tree('.')
310
file('hello', 'w').write('hello world')
312
wt.commit(message='add hello', strict=True)
314
def test_nonstrict_commit(self):
315
"""Try and commit with unknown files and strict = False, should work."""
316
wt = self.make_branch_and_tree('.')
318
file('hello', 'w').write('hello world')
320
file('goodbye', 'w').write('goodbye cruel world!')
321
wt.commit(message='add hello but not goodbye', strict=False)
323
def test_nonstrict_commit_without_unknowns(self):
324
"""Try and commit with no unknown files and strict = False,
326
wt = self.make_branch_and_tree('.')
328
file('hello', 'w').write('hello world')
330
wt.commit(message='add hello', strict=False)
332
def test_signed_commit(self):
334
import bzrlib.commit as commit
335
oldstrategy = bzrlib.gpg.GPGStrategy
336
wt = self.make_branch_and_tree('.')
338
wt.commit("base", allow_pointless=True, rev_id='A')
339
self.failIf(branch.repository.has_signature_for_revision_id('A'))
341
from bzrlib.testament import Testament
342
# monkey patch gpg signing mechanism
343
bzrlib.gpg.GPGStrategy = bzrlib.gpg.LoopbackGPGStrategy
344
commit.Commit(config=MustSignConfig(branch)).commit(message="base",
345
allow_pointless=True,
349
return bzrlib.gpg.LoopbackGPGStrategy(None).sign(text)
350
self.assertEqual(sign(Testament.from_revision(branch.repository,
351
'B').as_short_text()),
352
branch.repository.get_signature_text('B'))
354
bzrlib.gpg.GPGStrategy = oldstrategy
356
def test_commit_failed_signature(self):
358
import bzrlib.commit as commit
359
oldstrategy = bzrlib.gpg.GPGStrategy
360
wt = self.make_branch_and_tree('.')
362
wt.commit("base", allow_pointless=True, rev_id='A')
363
self.failIf(branch.repository.has_signature_for_revision_id('A'))
365
from bzrlib.testament import Testament
366
# monkey patch gpg signing mechanism
367
bzrlib.gpg.GPGStrategy = bzrlib.gpg.DisabledGPGStrategy
368
config = MustSignConfig(branch)
369
self.assertRaises(SigningFailed,
370
commit.Commit(config=config).commit,
372
allow_pointless=True,
375
branch = Branch.open(self.get_url('.'))
376
self.assertEqual(branch.revision_history(), ['A'])
377
self.failIf(branch.repository.has_revision('B'))
379
bzrlib.gpg.GPGStrategy = oldstrategy
381
def test_commit_invokes_hooks(self):
382
import bzrlib.commit as commit
383
wt = self.make_branch_and_tree('.')
386
def called(branch, rev_id):
387
calls.append('called')
388
bzrlib.ahook = called
390
config = BranchWithHooks(branch)
391
commit.Commit(config=config).commit(
393
allow_pointless=True,
394
rev_id='A', working_tree = wt)
395
self.assertEqual(['called', 'called'], calls)
399
def test_commit_object_doesnt_set_nick(self):
400
# using the Commit object directly does not set the branch nick.
401
wt = self.make_branch_and_tree('.')
403
c.commit(working_tree=wt, message='empty tree', allow_pointless=True)
404
self.assertEquals(wt.branch.revno(), 1)
406
wt.branch.repository.get_revision(
407
wt.branch.last_revision()).properties)
409
def test_safe_master_lock(self):
411
master = BzrDirMetaFormat1().initialize('master')
412
master.create_repository()
413
master_branch = master.create_branch()
414
master.create_workingtree()
415
bound = master.sprout('bound')
416
wt = bound.open_workingtree()
417
wt.branch.set_bound_location(os.path.realpath('master'))
419
orig_default = lockdir._DEFAULT_TIMEOUT_SECONDS
420
master_branch.lock_write()
422
lockdir._DEFAULT_TIMEOUT_SECONDS = 1
423
self.assertRaises(LockContention, wt.commit, 'silly')
425
lockdir._DEFAULT_TIMEOUT_SECONDS = orig_default
426
master_branch.unlock()
428
def test_commit_bound_merge(self):
429
# see bug #43959; commit of a merge in a bound branch fails to push
430
# the new commit into the master
431
master_branch = self.make_branch('master')
432
bound_tree = self.make_branch_and_tree('bound')
433
bound_tree.branch.bind(master_branch)
435
self.build_tree_contents([('bound/content_file', 'initial contents\n')])
436
bound_tree.add(['content_file'])
437
bound_tree.commit(message='woo!')
439
other_bzrdir = master_branch.bzrdir.sprout('other')
440
other_tree = other_bzrdir.open_workingtree()
442
# do a commit to the the other branch changing the content file so
443
# that our commit after merging will have a merged revision in the
444
# content file history.
445
self.build_tree_contents([('other/content_file', 'change in other\n')])
446
other_tree.commit('change in other')
448
# do a merge into the bound branch from other, and then change the
449
# content file locally to force a new revision (rather than using the
450
# revision from other). This forces extra processing in commit.
451
bound_tree.merge_from_branch(other_tree.branch)
452
self.build_tree_contents([('bound/content_file', 'change in bound\n')])
454
# before #34959 was fixed, this failed with 'revision not present in
455
# weave' when trying to implicitly push from the bound branch to the master
456
bound_tree.commit(message='commit of merge in bound tree')
458
def test_commit_reporting_after_merge(self):
459
# when doing a commit of a merge, the reporter needs to still
460
# be called for each item that is added/removed/deleted.
461
this_tree = self.make_branch_and_tree('this')
462
# we need a bunch of files and dirs, to perform one action on each.
465
'this/dirtoreparent/',
468
'this/filetoreparent',
485
this_tree.commit('create_files')
486
other_dir = this_tree.bzrdir.sprout('other')
487
other_tree = other_dir.open_workingtree()
488
other_tree.lock_write()
489
# perform the needed actions on the files and dirs.
491
other_tree.rename_one('dirtorename', 'renameddir')
492
other_tree.rename_one('dirtoreparent', 'renameddir/reparenteddir')
493
other_tree.rename_one('filetorename', 'renamedfile')
494
other_tree.rename_one('filetoreparent', 'renameddir/reparentedfile')
495
other_tree.remove(['dirtoremove', 'filetoremove'])
496
self.build_tree_contents([
498
('other/filetomodify', 'new content'),
499
('other/newfile', 'new file content')])
500
other_tree.add('newfile')
501
other_tree.add('newdir/')
502
other_tree.commit('modify all sample files and dirs.')
505
this_tree.merge_from_branch(other_tree.branch)
506
reporter = CapturingReporter()
507
this_tree.commit('do the commit', reporter=reporter)
509
('change', 'unchanged', ''),
510
('change', 'unchanged', 'dirtoleave'),
511
('change', 'unchanged', 'filetoleave'),
512
('change', 'modified', 'filetomodify'),
513
('change', 'added', 'newdir'),
514
('change', 'added', 'newfile'),
515
('renamed', 'renamed', 'dirtorename', 'renameddir'),
516
('renamed', 'renamed', 'dirtoreparent', 'renameddir/reparenteddir'),
517
('renamed', 'renamed', 'filetoreparent', 'renameddir/reparentedfile'),
518
('renamed', 'renamed', 'filetorename', 'renamedfile'),
519
('deleted', 'dirtoremove'),
520
('deleted', 'filetoremove'),
524
def test_commit_removals_respects_filespec(self):
525
"""Commit respects the specified_files for removals."""
526
tree = self.make_branch_and_tree('.')
527
self.build_tree(['a', 'b'])
529
tree.commit('added a, b')
530
tree.remove(['a', 'b'])
531
tree.commit('removed a', specific_files='a')
532
basis = tree.basis_tree().inventory
533
self.assertIs(None, basis.path2id('a'))
534
self.assertFalse(basis.path2id('b') is None)
536
def test_commit_saves_1ms_timestamp(self):
537
"""Passing in a timestamp is saved with 1ms resolution"""
538
tree = self.make_branch_and_tree('.')
539
self.build_tree(['a'])
541
tree.commit('added a', timestamp=1153248633.4186721, timezone=0,
544
rev = tree.branch.repository.get_revision('a1')
545
self.assertEqual(1153248633.419, rev.timestamp)
547
def test_commit_has_1ms_resolution(self):
548
"""Allowing commit to generate the timestamp also has 1ms resolution"""
549
tree = self.make_branch_and_tree('.')
550
self.build_tree(['a'])
552
tree.commit('added a', rev_id='a1')
554
rev = tree.branch.repository.get_revision('a1')
555
timestamp = rev.timestamp
556
timestamp_1ms = round(timestamp, 3)
557
self.assertEqual(timestamp_1ms, timestamp)
559
def test_commit_kind_changes(self):
560
if not osutils.has_symlinks():
561
raise tests.TestSkipped('Test requires symlink support')
562
tree = self.make_branch_and_tree('.')
563
os.symlink('target', 'name')
564
tree.add('name', 'a-file-id')
565
tree.commit('Added a symlink')
566
self.assertEqual('symlink', tree.basis_tree().kind('a-file-id'))
569
self.build_tree(['name'])
570
tree.commit('Changed symlink to file')
571
self.assertEqual('file', tree.basis_tree().kind('a-file-id'))
574
os.symlink('target', 'name')
575
tree.commit('file to symlink')
576
self.assertEqual('symlink', tree.basis_tree().kind('a-file-id'))
580
tree.commit('symlink to directory')
581
self.assertEqual('directory', tree.basis_tree().kind('a-file-id'))
584
os.symlink('target', 'name')
585
tree.commit('directory to symlink')
586
self.assertEqual('symlink', tree.basis_tree().kind('a-file-id'))
588
# prepare for directory <-> file tests
591
tree.commit('symlink to directory')
592
self.assertEqual('directory', tree.basis_tree().kind('a-file-id'))
595
self.build_tree(['name'])
596
tree.commit('Changed directory to file')
597
self.assertEqual('file', tree.basis_tree().kind('a-file-id'))
601
tree.commit('file to directory')
602
self.assertEqual('directory', tree.basis_tree().kind('a-file-id'))
604
def test_commit_unversioned_specified(self):
605
"""Commit should raise if specified files isn't in basis or worktree"""
606
tree = self.make_branch_and_tree('.')
607
self.assertRaises(errors.PathsNotVersionedError, tree.commit,
608
'message', specific_files=['bogus'])
610
class Callback(object):
612
def __init__(self, message, testcase):
614
self.message = message
615
self.testcase = testcase
617
def __call__(self, commit_obj):
619
self.testcase.assertTrue(isinstance(commit_obj, Commit))
622
def test_commit_callback(self):
623
"""Commit should invoke a callback to get the message"""
625
tree = self.make_branch_and_tree('.')
629
self.assertTrue(isinstance(e, BzrError))
630
self.assertEqual('The message or message_callback keyword'
631
' parameter is required for commit().', str(e))
633
self.fail('exception not raised')
634
cb = self.Callback(u'commit 1', self)
635
tree.commit(message_callback=cb)
636
self.assertTrue(cb.called)
637
repository = tree.branch.repository
638
message = repository.get_revision(tree.last_revision()).message
639
self.assertEqual('commit 1', message)
641
def test_no_callback_pointless(self):
642
"""Callback should not be invoked for pointless commit"""
643
tree = self.make_branch_and_tree('.')
644
cb = self.Callback(u'commit 2', self)
645
self.assertRaises(PointlessCommit, tree.commit, message_callback=cb,
646
allow_pointless=False)
647
self.assertFalse(cb.called)
649
def test_no_callback_netfailure(self):
650
"""Callback should not be invoked if connectivity fails"""
651
tree = self.make_branch_and_tree('.')
652
cb = self.Callback(u'commit 2', self)
653
repository = tree.branch.repository
654
# simulate network failure
655
def raise_(self, arg, arg2):
656
raise errors.NoSuchFile('foo')
657
repository.add_inventory = raise_
658
self.assertRaises(errors.NoSuchFile, tree.commit, message_callback=cb)
659
self.assertFalse(cb.called)