/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: 2020-03-22 01:35:14 UTC
  • mfrom: (7490.7.6 work)
  • mto: This revision was merged to the branch mainline in revision 7499.
  • Revision ID: jelmer@jelmer.uk-20200322013514-7vw1ntwho04rcuj3
merge lp:brz/3.1.

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
from io import BytesIO
 
20
 
 
21
import breezy
 
22
from .. import (
 
23
    config,
 
24
    controldir,
 
25
    errors,
 
26
    trace,
 
27
    )
 
28
from ..branch import Branch
 
29
from ..bzr.bzrdir import BzrDirMetaFormat1
 
30
from ..commit import (
 
31
    CannotCommitSelectedFileMerge,
 
32
    Commit,
 
33
    NullCommitReporter,
 
34
    PointlessCommit,
 
35
    filter_excluded,
 
36
    )
 
37
from ..errors import (
 
38
    BzrError,
 
39
    LockContention,
 
40
    )
 
41
from ..tree import TreeChange
 
42
from . import (
 
43
    TestCase,
 
44
    TestCaseWithTransport,
 
45
    test_foreign,
 
46
    )
 
47
from .features import (
 
48
    SymlinkFeature,
 
49
    )
 
50
from .matchers import MatchesAncestry
 
51
 
 
52
 
 
53
# TODO: Test commit with some added, and added-but-missing files
 
54
 
 
55
class MustSignConfig(config.MemoryStack):
 
56
 
 
57
    def __init__(self):
 
58
        super(MustSignConfig, self).__init__(b'''
 
59
create_signatures=always
 
60
''')
 
61
 
 
62
 
 
63
class CapturingReporter(NullCommitReporter):
 
64
    """This reporter captures the calls made to it for evaluation later."""
 
65
 
 
66
    def __init__(self):
 
67
        # a list of the calls this received
 
68
        self.calls = []
 
69
 
 
70
    def snapshot_change(self, change, path):
 
71
        self.calls.append(('change', change, path))
 
72
 
 
73
    def deleted(self, file_id):
 
74
        self.calls.append(('deleted', file_id))
 
75
 
 
76
    def missing(self, path):
 
77
        self.calls.append(('missing', path))
 
78
 
 
79
    def renamed(self, change, old_path, new_path):
 
80
        self.calls.append(('renamed', change, old_path, new_path))
 
81
 
 
82
    def is_verbose(self):
 
83
        return True
 
84
 
 
85
 
 
86
class TestCommit(TestCaseWithTransport):
 
87
 
 
88
    def test_simple_commit(self):
 
89
        """Commit and check two versions of a single file."""
 
90
        wt = self.make_branch_and_tree('.')
 
91
        b = wt.branch
 
92
        with open('hello', 'w') as f:
 
93
            f.write('hello world')
 
94
        wt.add('hello')
 
95
        rev1 = wt.commit(message='add hello')
 
96
 
 
97
        with open('hello', 'w') as f:
 
98
            f.write('version 2')
 
99
        rev2 = wt.commit(message='commit 2')
 
100
 
 
101
        eq = self.assertEqual
 
102
        eq(b.revno(), 2)
 
103
        rev = b.repository.get_revision(rev1)
 
104
        eq(rev.message, 'add hello')
 
105
 
 
106
        tree1 = b.repository.revision_tree(rev1)
 
107
        tree1.lock_read()
 
108
        text = tree1.get_file_text('hello')
 
109
        tree1.unlock()
 
110
        self.assertEqual(b'hello world', text)
 
111
 
 
112
        tree2 = b.repository.revision_tree(rev2)
 
113
        tree2.lock_read()
 
114
        text = tree2.get_file_text('hello')
 
115
        tree2.unlock()
 
116
        self.assertEqual(b'version 2', text)
 
117
 
 
118
    def test_commit_lossy_native(self):
 
119
        """Attempt a lossy commit to a native branch."""
 
120
        wt = self.make_branch_and_tree('.')
 
121
        b = wt.branch
 
122
        with open('hello', 'w') as f:
 
123
            f.write('hello world')
 
124
        wt.add('hello')
 
125
        revid = wt.commit(message='add hello', rev_id=b'revid', lossy=True)
 
126
        self.assertEqual(b'revid', revid)
 
127
 
 
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())
 
133
        b = wt.branch
 
134
        with open('hello', 'w') as f:
 
135
            f.write('hello world')
 
136
        wt.add('hello')
 
