/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: Gustav Hartvigsson
  • Date: 2021-01-09 21:36:27 UTC
  • Revision ID: gustav.hartvigsson@gmail.com-20210109213627-h1xwcutzy9m7a99b
Added 'Case Preserving Working Tree Use Cases' from Canonical Wiki

* Addod a page from the Canonical Bazaar wiki
  with information on the scmeatics of case
  perserving filesystems an a case insensitive
  filesystem works.
  
  * Needs re-work, but this will do as it is the
    same inforamoton as what was on the linked
    page in the currint documentation.

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 ..bzr.inventorytree import InventoryTreeChange
 
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
        with master_branch.lock_write():
 
533
            self.assertRaises(LockContention, wt.commit, 'silly')
 
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_unsupported_symlink_commit(self):
 
680
        self.requireFeature(SymlinkFeature)
 
681
        tree = self.make_branch_and_tree('.')
 
682
        self.build_tree(['hello'])
 
683
        tree.add('hello')
 
684
        tree.commit('added hello', rev_id=b'hello_id')
 
685
        os.symlink('hello', 'foo')
 
686
        tree.add('foo')
 
687
        tree.commit('added foo', rev_id=b'foo_id')
 
688
        log = BytesIO()
 
689
        trace.push_log_file(log)
 
690
        os_symlink = getattr(os, 'symlink', None)
 
691
        os.symlink = None
 
692
        try:
 
693
            # At this point as bzr thinks symlinks are not supported
 
694
            # we should get a warning about symlink foo and bzr should
 
695
            # not think its removed.
 
696
            os.unlink('foo')
 
697
            self.build_tree(['world'])
 
698
            tree.add('world')
 
699
            tree.commit('added world', rev_id=b'world_id')
 
700
        finally:
 
701
            if os_symlink:
 
702
                os.symlink = os_symlink
 
703
        self.assertContainsRe(
 
704
            log.getvalue(),
 
705
            b'Ignoring "foo" as symlinks are not '
 
706
            b'supported on this filesystem\\.')
 
707
 
 
708
    def test_commit_kind_changes(self):
 
709
        self.requireFeature(SymlinkFeature)
 
710
        tree = self.make_branch_and_tree('.')
 
711
        os.symlink('target', 'name')
 
712
        tree.add('name', b'a-file-id')
 
713
        tree.commit('Added a symlink')
 
714
        self.assertBasisTreeKind('symlink', tree, 'name')
 
715
 
 
716
        os.unlink('name')
 
717
        self.build_tree(['name'])
 
718
        tree.commit('Changed symlink to file')
 
719
        self.assertBasisTreeKind('file', tree, 'name')
 
720
 
 
721
        os.unlink('name')
 
722
        os.symlink('target', 'name')
 
723
        tree.commit('file to symlink')
 
724
        self.assertBasisTreeKind('symlink', tree, 'name')
 
725
 
 
726
        os.unlink('name')
 
727
        os.mkdir('name')
 
728
        tree.commit('symlink to directory')
 
729
        self.assertBasisTreeKind('directory', tree, 'name')
 
730
 
 
731
        os.rmdir('name')
 
732
        os.symlink('target', 'name')
 
733
        tree.commit('directory to symlink')
 
734
        self.assertBasisTreeKind('symlink', tree, 'name')
 
735
 
 
736
        # prepare for directory <-> file tests
 
737
        os.unlink('name')
 
738
        os.mkdir('name')
 
739
        tree.commit('symlink to directory')
 
740
        self.assertBasisTreeKind('directory', tree, 'name')
 
741
 
 
742
        os.rmdir('name')
 
743
        self.build_tree(['name'])
 
744
        tree.commit('Changed directory to file')
 
745
        self.assertBasisTreeKind('file', tree, 'name')
 
746
 
 
747
        os.unlink('name')
 
748
        os.mkdir('name')
 
749
        tree.commit('file to directory')
 
750
        self.assertBasisTreeKind('directory', tree, 'name')
 
751
 
 
752
    def test_commit_unversioned_specified(self):
 
753
        """Commit should raise if specified files isn't in basis or worktree"""
 
754
        tree = self.make_branch_and_tree('.')
 
755
        self.assertRaises(errors.PathsNotVersionedError, tree.commit,
 
756
                          'message', specific_files=['bogus'])
 
757
 
 
758
    class Callback(object):
 
