1
# Copyright (C) 2005, 2006 by 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
21
from bzrlib.tests import TestCaseWithTransport
22
from bzrlib.branch import Branch
23
from bzrlib.bzrdir import BzrDir, BzrDirMetaFormat1
24
from bzrlib.workingtree import WorkingTree
25
from bzrlib.commit import Commit, NullCommitReporter
26
from bzrlib.config import BranchConfig
27
from bzrlib.errors import (PointlessCommit, BzrError, SigningFailed,
31
# TODO: Test commit with some added, and added-but-missing files
33
class MustSignConfig(BranchConfig):
35
def signature_needed(self):
38
def gpg_signing_command(self):
42
class BranchWithHooks(BranchConfig):
44
def post_commit(self):
45
return "bzrlib.ahook bzrlib.ahook"
48
class CapturingReporter(NullCommitReporter):
49
"""This reporter captures the calls made to it for evaluation later."""
52
# a list of the calls this received
55
def snapshot_change(self, change, path):
56
self.calls.append(('change', change, path))
58
def deleted(self, file_id):
59
self.calls.append(('deleted', file_id))
61
def missing(self, path):
62
self.calls.append(('missing', path))
64
def renamed(self, change, old_path, new_path):
65
self.calls.append(('renamed', change, old_path, new_path))
68
class TestCommit(TestCaseWithTransport):
70
def test_simple_commit(self):
71
"""Commit and check two versions of a single file."""
72
wt = self.make_branch_and_tree('.')
74
file('hello', 'w').write('hello world')
76
wt.commit(message='add hello')
77
file_id = wt.path2id('hello')
79
file('hello', 'w').write('version 2')
80
wt.commit(message='commit 2')
82
eq = self.assertEquals
84
rh = b.revision_history()
85
rev = b.repository.get_revision(rh[0])
86
eq(rev.message, 'add hello')
88
tree1 = b.repository.revision_tree(rh[0])
89
text = tree1.get_file_text(file_id)
90
eq(text, 'hello world')
92
tree2 = b.repository.revision_tree(rh[1])
93
eq(tree2.get_file_text(file_id), 'version 2')
95
def test_delete_commit(self):
96
"""Test a commit with a deleted file"""
97
wt = self.make_branch_and_tree('.')
99
file('hello', 'w').write('hello world')
100
wt.add(['hello'], ['hello-id'])
101
wt.commit(message='add hello')
104
wt.commit('removed hello', rev_id='rev2')
106
tree = b.repository.revision_tree('rev2')
107
self.assertFalse(tree.has_id('hello-id'))
109
def test_pointless_commit(self):
110
"""Commit refuses unless there are changes or it's forced."""
111
wt = self.make_branch_and_tree('.')
113
file('hello', 'w').write('hello')
115
wt.commit(message='add hello')
116
self.assertEquals(b.revno(), 1)
117
self.assertRaises(PointlessCommit,
120
allow_pointless=False)
121
self.assertEquals(b.revno(), 1)
123
def test_commit_empty(self):
124
"""Commiting an empty tree works."""
125
wt = self.make_branch_and_tree('.')
127
wt.commit(message='empty tree', allow_pointless=True)
128
self.assertRaises(PointlessCommit,
130
message='empty tree',
131
allow_pointless=False)
132
wt.commit(message='empty tree', allow_pointless=True)
133
self.assertEquals(b.revno(), 2)
135
def test_selective_delete(self):
136
"""Selective commit in tree with deletions"""
137
wt = self.make_branch_and_tree('.')
139
file('hello', 'w').write('hello')
140
file('buongia', 'w').write('buongia')
141
wt.add(['hello', 'buongia'],
142
['hello-id', 'buongia-id'])
143
wt.commit(message='add files',
147
file('buongia', 'w').write('new text')
148
wt.commit(message='update text',
149
specific_files=['buongia'],
150
allow_pointless=False,
153
wt.commit(message='remove hello',
154
specific_files=['hello'],
155
allow_pointless=False,
158
eq = self.assertEquals
161
tree2 = b.repository.revision_tree('test@rev-2')
162
self.assertTrue(tree2.has_filename('hello'))
163
self.assertEquals(tree2.get_file_text('hello-id'), 'hello')
164
self.assertEquals(tree2.get_file_text('buongia-id'), 'new text')
166
tree3 = b.repository.revision_tree('test@rev-3')
167
self.assertFalse(tree3.has_filename('hello'))
168
self.assertEquals(tree3.get_file_text('buongia-id'), 'new text')
170
def test_commit_rename(self):
171
"""Test commit of a revision where a file is renamed."""
172
tree = self.make_branch_and_tree('.')
174
self.build_tree(['hello'], line_endings='binary')
175
tree.add(['hello'], ['hello-id'])
176
tree.commit(message='one', rev_id='test@rev-1', allow_pointless=False)
178
tree.rename_one('hello', 'fruity')
179
tree.commit(message='renamed', rev_id='test@rev-2', allow_pointless=False)
181
eq = self.assertEquals
182
tree1 = b.repository.revision_tree('test@rev-1')
183
eq(tree1.id2path('hello-id'), 'hello')
184
eq(tree1.get_file_text('hello-id'), 'contents of hello\n')
185
self.assertFalse(tree1.has_filename('fruity'))
186
self.check_inventory_shape(tree1.inventory, ['hello'])
187
ie = tree1.inventory['hello-id']
188
eq(ie.revision, 'test@rev-1')
190
tree2 = b.repository.revision_tree('test@rev-2')
191
eq(tree2.id2path('hello-id'), 'fruity')
192
eq(tree2.get_file_text('hello-id'), 'contents of hello\n')
193
self.check_inventory_shape(tree2.inventory, ['fruity'])
194
ie = tree2.inventory['hello-id']
195
eq(ie.revision, 'test@rev-2')
197
def test_reused_rev_id(self):
198
"""Test that a revision id cannot be reused in a branch"""
199
wt = self.make_branch_and_tree('.')
201
wt.commit('initial', rev_id='test@rev-1', allow_pointless=True)
202
self.assertRaises(Exception,
206
allow_pointless=True)
208
def test_commit_move(self):
209
"""Test commit of revisions with moved files and directories"""
210
eq = self.assertEquals
211
wt = self.make_branch_and_tree('.')
214
self.build_tree(['hello', 'a/', 'b/'])
215
wt.add(['hello', 'a', 'b'], ['hello-id', 'a-id', 'b-id'])
216
wt.commit('initial', rev_id=r1, allow_pointless=False)
217
wt.move(['hello'], 'a')
219
wt.commit('two', rev_id=r2, allow_pointless=False)
220
self.check_inventory_shape(wt.read_working_inventory(),
221
['a', 'a/hello', 'b'])
225
wt.commit('three', rev_id=r3, allow_pointless=False)
226
self.check_inventory_shape(wt.read_working_inventory(),
227
['a', 'a/hello', 'a/b'])
228
self.check_inventory_shape(b.repository.get_revision_inventory(r3),
229
['a', 'a/hello', 'a/b'])
231
wt.move(['a/hello'], 'a/b')
233
wt.commit('four', rev_id=r4, allow_pointless=False)
234
self.check_inventory_shape(wt.read_working_inventory(),
235
['a', 'a/b/hello', 'a/b'])
237
inv = b.repository.get_revision_inventory(r4)
238
eq(inv['hello-id'].revision, r4)
239
eq(inv['a-id'].revision, r1)
240
eq(inv['b-id'].revision, r3)
242
def test_removed_commit(self):
243
"""Commit with a removed file"""
244
wt = self.make_branch_and_tree('.')
246
file('hello', 'w').write('hello world')
247
wt.add(['hello'], ['hello-id'])
248
wt.commit(message='add hello')
250
wt.commit('removed hello', rev_id='rev2')
252
tree = b.repository.revision_tree('rev2')
253
self.assertFalse(tree.has_id('hello-id'))
255
def test_committed_ancestry(self):
256
"""Test commit appends revisions to ancestry."""
257
wt = self.make_branch_and_tree('.')
261
file('hello', 'w').write((str(i) * 4) + '\n')
263
wt.add(['hello'], ['hello-id'])
264
rev_id = 'test@rev-%d' % (i+1)
265
rev_ids.append(rev_id)
266
wt.commit(message='rev %d' % (i+1),
268
eq = self.assertEquals
269
eq(b.revision_history(), rev_ids)
271
anc = b.repository.get_ancestry(rev_ids[i])
272
eq(anc, [None] + rev_ids[:i+1])
274
def test_commit_new_subdir_child_selective(self):
275
wt = self.make_branch_and_tree('.')
277
self.build_tree(['dir/', 'dir/file1', 'dir/file2'])
278
wt.add(['dir', 'dir/file1', 'dir/file2'],
279
['dirid', 'file1id', 'file2id'])
280
wt.commit('dir/file1', specific_files=['dir/file1'], rev_id='1')
281
inv = b.repository.get_inventory('1')
282
self.assertEqual('1', inv['dirid'].revision)
283
self.assertEqual('1', inv['file1id'].revision)
284
# FIXME: This should raise a KeyError I think, rbc20051006
285
self.assertRaises(BzrError, inv.__getitem__, 'file2id')
287
def test_strict_commit(self):
288
"""Try and commit with unknown files and strict = True, should fail."""
289
from bzrlib.errors import StrictCommitFailed
290
wt = self.make_branch_and_tree('.')
292
file('hello', 'w').write('hello world')
294
file('goodbye', 'w').write('goodbye cruel world!')
295
self.assertRaises(StrictCommitFailed, wt.commit,
296
message='add hello but not goodbye', strict=True)
298
def test_strict_commit_without_unknowns(self):
299
"""Try and commit with no unknown files and strict = True,
301
from bzrlib.errors import StrictCommitFailed
302
wt = self.make_branch_and_tree('.')
304
file('hello', 'w').write('hello world')
306
wt.commit(message='add hello', strict=True)
308
def test_nonstrict_commit(self):
309
"""Try and commit with unknown files and strict = False, should work."""
310
wt = self.make_branch_and_tree('.')
312
file('hello', 'w').write('hello world')
314
file('goodbye', 'w').write('goodbye cruel world!')
315
wt.commit(message='add hello but not goodbye', strict=False)
317
def test_nonstrict_commit_without_unknowns(self):
318
"""Try and commit with no unknown files and strict = False,
320
wt = self.make_branch_and_tree('.')
322
file('hello', 'w').write('hello world')
324
wt.commit(message='add hello', strict=False)
326
def test_signed_commit(self):
328
import bzrlib.commit as commit
329
oldstrategy = bzrlib.gpg.GPGStrategy
330
wt = self.make_branch_and_tree('.')
332
wt.commit("base", allow_pointless=True, rev_id='A')
333
self.failIf(branch.repository.has_signature_for_revision_id('A'))
335
from bzrlib.testament import Testament
336
# monkey patch gpg signing mechanism
337
bzrlib.gpg.GPGStrategy = bzrlib.gpg.LoopbackGPGStrategy
338
commit.Commit(config=MustSignConfig(branch)).commit(message="base",
339
allow_pointless=True,
342
self.assertEqual(Testament.from_revision(branch.repository,
343
'B').as_short_text(),
344
branch.repository.get_signature_text('B'))
346
bzrlib.gpg.GPGStrategy = oldstrategy
348
def test_commit_failed_signature(self):
350
import bzrlib.commit as commit
351
oldstrategy = bzrlib.gpg.GPGStrategy
352
wt = self.make_branch_and_tree('.')
354
wt.commit("base", allow_pointless=True, rev_id='A')
355
self.failIf(branch.repository.has_signature_for_revision_id('A'))
357
from bzrlib.testament import Testament
358
# monkey patch gpg signing mechanism
359
bzrlib.gpg.GPGStrategy = bzrlib.gpg.DisabledGPGStrategy
360
config = MustSignConfig(branch)
361
self.assertRaises(SigningFailed,
362
commit.Commit(config=config).commit,
364
allow_pointless=True,
367
branch = Branch.open(self.get_url('.'))
368
self.assertEqual(branch.revision_history(), ['A'])
369
self.failIf(branch.repository.has_revision('B'))
371
bzrlib.gpg.GPGStrategy = oldstrategy
373
def test_commit_invokes_hooks(self):
374
import bzrlib.commit as commit
375
wt = self.make_branch_and_tree('.')
378
def called(branch, rev_id):
379
calls.append('called')
380
bzrlib.ahook = called
382
config = BranchWithHooks(branch)
383
commit.Commit(config=config).commit(
385
allow_pointless=True,
386
rev_id='A', working_tree = wt)
387
self.assertEqual(['called', 'called'], calls)
391
def test_commit_object_doesnt_set_nick(self):
392
# using the Commit object directly does not set the branch nick.
393
wt = self.make_branch_and_tree('.')
395
c.commit(working_tree=wt, message='empty tree', allow_pointless=True)
396
self.assertEquals(wt.branch.revno(), 1)
398
wt.branch.repository.get_revision(
399
wt.branch.last_revision()).properties)
401
def test_safe_master_lock(self):
403
master = BzrDirMetaFormat1().initialize('master')
404
master.create_repository()
405
master_branch = master.create_branch()
406
master.create_workingtree()
407
bound = master.sprout('bound')
408
wt = bound.open_workingtree()
409
wt.branch.set_bound_location(os.path.realpath('master'))
410
master_branch.lock_write()
412
self.assertRaises(LockContention, wt.commit, 'silly')
414
master_branch.unlock()
416
def test_commit_bound_merge(self):
417
# see bug #43959; commit of a merge in a bound branch fails to push
418
# the new commit into the master
419
master_branch = self.make_branch('master')
420
bound_tree = self.make_branch_and_tree('bound')
421
bound_tree.branch.bind(master_branch)
423
self.build_tree_contents([('bound/content_file', 'initial contents\n')])
424
bound_tree.add(['content_file'])
425
bound_tree.commit(message='woo!')
427
other_bzrdir = master_branch.bzrdir.sprout('other')
428
other_tree = other_bzrdir.open_workingtree()
430
# do a commit to the the other branch changing the content file so
431
# that our commit after merging will have a merged revision in the
432
# content file history.
433
self.build_tree_contents([('other/content_file', 'change in other\n')])
434
other_tree.commit('change in other')
436
# do a merge into the bound branch from other, and then change the
437
# content file locally to force a new revision (rather than using the
438
# revision from other). This forces extra processing in commit.
439
self.merge(other_tree.branch, bound_tree)
440
self.build_tree_contents([('bound/content_file', 'change in bound\n')])
442
# before #34959 was fixed, this failed with 'revision not present in
443
# weave' when trying to implicitly push from the bound branch to the master
444
bound_tree.commit(message='commit of merge in bound tree')
446
def test_commit_reporting_after_merge(self):
447
# when doing a commit of a merge, the reporter needs to still
448
# be called for each item that is added/removed/deleted.
449
this_tree = self.make_branch_and_tree('this')
450
# we need a bunch of files and dirs, to perform one action on each.
453
'this/dirtoreparent/',
456
'this/filetoreparent',
473
this_tree.commit('create_files')
474
other_dir = this_tree.bzrdir.sprout('other')
475
other_tree = other_dir.open_workingtree()
476
other_tree.lock_write()
477
# perform the needed actions on the files and dirs.
479
other_tree.rename_one('dirtorename', 'renameddir')
480
other_tree.rename_one('dirtoreparent', 'renameddir/reparenteddir')
481
other_tree.rename_one('filetorename', 'renamedfile')
482
other_tree.rename_one('filetoreparent', 'renameddir/reparentedfile')
483
other_tree.remove(['dirtoremove', 'filetoremove'])
484
self.build_tree_contents([
486
('other/filetomodify', 'new content'),
487
('other/newfile', 'new file content')])
488
other_tree.add('newfile')
489
other_tree.add('newdir/')
490
other_tree.commit('modify all sample files and dirs.')
493
self.merge(other_tree.branch, this_tree)
494
reporter = CapturingReporter()
495
this_tree.commit('do the commit', reporter=reporter)
497
('change', 'unchanged', 'dirtoleave'),
498
('change', 'unchanged', 'filetoleave'),
499
('change', 'modified', 'filetomodify'),
500
('change', 'added', 'newdir'),
501
('change', 'added', 'newfile'),
502
('renamed', 'renamed', 'dirtorename', 'renameddir'),
503
('renamed', 'renamed', 'dirtoreparent', 'renameddir/reparenteddir'),
504
('renamed', 'renamed', 'filetoreparent', 'renameddir/reparentedfile'),
505
('renamed', 'renamed', 'filetorename', 'renamedfile'),
506
('deleted', 'dirtoremove'),
507
('deleted', 'filetoremove'),
511
def test_commit_removals_respects_filespec(self):
512
"""Commit respects the specified_files for removals."""
513
tree = self.make_branch_and_tree('.')
514
self.build_tree(['a', 'b'])
516
tree.commit('added a, b')
517
tree.remove(['a', 'b'])
518
Commit().commit(message='removed a', working_tree=tree,
520
basis = tree.basis_tree().inventory
521
self.assertIs(None, basis.path2id('a'))
522
self.assertFalse(basis.path2id('b') is None)