137
        revid = wt.commit(message='add hello', lossy=True,
 
138
                          timestamp=1302659388, timezone=0)
 
139
        self.assertEqual(b'dummy-v1:1302659388-0-UNKNOWN', revid)
 
140
 
 
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")
 
147
        b = wt.branch
 
148
        with open('local/hello', 'w') as f:
 
149
            f.write('hello world')
 
150
        wt.add('hello')
 
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())
 
158
 
 
159
    def test_missing_commit(self):
 
160
        """Test a commit with a missing file"""
 
161
        wt = self.make_branch_and_tree('.')
 
162
        b = wt.branch
 
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')
 
167
 
 
168
        os.remove('hello')
 
169
        reporter = CapturingReporter()
 
170
        wt.commit('removed hello', rev_id=b'rev2', reporter=reporter)
 
171
        self.assertEqual(
 
172
            [('missing', u'hello'), ('deleted', u'hello')],
 
173
            reporter.calls)
 
174
 
 
175
        tree = b.repository.revision_tree(b'rev2')
 
176
        self.assertFalse(tree.has_filename('hello'))
 
177
 
 
178
    def test_partial_commit_move(self):
 
179
        """Test a partial commit where a file was renamed but not committed.
 
180
 
 
181
        https://bugs.launchpad.net/bzr/+bug/83039
 
182
 
 
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.
 
186
        """
 
187
        wt = self.make_branch_and_tree('.')
 
188
        b = wt.branch
 
189
        self.build_tree(['annotate/', 'annotate/foo.py',
 
190
                         'olive/', 'olive/dialog.py'
 
191
                         ])
 
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"])
 
197
 
 
198
    def test_pointless_commit(self):
 
199
        """Commit refuses unless there are changes or it's forced."""
 
200
        wt = self.make_branch_and_tree('.')
 
201
        b = wt.branch
 
202
        with open('hello', 'w') as f:
 
203
            f.write('hello')
 
204
        wt.add(['hello'])
 
205
        wt.commit(message='add hello')
 
206
        self.assertEqual(b.revno(), 1)
 
207
        self.assertRaises(PointlessCommit,
 
208
                          wt.commit,
 
209
                          message='fails',
 
210
                          allow_pointless=False)
 
211
        self.assertEqual(b.revno(), 1)
 
212
 
 
213
    def test_commit_empty(self):
 
214
        """Commiting an empty tree works."""
 
215
        wt = self.make_branch_and_tree('.')
 
216
        b = wt.branch
 
217
        wt.commit(message='empty tree', allow_pointless=True)
 
218
        self.assertRaises(PointlessCommit,
 
219
                          wt.commit,
 
220
                          message='empty tree',
 
221
                          allow_pointless=False)
 
222
        wt.commit(message='empty tree', allow_pointless=True)
 
223
        self.assertEqual(b.revno(), 2)
 
224
 
 
225
    def test_selective_delete(self):
 
226
        """Selective commit in tree with deletions"""
 
227
        wt = self.make_branch_and_tree('.')
 
228
        b = wt.branch
 
229
        with open('hello', 'w') as f:
 
230
            f.write('hello')
 
231
        with open('buongia', 'w') as f:
 
232
            f.write('buongia')
 
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')
 
237
 
 
238
        os.remove('hello')
 
239
        with open('buongia', 'w') as f:
 
240
            f.write('new text')
 
241
        wt.commit(message='update text',
 
242
                  specific_files=['buongia'],
 
243
                  allow_pointless=False,
 
244
                  rev_id=b'test@rev-2')
 
245
 
 
246
        wt.commit(message='remove hello',
 
247
                  specific_files=['hello'],
 
248
                  allow_pointless=False,
 
249
                  rev_id=b'test@rev-3')
 
250
 
 
251
        eq = self.assertEqual
 
252
        eq(b.revno(), 3)
 
253
 
 
254
        tree2 = b.repository.revision_tree(b'test@rev-2')
 
255
        tree2.lock_read()
 
256
        self.addCleanup(tree2.unlock)
 
257
        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')
 
260
 
 
261
        tree3 = b.repository.revision_tree(b'test@rev-3')
 
262
        tree3.lock_read()
 
263
        self.addCleanup(tree3.unlock)
 
264
        self.assertFalse(tree3.has_filename('hello'))
 
265
        self.assertEqual(tree3.get_file_text('buongia'), b'new text')
 