759
 
 
760
        def __init__(self, message, testcase):
 
761
            self.called = False
 
762
            self.message = message
 
763
            self.testcase = testcase
 
764
 
 
765
        def __call__(self, commit_obj):
 
766
            self.called = True
 
767
            self.testcase.assertTrue(isinstance(commit_obj, Commit))
 
768
            return self.message
 
769
 
 
770
    def test_commit_callback(self):
 
771
        """Commit should invoke a callback to get the message"""
 
772
 
 
773
        tree = self.make_branch_and_tree('.')
 
774
        try:
 
775
            tree.commit()
 
776
        except Exception as e:
 
777
            self.assertTrue(isinstance(e, BzrError))
 
778
            self.assertEqual('The message or message_callback keyword'
 
779
                             ' parameter is required for commit().', str(e))
 
780
        else:
 
781
            self.fail('exception not raised')
 
782
        cb = self.Callback(u'commit 1', self)
 
783
        tree.commit(message_callback=cb)
 
784
        self.assertTrue(cb.called)
 
785
        repository = tree.branch.repository
 
786
        message = repository.get_revision(tree.last_revision()).message
 
787
        self.assertEqual('commit 1', message)
 
788
 
 
789
    def test_no_callback_pointless(self):
 
790
        """Callback should not be invoked for pointless commit"""
 
791
        tree = self.make_branch_and_tree('.')
 
792
        cb = self.Callback(u'commit 2', self)
 
793
        self.assertRaises(PointlessCommit, tree.commit, message_callback=cb,
 
794
                          allow_pointless=False)
 
795
        self.assertFalse(cb.called)
 
796
 
 
797
    def test_no_callback_netfailure(self):
 
798
        """Callback should not be invoked if connectivity fails"""
 
799
        tree = self.make_branch_and_tree('.')
 
800
        cb = self.Callback(u'commit 2', self)
 
801
        repository = tree.branch.repository
 
802
        # simulate network failure
 
803
 
 
804
        def raise_(self, arg, arg2, arg3=None, arg4=None):
 
805
            raise errors.NoSuchFile('foo')
 
806
        repository.add_inventory = raise_
 
807
        repository.add_inventory_by_delta = raise_
 
808
        self.assertRaises(errors.NoSuchFile, tree.commit, message_callback=cb)
 
809
        self.assertFalse(cb.called)
 
810
 
 
811
    def test_selected_file_merge_commit(self):
 
812
        """Ensure the correct error is raised"""
 
813
        tree = self.make_branch_and_tree('foo')
 
814
        # pending merge would turn into a left parent
 
815
        tree.commit('commit 1')
 
816
        tree.add_parent_tree_id(b'example')
 
817
        self.build_tree(['foo/bar', 'foo/baz'])
 
818
        tree.add(['bar', 'baz'])
 
819
        err = self.assertRaises(CannotCommitSelectedFileMerge,
 
820
                                tree.commit, 'commit 2', specific_files=['bar', 'baz'])
 
821
        self.assertEqual(['bar', 'baz'], err.files)
 
822
        self.assertEqual('Selected-file commit of merges is not supported'
 
823
                         ' yet: files bar, baz', str(err))
 
824
 
 
825
    def test_commit_ordering(self):
 
826
        """Test of corner-case commit ordering error"""
 
827
        tree = self.make_branch_and_tree('.')
 
828
        self.build_tree(['a/', 'a/z/', 'a/c/', 'a/z/x', 'a/z/y'])
 
829
        tree.add(['a/', 'a/z/', 'a/c/', 'a/z/x', 'a/z/y'])
 
830
        tree.commit('setup')
 
831
        self.build_tree(['a/c/d/'])
 
832
        tree.add('a/c/d')
 
833
        tree.rename_one('a/z/x', 'a/c/d/x')
 
834
        tree.commit('test', specific_files=['a/z/y'])
 
835
 
 
836
    def test_commit_no_author(self):
 
837
        """The default kwarg author in MutableTree.commit should not add
 
838
        the 'author' revision property.
 
839
        """
 
840
        tree = self.make_branch_and_tree('foo')
 
841
        rev_id = tree.commit('commit 1')
 
842
        rev = tree.branch.repository.get_revision(rev_id)
 
843
        self.assertFalse('author' in rev.properties)
 
844
        self.assertFalse('authors' in rev.properties)
 
