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 import errors
22
from bzrlib.tests import TestCaseWithTransport
23
from bzrlib.branch import Branch
24
from bzrlib.bzrdir import BzrDir, BzrDirMetaFormat1
25
from bzrlib.workingtree import WorkingTree
26
from bzrlib.commit import Commit, NullCommitReporter
27
from bzrlib.config import BranchConfig
28
from bzrlib.errors import (PointlessCommit, BzrError, SigningFailed,
32
# TODO: Test commit with some added, and added-but-missing files
34
class MustSignConfig(BranchConfig):
36
def signature_needed(self):
39
def gpg_signing_command(self):
43
class BranchWithHooks(BranchConfig):
45
def post_commit(self):
46
return "bzrlib.ahook bzrlib.ahook"
49
class CapturingReporter(NullCommitReporter):
50
"""This reporter captures the calls made to it for evaluation later."""
53
# a list of the calls this received
56
def snapshot_change(self, change, path):
57
self.calls.append(('change', change, path))
59
def deleted(self, file_id):
60
self.calls.append(('deleted', file_id))
62
def missing(self, path):
63
self.calls.append(('missing', path))
65
def renamed(self, change, old_path, new_path):
66
self.calls.append(('renamed', change, old_path, new_path))
69
class TestCommit(TestCaseWithTransport):
71
def test_simple_commit(self):
72
"""Commit and check two versions of a single file."""
73
wt = self.make_branch_and_tree('.')
75
file('hello', 'w').write('hello world')
77
wt.commit(message='add hello')
78
file_id = wt.path2id('hello')
80
file('hello', 'w').write('version 2')
81
wt.commit(message='commit 2')
83
eq = self.assertEquals
85
rh = b.revision_history()
86
rev = b.repository.get_revision(rh[0])
87
eq(rev.message, 'add hello')
89
tree1 = b.repository.revision_tree(rh[0])
90
text = tree1.get_file_text(file_id)
91
eq(text, 'hello world')
93
tree2 = b.repository.revision_tree(rh[1])
94
eq(tree2.get_file_text(file_id), 'version 2')
96
def test_delete_commit(self):
97
"""Test a commit with a deleted file"""
98
wt = self.make_branch_and_tree('.')
100
file('hello', 'w').write('hello world')
101
wt.add(['hello'], ['hello-id'])
102
wt.commit(message='add hello')
105
wt.commit('removed hello', rev_id='rev2')
107
tree = b.repository.revision_tree('rev2')
108
self.assertFalse(tree.has_id('hello-id'))
110
def test_pointless_commit(self):
111
"""Commit refuses unless there are changes or it's forced."""
112
wt = self.make_branch_and_tree('.')
114
file('hello', 'w').write('hello')
116
wt.commit(message='add hello')
117
self.assertEquals(b.revno(), 1)
118
self.assertRaises(PointlessCommit,
121
allow_pointless=False)
122
self.assertEquals(b.revno(), 1)
124
def test_commit_empty(self):
125
"""Commiting an empty tree works."""
126
wt = self.make_branch_and_tree('.')
128
wt.commit(message='empty tree', allow_pointless=True)
129
self.assertRaises(PointlessCommit,
131
message='empty tree',
132
allow_pointless=False)
133
wt.commit(message='empty tree', allow_pointless=True)
134
self.assertEquals(b.revno(), 2)
136
def test_selective_delete(self):
137
"""Selective commit in tree with deletions"""
138
wt = self.make_branch_and_tree('.')
140
file('hello', 'w').write('hello')
141
file('buongia', 'w').write('buongia')
142
wt.add(['hello', 'buongia'],
143
['hello-id', 'buongia-id'])
144
wt.commit(message='add files',
148
file('buongia', 'w').write('new text')
149
wt.commit(message='update text',
150
specific_files=['buongia'],
151
allow_pointless=False,
154
wt.commit(message='remove hello',
155
specific_files=['hello'],
156
allow_pointless=False,
159
eq = self.assertEquals
162
tree2 = b.repository.revision_tree('test@rev-2')
163
self.assertTrue(tree2.has_filename('hello'))
164
self.assertEquals(tree2.get_file_text('hello-id'), 'hello')
165
self.assertEquals(tree2.get_file_text('buongia-id'), 'new text')
167
tree3 = b.repository.revision_tree('test@rev-3')
168
self.assertFalse(tree3.has_filename('hello'))
169
self.assertEquals(tree3.get_file_text('buongia-id'), 'new text')
171
def test_commit_rename(self):
172
"""Test commit of a revision where a file is renamed."""
173
tree = self.make_branch_and_tree('.')
175
self.build_tree(['hello'], line_endings='binary')
176
tree.add(['hello'], ['hello-id'])
177
tree.commit(message='one', rev_id='test@rev-1', allow_pointless=False)
179
tree.rename_one('hello', 'fruity')
180
tree.commit(message='renamed', rev_id='test@rev-2', allow_pointless=False)
182
eq = self.assertEquals
183
tree1 = b.repository.revision_tree('test@rev-1')
184
eq(tree1.id2path('hello-id'), 'hello')
185
eq(tree1.get_file_text('hello-id'), 'contents of hello\n')
186
self.assertFalse(tree1.has_filename('fruity'))
187
self.check_inventory_shape(tree1.inventory, ['hello'])
188
ie = tree1.inventory['hello-id']
189
eq(ie.revision, 'test@rev-1')
191
tree2 = b.repository.revision_tree('test@rev-2')
192
eq(tree2.id2path('hello-id'), 'fruity')
193
eq(tree2.get_file_text('hello-id'), 'contents of hello\n')
194
self.check_inventory_shape(tree2.inventory, ['fruity'])
195
ie = tree2.inventory['hello-id']
196
eq(ie.revision, 'test@rev-2')
198
def test_reused_rev_id(self):
199
"""Test that a revision id cannot be reused in a branch"""
200
wt = self.make_branch_and_tree('.')
202
wt.commit('initial', rev_id='test@rev-1', allow_pointless=True)
203
self.assertRaises(Exception,
207
allow_pointless=True)
209
def test_commit_move(self):
210
"""Test commit of revisions with moved files and directories"""
211
eq = self.assertEquals
212
wt = self.make_branch_and_tree('.')
215
self.build_tree(['hello', 'a/', 'b/'])
216
wt.add(['hello', 'a', 'b'], ['hello-id', 'a-id', 'b-id'])
217
wt.commit('initial', rev_id=r1, allow_pointless=False)
218
wt.move(['hello'], 'a')
220
wt.commit('two', rev_id=r2, allow_pointless=False)
221
self.check_inventory_shape(wt.read_working_inventory(),
222
['a', 'a/hello', 'b'])
226
wt.commit('three', rev_id=r3, allow_pointless=False)
227
self.check_inventory_shape(wt.read_working_inventory(),
228
['a', 'a/hello', 'a/b'])
229
self.check_inventory_shape(b.repository.get_revision_inventory(r3),
230
['a', 'a/hello', 'a/b'])
232
wt.move(['a/hello'], 'a/b')
234
wt.commit('four', rev_id=r4, allow_pointless=False)
235
self.check_inventory_shape(wt.read_working_inventory(),
236
['a', 'a/b/hello', 'a/b'])
238
inv = b.repository.get_revision_inventory(r4)
239
eq(inv['hello-id'].revision, r4)
240
eq(inv['a-id'].revision, r1)
241
eq(inv['b-id'].revision, r3)
243
def test_removed_commit(self):
244
"""Commit with a removed file"""
245
wt = self.make_branch_and_tree('.')
247
file('hello', 'w').write('hello world')
248
wt.add(['hello'], ['hello-id'])
249
wt.commit(message='add hello')
251
wt.commit('removed hello', rev_id='rev2')
253
tree = b.repository.revision_tree('rev2')
254
self.assertFalse(tree.has_id('hello-id'))
256
def test_committed_ancestry(self):
257
"""Test commit appends revisions to ancestry."""
258
wt = self.make_branch_and_tree('.')
262
file('hello', 'w').write((str(i) * 4) + '\n')
264
wt.add(['hello'], ['hello-id'])
265
rev_id = 'test@rev-%d' % (i+1)
266
rev_ids.append(rev_id)
267
wt.commit(message='rev %d' % (i+1),
269
eq = self.assertEquals
270
eq(b.revision_history(), rev_ids)
272
anc = b.repository.get_ancestry(rev_ids[i])
273
eq(anc, [None] + rev_ids[:i+1])
275
def test_commit_new_subdir_child_selective(self):
276
wt = self.make_branch_and_tree('.')
278
self.build_tree(['dir/', 'dir/file1', 'dir/file2'])
279
wt.add(['dir', 'dir/file1', 'dir/file2'],
280
['dirid', 'file1id', 'file2id'])
281
wt.commit('dir/file1', specific_files=['dir/file1'], rev_id='1')
282
inv = b.repository.get_inventory('1')
283
self.assertEqual('1', inv['dirid'].revision)
284
self.assertEqual('1', inv['file1id'].revision)
285
# FIXME: This should raise a KeyError I think, rbc20051006
286
self.assertRaises(BzrError, inv.__getitem__, 'file2id')
288
def test_strict_commit(self):
289
"""Try and commit with unknown files and strict = True, should fail."""
290
from bzrlib.errors import StrictCommitFailed
291
wt = self.make_branch_and_tree('.')
293
file('hello', 'w').write('hello world')
295
file('goodbye', 'w').write('goodbye cruel world!')
296
self.assertRaises(StrictCommitFailed, wt.commit,
297
message='add hello but not goodbye', strict=True)
299
def test_strict_commit_without_unknowns(self):
300
"""Try and commit with no unknown files and strict = True,
302
from bzrlib.errors import StrictCommitFailed
303
wt = self.make_branch_and_tree('.')
305
file('hello', 'w').write('hello world')
307
wt.commit(message='add hello', strict=True)
309
def test_nonstrict_commit(self):
310
"""Try and commit with unknown files and strict = False, should work."""
311
wt = self.make_branch_and_tree('.')
313
file('hello', 'w').write('hello world')
315
file('goodbye', 'w').write('goodbye cruel world!')
316
wt.commit(message='add hello but not goodbye', strict=False)
318
def test_nonstrict_commit_without_unknowns(self):
319
"""Try and commit with no unknown files and strict = False,
321
wt = self.make_branch_and_tree('.')
323
file('hello', 'w').write('hello world')
325
wt.commit(message='add hello', strict=False)
327
def test_signed_commit(self):
329
import bzrlib.commit as commit
330
oldstrategy = bzrlib.gpg.GPGStrategy
331
wt = self.make_branch_and_tree('.')
333
wt.commit("base", allow_pointless=True, rev_id='A')
334
self.failIf(branch.repository.has_signature_for_revision_id('A'))
336
from bzrlib.testament import Testament
337
# monkey patch gpg signing mechanism
338
bzrlib.gpg.GPGStrategy = bzrlib.gpg.LoopbackGPGStrategy
339
commit.Commit(config=MustSignConfig(branch)).commit(message="base",
340
allow_pointless=True,
343
self.assertEqual(Testament.from_revision(branch.repository,
344
'B').as_short_text(),
345
branch.repository.get_signature_text('B'))
347
bzrlib.gpg.GPGStrategy = oldstrategy
349
def test_commit_failed_signature(self):
351
import bzrlib.commit as commit
352
oldstrategy = bzrlib.gpg.GPGStrategy
353
wt = self.make_branch_and_tree('.')
355
wt.commit("base", allow_pointless=True, rev_id='A')
356
self.failIf(branch.repository.has_signature_for_revision_id('A'))
358
from bzrlib.testament import Testament
359
# monkey patch gpg signing mechanism
360
bzrlib.gpg.GPGStrategy = bzrlib.gpg.DisabledGPGStrategy
361
config = MustSignConfig(branch)
362
self.assertRaises(SigningFailed,
363
commit.Commit(config=config).commit,
365
allow_pointless=True,
368
branch = Branch.open(self.get_url('.'))
369
self.assertEqual(branch.revision_history(), ['A'])
370
self.failIf(branch.repository.has_revision('B'))
372
bzrlib.gpg.GPGStrategy = oldstrategy
374
def test_commit_invokes_hooks(self):
375
import bzrlib.commit as commit
376
wt = self.make_branch_and_tree('.')
379
def called(branch, rev_id):
380
calls.append('called')
381
bzrlib.ahook = called
383
config = BranchWithHooks(branch)
384
commit.Commit(config=config).commit(
386
allow_pointless=True,
387
rev_id='A', working_tree = wt)
388
self.assertEqual(['called', 'called'], calls)
392
def test_commit_object_doesnt_set_nick(self):
393
# using the Commit object directly does not set the branch nick.
394
wt = self.make_branch_and_tree('.')
396
c.commit(working_tree=wt, message='empty tree', allow_pointless=True)
397
self.assertEquals(wt.branch.revno(), 1)
399
wt.branch.repository.get_revision(
400
wt.branch.last_revision()).properties)
402
def test_safe_master_lock(self):
404
master = BzrDirMetaFormat1().initialize('master')
405
master.create_repository()
406
master_branch = master.create_branch()
407
master.create_workingtree()
408
bound = master.sprout('bound')
409
wt = bound.open_workingtree()
410
wt.branch.set_bound_location(os.path.realpath('master'))
411
master_branch.lock_write()
413
self.assertRaises(LockContention, wt.commit, 'silly')
415
master_branch.unlock()
417
def test_commit_bound_merge(self):
418
# see bug #43959; commit of a merge in a bound branch fails to push
419
# the new commit into the master
420
master_branch = self.make_branch('master')
421
bound_tree = self.make_branch_and_tree('bound')
422
bound_tree.branch.bind(master_branch)
424
self.build_tree_contents([('bound/content_file', 'initial contents\n')])
425
bound_tree.add(['content_file'])
426
bound_tree.commit(message='woo!')
428
other_bzrdir = master_branch.bzrdir.sprout('other')
429
other_tree = other_bzrdir.open_workingtree()
431
# do a commit to the the other branch changing the content file so
432
# that our commit after merging will have a merged revision in the
433
# content file history.
434
self.build_tree_contents([('other/content_file', 'change in other\n')])
435
other_tree.commit('change in other')
437
# do a merge into the bound branch from other, and then change the
438
# content file locally to force a new revision (rather than using the
439
# revision from other). This forces extra processing in commit.
440
bound_tree.merge_from_branch(other_tree.branch)
441
self.build_tree_contents([('bound/content_file', 'change in bound\n')])
443
# before #34959 was fixed, this failed with 'revision not present in
444
# weave' when trying to implicitly push from the bound branch to the master
445
bound_tree.commit(message='commit of merge in bound tree')
447
def test_commit_reporting_after_merge(self):
448
# when doing a commit of a merge, the reporter needs to still
449
# be called for each item that is added/removed/deleted.
450
this_tree = self.make_branch_and_tree('this')
451
# we need a bunch of files and dirs, to perform one action on each.
454
'this/dirtoreparent/',
457
'this/filetoreparent',
474
this_tree.commit('create_files')
475
other_dir = this_tree.bzrdir.sprout('other')
476
other_tree = other_dir.open_workingtree()
477
other_tree.lock_write()
478
# perform the needed actions on the files and dirs.
480
other_tree.rename_one('dirtorename', 'renameddir')
481
other_tree.rename_one('dirtoreparent', 'renameddir/reparenteddir')
482
other_tree.rename_one('filetorename', 'renamedfile')
483
other_tree.rename_one('filetoreparent', 'renameddir/reparentedfile')
484
other_tree.remove(['dirtoremove', 'filetoremove'])
485
self.build_tree_contents([
487
('other/filetomodify', 'new content'),
488
('other/newfile', 'new file content')])
489
other_tree.add('newfile')
490
other_tree.add('newdir/')
491
other_tree.commit('modify all sample files and dirs.')
494
this_tree.merge_from_branch(other_tree.branch)
495
reporter = CapturingReporter()
496
this_tree.commit('do the commit', reporter=reporter)
498
('change', 'unchanged', ''),
499
('change', 'unchanged', 'dirtoleave'),
500
('change', 'unchanged', 'filetoleave'),
501
('change', 'modified', 'filetomodify'),
502
('change', 'added', 'newdir'),
503
('change', 'added', 'newfile'),
504
('renamed', 'renamed', 'dirtorename', 'renameddir'),
505
('renamed', 'renamed', 'dirtoreparent', 'renameddir/reparenteddir'),
506
('renamed', 'renamed', 'filetoreparent', 'renameddir/reparentedfile'),
507
('renamed', 'renamed', 'filetorename', 'renamedfile'),
508
('deleted', 'dirtoremove'),
509
('deleted', 'filetoremove'),
513
def test_commit_removals_respects_filespec(self):
514
"""Commit respects the specified_files for removals."""
515
tree = self.make_branch_and_tree('.')
516
self.build_tree(['a', 'b'])
518
tree.commit('added a, b')
519
tree.remove(['a', 'b'])
520
tree.commit('removed a', specific_files='a')
521
basis = tree.basis_tree().inventory
522
self.assertIs(None, basis.path2id('a'))
523
self.assertFalse(basis.path2id('b') is None)
525
def test_commit_saves_1ms_timestamp(self):
526
"""Passing in a timestamp is saved with 1ms resolution"""
527
tree = self.make_branch_and_tree('.')
528
self.build_tree(['a'])
530
tree.commit('added a', timestamp=1153248633.4186721, timezone=0,
533
rev = tree.branch.repository.get_revision('a1')
534
self.assertEqual(1153248633.419, rev.timestamp)
536
def test_commit_has_1ms_resolution(self):
537
"""Allowing commit to generate the timestamp also has 1ms resolution"""
538
tree = self.make_branch_and_tree('.')
539
self.build_tree(['a'])
541
tree.commit('added a', rev_id='a1')
543
rev = tree.branch.repository.get_revision('a1')
544
timestamp = rev.timestamp
545
timestamp_1ms = round(timestamp, 3)
546
self.assertEqual(timestamp_1ms, timestamp)
548
def test_commit_unversioned_specified(self):
549
"""Commit should raise if specified files isn't in basis or worktree"""
550
tree = self.make_branch_and_tree('.')
551
self.assertRaises(errors.PathsNotVersionedError, tree.commit,
552
'message', specific_files=['bogus'])