266
 
 
267
    def test_commit_rename(self):
 
268
        """Test commit of a revision where a file is renamed."""
 
269
        tree = self.make_branch_and_tree('.')
 
270
        b = tree.branch
 
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)
 
274
 
 
275
        tree.rename_one('hello', 'fruity')
 
276
        tree.commit(message='renamed', rev_id=b'test@rev-2',
 
277
                    allow_pointless=False)
 
278
 
 
279
        eq = self.assertEqual
 
280
        tree1 = b.repository.revision_tree(b'test@rev-1')
 
281
        tree1.lock_read()
 
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')
 
285
        self.assertFalse(tree1.has_filename('fruity'))
 
286
        self.check_tree_shape(tree1, ['hello'])
 
287
        eq(tree1.get_file_revision('hello'), b'test@rev-1')
 
288
 
 
289
        tree2 = b.repository.revision_tree(b'test@rev-2')
 
290
        tree2.lock_read()
 
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')
 
296
 
 
297
    def test_reused_rev_id(self):
 
298
        """Test that a revision id cannot be reused in a branch"""
 
299
        wt = self.make_branch_and_tree('.')
 
300
        b = wt.branch
 
301
        wt.commit('initial', rev_id=b'test@rev-1', allow_pointless=True)
 
302
        self.assertRaises(Exception,
 
303
                          wt.commit,
 
304
                          message='reused id',
 
305
                          rev_id=b'test@rev-1',
 
306
                          allow_pointless=True)
 
307
 
 
308
    def test_commit_move(self):
 
309
        """Test commit of revisions with moved files and directories"""
 
310
        eq = self.assertEqual
 
311
        wt = self.make_branch_and_tree('.')
 
312
        b = wt.branch
 
313
        r1 = b'test@rev-1'
 
314
        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')
 
318
        r2 = b'test@rev-2'
 
319
        wt.commit('two', rev_id=r2, allow_pointless=False)
 
320
        wt.lock_read()
 
321
        try:
 
322
            self.check_tree_shape(wt, ['a/', 'a/hello', 'b/'])
 
323
        finally:
 
324
            wt.unlock()
 
325
 
 
326
        wt.move(['b'], 'a')
 
327
        r3 = b'test@rev-3'
 
328
        wt.commit('three', rev_id=r3, allow_pointless=False)
 
329
        wt.lock_read()
 
330
        try:
 
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/'])
 
335
        finally:
 
336
            wt.unlock()
 
337
 
 
338
        wt.move(['a/hello'], 'a/b')
 
339
        r4 = b'test@rev-4'
 
340
        wt.commit('four', rev_id=r4, allow_pointless=False)
 
341
        wt.lock_read()
 
342
        try:
 
343
            self.check_tree_shape(wt, ['a/', 'a/b/hello', 'a/b/'])
 
344
        finally:
 
345
            wt.unlock()
 
346
 
 
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)
 
351
 
 
352
    def test_removed_commit(self):
 
353
        """Commit with a removed file"""
 
354
        wt = self.make_branch_and_tree('.')
 
355
        b = wt.branch
 
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')
 
360
        wt.remove('hello')
 
361
        wt.commit('removed hello', rev_id=b'rev2')
 
362
 
 
363
        tree = b.repository.revision_tree(b'rev2')
 
364
        self.assertFalse(tree.has_filename('hello'))
 
365
 
 
366
    def test_committed_ancestry(self):
 
367
        """Test commit appends revisions to ancestry."""
 
368
        wt = self.make_branch_and_tree('.')
 
369
        b = wt.branch
 
370
        rev_ids = []
 
371
        for i in range(4):
 
372
            with open('hello', 'w') as f:
 
373
                f.write((str(i) * 4) + '\n')
 
374
            if i == 0:
 
375
                wt.add(['hello'], [b'hello-id'])
 
376
            rev_id = b'test@rev-%d' % (i + 1)
 
377
            rev_ids.append(rev_id)
 
378
            wt.commit(message='rev %d' % (i + 1),
 
379
                      rev_id=rev_id)
 
380
        for i in range(4):
 
381
            self.assertThat(rev_ids[:i + 1],
 
382
                            MatchesAncestry(b.repository, rev_ids[i]))
 
383
 
 
384
    def test_commit_new_subdir_child_selective(self):
 
385
        wt = self.make_branch_and_tree('.')
 
386
        b = wt.branch
 
387
        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)
 
