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
25
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
35
# 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):
74
def test_simple_commit(self):
75
"""Commit and check two versions of a single file."""
76
wt = self.make_branch_and_tree('.')
78
file('hello', 'w').write('hello world')
80
wt.commit(message='add hello')
81
file_id = wt.path2id('hello')
83
file('hello', 'w').write('version 2')
84
wt.commit(message='commit 2')
86
eq = self.assertEquals
88
rh = b.revision_history()
89
rev = b.repository.get_revision(rh[0])
90
eq(rev.message, 'add hello')
92
tree1 = b.repository.revision_tree(rh[0])
93
text = tree1.get_file_text(file_id)
94
eq(text, 'hello world')
96
tree2 = b.repository.revision_tree(rh[1])
97
eq(tree2.get_file_text(file_id), 'version 2')
99
def test_delete_commit(self):
100
"""Test a commit with a deleted file"""
101
wt = self.make_branch_and_tree('.')
103
file('hello', 'w').write('hello world')
104
wt.add(['hello'], ['hello-id'])
105
wt.commit(message='add hello')
108
wt.commit('removed hello', rev_id='rev2')
110
tree = b.repository.revision_tree('rev2')
111
self.assertFalse(tree.has_id('hello-id'))
113
def test_pointless_commit(self):
114
"""Commit refuses unless there are changes or it's forced."""
115
wt = self.make_branch_and_tree('.')
117
file('hello', 'w').write('hello')
119
wt.commit(message='add hello')
120
self.assertEquals(b.revno(), 1)
121
self.assertRaises(PointlessCommit,
124
allow_pointless=False)
125
self.assertEquals(b.revno(), 1)
127
def test_commit_empty(self):
128
"""Commiting an empty tree works."""
129
wt = self.make_branch_and_tree('.')
131
wt.commit(message='empty tree', allow_pointless=True)
132
self.assertRaises(PointlessCommit,
134
message='empty tree',
135
allow_pointless=False)
136
wt.commit(message='empty tree', allow_pointless=True)
137
self.assertEquals(b.revno(), 2)
139
def test_selective_delete(self):
140
"""Selective commit in tree with deletions"""
141
wt = self.make_branch_and_tree('.')
143
file('hello', 'w').write('hello')
144
file('buongia', 'w').write('buongia')
145
wt.add(['hello', 'buongia'],
146
['hello-id', 'buongia-id'])
147
wt.commit(message='add files',
151
file('buongia', 'w').write('new text')
152
wt.commit(message='update text',
153
specific_files=['buongia'],
154
allow_pointless=False,
157
wt.commit(message='remove hello',
158
specific_files=['hello'],
159
allow_pointless=False,
162
eq = self.assertEquals
165
tree2 = b.repository.revision_tree('test@rev-2')
166
self.assertTrue(tree2.has_filename('hello'))
167
self.assertEquals(tree2.get_file_text('hello-id'), 'hello')
168
self.assertEquals(tree2.get_file_text('buongia-id'), 'new text')
170
tree3 = b.repository.revision_tree('test@rev-3')
171
self.assertFalse(tree3.has_filename('hello'))
172
self.assertEquals(tree3.get_file_text('buongia-id'), 'new text')
174
def test_commit_rename(self):
175
"""Test commit of a revision where a file is renamed."""
176
tree = self.make_branch_and_tree('.')
178
self.build_tree(['hello'], line_endings='binary')
179
tree.add(['hello'], ['hello-id'])
180
tree.commit(message='one', rev_id='test@rev-1', allow_pointless=False)
182
tree.rename_one('hello', 'fruity')
183
tree.commit(message='renamed', rev_id='test@rev-2', allow_pointless=False)
185
eq = self.assertEquals
186
tree1 = b.repository.revision_tree('test@rev-1')
187
eq(tree1.id2path('hello-id'), 'hello')
188
eq(tree1.get_file_text('hello-id'), 'contents of hello\n')
189
self.assertFalse(tree1.has_filename('fruity'))
190
self.check_inventory_shape(tree1.inventory, ['hello'])
191
ie = tree1.inventory['hello-id']
192
eq(ie.revision, 'test@rev-1')
194
tree2 = b.repository.revision_tree('test@rev-2')
195
eq(tree2.id2path('hello-id'), 'fruity')
196
eq(tree2.get_file_text('hello-id'), 'contents of hello\n')
197
self.check_inventory_shape(tree2.inventory, ['fruity'])
198
ie = tree2.inventory['hello-id']
199
eq(ie.revision, 'test@rev-2')
201
def test_reused_rev_id(self):
202
"""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)
206
self.assertRaises(Exception,
210
allow_pointless=True)
212
def test_commit_move(self):
213
"""Test commit of revisions with moved files and directories"""
214
eq = self.assertEquals
215
wt = self.make_branch_and_tree('.')
218
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')
223
wt.commit('two', rev_id=r2, allow_pointless=False)
224
self.check_inventory_shape(wt.read_working_inventory(),
225
['a', 'a/hello', 'b'])
229
wt.commit('three', rev_id=r3, allow_pointless=False)
230
self.check_inventory_shape(wt.read_working_inventory(),
231
['a', 'a/hello', 'a/b'])
232
self.check_inventory_shape(b.repository.get_revision_inventory(r3),
233
['a', 'a/hello', 'a/b'])
235
wt.move(['a/hello'], 'a/b')
237
wt.commit('four', rev_id=r4, allow_pointless=False)
238
self.check_inventory_shape(wt.read_working_inventory(),
239
['a', 'a/b/hello', 'a/b'])
241
inv = b.repository.get_revision_inventory(r4)
242
eq(inv['hello-id'].revision, r4)
243
eq(inv['a-id'].revision, r1)
244
eq(inv['b-id'].revision, r3)
246
def test_removed_commit(self):
247
"""Commit with a removed file"""
248
wt = self.make_branch_and_tree('.')
250
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')
257
self.assertFalse(tree.has_id('hello-id'))
259
def test_committed_ancestry(self):
260
"""Test commit appends revisions to ancestry."""
261
wt = self.make_branch_and_tree('.')
265
file('hello', 'w').write((str(i) * 4) + '\n')
267
wt.add(['hello'], ['hello-id'])
268
rev_id = 'test@rev-%d' % (i+1)
269
rev_ids.append(rev_id)
270
wt.commit(message='rev %d' % (i+1),
272
eq = self.assertEquals
273
eq(b.revision_history(), rev_ids)
275
anc = b.repository.get_ancestry(rev_ids[i])
276
eq(anc, [None] + rev_ids[:i+1])
278
def test_commit_new_subdir_child_selective(self):
279
wt = self.make_branch_and_tree('.')
281
self.build_tree(['dir/', 'dir/file1', 'dir/file2'])
282
wt.add(['dir', 'dir/file1', 'dir/file2'],
283
['dirid', 'file1id', 'file2id'])
284
wt.commit('dir/file1', specific_files=['dir/file1'], rev_id='1')
285
inv = b.repository.get_inventory('1')
286
self.assertEqual('1', inv['dirid'].revision)
287
self.assertEqual('1', inv['file1id'].revision)
288
# FIXME: This should raise a KeyError I think, rbc20051006
289
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'])
561
class Callback(object):
563
def __init__(self, message, testcase):
565
self.message = message
566
self.testcase = testcase
568
def __call__(self, commit_obj):
570
self.testcase.assertTrue(isinstance(commit_obj, Commit))
573
def test_commit_callback(self):
574
"""Commit should invoke a callback to get the message"""
576
tree = self.make_branch_and_tree('.')
580
self.assertTrue(isinstance(e, BzrError))
581
self.assertEqual('The message or message_callback keyword'
582
' parameter is required for commit().', str(e))
584
self.fail('exception not raised')
585
cb = self.Callback(u'commit 1', self)
586
tree.commit(message_callback=cb)
587
self.assertTrue(cb.called)
588
repository = tree.branch.repository
589
message = repository.get_revision(tree.last_revision()).message
590
self.assertEqual('commit 1', message)
592
def test_no_callback_pointless(self):
593
"""Callback should not be invoked for pointless commit"""
594
tree = self.make_branch_and_tree('.')
595
cb = self.Callback(u'commit 2', self)
596
self.assertRaises(PointlessCommit, tree.commit, message_callback=cb,
597
allow_pointless=False)
598
self.assertFalse(cb.called)
600
def test_no_callback_netfailure(self):
601
"""Callback should not be invoked if connectivity fails"""
602
tree = self.make_branch_and_tree('.')
603
cb = self.Callback(u'commit 2', self)
604
repository = tree.branch.repository
605
# simulate network failure
606
def raise_(self, arg, arg2):
607
raise errors.NoSuchFile('foo')
608
repository.add_inventory = raise_
609
self.assertRaises(errors.NoSuchFile, tree.commit, message_callback=cb)
610
self.assertFalse(cb.called)