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,
348
self.assertEqual(Testament.from_revision(branch.repository,
349
'B').as_short_text(),
350
branch.repository.get_signature_text('B'))
352
bzrlib.gpg.GPGStrategy = oldstrategy
354
def test_commit_failed_signature(self):
356
import bzrlib.commit as commit
357
oldstrategy = bzrlib.gpg.GPGStrategy
358
wt = self.make_branch_and_tree('.')
360
wt.commit("base", allow_pointless=True, rev_id='A')
361
self.failIf(branch.repository.has_signature_for_revision_id('A'))
363
from bzrlib.testament import Testament
364
# monkey patch gpg signing mechanism
365
bzrlib.gpg.GPGStrategy = bzrlib.gpg.DisabledGPGStrategy
366
config = MustSignConfig(branch)
367
self.assertRaises(SigningFailed,
368
commit.Commit(config=config).commit,
370
allow_pointless=True,
373
branch = Branch.open(self.get_url('.'))
374
self.assertEqual(branch.revision_history(), ['A'])
375
self.failIf(branch.repository.has_revision('B'))
377
bzrlib.gpg.GPGStrategy = oldstrategy
379
def test_commit_invokes_hooks(self):
380
import bzrlib.commit as commit
381
wt = self.make_branch_and_tree('.')
384
def called(branch, rev_id):
385
calls.append('called')
386
bzrlib.ahook = called
388
config = BranchWithHooks(branch)
389
commit.Commit(config=config).commit(
391
allow_pointless=True,
392
rev_id='A', working_tree = wt)
393
self.assertEqual(['called', 'called'], calls)
397
def test_commit_object_doesnt_set_nick(self):
398
# using the Commit object directly does not set the branch nick.
399
wt = self.make_branch_and_tree('.')
401
c.commit(working_tree=wt, message='empty tree', allow_pointless=True)
402
self.assertEquals(wt.branch.revno(), 1)
404
wt.branch.repository.get_revision(
405
wt.branch.last_revision()).properties)
407
def test_safe_master_lock(self):
409
master = BzrDirMetaFormat1().initialize('master')
410
master.create_repository()
411
master_branch = master.create_branch()
412
master.create_workingtree()
413
bound = master.sprout('bound')
414
wt = bound.open_workingtree()
415
wt.branch.set_bound_location(os.path.realpath('master'))
417
orig_default = lockdir._DEFAULT_TIMEOUT_SECONDS
418
master_branch.lock_write()
420
lockdir._DEFAULT_TIMEOUT_SECONDS = 1
421
self.assertRaises(LockContention, wt.commit, 'silly')
423
lockdir._DEFAULT_TIMEOUT_SECONDS = orig_default
424
master_branch.unlock()
426
def test_commit_bound_merge(self):
427
# see bug #43959; commit of a merge in a bound branch fails to push
428
# the new commit into the master
429
master_branch = self.make_branch('master')
430
bound_tree = self.make_branch_and_tree('bound')
431
bound_tree.branch.bind(master_branch)
433
self.build_tree_contents([('bound/content_file', 'initial contents\n')])
434
bound_tree.add(['content_file'])
435
bound_tree.commit(message='woo!')
437
other_bzrdir = master_branch.bzrdir.sprout('other')
438
other_tree = other_bzrdir.open_workingtree()
440
# do a commit to the the other branch changing the content file so
441
# that our commit after merging will have a merged revision in the
442
# content file history.
443
self.build_tree_contents([('other/content_file', 'change in other\n')])
444
other_tree.commit('change in other')
446
# do a merge into the bound branch from other, and then change the
447
# content file locally to force a new revision (rather than using the
448
# revision from other). This forces extra processing in commit.
449
bound_tree.merge_from_branch(other_tree.branch)
450
self.build_tree_contents([('bound/content_file', 'change in bound\n')])
452
# before #34959 was fixed, this failed with 'revision not present in
453
# weave' when trying to implicitly push from the bound branch to the master
454
bound_tree.commit(message='commit of merge in bound tree')
456
def test_commit_reporting_after_merge(self):
457
# when doing a commit of a merge, the reporter needs to still
458
# be called for each item that is added/removed/deleted.
459
this_tree = self.make_branch_and_tree('this')
460
# we need a bunch of files and dirs, to perform one action on each.
463
'this/dirtoreparent/',
466
'this/filetoreparent',
483
this_tree.commit('create_files')
484
other_dir = this_tree.bzrdir.sprout('other')
485
other_tree = other_dir.open_workingtree()
486
other_tree.lock_write()
487
# perform the needed actions on the files and dirs.
489
other_tree.rename_one('dirtorename', 'renameddir')
490
other_tree.rename_one('dirtoreparent', 'renameddir/reparenteddir')
491
other_tree.rename_one('filetorename', 'renamedfile')
492
other_tree.rename_one('filetoreparent', 'renameddir/reparentedfile')
493
other_tree.remove(['dirtoremove', 'filetoremove'])
494
self.build_tree_contents([
496
('other/filetomodify', 'new content'),
497
('other/newfile', 'new file content')])
498
other_tree.add('newfile')
499
other_tree.add('newdir/')
500
other_tree.commit('modify all sample files and dirs.')
503
this_tree.merge_from_branch(other_tree.branch)
504
reporter = CapturingReporter()
505
this_tree.commit('do the commit', reporter=reporter)
507
('change', 'unchanged', ''),
508
('change', 'unchanged', 'dirtoleave'),
509
('change', 'unchanged', 'filetoleave'),
510
('change', 'modified', 'filetomodify'),
511
('change', 'added', 'newdir'),
512
('change', 'added', 'newfile'),
513
('renamed', 'renamed', 'dirtorename', 'renameddir'),
514
('renamed', 'renamed', 'dirtoreparent', 'renameddir/reparenteddir'),
515
('renamed', 'renamed', 'filetoreparent', 'renameddir/reparentedfile'),
516
('renamed', 'renamed', 'filetorename', 'renamedfile'),
517
('deleted', 'dirtoremove'),
518
('deleted', 'filetoremove'),
522
def test_commit_removals_respects_filespec(self):
523
"""Commit respects the specified_files for removals."""
524
tree = self.make_branch_and_tree('.')
525
self.build_tree(['a', 'b'])
527
tree.commit('added a, b')
528
tree.remove(['a', 'b'])
529
tree.commit('removed a', specific_files='a')
530
basis = tree.basis_tree().inventory
531
self.assertIs(None, basis.path2id('a'))
532
self.assertFalse(basis.path2id('b') is None)
534
def test_commit_saves_1ms_timestamp(self):
535
"""Passing in a timestamp is saved with 1ms resolution"""
536
tree = self.make_branch_and_tree('.')
537
self.build_tree(['a'])
539
tree.commit('added a', timestamp=1153248633.4186721, timezone=0,
542
rev = tree.branch.repository.get_revision('a1')
543
self.assertEqual(1153248633.419, rev.timestamp)
545
def test_commit_has_1ms_resolution(self):
546
"""Allowing commit to generate the timestamp also has 1ms resolution"""
547
tree = self.make_branch_and_tree('.')
548
self.build_tree(['a'])
550
tree.commit('added a', rev_id='a1')
552
rev = tree.branch.repository.get_revision('a1')
553
timestamp = rev.timestamp
554
timestamp_1ms = round(timestamp, 3)
555
self.assertEqual(timestamp_1ms, timestamp)
557
def test_commit_kind_changes(self):
558
if not osutils.has_symlinks():
559
raise tests.TestSkipped('Test requires symlink support')
560
tree = self.make_branch_and_tree('.')
561
os.symlink('target', 'name')
562
tree.add('name', 'a-file-id')
563
tree.commit('Added a symlink')
564
self.assertEqual('symlink', tree.basis_tree().kind('a-file-id'))
567
self.build_tree(['name'])
568
tree.commit('Changed symlink to file')
569
self.assertEqual('file', tree.basis_tree().kind('a-file-id'))
572
os.symlink('target', 'name')
573
tree.commit('file to symlink')
574
self.assertEqual('symlink', tree.basis_tree().kind('a-file-id'))
578
tree.commit('symlink to directory')
579
self.assertEqual('directory', tree.basis_tree().kind('a-file-id'))
582
os.symlink('target', 'name')
583
tree.commit('directory to symlink')
584
self.assertEqual('symlink', tree.basis_tree().kind('a-file-id'))
586
# prepare for directory <-> file tests
589
tree.commit('symlink to directory')
590
self.assertEqual('directory', tree.basis_tree().kind('a-file-id'))
593
self.build_tree(['name'])
594
tree.commit('Changed directory to file')
595
self.assertEqual('file', tree.basis_tree().kind('a-file-id'))
599
tree.commit('file to directory')
600
self.assertEqual('directory', tree.basis_tree().kind('a-file-id'))
602
def test_commit_unversioned_specified(self):
603
"""Commit should raise if specified files isn't in basis or worktree"""
604
tree = self.make_branch_and_tree('.')
605
self.assertRaises(errors.PathsNotVersionedError, tree.commit,
606
'message', specific_files=['bogus'])
608
class Callback(object):
610
def __init__(self, message, testcase):
612
self.message = message
613
self.testcase = testcase
615
def __call__(self, commit_obj):
617
self.testcase.assertTrue(isinstance(commit_obj, Commit))
620
def test_commit_callback(self):
621
"""Commit should invoke a callback to get the message"""
623
tree = self.make_branch_and_tree('.')
627
self.assertTrue(isinstance(e, BzrError))
628
self.assertEqual('The message or message_callback keyword'
629
' parameter is required for commit().', str(e))
631
self.fail('exception not raised')
632
cb = self.Callback(u'commit 1', self)
633
tree.commit(message_callback=cb)
634
self.assertTrue(cb.called)
635
repository = tree.branch.repository
636
message = repository.get_revision(tree.last_revision()).message
637
self.assertEqual('commit 1', message)
639
def test_no_callback_pointless(self):
640
"""Callback should not be invoked for pointless commit"""
641
tree = self.make_branch_and_tree('.')
642
cb = self.Callback(u'commit 2', self)
643
self.assertRaises(PointlessCommit, tree.commit, message_callback=cb,
644
allow_pointless=False)
645
self.assertFalse(cb.called)
647
def test_no_callback_netfailure(self):
648
"""Callback should not be invoked if connectivity fails"""
649
tree = self.make_branch_and_tree('.')
650
cb = self.Callback(u'commit 2', self)
651
repository = tree.branch.repository
652
# simulate network failure
653
def raise_(self, arg, arg2):
654
raise errors.NoSuchFile('foo')
655
repository.add_inventory = raise_
656
self.assertRaises(errors.NoSuchFile, tree.commit, message_callback=cb)
657
self.assertFalse(cb.called)