394
        # FIXME: This should raise a KeyError I think, rbc20051006
 
395
        self.assertRaises(BzrError, inv.get_entry, b'file2id')
 
396
 
 
397
    def test_strict_commit(self):
 
398
        """Try and commit with unknown files and strict = True, should fail."""
 
399
        from ..errors import StrictCommitFailed
 
400
        wt = self.make_branch_and_tree('.')
 
401
        b = wt.branch
 
402
        with open('hello', 'w') as f:
 
403
            f.write('hello world')
 
404
        wt.add('hello')
 
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)
 
409
 
 
410
    def test_strict_commit_without_unknowns(self):
 
411
        """Try and commit with no unknown files and strict = True,
 
412
        should work."""
 
413
        wt = self.make_branch_and_tree('.')
 
414
        b = wt.branch
 
415
        with open('hello', 'w') as f:
 
416
            f.write('hello world')
 
417
        wt.add('hello')
 
418
        wt.commit(message='add hello', strict=True)
 
419
 
 
420
    def test_nonstrict_commit(self):
 
421
        """Try and commit with unknown files and strict = False, should work."""
 
422
        wt = self.make_branch_and_tree('.')
 
423
        b = wt.branch
 
424
        with open('hello', 'w') as f:
 
425
            f.write('hello world')
 
426
        wt.add('hello')
 
427
        with open('goodbye', 'w') as f:
 
428
            f.write('goodbye cruel world!')
 
429
        wt.commit(message='add hello but not goodbye', strict=False)
 
430
 
 
431
    def test_nonstrict_commit_without_unknowns(self):
 
432
        """Try and commit with no unknown files and strict = False,
 
433
        should work."""
 
434
        wt = self.make_branch_and_tree('.')
 
435
        b = wt.branch
 
436
        with open('hello', 'w') as f:
 
437
            f.write('hello world')
 
438
        wt.add('hello')
 
439
        wt.commit(message='add hello', strict=False)
 
440
 
 
441
    def test_signed_commit(self):
 
442
        import breezy.gpg
 
443
        import breezy.commit as commit
 
444
        oldstrategy = breezy.gpg.GPGStrategy
 
445
        wt = self.make_branch_and_tree('.')
 
446
        branch = wt.branch
 
447
        wt.commit("base", allow_pointless=True, rev_id=b'A')
 
448
        self.assertFalse(branch.repository.has_signature_for_revision_id(b'A'))
 
449
        try:
 
450
            from ..bzr.testament import Testament
 
451
            # monkey patch gpg signing mechanism
 
452
            breezy.gpg.GPGStrategy = breezy.gpg.LoopbackGPGStrategy
 
453
            conf = config.MemoryStack(b'''
 
454
create_signatures=always
 
455
''')
 
456
            commit.Commit(config_stack=conf).commit(
 
457
                message="base", allow_pointless=True, rev_id=b'B',
 
458
                working_tree=wt)
 
459
 
 
460
            def sign(text):
 
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'))
 
466
        finally:
 
467
            breezy.gpg.GPGStrategy = oldstrategy
 
468
 
 
469
    def test_commit_failed_signature(self):
 
470
        import breezy.gpg
 
471
        import breezy.commit as commit
 
472
        oldstrategy = breezy.gpg.GPGStrategy
 
473
        wt = self.make_branch_and_tree('.')
 
474
        branch = wt.branch
 
475
        wt.commit("base", allow_pointless=True, rev_id=b'A')
 
476
        self.assertFalse(branch.repository.has_signature_for_revision_id(b'A'))
 
477
        try:
 
478
            # monkey patch gpg signing mechanism
 
479
            breezy.gpg.GPGStrategy = breezy.gpg.DisabledGPGStrategy
 
480
            conf = config.MemoryStack(b'''
 
481
create_signatures=always
 
482
''')
 
483
            self.assertRaises(breezy.gpg.SigningFailed,
 
484
                              commit.Commit(config_stack=conf).commit,
 
485
                              message="base",
 
486
                              allow_pointless=True,
 
487
                              rev_id=b'B',
 
488
                              working_tree=wt)
 
489
            branch = Branch.open(self.get_url('.'))
 
490
            self.assertEqual(branch.last_revision(), b'A')
 
491
            self.assertFalse(branch.repository.has_revision(b'B'))
 
492
        finally:
 
493
            breezy.gpg.GPGStrategy = oldstrategy
 
494
 
 
495
    def test_commit_invokes_hooks(self):
 
