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 SymlinkFeature, 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))
77
class TestCommit(TestCaseWithTransport):
79
def test_simple_commit(self):
80
"""Commit and check two versions of a single file."""
81
wt = self.make_branch_and_tree('.')
83
file('hello', 'w').write('hello world')
85
wt.commit(message='add hello')
86
file_id = wt.path2id('hello')
88
file('hello', 'w').write('version 2')
89
wt.commit(message='commit 2')
91
eq = self.assertEquals
93
rh = b.revision_history()
94
rev = b.repository.get_revision(rh[0])
95
eq(rev.message, 'add hello')
97
tree1 = b.repository.revision_tree(rh[0])
98
text = tree1.get_file_text(file_id)
99
eq(text, 'hello world')
101
tree2 = b.repository.revision_tree(rh[1])
102
eq(tree2.get_file_text(file_id), 'version 2')
104
def test_delete_commit(self):
105
"""Test a commit with a deleted file"""
106
wt = self.make_branch_and_tree('.')
108
file('hello', 'w').write('hello world')
109
wt.add(['hello'], ['hello-id'])
110
wt.commit(message='add hello')
113
wt.commit('removed hello', rev_id='rev2')
115
tree = b.repository.revision_tree('rev2')
116
self.assertFalse(tree.has_id('hello-id'))
118
def test_pointless_commit(self):
119
"""Commit refuses unless there are changes or it's forced."""
120
wt = self.make_branch_and_tree('.')
122
file('hello', 'w').write('hello')
124
wt.commit(message='add hello')
125
self.assertEquals(b.revno(), 1)
126
self.assertRaises(PointlessCommit,
129
allow_pointless=False)
130
self.assertEquals(b.revno(), 1)
132
def test_commit_empty(self):
133
"""Commiting an empty tree works."""
134
wt = self.make_branch_and_tree('.')
136
wt.commit(message='empty tree', allow_pointless=True)
137
self.assertRaises(PointlessCommit,
139
message='empty tree',
140
allow_pointless=False)
141
wt.commit(message='empty tree', allow_pointless=True)
142
self.assertEquals(b.revno(), 2)
144
def test_selective_delete(self):
145
"""Selective commit in tree with deletions"""
146
wt = self.make_branch_and_tree('.')
148
file('hello', 'w').write('hello')
149
file('buongia', 'w').write('buongia')
150
wt.add(['hello', 'buongia'],
151
['hello-id', 'buongia-id'])
152
wt.commit(message='add files',
156
file('buongia', 'w').write('new text')
157
wt.commit(message='update text',
158
specific_files=['buongia'],
159
allow_pointless=False,
162
wt.commit(message='remove hello',
163
specific_files=['hello'],
164
allow_pointless=False,
167
eq = self.assertEquals
170
tree2 = b.repository.revision_tree('test@rev-2')
171
self.assertTrue(tree2.has_filename('hello'))
172
self.assertEquals(tree2.get_file_text('hello-id'), 'hello')
173
self.assertEquals(tree2.get_file_text('buongia-id'), 'new text')
175
tree3 = b.repository.revision_tree('test@rev-3')
176
self.assertFalse(tree3.has_filename('hello'))
177
self.assertEquals(tree3.get_file_text('buongia-id'), 'new text')
179
def test_commit_rename(self):
180
"""Test commit of a revision where a file is renamed."""
181
tree = self.make_branch_and_tree('.')
183
self.build_tree(['hello'], line_endings='binary')
184
tree.add(['hello'], ['hello-id'])
185
tree.commit(message='one', rev_id='test@rev-1', allow_pointless=False)
187
tree.rename_one('hello', 'fruity')
188
tree.commit(message='renamed', rev_id='test@rev-2', allow_pointless=False)
190
eq = self.assertEquals
191
tree1 = b.repository.revision_tree('test@rev-1')
192
eq(tree1.id2path('hello-id'), 'hello')
193
eq(tree1.get_file_text('hello-id'), 'contents of hello\n')
194
self.assertFalse(tree1.has_filename('fruity'))
195
self.check_inventory_shape(tree1.inventory, ['hello'])
196
ie = tree1.inventory['hello-id']
197
eq(ie.revision, 'test@rev-1')
199
tree2 = b.repository.revision_tree('test@rev-2')
200
eq(tree2.id2path('hello-id'), 'fruity')
201
eq(tree2.get_file_text('hello-id'), 'contents of hello\n')
202
self.check_inventory_shape(tree2.inventory, ['fruity'])
203
ie = tree2.inventory['hello-id']
204
eq(ie.revision, 'test@rev-2')
206
def test_reused_rev_id(self):
207
"""Test that a revision id cannot be reused in a branch"""
208
wt = self.make_branch_and_tree('.')
210
wt.commit('initial', rev_id='test@rev-1', allow_pointless=True)
211
self.assertRaises(Exception,
215
allow_pointless=True)
217
def test_commit_move(self):
218
"""Test commit of revisions with moved files and directories"""
219
eq = self.assertEquals
220
wt = self.make_branch_and_tree('.')
223
self.build_tree(['hello', 'a/', 'b/'])
224
wt.add(['hello', 'a', 'b'], ['hello-id', 'a-id', 'b-id'])
225
wt.commit('initial', rev_id=r1, allow_pointless=False)
226
wt.move(['hello'], 'a')
228
wt.commit('two', rev_id=r2, allow_pointless=False)
231
self.check_inventory_shape(wt.read_working_inventory(),
232
['a/', 'a/hello', 'b/'])
238
wt.commit('three', rev_id=r3, allow_pointless=False)
241
self.check_inventory_shape(wt.read_working_inventory(),
242
['a/', 'a/hello', 'a/b/'])
243
self.check_inventory_shape(b.repository.get_revision_inventory(r3),
244
['a/', 'a/hello', 'a/b/'])
248
wt.move(['a/hello'], 'a/b')
250
wt.commit('four', rev_id=r4, allow_pointless=False)
253
self.check_inventory_shape(wt.read_working_inventory(),
254
['a/', 'a/b/hello', 'a/b/'])
258
inv = b.repository.get_revision_inventory(r4)
259
eq(inv['hello-id'].revision, r4)
260
eq(inv['a-id'].revision, r1)
261
eq(inv['b-id'].revision, r3)
263
def test_removed_commit(self):
264
"""Commit with a removed file"""
265
wt = self.make_branch_and_tree('.')
267
file('hello', 'w').write('hello world')
268
wt.add(['hello'], ['hello-id'])
269
wt.commit(message='add hello')
271
wt.commit('removed hello', rev_id='rev2')
273
tree = b.repository.revision_tree('rev2')
274
self.assertFalse(tree.has_id('hello-id'))
276
def test_committed_ancestry(self):
277
"""Test commit appends revisions to ancestry."""
278
wt = self.make_branch_and_tree('.')
282
file('hello', 'w').write((str(i) * 4) + '\n')
284
wt.add(['hello'], ['hello-id'])
285
rev_id = 'test@rev-%d' % (i+1)
286
rev_ids.append(rev_id)
287
wt.commit(message='rev %d' % (i+1),
289
eq = self.assertEquals
290
eq(b.revision_history(), rev_ids)
292
anc = b.repository.get_ancestry(rev_ids[i])
293
eq(anc, [None] + rev_ids[:i+1])
295
def test_commit_new_subdir_child_selective(self):
296
wt = self.make_branch_and_tree('.')
298
self.build_tree(['dir/', 'dir/file1', 'dir/file2'])
299
wt.add(['dir', 'dir/file1', 'dir/file2'],
300
['dirid', 'file1id', 'file2id'])
301
wt.commit('dir/file1', specific_files=['dir/file1'], rev_id='1')
302
inv = b.repository.get_inventory('1')
303
self.assertEqual('1', inv['dirid'].revision)
304
self.assertEqual('1', inv['file1id'].revision)
305
# FIXME: This should raise a KeyError I think, rbc20051006
306
self.assertRaises(BzrError, inv.__getitem__, 'file2id')
308
def test_strict_commit(self):
309
"""Try and commit with unknown files and strict = True, should fail."""
310
from bzrlib.errors import StrictCommitFailed
311
wt = self.make_branch_and_tree('.')
313
file('hello', 'w').write('hello world')
315
file('goodbye', 'w').write('goodbye cruel world!')
316
self.assertRaises(StrictCommitFailed, wt.commit,
317
message='add hello but not goodbye', strict=True)
319
def test_strict_commit_without_unknowns(self):
320
"""Try and commit with no unknown files and strict = True,
322
from bzrlib.errors import StrictCommitFailed
323
wt = self.make_branch_and_tree('.')
325
file('hello', 'w').write('hello world')
327
wt.commit(message='add hello', strict=True)
329
def test_nonstrict_commit(self):
330
"""Try and commit with unknown files and strict = False, should work."""
331
wt = self.make_branch_and_tree('.')
333
file('hello', 'w').write('hello world')
335
file('goodbye', 'w').write('goodbye cruel world!')
336
wt.commit(message='add hello but not goodbye', strict=False)
338
def test_nonstrict_commit_without_unknowns(self):
339
"""Try and commit with no unknown files and strict = False,
341
wt = self.make_branch_and_tree('.')
343
file('hello', 'w').write('hello world')
345
wt.commit(message='add hello', strict=False)
347
def test_signed_commit(self):
349
import bzrlib.commit as commit
350
oldstrategy = bzrlib.gpg.GPGStrategy
351
wt = self.make_branch_and_tree('.')
353
wt.commit("base", allow_pointless=True, rev_id='A')
354
self.failIf(branch.repository.has_signature_for_revision_id('A'))
356
from bzrlib.testament import Testament
357
# monkey patch gpg signing mechanism
358
bzrlib.gpg.GPGStrategy = bzrlib.gpg.LoopbackGPGStrategy
359
commit.Commit(config=MustSignConfig(branch)).commit(message="base",
360
allow_pointless=True,
364
return bzrlib.gpg.LoopbackGPGStrategy(None).sign(text)
365
self.assertEqual(sign(Testament.from_revision(branch.repository,
366
'B').as_short_text()),
367
branch.repository.get_signature_text('B'))
369
bzrlib.gpg.GPGStrategy = oldstrategy
371
def test_commit_failed_signature(self):
373
import bzrlib.commit as commit
374
oldstrategy = bzrlib.gpg.GPGStrategy
375
wt = self.make_branch_and_tree('.')
377
wt.commit("base", allow_pointless=True, rev_id='A')
378
self.failIf(branch.repository.has_signature_for_revision_id('A'))
380
from bzrlib.testament import Testament
381
# monkey patch gpg signing mechanism
382
bzrlib.gpg.GPGStrategy = bzrlib.gpg.DisabledGPGStrategy
383
config = MustSignConfig(branch)
384
self.assertRaises(SigningFailed,
385
commit.Commit(config=config).commit,
387
allow_pointless=True,
390
branch = Branch.open(self.get_url('.'))
391
self.assertEqual(branch.revision_history(), ['A'])
392
self.failIf(branch.repository.has_revision('B'))
394
bzrlib.gpg.GPGStrategy = oldstrategy
396
def test_commit_invokes_hooks(self):
397
import bzrlib.commit as commit
398
wt = self.make_branch_and_tree('.')
401
def called(branch, rev_id):
402
calls.append('called')
403
bzrlib.ahook = called
405
config = BranchWithHooks(branch)
406
commit.Commit(config=config).commit(
408
allow_pointless=True,
409
rev_id='A', working_tree = wt)
410
self.assertEqual(['called', 'called'], calls)
414
def test_commit_object_doesnt_set_nick(self):
415
# using the Commit object directly does not set the branch nick.
416
wt = self.make_branch_and_tree('.')
418
c.commit(working_tree=wt, message='empty tree', allow_pointless=True)
419
self.assertEquals(wt.branch.revno(), 1)
421
wt.branch.repository.get_revision(
422
wt.branch.last_revision()).properties)
424
def test_safe_master_lock(self):
426
master = BzrDirMetaFormat1().initialize('master')
427
master.create_repository()
428
master_branch = master.create_branch()
429
master.create_workingtree()
430
bound = master.sprout('bound')
431
wt = bound.open_workingtree()
432
wt.branch.set_bound_location(os.path.realpath('master'))
434
orig_default = lockdir._DEFAULT_TIMEOUT_SECONDS
435
master_branch.lock_write()
437
lockdir._DEFAULT_TIMEOUT_SECONDS = 1
438
self.assertRaises(LockContention, wt.commit, 'silly')
440
lockdir._DEFAULT_TIMEOUT_SECONDS = orig_default
441
master_branch.unlock()
443
def test_commit_bound_merge(self):
444
# see bug #43959; commit of a merge in a bound branch fails to push
445
# the new commit into the master
446
master_branch = self.make_branch('master')
447
bound_tree = self.make_branch_and_tree('bound')
448
bound_tree.branch.bind(master_branch)
450
self.build_tree_contents([('bound/content_file', 'initial contents\n')])
451
bound_tree.add(['content_file'])
452
bound_tree.commit(message='woo!')
454
other_bzrdir = master_branch.bzrdir.sprout('other')
455
other_tree = other_bzrdir.open_workingtree()
457
# do a commit to the the other branch changing the content file so
458
# that our commit after merging will have a merged revision in the
459
# content file history.
460
self.build_tree_contents([('other/content_file', 'change in other\n')])
461
other_tree.commit('change in other')
463
# do a merge into the bound branch from other, and then change the
464
# content file locally to force a new revision (rather than using the
465
# revision from other). This forces extra processing in commit.
466
bound_tree.merge_from_branch(other_tree.branch)
467
self.build_tree_contents([('bound/content_file', 'change in bound\n')])
469
# before #34959 was fixed, this failed with 'revision not present in
470
# weave' when trying to implicitly push from the bound branch to the master
471
bound_tree.commit(message='commit of merge in bound tree')
473
def test_commit_reporting_after_merge(self):
474
# when doing a commit of a merge, the reporter needs to still
475
# be called for each item that is added/removed/deleted.
476
this_tree = self.make_branch_and_tree('this')
477
# we need a bunch of files and dirs, to perform one action on each.
480
'this/dirtoreparent/',
483
'this/filetoreparent',
500
this_tree.commit('create_files')
501
other_dir = this_tree.bzrdir.sprout('other')
502
other_tree = other_dir.open_workingtree()
503
other_tree.lock_write()
504
# perform the needed actions on the files and dirs.
506
other_tree.rename_one('dirtorename', 'renameddir')
507
other_tree.rename_one('dirtoreparent', 'renameddir/reparenteddir')
508
other_tree.rename_one('filetorename', 'renamedfile')
509
other_tree.rename_one('filetoreparent', 'renameddir/reparentedfile')
510
other_tree.remove(['dirtoremove', 'filetoremove'])
511
self.build_tree_contents([
513
('other/filetomodify', 'new content'),
514
('other/newfile', 'new file content')])
515
other_tree.add('newfile')
516
other_tree.add('newdir/')
517
other_tree.commit('modify all sample files and dirs.')
520
this_tree.merge_from_branch(other_tree.branch)
521
reporter = CapturingReporter()
522
this_tree.commit('do the commit', reporter=reporter)
524
('change', 'unchanged', ''),
525
('change', 'unchanged', 'dirtoleave'),
526
('change', 'unchanged', 'filetoleave'),
527
('change', 'modified', 'filetomodify'),
528
('change', 'added', 'newdir'),
529
('change', 'added', 'newfile'),
530
('renamed', 'renamed', 'dirtorename', 'renameddir'),
531
('renamed', 'renamed', 'filetorename', 'renamedfile'),
532
('renamed', 'renamed', 'dirtoreparent', 'renameddir/reparenteddir'),
533
('renamed', 'renamed', 'filetoreparent', 'renameddir/reparentedfile'),
534
('deleted', 'dirtoremove'),
535
('deleted', 'filetoremove'),
539
def test_commit_removals_respects_filespec(self):
540
"""Commit respects the specified_files for removals."""
541
tree = self.make_branch_and_tree('.')
542
self.build_tree(['a', 'b'])
544
tree.commit('added a, b')
545
tree.remove(['a', 'b'])
546
tree.commit('removed a', specific_files='a')
547
basis = tree.basis_tree()
550
self.assertIs(None, basis.path2id('a'))
551
self.assertFalse(basis.path2id('b') is None)
555
def test_commit_saves_1ms_timestamp(self):
556
"""Passing in a timestamp is saved with 1ms resolution"""
557
tree = self.make_branch_and_tree('.')
558
self.build_tree(['a'])
560
tree.commit('added a', timestamp=1153248633.4186721, timezone=0,
563
rev = tree.branch.repository.get_revision('a1')
564
self.assertEqual(1153248633.419, rev.timestamp)
566
def test_commit_has_1ms_resolution(self):
567
"""Allowing commit to generate the timestamp also has 1ms resolution"""
568
tree = self.make_branch_and_tree('.')
569
self.build_tree(['a'])
571
tree.commit('added a', rev_id='a1')
573
rev = tree.branch.repository.get_revision('a1')
574
timestamp = rev.timestamp
575
timestamp_1ms = round(timestamp, 3)
576
self.assertEqual(timestamp_1ms, timestamp)
578
def assertBasisTreeKind(self, kind, tree, file_id):
579
basis = tree.basis_tree()
582
self.assertEqual(kind, basis.kind(file_id))
586
def test_commit_kind_changes(self):
587
self.requireFeature(SymlinkFeature)
588
tree = self.make_branch_and_tree('.')
589
os.symlink('target', 'name')
590
tree.add('name', 'a-file-id')
591
tree.commit('Added a symlink')
592
self.assertBasisTreeKind('symlink', tree, 'a-file-id')
595
self.build_tree(['name'])
596
tree.commit('Changed symlink to file')
597
self.assertBasisTreeKind('file', tree, 'a-file-id')
600
os.symlink('target', 'name')
601
tree.commit('file to symlink')
602
self.assertBasisTreeKind('symlink', tree, 'a-file-id')
606
tree.commit('symlink to directory')
607
self.assertBasisTreeKind('directory', tree, 'a-file-id')
610
os.symlink('target', 'name')
611
tree.commit('directory to symlink')
612
self.assertBasisTreeKind('symlink', tree, 'a-file-id')
614
# prepare for directory <-> file tests
617
tree.commit('symlink to directory')
618
self.assertBasisTreeKind('directory', tree, 'a-file-id')
621
self.build_tree(['name'])
622
tree.commit('Changed directory to file')
623
self.assertBasisTreeKind('file', tree, 'a-file-id')
627
tree.commit('file to directory')
628
self.assertBasisTreeKind('directory', tree, 'a-file-id')
630
def test_commit_unversioned_specified(self):
631
"""Commit should raise if specified files isn't in basis or worktree"""
632
tree = self.make_branch_and_tree('.')
633
self.assertRaises(errors.PathsNotVersionedError, tree.commit,
634
'message', specific_files=['bogus'])
636
class Callback(object):
638
def __init__(self, message, testcase):
640
self.message = message
641
self.testcase = testcase
643
def __call__(self, commit_obj):
645
self.testcase.assertTrue(isinstance(commit_obj, Commit))
648
def test_commit_callback(self):
649
"""Commit should invoke a callback to get the message"""
651
tree = self.make_branch_and_tree('.')
655
self.assertTrue(isinstance(e, BzrError))
656
self.assertEqual('The message or message_callback keyword'
657
' parameter is required for commit().', str(e))
659
self.fail('exception not raised')
660
cb = self.Callback(u'commit 1', self)
661
tree.commit(message_callback=cb)
662
self.assertTrue(cb.called)
663
repository = tree.branch.repository
664
message = repository.get_revision(tree.last_revision()).message
665
self.assertEqual('commit 1', message)
667
def test_no_callback_pointless(self):
668
"""Callback should not be invoked for pointless commit"""
669
tree = self.make_branch_and_tree('.')
670
cb = self.Callback(u'commit 2', self)
671
self.assertRaises(PointlessCommit, tree.commit, message_callback=cb,
672
allow_pointless=False)
673
self.assertFalse(cb.called)
675
def test_no_callback_netfailure(self):
676
"""Callback should not be invoked if connectivity fails"""
677
tree = self.make_branch_and_tree('.')
678
cb = self.Callback(u'commit 2', self)
679
repository = tree.branch.repository
680
# simulate network failure
681
def raise_(self, arg, arg2):
682
raise errors.NoSuchFile('foo')
683
repository.add_inventory = raise_
684
self.assertRaises(errors.NoSuchFile, tree.commit, message_callback=cb)
685
self.assertFalse(cb.called)
687
def test_selected_file_merge_commit(self):
688
"""Ensure the correct error is raised"""
689
tree = self.make_branch_and_tree('foo')
690
# pending merge would turn into a left parent
691
tree.commit('commit 1')
692
tree.add_parent_tree_id('example')
693
self.build_tree(['foo/bar', 'foo/baz'])
694
tree.add(['bar', 'baz'])
695
err = self.assertRaises(errors.CannotCommitSelectedFileMerge,
696
tree.commit, 'commit 2', specific_files=['bar', 'baz'])
697
self.assertEqual(['bar', 'baz'], err.files)
698
self.assertEqual('Selected-file commit of merges is not supported'
699
' yet: files bar, baz', str(err))
701
def test_commit_ordering(self):
702
"""Test of corner-case commit ordering error"""
703
tree = self.make_branch_and_tree('.')
704
self.build_tree(['a/', 'a/z/', 'a/c/', 'a/z/x', 'a/z/y'])
705
tree.add(['a/', 'a/z/', 'a/c/', 'a/z/x', 'a/z/y'])
707
self.build_tree(['a/c/d/'])
709
tree.rename_one('a/z/x', 'a/c/d/x')
710
tree.commit('test', specific_files=['a/z/y'])
712
def test_commit_no_author(self):
713
"""The default kwarg author in MutableTree.commit should not add
714
the 'author' revision property.
716
tree = self.make_branch_and_tree('foo')
717
rev_id = tree.commit('commit 1')
718
rev = tree.branch.repository.get_revision(rev_id)
719
self.assertFalse('author' in rev.properties)
721
def test_commit_author(self):
722
"""Passing a non-empty author kwarg to MutableTree.commit should add
723
the 'author' revision property.
725
tree = self.make_branch_and_tree('foo')
726
rev_id = tree.commit('commit 1', author='John Doe <jdoe@example.com>')
727
rev = tree.branch.repository.get_revision(rev_id)
728
self.assertEqual('John Doe <jdoe@example.com>',
729
rev.properties['author'])