845
 
 
846
    def test_commit_author(self):
 
847
        """Passing a non-empty authors kwarg to MutableTree.commit should add
 
848
        the 'author' revision property.
 
849
        """
 
850
        tree = self.make_branch_and_tree('foo')
 
851
        rev_id = tree.commit(
 
852
            'commit 1',
 
853
            authors=['John Doe <jdoe@example.com>'])
 
854
        rev = tree.branch.repository.get_revision(rev_id)
 
855
        self.assertEqual('John Doe <jdoe@example.com>',
 
856
                         rev.properties['authors'])
 
857
        self.assertFalse('author' in rev.properties)
 
858
 
 
859
    def test_commit_empty_authors_list(self):
 
860
        """Passing an empty list to authors shouldn't add the property."""
 
861
        tree = self.make_branch_and_tree('foo')
 
862
        rev_id = tree.commit('commit 1', authors=[])
 
863
        rev = tree.branch.repository.get_revision(rev_id)
 
864
        self.assertFalse('author' in rev.properties)
 
865
        self.assertFalse('authors' in rev.properties)
 
866
 
 
867
    def test_multiple_authors(self):
 
868
        tree = self.make_branch_and_tree('foo')
 
869
        rev_id = tree.commit('commit 1',
 
870
                             authors=['John Doe <jdoe@example.com>',
 
871
                                      'Jane Rey <jrey@example.com>'])
 
872
        rev = tree.branch.repository.get_revision(rev_id)
 
873
        self.assertEqual('John Doe <jdoe@example.com>\n'
 
874
                         'Jane Rey <jrey@example.com>', rev.properties['authors'])
 
875
        self.assertFalse('author' in rev.properties)
 
876
 
 
877
    def test_author_with_newline_rejected(self):
 
878
        tree = self.make_branch_and_tree('foo')
 
879
        self.assertRaises(AssertionError, tree.commit, 'commit 1',
 
880
                          authors=['John\nDoe <jdoe@example.com>'])
 
881
 
 
882
    def test_commit_with_checkout_and_branch_sharing_repo(self):
 
883
        repo = self.make_repository('repo', shared=True)
 
884
        # make_branch_and_tree ignores shared repos
 
885
        branch = controldir.ControlDir.create_branch_convenience('repo/branch')
 
886
        tree2 = branch.create_checkout('repo/tree2')
 
887
        tree2.commit('message', rev_id=b'rev1')
 
888
        self.assertTrue(tree2.branch.repository.has_revision(b'rev1'))
 
889
 
 
890
 
 
891
class FilterExcludedTests(TestCase):
 
892
 
 
893
    def test_add_file_not_excluded(self):
 
894
        changes = [
 
895
            InventoryTreeChange(
 
896
                'fid', (None, 'newpath'),
 
897
                0, (False, False), ('pid', 'pid'), ('newpath', 'newpath'),
 
898
                ('file', 'file'), (True, True))]
 
899
        self.assertEqual(changes, list(
 
900
            filter_excluded(changes, ['otherpath'])))
 
901
 
 
902
    def test_add_file_excluded(self):
 
903
        changes = [
 
904
            InventoryTreeChange(
 
905
                'fid', (None, 'newpath'),
 
906
                0, (False, False), ('pid', 'pid'), ('newpath', 'newpath'),
 
907
                ('file', 'file'), (True, True))]
 
908
        self.assertEqual([], list(filter_excluded(changes, ['newpath'])))
 
909
 
 
910
    def test_delete_file_excluded(self):
 
911
        changes = [
 
912
            InventoryTreeChange(
 
913
                'fid', ('somepath', None),
 
914
                0, (False, None), ('pid', None), ('newpath', None),
 
915
                ('file', None), (True, None))]
 
916
        self.assertEqual([], list(filter_excluded(changes, ['somepath'])))
 
917
 
 
918
    def test_move_from_or_to_excluded(self):
 
919
        changes = [
 
920
            InventoryTreeChange(
 
921
                'fid', ('oldpath', 'newpath'),
 
922
                0, (False, False), ('pid', 'pid'), ('oldpath', 'newpath'),
 
923
                ('file', 'file'), (True, True))]
 
924
        self.assertEqual([], list(filter_excluded(changes, ['oldpath'])))
 
925
        self.assertEqual([], list(filter_excluded(changes, ['newpath'])))