496
        import breezy.commit as commit
 
497
        wt = self.make_branch_and_tree('.')
 
498
        branch = wt.branch
 
499
        calls = []
 
500
 
 
501
        def called(branch, rev_id):
 
502
            calls.append('called')
 
503
        breezy.ahook = called
 
504
        try:
 
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',
 
508
                working_tree=wt)
 
509
            self.assertEqual(['called', 'called'], calls)
 
510
        finally:
 
511
            del breezy.ahook
 
512
 
 
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('.')
 
516
        c = Commit()
 
517
        c.commit(working_tree=wt, message='empty tree', allow_pointless=True)
 
518
        self.assertEqual(wt.branch.revno(), 1)
 
519
        self.assertEqual({},
 
520
                         wt.branch.repository.get_revision(
 
521
            wt.branch.last_revision()).properties)
 
522
 
 
523
    def test_safe_master_lock(self):
 
524
        os.mkdir('master')
 
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()
 
533
        try:
 
534
            self.assertRaises(LockContention, wt.commit, 'silly')
 
535
        finally:
 
536
            master_branch.unlock()
 
537
 
 
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)
 
544
 
 
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!')
 
549
 
 
550
        other_bzrdir = master_branch.controldir.sprout('other')
 
551
        other_tree = other_bzrdir.open_workingtree()
 
552
 
 
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')
 
559
 
 
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')])
 
566
 
 
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')
 
570
 
 
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.
 
576
        self.build_tree([
 
577
            'this/dirtorename/',
 
578
            'this/dirtoreparent/',
 
579
            'this/dirtoleave/',
 
580
            'this/dirtoremove/',
 
581
            'this/filetoreparent',
 
582
            'this/filetorename',
 
583
            'this/filetomodify',
 
584
            'this/filetoremove',
 
585
            'this/filetoleave']
 
586
            )
 
587
        this_tree.add([
 
588
            'dirtorename',
 
589
            'dirtoreparent',
 
590
            'dirtoleave',
 
591
            'dirtoremove',
 
592
            'filetoreparent',
 
593
            'filetorename',
 
594
            'filetomodify',
 
595
            'filetoremove',
 
596
            'filetoleave']
 
597
            )
 
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.
 
