/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: 2018-02-18 19:34:37 UTC
  • mfrom: (6857.1.1 fix-import-stacked)
  • Revision ID: jelmer@jelmer.uk-20180218193437-pytr0vyldq867owo
Merge lp:~jelmer/brz/fix-import-stacked.

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