1
# Copyright (C) 2005-2012, 2016 Canonical Ltd
1
# Copyright (C) 2005 by 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
19
from io import BytesIO
28
from ..branch import Branch
29
from ..bzr.bzrdir import BzrDirMetaFormat1
30
from ..commit import (
31
CannotCommitSelectedFileMerge,
37
from ..errors import (
41
from ..tree import TreeChange
44
TestCaseWithTransport,
47
from .features import (
50
from .matchers import MatchesAncestry
21
from bzrlib.selftest import TestCaseInTempDir
22
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
53
29
# TODO: Test commit with some added, and added-but-missing files
55
class MustSignConfig(config.MemoryStack):
58
super(MustSignConfig, self).__init__(b'''
59
create_signatures=always
63
class CapturingReporter(NullCommitReporter):
64
"""This reporter captures the calls made to it for evaluation later."""
67
# a list of the calls this received
70
def snapshot_change(self, change, path):
71
self.calls.append(('change', change, path))
73
def deleted(self, file_id):
74
self.calls.append(('deleted', file_id))
76
def missing(self, path):
77
self.calls.append(('missing', path))
79
def renamed(self, change, old_path, new_path):
80
self.calls.append(('renamed', change, old_path, new_path))
31
class MustSignConfig(BranchConfig):
33
def signature_needed(self):
86
class TestCommit(TestCaseWithTransport):
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):
88
48
def test_simple_commit(self):
89
49
"""Commit and check two versions of a single file."""
90
wt = self.make_branch_and_tree('.')
92
with open('hello', 'w') as f:
93
f.write('hello world')
95
rev1 = wt.commit(message='add hello')
97
with open('hello', 'w') as f:
99
rev2 = wt.commit(message='commit 2')
101
eq = self.assertEqual
50
b = Branch.initialize('.')
51
file('hello', 'w').write('hello world')
53
b.working_tree().commit(message='add hello')
54
file_id = b.working_tree().path2id('hello')
56
file('hello', 'w').write('version 2')
57
b.working_tree().commit(message='commit 2')
59
eq = self.assertEquals
103
rev = b.repository.get_revision(rev1)
61
rh = b.revision_history()
62
rev = b.get_revision(rh[0])
104
63
eq(rev.message, 'add hello')
106
tree1 = b.repository.revision_tree(rev1)
108
text = tree1.get_file_text('hello')
110
self.assertEqual(b'hello world', text)
112
tree2 = b.repository.revision_tree(rev2)
114
text = tree2.get_file_text('hello')
116
self.assertEqual(b'version 2', text)
118
def test_commit_lossy_native(self):
119
"""Attempt a lossy commit to a native branch."""
120
wt = self.make_branch_and_tree('.')
122
with open('hello', 'w') as f:
123
f.write('hello world')
125
revid = wt.commit(message='add hello', rev_id=b'revid', lossy=True)
126
self.assertEqual(b'revid', revid)
128
def test_commit_lossy_foreign(self):
129
"""Attempt a lossy commit to a foreign branch."""
130
test_foreign.register_dummy_foreign_for_test(self)
131
wt = self.make_branch_and_tree('.',
132
format=test_foreign.DummyForeignVcsDirFormat())
134
with open('hello', 'w') as f:
135
f.write('hello world')
137
revid = wt.commit(message='add hello', lossy=True,
138
timestamp=1302659388, timezone=0)
139
self.assertEqual(b'dummy-v1:1302659388-0-UNKNOWN', revid)
141
def test_commit_bound_lossy_foreign(self):
142
"""Attempt a lossy commit to a bzr branch bound to a foreign branch."""
143
test_foreign.register_dummy_foreign_for_test(self)
144
foreign_branch = self.make_branch('foreign',
145
format=test_foreign.DummyForeignVcsDirFormat())
146
wt = foreign_branch.create_checkout("local")
148
with open('local/hello', 'w') as f:
149
f.write('hello world')
151
revid = wt.commit(message='add hello', lossy=True,
152
timestamp=1302659388, timezone=0)
153
self.assertEqual(b'dummy-v1:1302659388-0-0', revid)
154
self.assertEqual(b'dummy-v1:1302659388-0-0',
155
foreign_branch.last_revision())
156
self.assertEqual(b'dummy-v1:1302659388-0-0',
157
wt.branch.last_revision())
159
def test_missing_commit(self):
160
"""Test a commit with a missing file"""
161
wt = self.make_branch_and_tree('.')
163
with open('hello', 'w') as f:
164
f.write('hello world')
165
wt.add(['hello'], [b'hello-id'])
166
wt.commit(message='add hello')
65
tree1 = b.revision_tree(rh[0])
66
text = tree1.get_file_text(file_id)
67
eq(text, 'hello world')
69
tree2 = b.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('.')
75
file('hello', 'w').write('hello world')
76
b.add(['hello'], ['hello-id'])
77
b.working_tree().commit(message='add hello')
168
79
os.remove('hello')
169
reporter = CapturingReporter()
170
wt.commit('removed hello', rev_id=b'rev2', reporter=reporter)
172
[('missing', u'hello'), ('deleted', u'hello')],
175
tree = b.repository.revision_tree(b'rev2')
176
self.assertFalse(tree.has_filename('hello'))
178
def test_partial_commit_move(self):
179
"""Test a partial commit where a file was renamed but not committed.
181
https://bugs.launchpad.net/bzr/+bug/83039
183
If not handled properly, commit will try to snapshot
184
dialog.py with olive/ as a parent, while
185
olive/ has not been snapshotted yet.
187
wt = self.make_branch_and_tree('.')
189
self.build_tree(['annotate/', 'annotate/foo.py',
190
'olive/', 'olive/dialog.py'
192
wt.add(['annotate', 'olive', 'annotate/foo.py', 'olive/dialog.py'])
193
wt.commit(message='add files')
194
wt.rename_one("olive/dialog.py", "aaa")
195
self.build_tree_contents([('annotate/foo.py', b'modified\n')])
196
wt.commit('renamed hello', specific_files=["annotate"])
80
b.working_tree().commit('removed hello', rev_id='rev2')
82
tree = b.revision_tree('rev2')
83
self.assertFalse(tree.has_id('hello-id'))
198
85
def test_pointless_commit(self):
199
86
"""Commit refuses unless there are changes or it's forced."""
200
wt = self.make_branch_and_tree('.')
202
with open('hello', 'w') as f:
205
wt.commit(message='add hello')
206
self.assertEqual(b.revno(), 1)
87
b = Branch.initialize('.')
88
file('hello', 'w').write('hello')
90
b.working_tree().commit(message='add hello')
91
self.assertEquals(b.revno(), 1)
207
92
self.assertRaises(PointlessCommit,
93
b.working_tree().commit,
210
95
allow_pointless=False)
211
self.assertEqual(b.revno(), 1)
96
self.assertEquals(b.revno(), 1)
213
98
def test_commit_empty(self):
214
99
"""Commiting an empty tree works."""
215
wt = self.make_branch_and_tree('.')
217
wt.commit(message='empty tree', allow_pointless=True)
100
b = Branch.initialize('.')
101
b.working_tree().commit(message='empty tree', allow_pointless=True)
218
102
self.assertRaises(PointlessCommit,
103
b.working_tree().commit,
220
104
message='empty tree',
221
105
allow_pointless=False)
222
wt.commit(message='empty tree', allow_pointless=True)
223
self.assertEqual(b.revno(), 2)
106
b.working_tree().commit(message='empty tree', allow_pointless=True)
107
self.assertEquals(b.revno(), 2)
225
110
def test_selective_delete(self):
226
111
"""Selective commit in tree with deletions"""
227
wt = self.make_branch_and_tree('.')
229
with open('hello', 'w') as f:
231
with open('buongia', 'w') as f:
233
wt.add(['hello', 'buongia'],
234
[b'hello-id', b'buongia-id'])
235
wt.commit(message='add files',
236
rev_id=b'test@rev-1')
112
b = Branch.initialize('.')
113
file('hello', 'w').write('hello')
114
file('buongia', 'w').write('buongia')
115
b.add(['hello', 'buongia'],
116
['hello-id', 'buongia-id'])
117
b.working_tree().commit(message='add files',
238
120
os.remove('hello')
239
with open('buongia', 'w') as f:
241
wt.commit(message='update text',
242
specific_files=['buongia'],
243
allow_pointless=False,
244
rev_id=b'test@rev-2')
246
wt.commit(message='remove hello',
247
specific_files=['hello'],
248
allow_pointless=False,
249
rev_id=b'test@rev-3')
251
eq = self.assertEqual
121
file('buongia', 'w').write('new text')
122
b.working_tree().commit(message='update text',
123
specific_files=['buongia'],
124
allow_pointless=False,
127
b.working_tree().commit(message='remove hello',
128
specific_files=['hello'],
129
allow_pointless=False,
132
eq = self.assertEquals
254
tree2 = b.repository.revision_tree(b'test@rev-2')
256
self.addCleanup(tree2.unlock)
135
tree2 = b.revision_tree('test@rev-2')
257
136
self.assertTrue(tree2.has_filename('hello'))
258
self.assertEqual(tree2.get_file_text('hello'), b'hello')
259
self.assertEqual(tree2.get_file_text('buongia'), b'new text')
261
tree3 = b.repository.revision_tree(b'test@rev-3')
263
self.addCleanup(tree3.unlock)
137
self.assertEquals(tree2.get_file_text('hello-id'), 'hello')
138
self.assertEquals(tree2.get_file_text('buongia-id'), 'new text')
140
tree3 = b.revision_tree('test@rev-3')
264
141
self.assertFalse(tree3.has_filename('hello'))
265
self.assertEqual(tree3.get_file_text('buongia'), b'new text')
142
self.assertEquals(tree3.get_file_text('buongia-id'), 'new text')
267
145
def test_commit_rename(self):
268
146
"""Test commit of a revision where a file is renamed."""
269
tree = self.make_branch_and_tree('.')
271
self.build_tree(['hello'], line_endings='binary')
272
tree.add(['hello'], [b'hello-id'])
273
tree.commit(message='one', rev_id=b'test@rev-1', allow_pointless=False)
275
tree.rename_one('hello', 'fruity')
276
tree.commit(message='renamed', rev_id=b'test@rev-2',
277
allow_pointless=False)
279
eq = self.assertEqual
280
tree1 = b.repository.revision_tree(b'test@rev-1')
282
self.addCleanup(tree1.unlock)
283
eq(tree1.id2path(b'hello-id'), 'hello')
284
eq(tree1.get_file_text('hello'), b'contents of hello\n')
147
b = Branch.initialize('.')
148
self.build_tree(['hello'])
149
b.add(['hello'], ['hello-id'])
150
b.working_tree().commit(message='one', rev_id='test@rev-1', allow_pointless=False)
152
b.rename_one('hello', 'fruity')
153
b.working_tree().commit(message='renamed', rev_id='test@rev-2', allow_pointless=False)
155
eq = self.assertEquals
156
tree1 = b.revision_tree('test@rev-1')
157
eq(tree1.id2path('hello-id'), 'hello')
158
eq(tree1.get_file_text('hello-id'), 'contents of hello\n')
285
159
self.assertFalse(tree1.has_filename('fruity'))
286
self.check_tree_shape(tree1, ['hello'])
287
eq(tree1.get_file_revision('hello'), b'test@rev-1')
289
tree2 = b.repository.revision_tree(b'test@rev-2')
291
self.addCleanup(tree2.unlock)
292
eq(tree2.id2path(b'hello-id'), 'fruity')
293
eq(tree2.get_file_text('fruity'), b'contents of hello\n')
294
self.check_tree_shape(tree2, ['fruity'])
295
eq(tree2.get_file_revision('fruity'), b'test@rev-2')
160
self.check_inventory_shape(tree1.inventory, ['hello'])
161
ie = tree1.inventory['hello-id']
162
eq(ie.revision, 'test@rev-1')
164
tree2 = b.revision_tree('test@rev-2')
165
eq(tree2.id2path('hello-id'), 'fruity')
166
eq(tree2.get_file_text('hello-id'), 'contents of hello\n')
167
self.check_inventory_shape(tree2.inventory, ['fruity'])
168
ie = tree2.inventory['hello-id']
169
eq(ie.revision, 'test@rev-2')
297
172
def test_reused_rev_id(self):
298
173
"""Test that a revision id cannot be reused in a branch"""
299
wt = self.make_branch_and_tree('.')
301
wt.commit('initial', rev_id=b'test@rev-1', allow_pointless=True)
174
b = Branch.initialize('.')
175
b.working_tree().commit('initial', rev_id='test@rev-1', allow_pointless=True)
302
176
self.assertRaises(Exception,
177
b.working_tree().commit,
304
178
message='reused id',
305
rev_id=b'test@rev-1',
306
180
allow_pointless=True)
308
184
def test_commit_move(self):
309
185
"""Test commit of revisions with moved files and directories"""
310
eq = self.assertEqual
311
wt = self.make_branch_and_tree('.')
186
eq = self.assertEquals
187
b = Branch.initialize('.')
314
189
self.build_tree(['hello', 'a/', 'b/'])
315
wt.add(['hello', 'a', 'b'], [b'hello-id', b'a-id', b'b-id'])
316
wt.commit('initial', rev_id=r1, allow_pointless=False)
317
wt.move(['hello'], 'a')
319
wt.commit('two', rev_id=r2, allow_pointless=False)
322
self.check_tree_shape(wt, ['a/', 'a/hello', 'b/'])
328
wt.commit('three', rev_id=r3, allow_pointless=False)
331
self.check_tree_shape(wt,
332
['a/', 'a/hello', 'a/b/'])
333
self.check_tree_shape(b.repository.revision_tree(r3),
334
['a/', 'a/hello', 'a/b/'])
338
wt.move(['a/hello'], 'a/b')
340
wt.commit('four', rev_id=r4, allow_pointless=False)
343
self.check_tree_shape(wt, ['a/', 'a/b/hello', 'a/b/'])
347
inv = b.repository.get_inventory(r4)
348
eq(inv.get_entry(b'hello-id').revision, r4)
349
eq(inv.get_entry(b'a-id').revision, r1)
350
eq(inv.get_entry(b'b-id').revision, r3)
190
b.add(['hello', 'a', 'b'], ['hello-id', 'a-id', 'b-id'])
191
b.working_tree().commit('initial', rev_id=r1, allow_pointless=False)
193
b.move(['hello'], 'a')
195
b.working_tree().commit('two', rev_id=r2, allow_pointless=False)
196
self.check_inventory_shape(b.working_tree().read_working_inventory(),
197
['a', 'a/hello', 'b'])
201
b.working_tree().commit('three', rev_id=r3, allow_pointless=False)
202
self.check_inventory_shape(b.working_tree().read_working_inventory(),
203
['a', 'a/hello', 'a/b'])
204
self.check_inventory_shape(b.get_revision_inventory(r3),
205
['a', 'a/hello', 'a/b'])
207
b.move([os.sep.join(['a', 'hello'])],
208
os.sep.join(['a', 'b']))
210
b.working_tree().commit('four', rev_id=r4, allow_pointless=False)
211
self.check_inventory_shape(b.working_tree().read_working_inventory(),
212
['a', 'a/b/hello', 'a/b'])
214
inv = b.get_revision_inventory(r4)
215
eq(inv['hello-id'].revision, r4)
216
eq(inv['a-id'].revision, r1)
217
eq(inv['b-id'].revision, r3)
352
220
def test_removed_commit(self):
353
221
"""Commit with a removed file"""
354
wt = self.make_branch_and_tree('.')
356
with open('hello', 'w') as f:
357
f.write('hello world')
358
wt.add(['hello'], [b'hello-id'])
359
wt.commit(message='add hello')
222
b = Branch.initialize('.')
223
wt = b.working_tree()
224
file('hello', 'w').write('hello world')
225
b.add(['hello'], ['hello-id'])
226
b.working_tree().commit(message='add hello')
228
wt = b.working_tree() # FIXME: kludge for aliasing of working inventory
360
229
wt.remove('hello')
361
wt.commit('removed hello', rev_id=b'rev2')
363
tree = b.repository.revision_tree(b'rev2')
364
self.assertFalse(tree.has_filename('hello'))
230
b.working_tree().commit('removed hello', rev_id='rev2')
232
tree = b.revision_tree('rev2')
233
self.assertFalse(tree.has_id('hello-id'))
366
236
def test_committed_ancestry(self):
367
237
"""Test commit appends revisions to ancestry."""
368
wt = self.make_branch_and_tree('.')
238
b = Branch.initialize('.')
371
240
for i in range(4):
372
with open('hello', 'w') as f:
373
f.write((str(i) * 4) + '\n')
241
file('hello', 'w').write((str(i) * 4) + '\n')
375
wt.add(['hello'], [b'hello-id'])
376
rev_id = b'test@rev-%d' % (i + 1)
243
b.add(['hello'], ['hello-id'])
244
rev_id = 'test@rev-%d' % (i+1)
377
245
rev_ids.append(rev_id)
378
wt.commit(message='rev %d' % (i + 1),
246
b.working_tree().commit(message='rev %d' % (i+1),
248
eq = self.assertEquals
249
eq(b.revision_history(), rev_ids)
380
250
for i in range(4):
381
self.assertThat(rev_ids[:i + 1],
382
MatchesAncestry(b.repository, rev_ids[i]))
251
anc = b.get_ancestry(rev_ids[i])
252
eq(anc, [None] + rev_ids[:i+1])
384
254
def test_commit_new_subdir_child_selective(self):
385
wt = self.make_branch_and_tree('.')
255
b = Branch.initialize('.')
387
256
self.build_tree(['dir/', 'dir/file1', 'dir/file2'])
388
wt.add(['dir', 'dir/file1', 'dir/file2'],
389
[b'dirid', b'file1id', b'file2id'])
390
wt.commit('dir/file1', specific_files=['dir/file1'], rev_id=b'1')
391
inv = b.repository.get_inventory(b'1')
392
self.assertEqual(b'1', inv.get_entry(b'dirid').revision)
393
self.assertEqual(b'1', inv.get_entry(b'file1id').revision)
257
b.add(['dir', 'dir/file1', 'dir/file2'],
258
['dirid', 'file1id', 'file2id'])
259
b.working_tree().commit('dir/file1', specific_files=['dir/file1'], rev_id='1')
260
inv = b.get_inventory('1')
261
self.assertEqual('1', inv['dirid'].revision)
262
self.assertEqual('1', inv['file1id'].revision)
394
263
# FIXME: This should raise a KeyError I think, rbc20051006
395
self.assertRaises(BzrError, inv.get_entry, b'file2id')
264
self.assertRaises(BzrError, inv.__getitem__, 'file2id')
397
266
def test_strict_commit(self):
398
267
"""Try and commit with unknown files and strict = True, should fail."""
399
from ..errors import StrictCommitFailed
400
wt = self.make_branch_and_tree('.')
402
with open('hello', 'w') as f:
403
f.write('hello world')
405
with open('goodbye', 'w') as f:
406
f.write('goodbye cruel world!')
407
self.assertRaises(StrictCommitFailed, wt.commit,
408
message='add hello but not goodbye', strict=True)
268
from bzrlib.errors import StrictCommitFailed
269
b = Branch.initialize('.')
270
file('hello', 'w').write('hello world')
272
file('goodbye', 'w').write('goodbye cruel world!')
273
self.assertRaises(StrictCommitFailed, b.working_tree().commit,
274
message='add hello but not goodbye', strict=True)
410
276
def test_strict_commit_without_unknowns(self):
411
277
"""Try and commit with no unknown files and strict = True,
413
wt = self.make_branch_and_tree('.')
415
with open('hello', 'w') as f:
416
f.write('hello world')
418
wt.commit(message='add hello', strict=True)
279
from bzrlib.errors import StrictCommitFailed
280
b = Branch.initialize('.')
281
file('hello', 'w').write('hello world')
283
b.working_tree().commit(message='add hello', strict=True)
420
285
def test_nonstrict_commit(self):
421
286
"""Try and commit with unknown files and strict = False, should work."""
422
wt = self.make_branch_and_tree('.')
424
with open('hello', 'w') as f:
425
f.write('hello world')
427
with open('goodbye', 'w') as f:
428
f.write('goodbye cruel world!')
429
wt.commit(message='add hello but not goodbye', strict=False)
287
b = Branch.initialize('.')
288
file('hello', 'w').write('hello world')
290
file('goodbye', 'w').write('goodbye cruel world!')
291
b.working_tree().commit(message='add hello but not goodbye', strict=False)
431
293
def test_nonstrict_commit_without_unknowns(self):
432
294
"""Try and commit with no unknown files and strict = False,
434
wt = self.make_branch_and_tree('.')
436
with open('hello', 'w') as f:
437
f.write('hello world')
439
wt.commit(message='add hello', strict=False)
296
b = Branch.initialize('.')
297
file('hello', 'w').write('hello world')
299
b.working_tree().commit(message='add hello', strict=False)
441
301
def test_signed_commit(self):
443
import breezy.commit as commit
444
oldstrategy = breezy.gpg.GPGStrategy
445
wt = self.make_branch_and_tree('.')
447
wt.commit("base", allow_pointless=True, rev_id=b'A')
448
self.assertFalse(branch.repository.has_signature_for_revision_id(b'A'))
303
import bzrlib.commit as commit
304
oldstrategy = bzrlib.gpg.GPGStrategy
305
branch = Branch.initialize('.')
306
branch.working_tree().commit("base", allow_pointless=True, rev_id='A')
307
self.failIf(branch.revision_store.has_id('A', 'sig'))
450
from ..bzr.testament import Testament
309
from bzrlib.testament import Testament
451
310
# monkey patch gpg signing mechanism
452
breezy.gpg.GPGStrategy = breezy.gpg.LoopbackGPGStrategy
453
conf = config.MemoryStack(b'''
454
create_signatures=always
456
commit.Commit(config_stack=conf).commit(
457
message="base", allow_pointless=True, rev_id=b'B',
461
return breezy.gpg.LoopbackGPGStrategy(None).sign(
462
text, breezy.gpg.MODE_CLEAR)
463
self.assertEqual(sign(Testament.from_revision(branch.repository,
464
b'B').as_short_text()),
465
branch.repository.get_signature_text(b'B'))
311
bzrlib.gpg.GPGStrategy = bzrlib.gpg.LoopbackGPGStrategy
312
commit.Commit(config=MustSignConfig(branch)).commit(branch, "base",
313
allow_pointless=True,
315
self.assertEqual(Testament.from_revision(branch,'B').as_short_text(),
316
branch.revision_store.get('B', 'sig').read())
467
breezy.gpg.GPGStrategy = oldstrategy
318
bzrlib.gpg.GPGStrategy = oldstrategy
469
320
def test_commit_failed_signature(self):
471
import breezy.commit as commit
472
oldstrategy = breezy.gpg.GPGStrategy
473
wt = self.make_branch_and_tree('.')
475
wt.commit("base", allow_pointless=True, rev_id=b'A')
476
self.assertFalse(branch.repository.has_signature_for_revision_id(b'A'))
322
import bzrlib.commit as commit
323
oldstrategy = bzrlib.gpg.GPGStrategy
324
branch = Branch.initialize('.')
325
branch.working_tree().commit("base", allow_pointless=True, rev_id='A')
326
self.failIf(branch.revision_store.has_id('A', 'sig'))
328
from bzrlib.testament import Testament
478
329
# monkey patch gpg signing mechanism
479
breezy.gpg.GPGStrategy = breezy.gpg.DisabledGPGStrategy
480
conf = config.MemoryStack(b'''
481
create_signatures=always
483
self.assertRaises(breezy.gpg.SigningFailed,
484
commit.Commit(config_stack=conf).commit,
330
bzrlib.gpg.GPGStrategy = bzrlib.gpg.DisabledGPGStrategy
331
config = MustSignConfig(branch)
332
self.assertRaises(SigningFailed,
333
commit.Commit(config=config).commit,
486
335
allow_pointless=True,
489
branch = Branch.open(self.get_url('.'))
490
self.assertEqual(branch.last_revision(), b'A')
491
self.assertFalse(branch.repository.has_revision(b'B'))
337
branch = Branch.open('.')
338
self.assertEqual(branch.revision_history(), ['A'])
339
self.failIf(branch.revision_store.has_id('B'))
493
breezy.gpg.GPGStrategy = oldstrategy
341
bzrlib.gpg.GPGStrategy = oldstrategy
495
343
def test_commit_invokes_hooks(self):
496
import breezy.commit as commit
497
wt = self.make_branch_and_tree('.')
344
import bzrlib.commit as commit
345
branch = Branch.initialize('.')
501
347
def called(branch, rev_id):
502
348
calls.append('called')
503
breezy.ahook = called
349
bzrlib.ahook = called
505
conf = config.MemoryStack(b'post_commit=breezy.ahook breezy.ahook')
506
commit.Commit(config_stack=conf).commit(
507
message="base", allow_pointless=True, rev_id=b'A',
351
config = BranchWithHooks(branch)
352
commit.Commit(config=config).commit(
354
allow_pointless=True,
509
356
self.assertEqual(['called', 'called'], calls)
513
def test_commit_object_doesnt_set_nick(self):
514
# using the Commit object directly does not set the branch nick.
515
wt = self.make_branch_and_tree('.')
517
c.commit(working_tree=wt, message='empty tree', allow_pointless=True)
518
self.assertEqual(wt.branch.revno(), 1)
520
wt.branch.repository.get_revision(
521
wt.branch.last_revision()).properties)
523
def test_safe_master_lock(self):
525
master = BzrDirMetaFormat1().initialize('master')
526
master.create_repository()
527
master_branch = master.create_branch()
528
master.create_workingtree()
529
bound = master.sprout('bound')
530
wt = bound.open_workingtree()
531
wt.branch.set_bound_location(os.path.realpath('master'))
532
master_branch.lock_write()
534
self.assertRaises(LockContention, wt.commit, 'silly')
536
master_branch.unlock()
538
def test_commit_bound_merge(self):
539
# see bug #43959; commit of a merge in a bound branch fails to push
540
# the new commit into the master
541
master_branch = self.make_branch('master')
542
bound_tree = self.make_branch_and_tree('bound')
543
bound_tree.branch.bind(master_branch)
545
self.build_tree_contents(
546
[('bound/content_file', b'initial contents\n')])
547
bound_tree.add(['content_file'])
548
bound_tree.commit(message='woo!')
550
other_bzrdir = master_branch.controldir.sprout('other')
551
other_tree = other_bzrdir.open_workingtree()
553
# do a commit to the other branch changing the content file so
554
# that our commit after merging will have a merged revision in the
555
# content file history.
556
self.build_tree_contents(
557
[('other/content_file', b'change in other\n')])
558
other_tree.commit('change in other')
560
# do a merge into the bound branch from other, and then change the
561
# content file locally to force a new revision (rather than using the
562
# revision from other). This forces extra processing in commit.
563
bound_tree.merge_from_branch(other_tree.branch)
564
self.build_tree_contents(
565
[('bound/content_file', b'change in bound\n')])
567
# before #34959 was fixed, this failed with 'revision not present in
568
# weave' when trying to implicitly push from the bound branch to the master
569
bound_tree.commit(message='commit of merge in bound tree')
571
def test_commit_reporting_after_merge(self):
572
# when doing a commit of a merge, the reporter needs to still
573
# be called for each item that is added/removed/deleted.
574
this_tree = self.make_branch_and_tree('this')
575
# we need a bunch of files and dirs, to perform one action on each.
578
'this/dirtoreparent/',
581
'this/filetoreparent',
598
this_tree.commit('create_files')
599
other_dir = this_tree.controldir.sprout('other')
600
other_tree = other_dir.open_workingtree()
601
other_tree.lock_write()
602
# perform the needed actions on the files and dirs.
604
other_tree.rename_one('dirtorename', 'renameddir')
605
other_tree.rename_one('dirtoreparent', 'renameddir/reparenteddir')
606
other_tree.rename_one('filetorename', 'renamedfile')
607
other_tree.rename_one(
608
'filetoreparent', 'renameddir/reparentedfile')
609
other_tree.remove(['dirtoremove', 'filetoremove'])
610
self.build_tree_contents([
612
('other/filetomodify', b'new content'),
613
('other/newfile', b'new file content')])
614
other_tree.add('newfile')
615
other_tree.add('newdir/')
616
other_tree.commit('modify all sample files and dirs.')
619
this_tree.merge_from_branch(other_tree.branch)
620
reporter = CapturingReporter()
621
this_tree.commit('do the commit', reporter=reporter)
623
('change', 'modified', 'filetomodify'),
624
('change', 'added', 'newdir'),
625
('change', 'added', 'newfile'),
626
('renamed', 'renamed', 'dirtorename', 'renameddir'),
627
('renamed', 'renamed', 'filetorename', 'renamedfile'),
628
('renamed', 'renamed', 'dirtoreparent', 'renameddir/reparenteddir'),
629
('renamed', 'renamed', 'filetoreparent', 'renameddir/reparentedfile'),
630
('deleted', 'dirtoremove'),
631
('deleted', 'filetoremove'),
633
result = set(reporter.calls)
634
missing = expected - result
635
new = result - expected
636
self.assertEqual((set(), set()), (missing, new))
638
def test_commit_removals_respects_filespec(self):
639
"""Commit respects the specified_files for removals."""
640
tree = self.make_branch_and_tree('.')
641
self.build_tree(['a', 'b'])
643
tree.commit('added a, b')
644
tree.remove(['a', 'b'])
645
tree.commit('removed a', specific_files='a')
646
basis = tree.basis_tree()
647
with tree.lock_read():
648
self.assertFalse(basis.is_versioned('a'))
649
self.assertTrue(basis.is_versioned('b'))
651
def test_commit_saves_1ms_timestamp(self):
652
"""Passing in a timestamp is saved with 1ms resolution"""
653
tree = self.make_branch_and_tree('.')
654
self.build_tree(['a'])
656
tree.commit('added a', timestamp=1153248633.4186721, timezone=0,
659
rev = tree.branch.repository.get_revision(b'a1')
660
self.assertEqual(1153248633.419, rev.timestamp)
662
def test_commit_has_1ms_resolution(self):
663
"""Allowing commit to generate the timestamp also has 1ms resolution"""
664
tree = self.make_branch_and_tree('.')
665
self.build_tree(['a'])
667
tree.commit('added a', rev_id=b'a1')
669
rev = tree.branch.repository.get_revision(b'a1')
670
timestamp = rev.timestamp
671
timestamp_1ms = round(timestamp, 3)
672
self.assertEqual(timestamp_1ms, timestamp)
674
def assertBasisTreeKind(self, kind, tree, path):
675
basis = tree.basis_tree()
678
self.assertEqual(kind, basis.kind(path))
682
def test_unsupported_symlink_commit(self):
683
self.requireFeature(SymlinkFeature)
684
tree = self.make_branch_and_tree('.')
685
self.build_tree(['hello'])
687
tree.commit('added hello', rev_id=b'hello_id')
688
os.symlink('hello', 'foo')
690
tree.commit('added foo', rev_id=b'foo_id')
692
trace.push_log_file(log)
693
os_symlink = getattr(os, 'symlink', None)
696
# At this point as bzr thinks symlinks are not supported
697
# we should get a warning about symlink foo and bzr should
698
# not think its removed.
700
self.build_tree(['world'])
702
tree.commit('added world', rev_id=b'world_id')
705
os.symlink = os_symlink
706
self.assertContainsRe(
708
b'Ignoring "foo" as symlinks are not '
709
b'supported on this filesystem\\.')
711
def test_commit_kind_changes(self):
712
self.requireFeature(SymlinkFeature)
713
tree = self.make_branch_and_tree('.')
714
os.symlink('target', 'name')
715
tree.add('name', b'a-file-id')
716
tree.commit('Added a symlink')
717
self.assertBasisTreeKind('symlink', tree, 'name')
720
self.build_tree(['name'])
721
tree.commit('Changed symlink to file')
722
self.assertBasisTreeKind('file', tree, 'name')
725
os.symlink('target', 'name')
726
tree.commit('file to symlink')
727
self.assertBasisTreeKind('symlink', tree, 'name')
731
tree.commit('symlink to directory')
732
self.assertBasisTreeKind('directory', tree, 'name')
735
os.symlink('target', 'name')
736
tree.commit('directory to symlink')
737
self.assertBasisTreeKind('symlink', tree, 'name')
739
# prepare for directory <-> file tests
742
tree.commit('symlink to directory')
743
self.assertBasisTreeKind('directory', tree, 'name')
746
self.build_tree(['name'])
747
tree.commit('Changed directory to file')
748
self.assertBasisTreeKind('file', tree, 'name')
752
tree.commit('file to directory')
753
self.assertBasisTreeKind('directory', tree, 'name')
755
def test_commit_unversioned_specified(self):
756
"""Commit should raise if specified files isn't in basis or worktree"""
757
tree = self.make_branch_and_tree('.')
758
self.assertRaises(errors.PathsNotVersionedError, tree.commit,
759
'message', specific_files=['bogus'])
761
class Callback(object):
763
def __init__(self, message, testcase):
765
self.message = message
766
self.testcase = testcase
768
def __call__(self, commit_obj):
770
self.testcase.assertTrue(isinstance(commit_obj, Commit))
773
def test_commit_callback(self):
774
"""Commit should invoke a callback to get the message"""
776
tree = self.make_branch_and_tree('.')
779
except Exception as e:
780
self.assertTrue(isinstance(e, BzrError))
781
self.assertEqual('The message or message_callback keyword'
782
' parameter is required for commit().', str(e))
784
self.fail('exception not raised')
785
cb = self.Callback(u'commit 1', self)
786
tree.commit(message_callback=cb)
787
self.assertTrue(cb.called)
788
repository = tree.branch.repository
789
message = repository.get_revision(tree.last_revision()).message
790
self.assertEqual('commit 1', message)
792
def test_no_callback_pointless(self):
793
"""Callback should not be invoked for pointless commit"""
794
tree = self.make_branch_and_tree('.')
795
cb = self.Callback(u'commit 2', self)
796
self.assertRaises(PointlessCommit, tree.commit, message_callback=cb,
797
allow_pointless=False)
798
self.assertFalse(cb.called)
800
def test_no_callback_netfailure(self):
801
"""Callback should not be invoked if connectivity fails"""
802
tree = self.make_branch_and_tree('.')
803
cb = self.Callback(u'commit 2', self)
804
repository = tree.branch.repository
805
# simulate network failure
807
def raise_(self, arg, arg2, arg3=None, arg4=None):
808
raise errors.NoSuchFile('foo')
809
repository.add_inventory = raise_
810
repository.add_inventory_by_delta = raise_
811
self.assertRaises(errors.NoSuchFile, tree.commit, message_callback=cb)
812
self.assertFalse(cb.called)
814
def test_selected_file_merge_commit(self):
815
"""Ensure the correct error is raised"""
816
tree = self.make_branch_and_tree('foo')
817
# pending merge would turn into a left parent
818
tree.commit('commit 1')
819
tree.add_parent_tree_id(b'example')
820
self.build_tree(['foo/bar', 'foo/baz'])
821
tree.add(['bar', 'baz'])
822
err = self.assertRaises(CannotCommitSelectedFileMerge,
823
tree.commit, 'commit 2', specific_files=['bar', 'baz'])
824
self.assertEqual(['bar', 'baz'], err.files)
825
self.assertEqual('Selected-file commit of merges is not supported'
826
' yet: files bar, baz', str(err))
828
def test_commit_ordering(self):
829
"""Test of corner-case commit ordering error"""
830
tree = self.make_branch_and_tree('.')
831
self.build_tree(['a/', 'a/z/', 'a/c/', 'a/z/x', 'a/z/y'])
832
tree.add(['a/', 'a/z/', 'a/c/', 'a/z/x', 'a/z/y'])
834
self.build_tree(['a/c/d/'])
836
tree.rename_one('a/z/x', 'a/c/d/x')
837
tree.commit('test', specific_files=['a/z/y'])
839
def test_commit_no_author(self):
840
"""The default kwarg author in MutableTree.commit should not add
841
the 'author' revision property.
843
tree = self.make_branch_and_tree('foo')
844
rev_id = tree.commit('commit 1')
845
rev = tree.branch.repository.get_revision(rev_id)
846
self.assertFalse('author' in rev.properties)
847
self.assertFalse('authors' in rev.properties)
849
def test_commit_author(self):
850
"""Passing a non-empty authors kwarg to MutableTree.commit should add
851
the 'author' revision property.
853
tree = self.make_branch_and_tree('foo')
854
rev_id = tree.commit(
856
authors=['John Doe <jdoe@example.com>'])
857
rev = tree.branch.repository.get_revision(rev_id)
858
self.assertEqual('John Doe <jdoe@example.com>',
859
rev.properties['authors'])
860
self.assertFalse('author' in rev.properties)
862
def test_commit_empty_authors_list(self):
863
"""Passing an empty list to authors shouldn't add the property."""
864
tree = self.make_branch_and_tree('foo')
865
rev_id = tree.commit('commit 1', authors=[])
866
rev = tree.branch.repository.get_revision(rev_id)
867
self.assertFalse('author' in rev.properties)
868
self.assertFalse('authors' in rev.properties)
870
def test_multiple_authors(self):
871
tree = self.make_branch_and_tree('foo')
872
rev_id = tree.commit('commit 1',
873
authors=['John Doe <jdoe@example.com>',
874
'Jane Rey <jrey@example.com>'])
875
rev = tree.branch.repository.get_revision(rev_id)
876
self.assertEqual('John Doe <jdoe@example.com>\n'
877
'Jane Rey <jrey@example.com>', rev.properties['authors'])
878
self.assertFalse('author' in rev.properties)
880
def test_author_with_newline_rejected(self):
881
tree = self.make_branch_and_tree('foo')
882
self.assertRaises(AssertionError, tree.commit, 'commit 1',
883
authors=['John\nDoe <jdoe@example.com>'])
885
def test_commit_with_checkout_and_branch_sharing_repo(self):
886
repo = self.make_repository('repo', shared=True)
887
# make_branch_and_tree ignores shared repos
888
branch = controldir.ControlDir.create_branch_convenience('repo/branch')
889
tree2 = branch.create_checkout('repo/tree2')
890
tree2.commit('message', rev_id=b'rev1')
891
self.assertTrue(tree2.branch.repository.has_revision(b'rev1'))
894
class FilterExcludedTests(TestCase):
896
def test_add_file_not_excluded(self):
899
'fid', (None, 'newpath'),
900
0, (False, False), ('pid', 'pid'), ('newpath', 'newpath'),
901
('file', 'file'), (True, True))]
902
self.assertEqual(changes, list(
903
filter_excluded(changes, ['otherpath'])))
905
def test_add_file_excluded(self):
908
'fid', (None, 'newpath'),
909
0, (False, False), ('pid', 'pid'), ('newpath', 'newpath'),
910
('file', 'file'), (True, True))]
911
self.assertEqual([], list(filter_excluded(changes, ['newpath'])))
913
def test_delete_file_excluded(self):
916
'fid', ('somepath', None),
917
0, (False, None), ('pid', None), ('newpath', None),
918
('file', None), (True, None))]
919
self.assertEqual([], list(filter_excluded(changes, ['somepath'])))
921
def test_move_from_or_to_excluded(self):
924
'fid', ('oldpath', 'newpath'),
925
0, (False, False), ('pid', 'pid'), ('oldpath', 'newpath'),
926
('file', 'file'), (True, True))]
927
self.assertEqual([], list(filter_excluded(changes, ['oldpath'])))
928
self.assertEqual([], list(filter_excluded(changes, ['newpath'])))