/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to breezy/tests/test_commit.py

  • Committer: Jelmer Vernooij
  • Date: 2019-08-12 20:24:50 UTC
  • mto: (7290.1.35 work)
  • mto: This revision was merged to the branch mainline in revision 7405.
  • Revision ID: jelmer@jelmer.uk-20190812202450-vdpamxay6sebo93w
Fix path to brz.

Show diffs side-by-side

added added

removed removed

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