603
        try:
 
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([
 
611
                ('other/newdir/', ),
 
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.')
 
617
        finally:
 
618
            other_tree.unlock()
 
619
        this_tree.merge_from_branch(other_tree.branch)
 
620
        reporter = CapturingReporter()
 
621
        this_tree.commit('do the commit', reporter=reporter)
 
622
        expected = {
 
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'),
 
632
            }
 
633
        result = set(reporter.calls)
 
634
        missing = expected - result
 
635
        new = result - expected
 
636
        self.assertEqual((set(), set()), (missing, new))
 
637
 
 
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'])
 
642
        tree.add(['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'))
 
650
 
 
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'])
 
655
        tree.add('a')
 
656
        tree.commit('added a', timestamp=1153248633.4186721, timezone=0,
 
657
                    rev_id=b'a1')
 
658
 
 
659
        rev = tree.branch.repository.get_revision(b'a1')
 
660
        self.assertEqual(1153248633.419, rev.timestamp)
 
661
 
 
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'])
 
666
        tree.add('a')
 
667
        tree.commit('added a', rev_id=b'a1')
 
668
 
 
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)
 
673
 
 
674
    def assertBasisTreeKind(self, kind, tree, path):
 
675
        basis = tree.basis_tree()
 
676
        basis.lock_read()
 
677
        try:
 
678
            self.assertEqual(kind, basis.kind(path))
 
679
        finally:
 
680
            basis.unlock()
 
681
 
 
682
    def test_unsupported_symlink_commit(self):
 
683
        self.requireFeature(SymlinkFeature)
 
684
        tree = self.make_branch_and_tree('.')
 
685
        self.build_tree(['hello'])
 
686
        tree.add('hello')
 
687
        tree.commit('added hello', rev_id=b'hello_id')
 
688
        os.symlink('hello', 'foo')
 
689
        tree.add('foo')
 
690
        tree.commit('added foo', rev_id=b'foo_id')
 
691
        log = BytesIO()
 
692
        trace.push_log_file(log)
 
693
        os_symlink = getattr(os, 'symlink', None)
 
694
        os.symlink = None
 
695
        try:
 
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.
 
699
            os.unlink('foo')
 
700
            self.build_tree(['world'])
 
701
            tree.add('world')
 
702
            tree.commit('added world', rev_id=b'world_id')
 
703
        finally:
 
704
            if os_symlink:
 
705
                os.symlink = os_symlink
 
706
        self.assertContainsRe(
 
707
            log.getvalue(),
 
708
            b'Ignoring "foo" as symlinks are not '
 
709
            b'supported on this filesystem\\.')
 
710
 
 
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')
 
718
 
 
719
        os.unlink('name')
 
720
        self.build_tree(['name'])
 
721
        tree.commit('Changed symlink to file')
 
722
        self.assertBasisTreeKind('file', tree, 'name')
 
723
 
 
724
        os.unlink('name')
 
725
        os.symlink('target', 'name')
 
726
        tree.commit('file to symlink')
 
727
        self.assertBasisTreeKind('symlink', tree, 'name')
 
728
 
 
729
        os.unlink('name')
 
730
        os.mkdir('name')
 
731
        tree.commit('symlink to directory')
 
732
        self.assertBasisTreeKind('directory', tree, 'name')
 
733
 
 
734
        os.rmdir('name')
 
735
        os.symlink('target', 'name')
 
736
        tree.commit('directory to symlink')
 
737
        self.assertBasisTreeKind('symlink', tree, 'name')
 
738
 
 
739
        # prepare for directory <-> file tests
 
740
        os.unlink('name')
 
741
        os.mkdir('name')
 
742
        tree.commit('symlink to directory')
 
743
        self.assertBasisTreeKind('directory', tree, 'name')
 
744
 
 
745
        os.rmdir('name')
 
746
        self.build_tree(['name'])
 
747
        tree.commit('Changed directory to file')
 
748
        self.assertBasisTreeKind('file', tree, 'name')
 
749
 
 
750
        os.unlink('name')
 
751
        os.mkdir('name')
 
752
        tree.commit('file to directory')
 
753
        self.assertBasisTreeKind('directory', tree, 'name')
 
754
 
 
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'])
 
760
 
 
761
    class Callback(object):
 
762
 
 
763
        def __init__(self, message, testcase):
 
764
            self.called = False
 
765
            self.message = message
 
766
            self.testcase = testcase
 
767
 
 
768
        def __call__(self, commit_obj):
 
769
            self.called = True
 
770
            self.testcase.assertTrue(isinstance(commit_obj, Commit))
 
771
            return self.message
 
772
 
 
773
    def test_commit_callback(self):
 
774
        """Commit should invoke a callback to get the message"""
 
775
 
 
776
        tree = self.make_branch_and_tree('.')
 
777
        try:
 
778
            tree.commit()
 
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))
 
783
        else:
 
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)
 
791
 
 
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)
 
799
 
 
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
 
806
 
 
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)
 
813
 
 
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))
 
827
 
 
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'])
 
833
        tree.commit('setup')
 
834
        self.build_tree(['a/c/d/'])
 
835
        tree.add('a/c/d')
 
836
        tree.rename_one('a/z/x', 'a/c/d/x')
 
837
        tree.commit('test', specific_files=['a/z/y'])
 
838
 
 
839
    def test_commit_no_author(self):
 
840
        """The default kwarg author in MutableTree.commit should not add
 
841
        the 'author' revision property.
 
842
        """
 
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)
 
848
 
 
849
    def test_commit_author(self):
 
850
        """Passing a non-empty authors kwarg to MutableTree.commit should add
 
851
        the 'author' revision property.
 
852
        """
 
853
        tree = self.make_branch_and_tree('foo')
 
854
        rev_id = tree.commit(
 
855
            'commit 1',
 
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)
 
861
 
 
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)
 
869
 
 
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)
 
879
 
 
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>'])
 
884
 
 
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'))
 
892
 
 
893
 
 
894
class FilterExcludedTests(TestCase):
 
895
 
 
896
    def test_add_file_not_excluded(self):
 
897
        changes = [
 
898
            TreeChange(
 
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'])))
 
904
 
 
905
    def test_add_file_excluded(self):
 
906
        changes = [
 
907
            TreeChange(
 
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'])))
 
912
 
 
913
    def test_delete_file_excluded(self):
 
914
        changes = [
 
915
            TreeChange(
 
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'])))
 
920
 
 
921
    def test_move_from_or_to_excluded(self):
 
922
        changes = [
 
923
            TreeChange(
 
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'])))