1
# Copyright (C) 2005 by Canonical Ltd
1
# Copyright (C) 2005-2011 Canonical Ltd
3
3
# This program is free software; you can redistribute it and/or modify
4
4
# it under the terms of the GNU General Public License as published by
5
5
# the Free Software Foundation; either version 2 of the License, or
6
6
# (at your option) any later version.
8
8
# This program is distributed in the hope that it will be useful,
9
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
11
# GNU General Public License for more details.
13
13
# You should have received a copy of the GNU General Public License
14
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
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
21
from bzrlib.tests import TestCaseInTempDir
22
26
from bzrlib.branch import Branch
23
from bzrlib.workingtree import WorkingTree
24
from bzrlib.commit import Commit
25
from bzrlib.config import BranchConfig
26
from bzrlib.errors import PointlessCommit, BzrError, SigningFailed
27
from bzrlib.bzrdir import BzrDirMetaFormat1
28
from bzrlib.commit import Commit, NullCommitReporter
29
from bzrlib.errors import (
35
from bzrlib.tests import (
36
TestCaseWithTransport,
39
from bzrlib.tests.features import (
42
from bzrlib.tests.matchers import MatchesAncestry
29
45
# TODO: Test commit with some added, and added-but-missing files
31
class MustSignConfig(BranchConfig):
33
def signature_needed(self):
47
class MustSignConfig(config.MemoryStack):
50
super(MustSignConfig, self).__init__('''
51
gpg_signing_command=cat -
52
create_signatures=always
56
class CapturingReporter(NullCommitReporter):
57
"""This reporter captures the calls made to it for evaluation later."""
60
# a list of the calls this received
63
def snapshot_change(self, change, path):
64
self.calls.append(('change', change, path))
66
def deleted(self, file_id):
67
self.calls.append(('deleted', file_id))
69
def missing(self, path):
70
self.calls.append(('missing', path))
72
def renamed(self, change, old_path, new_path):
73
self.calls.append(('renamed', change, old_path, new_path))
36
def gpg_signing_command(self):
40
class BranchWithHooks(BranchConfig):
42
def post_commit(self):
43
return "bzrlib.ahook bzrlib.ahook"
46
class TestCommit(TestCaseInTempDir):
79
class TestCommit(TestCaseWithTransport):
48
81
def test_simple_commit(self):
49
82
"""Commit and check two versions of a single file."""
50
b = Branch.initialize(u'.')
51
file('hello', 'w').write('hello world')
52
b.working_tree().add('hello')
53
b.working_tree().commit(message='add hello')
54
file_id = b.working_tree().path2id('hello')
83
wt = self.make_branch_and_tree('.')
85
with file('hello', 'w') as f: f.write('hello world')
87
rev1 = wt.commit(message='add hello')
88
file_id = wt.path2id('hello')
56
file('hello', 'w').write('version 2')
57
b.working_tree().commit(message='commit 2')
90
with file('hello', 'w') as f: f.write('version 2')
91
rev2 = wt.commit(message='commit 2')
59
93
eq = self.assertEquals
61
rh = b.revision_history()
62
rev = b.repository.get_revision(rh[0])
95
rev = b.repository.get_revision(rev1)
63
96
eq(rev.message, 'add hello')
65
tree1 = b.repository.revision_tree(rh[0])
98
tree1 = b.repository.revision_tree(rev1)
66
100
text = tree1.get_file_text(file_id)
67
eq(text, 'hello world')
69
tree2 = b.repository.revision_tree(rh[1])
70
eq(tree2.get_file_text(file_id), 'version 2')
72
def test_delete_commit(self):
73
"""Test a commit with a deleted file"""
74
b = Branch.initialize(u'.')
75
file('hello', 'w').write('hello world')
76
b.working_tree().add(['hello'], ['hello-id'])
77
b.working_tree().commit(message='add hello')
102
self.assertEqual('hello world', text)
104
tree2 = b.repository.revision_tree(rev2)
106
text = tree2.get_file_text(file_id)
108
self.assertEqual('version 2', text)
110
def test_commit_lossy_native(self):
111
"""Attempt a lossy commit to a native branch."""
112
wt = self.make_branch_and_tree('.')
114
with file('hello', 'w') as f: f.write('hello world')
116
revid = wt.commit(message='add hello', rev_id='revid', lossy=True)
117
self.assertEquals('revid', revid)
119
def test_commit_lossy_foreign(self):
120
"""Attempt a lossy commit to a foreign branch."""
121
test_foreign.register_dummy_foreign_for_test(self)
122
wt = self.make_branch_and_tree('.',
123
format=test_foreign.DummyForeignVcsDirFormat())
125
with file('hello', 'w') as f: f.write('hello world')
127
revid = wt.commit(message='add hello', lossy=True,
128
timestamp=1302659388, timezone=0)
129
self.assertEquals('dummy-v1:1302659388.0-0-UNKNOWN', revid)
131
def test_commit_bound_lossy_foreign(self):
132
"""Attempt a lossy commit to a bzr branch bound to a foreign branch."""
133
test_foreign.register_dummy_foreign_for_test(self)
134
foreign_branch = self.make_branch('foreign',
135
format=test_foreign.DummyForeignVcsDirFormat())
136
wt = foreign_branch.create_checkout("local")
138
with file('local/hello', 'w') as f: f.write('hello world')
140
revid = wt.commit(message='add hello', lossy=True,
141
timestamp=1302659388, timezone=0)
142
self.assertEquals('dummy-v1:1302659388.0-0-0', revid)
143
self.assertEquals('dummy-v1:1302659388.0-0-0',
144
foreign_branch.last_revision())
145
self.assertEquals('dummy-v1:1302659388.0-0-0',
146
wt.branch.last_revision())
148
def test_missing_commit(self):
149
"""Test a commit with a missing file"""
150
wt = self.make_branch_and_tree('.')
152
with file('hello', 'w') as f: f.write('hello world')
153
wt.add(['hello'], ['hello-id'])
154
wt.commit(message='add hello')
79
156
os.remove('hello')
80
b.working_tree().commit('removed hello', rev_id='rev2')
157
reporter = CapturingReporter()
158
wt.commit('removed hello', rev_id='rev2', reporter=reporter)
160
[('missing', u'hello'), ('deleted', u'hello')],
82
163
tree = b.repository.revision_tree('rev2')
83
164
self.assertFalse(tree.has_id('hello-id'))
166
def test_partial_commit_move(self):
167
"""Test a partial commit where a file was renamed but not committed.
169
https://bugs.launchpad.net/bzr/+bug/83039
171
If not handled properly, commit will try to snapshot
172
dialog.py with olive/ as a parent, while
173
olive/ has not been snapshotted yet.
175
wt = self.make_branch_and_tree('.')
177
self.build_tree(['annotate/', 'annotate/foo.py',
178
'olive/', 'olive/dialog.py'
180
wt.add(['annotate', 'olive', 'annotate/foo.py', 'olive/dialog.py'])
181
wt.commit(message='add files')
182
wt.rename_one("olive/dialog.py", "aaa")
183
self.build_tree_contents([('annotate/foo.py', 'modified\n')])
184
wt.commit('renamed hello', specific_files=["annotate"])
85
186
def test_pointless_commit(self):
86
187
"""Commit refuses unless there are changes or it's forced."""
87
b = Branch.initialize(u'.')
88
file('hello', 'w').write('hello')
89
b.working_tree().add(['hello'])
90
b.working_tree().commit(message='add hello')
188
wt = self.make_branch_and_tree('.')
190
with file('hello', 'w') as f: f.write('hello')
192
wt.commit(message='add hello')
91
193
self.assertEquals(b.revno(), 1)
92
194
self.assertRaises(PointlessCommit,
93
b.working_tree().commit,
95
197
allow_pointless=False)
96
198
self.assertEquals(b.revno(), 1)
98
200
def test_commit_empty(self):
99
201
"""Commiting an empty tree works."""
100
b = Branch.initialize(u'.')
101
b.working_tree().commit(message='empty tree', allow_pointless=True)
202
wt = self.make_branch_and_tree('.')
204
wt.commit(message='empty tree', allow_pointless=True)
102
205
self.assertRaises(PointlessCommit,
103
b.working_tree().commit,
104
207
message='empty tree',
105
208
allow_pointless=False)
106
b.working_tree().commit(message='empty tree', allow_pointless=True)
209
wt.commit(message='empty tree', allow_pointless=True)
107
210
self.assertEquals(b.revno(), 2)
110
212
def test_selective_delete(self):
111
213
"""Selective commit in tree with deletions"""
112
b = Branch.initialize(u'.')
113
file('hello', 'w').write('hello')
114
file('buongia', 'w').write('buongia')
115
b.working_tree().add(['hello', 'buongia'],
214
wt = self.make_branch_and_tree('.')
216
with file('hello', 'w') as f: f.write('hello')
217
with file('buongia', 'w') as f: f.write('buongia')
218
wt.add(['hello', 'buongia'],
116
219
['hello-id', 'buongia-id'])
117
b.working_tree().commit(message='add files',
220
wt.commit(message='add files',
118
221
rev_id='test@rev-1')
120
223
os.remove('hello')
121
file('buongia', 'w').write('new text')
122
b.working_tree().commit(message='update text',
224
with file('buongia', 'w') as f: f.write('new text')
225
wt.commit(message='update text',
123
226
specific_files=['buongia'],
124
227
allow_pointless=False,
125
228
rev_id='test@rev-2')
127
b.working_tree().commit(message='remove hello',
230
wt.commit(message='remove hello',
128
231
specific_files=['hello'],
129
232
allow_pointless=False,
130
233
rev_id='test@rev-3')
182
291
def test_commit_move(self):
183
292
"""Test commit of revisions with moved files and directories"""
184
293
eq = self.assertEquals
185
b = Branch.initialize(u'.')
294
wt = self.make_branch_and_tree('.')
186
296
r1 = 'test@rev-1'
187
297
self.build_tree(['hello', 'a/', 'b/'])
188
b.working_tree().add(['hello', 'a', 'b'], ['hello-id', 'a-id', 'b-id'])
189
b.working_tree().commit('initial', rev_id=r1, allow_pointless=False)
190
b.working_tree().move(['hello'], 'a')
298
wt.add(['hello', 'a', 'b'], ['hello-id', 'a-id', 'b-id'])
299
wt.commit('initial', rev_id=r1, allow_pointless=False)
300
wt.move(['hello'], 'a')
191
301
r2 = 'test@rev-2'
192
b.working_tree().commit('two', rev_id=r2, allow_pointless=False)
193
self.check_inventory_shape(b.working_tree().read_working_inventory(),
194
['a', 'a/hello', 'b'])
302
wt.commit('two', rev_id=r2, allow_pointless=False)
305
self.check_tree_shape(wt, ['a/', 'a/hello', 'b/'])
196
b.working_tree().move(['b'], 'a')
197
310
r3 = 'test@rev-3'
198
b.working_tree().commit('three', rev_id=r3, allow_pointless=False)
199
self.check_inventory_shape(b.working_tree().read_working_inventory(),
200
['a', 'a/hello', 'a/b'])
201
self.check_inventory_shape(b.repository.get_revision_inventory(r3),
202
['a', 'a/hello', 'a/b'])
311
wt.commit('three', rev_id=r3, allow_pointless=False)
314
self.check_tree_shape(wt,
315
['a/', 'a/hello', 'a/b/'])
316
self.check_tree_shape(b.repository.revision_tree(r3),
317
['a/', 'a/hello', 'a/b/'])
204
b.working_tree().move(['a/hello'], 'a/b')
321
wt.move(['a/hello'], 'a/b')
205
322
r4 = 'test@rev-4'
206
b.working_tree().commit('four', rev_id=r4, allow_pointless=False)
207
self.check_inventory_shape(b.working_tree().read_working_inventory(),
208
['a', 'a/b/hello', 'a/b'])
323
wt.commit('four', rev_id=r4, allow_pointless=False)
326
self.check_tree_shape(wt, ['a/', 'a/b/hello', 'a/b/'])
210
inv = b.repository.get_revision_inventory(r4)
330
inv = b.repository.get_inventory(r4)
211
331
eq(inv['hello-id'].revision, r4)
212
332
eq(inv['a-id'].revision, r1)
213
333
eq(inv['b-id'].revision, r3)
215
335
def test_removed_commit(self):
216
336
"""Commit with a removed file"""
217
b = Branch.initialize(u'.')
218
wt = b.working_tree()
219
file('hello', 'w').write('hello world')
220
b.working_tree().add(['hello'], ['hello-id'])
221
b.working_tree().commit(message='add hello')
223
wt = b.working_tree() # FIXME: kludge for aliasing of working inventory
337
wt = self.make_branch_and_tree('.')
339
with file('hello', 'w') as f: f.write('hello world')
340
wt.add(['hello'], ['hello-id'])
341
wt.commit(message='add hello')
224
342
wt.remove('hello')
225
b.working_tree().commit('removed hello', rev_id='rev2')
343
wt.commit('removed hello', rev_id='rev2')
227
345
tree = b.repository.revision_tree('rev2')
228
346
self.assertFalse(tree.has_id('hello-id'))
231
348
def test_committed_ancestry(self):
232
349
"""Test commit appends revisions to ancestry."""
233
b = Branch.initialize(u'.')
350
wt = self.make_branch_and_tree('.')
235
353
for i in range(4):
236
file('hello', 'w').write((str(i) * 4) + '\n')
354
with file('hello', 'w') as f: f.write((str(i) * 4) + '\n')
238
b.working_tree().add(['hello'], ['hello-id'])
356
wt.add(['hello'], ['hello-id'])
239
357
rev_id = 'test@rev-%d' % (i+1)
240
358
rev_ids.append(rev_id)
241
b.working_tree().commit(message='rev %d' % (i+1),
359
wt.commit(message='rev %d' % (i+1),
243
eq = self.assertEquals
244
eq(b.revision_history(), rev_ids)
245
361
for i in range(4):
246
anc = b.repository.get_ancestry(rev_ids[i])
247
eq(anc, [None] + rev_ids[:i+1])
362
self.assertThat(rev_ids[:i+1],
363
MatchesAncestry(b.repository, rev_ids[i]))
249
365
def test_commit_new_subdir_child_selective(self):
250
b = Branch.initialize(u'.')
366
wt = self.make_branch_and_tree('.')
251
368
self.build_tree(['dir/', 'dir/file1', 'dir/file2'])
252
b.working_tree().add(['dir', 'dir/file1', 'dir/file2'],
369
wt.add(['dir', 'dir/file1', 'dir/file2'],
253
370
['dirid', 'file1id', 'file2id'])
254
b.working_tree().commit('dir/file1', specific_files=['dir/file1'], rev_id='1')
371
wt.commit('dir/file1', specific_files=['dir/file1'], rev_id='1')
255
372
inv = b.repository.get_inventory('1')
256
373
self.assertEqual('1', inv['dirid'].revision)
257
374
self.assertEqual('1', inv['file1id'].revision)
261
378
def test_strict_commit(self):
262
379
"""Try and commit with unknown files and strict = True, should fail."""
263
380
from bzrlib.errors import StrictCommitFailed
264
b = Branch.initialize(u'.')
265
file('hello', 'w').write('hello world')
266
b.working_tree().add('hello')
267
file('goodbye', 'w').write('goodbye cruel world!')
268
self.assertRaises(StrictCommitFailed, b.working_tree().commit,
381
wt = self.make_branch_and_tree('.')
383
with file('hello', 'w') as f: f.write('hello world')
385
with file('goodbye', 'w') as f: f.write('goodbye cruel world!')
386
self.assertRaises(StrictCommitFailed, wt.commit,
269
387
message='add hello but not goodbye', strict=True)
271
389
def test_strict_commit_without_unknowns(self):
272
390
"""Try and commit with no unknown files and strict = True,
274
from bzrlib.errors import StrictCommitFailed
275
b = Branch.initialize(u'.')
276
file('hello', 'w').write('hello world')
277
b.working_tree().add('hello')
278
b.working_tree().commit(message='add hello', strict=True)
392
wt = self.make_branch_and_tree('.')
394
with file('hello', 'w') as f: f.write('hello world')
396
wt.commit(message='add hello', strict=True)
280
398
def test_nonstrict_commit(self):
281
399
"""Try and commit with unknown files and strict = False, should work."""
282
b = Branch.initialize(u'.')
283
file('hello', 'w').write('hello world')
284
b.working_tree().add('hello')
285
file('goodbye', 'w').write('goodbye cruel world!')
286
b.working_tree().commit(message='add hello but not goodbye', strict=False)
400
wt = self.make_branch_and_tree('.')
402
with file('hello', 'w') as f: f.write('hello world')
404
with file('goodbye', 'w') as f: f.write('goodbye cruel world!')
405
wt.commit(message='add hello but not goodbye', strict=False)
288
407
def test_nonstrict_commit_without_unknowns(self):
289
408
"""Try and commit with no unknown files and strict = False,
291
b = Branch.initialize(u'.')
292
file('hello', 'w').write('hello world')
293
b.working_tree().add('hello')
294
b.working_tree().commit(message='add hello', strict=False)
410
wt = self.make_branch_and_tree('.')
412
with file('hello', 'w') as f: f.write('hello world')
414
wt.commit(message='add hello', strict=False)
296
416
def test_signed_commit(self):
297
417
import bzrlib.gpg
298
418
import bzrlib.commit as commit
299
419
oldstrategy = bzrlib.gpg.GPGStrategy
300
branch = Branch.initialize(u'.')
301
branch.working_tree().commit("base", allow_pointless=True, rev_id='A')
302
self.failIf(branch.repository.revision_store.has_id('A', 'sig'))
420
wt = self.make_branch_and_tree('.')
422
wt.commit("base", allow_pointless=True, rev_id='A')
423
self.assertFalse(branch.repository.has_signature_for_revision_id('A'))
304
425
from bzrlib.testament import Testament
305
426
# monkey patch gpg signing mechanism
306
427
bzrlib.gpg.GPGStrategy = bzrlib.gpg.LoopbackGPGStrategy
307
commit.Commit(config=MustSignConfig(branch)).commit(branch, "base",
308
allow_pointless=True,
310
self.assertEqual(Testament.from_revision(branch.repository,
311
'B').as_short_text(),
312
branch.repository.revision_store.get('B',
428
conf = config.MemoryStack('''
429
gpg_signing_command=cat -
430
create_signatures=always
432
commit.Commit(config_stack=conf).commit(
433
message="base", allow_pointless=True, rev_id='B',
436
return bzrlib.gpg.LoopbackGPGStrategy(None).sign(text)
437
self.assertEqual(sign(Testament.from_revision(branch.repository,
438
'B').as_short_text()),
439
branch.repository.get_signature_text('B'))
315
441
bzrlib.gpg.GPGStrategy = oldstrategy
318
444
import bzrlib.gpg
319
445
import bzrlib.commit as commit
320
446
oldstrategy = bzrlib.gpg.GPGStrategy
321
branch = Branch.initialize(u'.')
322
branch.working_tree().commit("base", allow_pointless=True, rev_id='A')
323
self.failIf(branch.repository.revision_store.has_id('A', 'sig'))
447
wt = self.make_branch_and_tree('.')
449
wt.commit("base", allow_pointless=True, rev_id='A')
450
self.assertFalse(branch.repository.has_signature_for_revision_id('A'))
325
from bzrlib.testament import Testament
326
452
# monkey patch gpg signing mechanism
327
453
bzrlib.gpg.GPGStrategy = bzrlib.gpg.DisabledGPGStrategy
328
config = MustSignConfig(branch)
454
conf = config.MemoryStack('''
455
gpg_signing_command=cat -
456
create_signatures=always
329
458
self.assertRaises(SigningFailed,
330
commit.Commit(config=config).commit,
459
commit.Commit(config_stack=conf).commit,
332
461
allow_pointless=True,
334
branch = Branch.open(u'.')
335
self.assertEqual(branch.revision_history(), ['A'])
336
self.failIf(branch.repository.revision_store.has_id('B'))
464
branch = Branch.open(self.get_url('.'))
465
self.assertEqual(branch.last_revision(), 'A')
466
self.assertFalse(branch.repository.has_revision('B'))
338
468
bzrlib.gpg.GPGStrategy = oldstrategy
340
470
def test_commit_invokes_hooks(self):
341
471
import bzrlib.commit as commit
342
branch = Branch.initialize(u'.')
472
wt = self.make_branch_and_tree('.')
344
475
def called(branch, rev_id):
345
476
calls.append('called')
346
477
bzrlib.ahook = called
348
config = BranchWithHooks(branch)
349
commit.Commit(config=config).commit(
351
allow_pointless=True,
479
conf = config.MemoryStack('post_commit=bzrlib.ahook bzrlib.ahook')
480
commit.Commit(config_stack=conf).commit(
481
message = "base", allow_pointless=True, rev_id='A',
353
483
self.assertEqual(['called', 'called'], calls)
487
def test_commit_object_doesnt_set_nick(self):
488
# using the Commit object directly does not set the branch nick.
489
wt = self.make_branch_and_tree('.')
491
c.commit(working_tree=wt, message='empty tree', allow_pointless=True)
492
self.assertEquals(wt.branch.revno(), 1)
494
wt.branch.repository.get_revision(
495
wt.branch.last_revision()).properties)
497
def test_safe_master_lock(self):
499
master = BzrDirMetaFormat1().initialize('master')
500
master.create_repository()
501
master_branch = master.create_branch()
502
master.create_workingtree()
503
bound = master.sprout('bound')
504
wt = bound.open_workingtree()
505
wt.branch.set_bound_location(os.path.realpath('master'))
506
master_branch.lock_write()
508
self.assertRaises(LockContention, wt.commit, 'silly')
510
master_branch.unlock()
512
def test_commit_bound_merge(self):
513
# see bug #43959; commit of a merge in a bound branch fails to push
514
# the new commit into the master
515
master_branch = self.make_branch('master')
516
bound_tree = self.make_branch_and_tree('bound')
517
bound_tree.branch.bind(master_branch)
519
self.build_tree_contents([('bound/content_file', 'initial contents\n')])
520
bound_tree.add(['content_file'])
521
bound_tree.commit(message='woo!')
523
other_bzrdir = master_branch.bzrdir.sprout('other')
524
other_tree = other_bzrdir.open_workingtree()
526
# do a commit to the other branch changing the content file so
527
# that our commit after merging will have a merged revision in the
528
# content file history.
529
self.build_tree_contents([('other/content_file', 'change in other\n')])
530
other_tree.commit('change in other')
532
# do a merge into the bound branch from other, and then change the
533
# content file locally to force a new revision (rather than using the
534
# revision from other). This forces extra processing in commit.
535
bound_tree.merge_from_branch(other_tree.branch)
536
self.build_tree_contents([('bound/content_file', 'change in bound\n')])
538
# before #34959 was fixed, this failed with 'revision not present in
539
# weave' when trying to implicitly push from the bound branch to the master
540
bound_tree.commit(message='commit of merge in bound tree')
542
def test_commit_reporting_after_merge(self):
543
# when doing a commit of a merge, the reporter needs to still
544
# be called for each item that is added/removed/deleted.
545
this_tree = self.make_branch_and_tree('this')
546
# we need a bunch of files and dirs, to perform one action on each.
549
'this/dirtoreparent/',
552
'this/filetoreparent',
569
this_tree.commit('create_files')
570
other_dir = this_tree.bzrdir.sprout('other')
571
other_tree = other_dir.open_workingtree()
572
other_tree.lock_write()
573
# perform the needed actions on the files and dirs.
575
other_tree.rename_one('dirtorename', 'renameddir')
576
other_tree.rename_one('dirtoreparent', 'renameddir/reparenteddir')
577
other_tree.rename_one('filetorename', 'renamedfile')
578
other_tree.rename_one('filetoreparent', 'renameddir/reparentedfile')
579
other_tree.remove(['dirtoremove', 'filetoremove'])
580
self.build_tree_contents([
582
('other/filetomodify', 'new content'),
583
('other/newfile', 'new file content')])
584
other_tree.add('newfile')
585
other_tree.add('newdir/')
586
other_tree.commit('modify all sample files and dirs.')
589
this_tree.merge_from_branch(other_tree.branch)
590
reporter = CapturingReporter()
591
this_tree.commit('do the commit', reporter=reporter)
593
('change', 'modified', 'filetomodify'),
594
('change', 'added', 'newdir'),
595
('change', 'added', 'newfile'),
596
('renamed', 'renamed', 'dirtorename', 'renameddir'),
597
('renamed', 'renamed', 'filetorename', 'renamedfile'),
598
('renamed', 'renamed', 'dirtoreparent', 'renameddir/reparenteddir'),
599
('renamed', 'renamed', 'filetoreparent', 'renameddir/reparentedfile'),
600
('deleted', 'dirtoremove'),
601
('deleted', 'filetoremove'),
603
result = set(reporter.calls)
604
missing = expected - result
605
new = result - expected
606
self.assertEqual((set(), set()), (missing, new))
608
def test_commit_removals_respects_filespec(self):
609
"""Commit respects the specified_files for removals."""
610
tree = self.make_branch_and_tree('.')
611
self.build_tree(['a', 'b'])
613
tree.commit('added a, b')
614
tree.remove(['a', 'b'])
615
tree.commit('removed a', specific_files='a')
616
basis = tree.basis_tree()
619
self.assertIs(None, basis.path2id('a'))
620
self.assertFalse(basis.path2id('b') is None)
624
def test_commit_saves_1ms_timestamp(self):
625
"""Passing in a timestamp is saved with 1ms resolution"""
626
tree = self.make_branch_and_tree('.')
627
self.build_tree(['a'])
629
tree.commit('added a', timestamp=1153248633.4186721, timezone=0,
632
rev = tree.branch.repository.get_revision('a1')
633
self.assertEqual(1153248633.419, rev.timestamp)
635
def test_commit_has_1ms_resolution(self):
636
"""Allowing commit to generate the timestamp also has 1ms resolution"""
637
tree = self.make_branch_and_tree('.')
638
self.build_tree(['a'])
640
tree.commit('added a', rev_id='a1')
642
rev = tree.branch.repository.get_revision('a1')
643
timestamp = rev.timestamp
644
timestamp_1ms = round(timestamp, 3)
645
self.assertEqual(timestamp_1ms, timestamp)
647
def assertBasisTreeKind(self, kind, tree, file_id):
648
basis = tree.basis_tree()
651
self.assertEqual(kind, basis.kind(file_id))
655
def test_commit_kind_changes(self):
656
self.requireFeature(SymlinkFeature)
657
tree = self.make_branch_and_tree('.')
658
os.symlink('target', 'name')
659
tree.add('name', 'a-file-id')
660
tree.commit('Added a symlink')
661
self.assertBasisTreeKind('symlink', tree, 'a-file-id')
664
self.build_tree(['name'])
665
tree.commit('Changed symlink to file')
666
self.assertBasisTreeKind('file', tree, 'a-file-id')
669
os.symlink('target', 'name')
670
tree.commit('file to symlink')
671
self.assertBasisTreeKind('symlink', tree, 'a-file-id')
675
tree.commit('symlink to directory')
676
self.assertBasisTreeKind('directory', tree, 'a-file-id')
679
os.symlink('target', 'name')
680
tree.commit('directory to symlink')
681
self.assertBasisTreeKind('symlink', tree, 'a-file-id')
683
# prepare for directory <-> file tests
686
tree.commit('symlink to directory')
687
self.assertBasisTreeKind('directory', tree, 'a-file-id')
690
self.build_tree(['name'])
691
tree.commit('Changed directory to file')
692
self.assertBasisTreeKind('file', tree, 'a-file-id')
696
tree.commit('file to directory')
697
self.assertBasisTreeKind('directory', tree, 'a-file-id')
699
def test_commit_unversioned_specified(self):
700
"""Commit should raise if specified files isn't in basis or worktree"""
701
tree = self.make_branch_and_tree('.')
702
self.assertRaises(errors.PathsNotVersionedError, tree.commit,
703
'message', specific_files=['bogus'])
705
class Callback(object):
707
def __init__(self, message, testcase):
709
self.message = message
710
self.testcase = testcase
712
def __call__(self, commit_obj):
714
self.testcase.assertTrue(isinstance(commit_obj, Commit))
717
def test_commit_callback(self):
718
"""Commit should invoke a callback to get the message"""
720
tree = self.make_branch_and_tree('.')
724
self.assertTrue(isinstance(e, BzrError))
725
self.assertEqual('The message or message_callback keyword'
726
' parameter is required for commit().', str(e))
728
self.fail('exception not raised')
729
cb = self.Callback(u'commit 1', self)
730
tree.commit(message_callback=cb)
731
self.assertTrue(cb.called)
732
repository = tree.branch.repository
733
message = repository.get_revision(tree.last_revision()).message
734
self.assertEqual('commit 1', message)
736
def test_no_callback_pointless(self):
737
"""Callback should not be invoked for pointless commit"""
738
tree = self.make_branch_and_tree('.')
739
cb = self.Callback(u'commit 2', self)
740
self.assertRaises(PointlessCommit, tree.commit, message_callback=cb,
741
allow_pointless=False)
742
self.assertFalse(cb.called)
744
def test_no_callback_netfailure(self):
745
"""Callback should not be invoked if connectivity fails"""
746
tree = self.make_branch_and_tree('.')
747
cb = self.Callback(u'commit 2', self)
748
repository = tree.branch.repository
749
# simulate network failure
750
def raise_(self, arg, arg2, arg3=None, arg4=None):
751
raise errors.NoSuchFile('foo')
752
repository.add_inventory = raise_
753
repository.add_inventory_by_delta = raise_
754
self.assertRaises(errors.NoSuchFile, tree.commit, message_callback=cb)
755
self.assertFalse(cb.called)
757
def test_selected_file_merge_commit(self):
758
"""Ensure the correct error is raised"""
759
tree = self.make_branch_and_tree('foo')
760
# pending merge would turn into a left parent
761
tree.commit('commit 1')
762
tree.add_parent_tree_id('example')
763
self.build_tree(['foo/bar', 'foo/baz'])
764
tree.add(['bar', 'baz'])
765
err = self.assertRaises(errors.CannotCommitSelectedFileMerge,
766
tree.commit, 'commit 2', specific_files=['bar', 'baz'])
767
self.assertEqual(['bar', 'baz'], err.files)
768
self.assertEqual('Selected-file commit of merges is not supported'
769
' yet: files bar, baz', str(err))
771
def test_commit_ordering(self):
772
"""Test of corner-case commit ordering error"""
773
tree = self.make_branch_and_tree('.')
774
self.build_tree(['a/', 'a/z/', 'a/c/', 'a/z/x', 'a/z/y'])
775
tree.add(['a/', 'a/z/', 'a/c/', 'a/z/x', 'a/z/y'])
777
self.build_tree(['a/c/d/'])
779
tree.rename_one('a/z/x', 'a/c/d/x')
780
tree.commit('test', specific_files=['a/z/y'])
782
def test_commit_no_author(self):
783
"""The default kwarg author in MutableTree.commit should not add
784
the 'author' revision property.
786
tree = self.make_branch_and_tree('foo')
787
rev_id = tree.commit('commit 1')
788
rev = tree.branch.repository.get_revision(rev_id)
789
self.assertFalse('author' in rev.properties)
790
self.assertFalse('authors' in rev.properties)
792
def test_commit_author(self):
793
"""Passing a non-empty author kwarg to MutableTree.commit should add
794
the 'author' revision property.
796
tree = self.make_branch_and_tree('foo')
797
rev_id = self.callDeprecated(['The parameter author was '
798
'deprecated in version 1.13. Use authors instead'],
799
tree.commit, 'commit 1', author='John Doe <jdoe@example.com>')
800
rev = tree.branch.repository.get_revision(rev_id)
801
self.assertEqual('John Doe <jdoe@example.com>',
802
rev.properties['authors'])
803
self.assertFalse('author' in rev.properties)
805
def test_commit_empty_authors_list(self):
806
"""Passing an empty list to authors shouldn't add the property."""
807
tree = self.make_branch_and_tree('foo')
808
rev_id = tree.commit('commit 1', authors=[])
809
rev = tree.branch.repository.get_revision(rev_id)
810
self.assertFalse('author' in rev.properties)
811
self.assertFalse('authors' in rev.properties)
813
def test_multiple_authors(self):
814
tree = self.make_branch_and_tree('foo')
815
rev_id = tree.commit('commit 1',
816
authors=['John Doe <jdoe@example.com>',
817
'Jane Rey <jrey@example.com>'])
818
rev = tree.branch.repository.get_revision(rev_id)
819
self.assertEqual('John Doe <jdoe@example.com>\n'
820
'Jane Rey <jrey@example.com>', rev.properties['authors'])
821
self.assertFalse('author' in rev.properties)
823
def test_author_and_authors_incompatible(self):
824
tree = self.make_branch_and_tree('foo')
825
self.assertRaises(AssertionError, tree.commit, 'commit 1',
826
authors=['John Doe <jdoe@example.com>',
827
'Jane Rey <jrey@example.com>'],
828
author="Jack Me <jme@example.com>")
830
def test_author_with_newline_rejected(self):
831
tree = self.make_branch_and_tree('foo')
832
self.assertRaises(AssertionError, tree.commit, 'commit 1',
833
authors=['John\nDoe <jdoe@example.com>'])
835
def test_commit_with_checkout_and_branch_sharing_repo(self):
836
repo = self.make_repository('repo', shared=True)
837
# make_branch_and_tree ignores shared repos
838
branch = controldir.ControlDir.create_branch_convenience('repo/branch')
839
tree2 = branch.create_checkout('repo/tree2')
840
tree2.commit('message', rev_id='rev1')
841
self.assertTrue(tree2.branch.repository.has_revision('rev1'))