~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/tests/test_commit.py

(jelmer) Convert bzrlib.smtp_connection to use config stacks. (Jelmer
 Vernooij)

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005, 2006 by Canonical Ltd
 
1
# Copyright (C) 2005-2011 Canonical Ltd
2
2
#
3
3
# This program is free software; you can redistribute it and/or modify
4
4
# it under the terms of the GNU General Public License as published by
12
12
#
13
13
# You should have received a copy of the GNU General Public License
14
14
# along with this program; if not, write to the Free Software
15
 
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16
16
 
17
17
 
18
18
import os
19
19
 
20
20
import bzrlib
21
 
from bzrlib.tests import TestCaseWithTransport
 
21
from bzrlib import (
 
22
    bzrdir,
 
23
    config,
 
24
    errors,
 
25
    )
22
26
from bzrlib.branch import Branch
23
 
from bzrlib.bzrdir import BzrDir, BzrDirMetaFormat1
24
 
from bzrlib.workingtree import WorkingTree
 
27
from bzrlib.bzrdir import BzrDirMetaFormat1
25
28
from bzrlib.commit import Commit, NullCommitReporter
26
 
from bzrlib.config import BranchConfig
27
 
from bzrlib.errors import (PointlessCommit, BzrError, SigningFailed, 
28
 
                           LockContention)
 
29
from bzrlib.errors import (
 
30
    PointlessCommit,
 
31
    BzrError,
 
32
    SigningFailed,
 
33
    LockContention,
 
34
    )
 
35
from bzrlib.tests import (
 
36
    TestCaseWithTransport,
 
37
    test_foreign,
 
38
    )
 
39
from bzrlib.tests.features import (
 
40
    SymlinkFeature,
 
41
    )
 
42
from bzrlib.tests.matchers import MatchesAncestry
29
43
 
30
44
 
31
45
# TODO: Test commit with some added, and added-but-missing files
32
46
 
33
 
class MustSignConfig(BranchConfig):
34
 
 
35
 
    def signature_needed(self):
36
 
        return True
37
 
 
38
 
    def gpg_signing_command(self):
39
 
        return ['cat', '-']
40
 
 
41
 
 
42
 
class BranchWithHooks(BranchConfig):
43
 
 
44
 
    def post_commit(self):
45
 
        return "bzrlib.ahook bzrlib.ahook"
 
47
class MustSignConfig(config.Stack):
 
48
 
 
49
    def __init__(self, branch):
 
50
        store = config.IniFileStore()
 
51
        store._load_from_string('''
 
52
gpg_signing_command=cat -
 
53
create_signatures=always
 
54
''')
 
55
        super(MustSignConfig, self).__init__([store.get_sections])
 
56
        # FIXME: Strictly speaking we should fallback to the no-name section in
 
57
        # branch.conf but no tests need that so far -- vila 2011-12-14
 
58
 
 
59
 
 
60
class BranchWithHooks(config.Stack):
 
61
 
 
62
    def __init__(self, branch):
 
63
        store = config.IniFileStore()
 
64
        store._load_from_string('post_commit=bzrlib.ahook bzrlib.ahook')
 
65
        super(BranchWithHooks, self).__init__([store.get_sections])
 
66
        # FIXME: Strictly speaking we should fallback to the no-name section in
 
67
        # branch.conf but no tests need that so far -- vila 2011-12-14
46
68
 
47
69
 
48
70
class CapturingReporter(NullCommitReporter):
64
86
    def renamed(self, change, old_path, new_path):
65
87
        self.calls.append(('renamed', change, old_path, new_path))
66
88
 
 
89
    def is_verbose(self):
 
90
        return True
 
91
 
67
92
 
68
93
class TestCommit(TestCaseWithTransport):
69
94
 
73
98
        b = wt.branch
74
99
        file('hello', 'w').write('hello world')
75
100
        wt.add('hello')
76
 
        wt.commit(message='add hello')
 
101
        rev1 = wt.commit(message='add hello')
77
102
        file_id = wt.path2id('hello')
78
103
 
79
104
        file('hello', 'w').write('version 2')
80
 
        wt.commit(message='commit 2')
 
105
        rev2 = wt.commit(message='commit 2')
81
106
 
82
107
        eq = self.assertEquals
83
108
        eq(b.revno(), 2)
84
 
        rh = b.revision_history()
85
 
        rev = b.repository.get_revision(rh[0])
 
109
        rev = b.repository.get_revision(rev1)
86
110
        eq(rev.message, 'add hello')
87
111
 
88
 
        tree1 = b.repository.revision_tree(rh[0])
 
112
        tree1 = b.repository.revision_tree(rev1)
 
113
        tree1.lock_read()
89
114
        text = tree1.get_file_text(file_id)
90
 
        eq(text, 'hello world')
91
 
 
92
 
        tree2 = b.repository.revision_tree(rh[1])
93
 
        eq(tree2.get_file_text(file_id), 'version 2')
94
 
 
95
 
    def test_delete_commit(self):
96
 
        """Test a commit with a deleted file"""
 
115
        tree1.unlock()
 
116
        self.assertEqual('hello world', text)
 
117
 
 
118
        tree2 = b.repository.revision_tree(rev2)
 
119
        tree2.lock_read()
 
120
        text = tree2.get_file_text(file_id)
 
121
        tree2.unlock()
 
122
        self.assertEqual('version 2', text)
 
123
 
 
124
    def test_commit_lossy_native(self):
 
125
        """Attempt a lossy commit to a native branch."""
 
126
        wt = self.make_branch_and_tree('.')
 
127
        b = wt.branch
 
128
        file('hello', 'w').write('hello world')
 
129
        wt.add('hello')
 
130
        revid = wt.commit(message='add hello', rev_id='revid', lossy=True)
 
131
        self.assertEquals('revid', revid)
 
132
 
 
133
    def test_commit_lossy_foreign(self):
 
134
        """Attempt a lossy commit to a foreign branch."""
 
135
        test_foreign.register_dummy_foreign_for_test(self)
 
136
        wt = self.make_branch_and_tree('.',
 
137
            format=test_foreign.DummyForeignVcsDirFormat())
 
138
        b = wt.branch
 
139
        file('hello', 'w').write('hello world')
 
140
        wt.add('hello')
 
141
        revid = wt.commit(message='add hello', lossy=True,
 
142
            timestamp=1302659388, timezone=0)
 
143
        self.assertEquals('dummy-v1:1302659388.0-0-UNKNOWN', revid)
 
144
 
 
145
    def test_commit_bound_lossy_foreign(self):
 
146
        """Attempt a lossy commit to a bzr branch bound to a foreign branch."""
 
147
        test_foreign.register_dummy_foreign_for_test(self)
 
148
        foreign_branch = self.make_branch('foreign',
 
149
            format=test_foreign.DummyForeignVcsDirFormat())
 
150
        wt = foreign_branch.create_checkout("local")
 
151
        b = wt.branch
 
152
        file('local/hello', 'w').write('hello world')
 
153
        wt.add('hello')
 
154
        revid = wt.commit(message='add hello', lossy=True,
 
155
            timestamp=1302659388, timezone=0)
 
156
        self.assertEquals('dummy-v1:1302659388.0-0-0', revid)
 
157
        self.assertEquals('dummy-v1:1302659388.0-0-0',
 
158
            foreign_branch.last_revision())
 
159
        self.assertEquals('dummy-v1:1302659388.0-0-0',
 
160
            wt.branch.last_revision())
 
161
 
 
162
    def test_missing_commit(self):
 
163
        """Test a commit with a missing file"""
97
164
        wt = self.make_branch_and_tree('.')
98
165
        b = wt.branch
99
166
        file('hello', 'w').write('hello world')
101
168
        wt.commit(message='add hello')
102
169
 
103
170
        os.remove('hello')
104
 
        wt.commit('removed hello', rev_id='rev2')
 
171
        reporter = CapturingReporter()
 
172
        wt.commit('removed hello', rev_id='rev2', reporter=reporter)
 
173
        self.assertEquals(
 
174
            [('missing', u'hello'), ('deleted', u'hello')],
 
175
            reporter.calls)
105
176
 
106
177
        tree = b.repository.revision_tree('rev2')
107
178
        self.assertFalse(tree.has_id('hello-id'))
108
179
 
 
180
    def test_partial_commit_move(self):
 
181
        """Test a partial commit where a file was renamed but not committed.
 
182
 
 
183
        https://bugs.launchpad.net/bzr/+bug/83039
 
184
 
 
185
        If not handled properly, commit will try to snapshot
 
186
        dialog.py with olive/ as a parent, while
 
187
        olive/ has not been snapshotted yet.
 
188
        """
 
189
        wt = self.make_branch_and_tree('.')
 
190
        b = wt.branch
 
191
        self.build_tree(['annotate/', 'annotate/foo.py',
 
192
                         'olive/', 'olive/dialog.py'
 
193
                        ])
 
194
        wt.add(['annotate', 'olive', 'annotate/foo.py', 'olive/dialog.py'])
 
195
        wt.commit(message='add files')
 
196
        wt.rename_one("olive/dialog.py", "aaa")
 
197
        self.build_tree_contents([('annotate/foo.py', 'modified\n')])
 
198
        wt.commit('renamed hello', specific_files=["annotate"])
 
199
 
109
200
    def test_pointless_commit(self):
110
201
        """Commit refuses unless there are changes or it's forced."""
111
202
        wt = self.make_branch_and_tree('.')
119
210
                          message='fails',
120
211
                          allow_pointless=False)
121
212
        self.assertEquals(b.revno(), 1)
122
 
        
 
213
 
123
214
    def test_commit_empty(self):
124
215
        """Commiting an empty tree works."""
125
216
        wt = self.make_branch_and_tree('.')
142
233
              ['hello-id', 'buongia-id'])
143
234
        wt.commit(message='add files',
144
235
                 rev_id='test@rev-1')
145
 
        
 
236
 
146
237
        os.remove('hello')
147
238
        file('buongia', 'w').write('new text')
148
239
        wt.commit(message='update text',
159
250
        eq(b.revno(), 3)
160
251
 
161
252
        tree2 = b.repository.revision_tree('test@rev-2')
 
253
        tree2.lock_read()
 
254
        self.addCleanup(tree2.unlock)
162
255
        self.assertTrue(tree2.has_filename('hello'))
163
256
        self.assertEquals(tree2.get_file_text('hello-id'), 'hello')
164
257
        self.assertEquals(tree2.get_file_text('buongia-id'), 'new text')
165
 
        
 
258
 
166
259
        tree3 = b.repository.revision_tree('test@rev-3')
 
260
        tree3.lock_read()
 
261
        self.addCleanup(tree3.unlock)
167
262
        self.assertFalse(tree3.has_filename('hello'))
168
263
        self.assertEquals(tree3.get_file_text('buongia-id'), 'new text')
169
264
 
180
275
 
181
276
        eq = self.assertEquals
182
277
        tree1 = b.repository.revision_tree('test@rev-1')
 
278
        tree1.lock_read()
 
279
        self.addCleanup(tree1.unlock)
183
280
        eq(tree1.id2path('hello-id'), 'hello')
184
281
        eq(tree1.get_file_text('hello-id'), 'contents of hello\n')
185
282
        self.assertFalse(tree1.has_filename('fruity'))
186
 
        self.check_inventory_shape(tree1.inventory, ['hello'])
187
 
        ie = tree1.inventory['hello-id']
188
 
        eq(ie.revision, 'test@rev-1')
 
283
        self.check_tree_shape(tree1, ['hello'])
 
284
        eq(tree1.get_file_revision('hello-id'), 'test@rev-1')
189
285
 
190
286
        tree2 = b.repository.revision_tree('test@rev-2')
 
287
        tree2.lock_read()
 
288
        self.addCleanup(tree2.unlock)
191
289
        eq(tree2.id2path('hello-id'), 'fruity')
192
290
        eq(tree2.get_file_text('hello-id'), 'contents of hello\n')
193
 
        self.check_inventory_shape(tree2.inventory, ['fruity'])
194
 
        ie = tree2.inventory['hello-id']
195
 
        eq(ie.revision, 'test@rev-2')
 
291
        self.check_tree_shape(tree2, ['fruity'])
 
292
        eq(tree2.get_file_revision('hello-id'), 'test@rev-2')
196
293
 
197
294
    def test_reused_rev_id(self):
198
295
        """Test that a revision id cannot be reused in a branch"""
217
314
        wt.move(['hello'], 'a')
218
315
        r2 = 'test@rev-2'
219
316
        wt.commit('two', rev_id=r2, allow_pointless=False)
220
 
        self.check_inventory_shape(wt.read_working_inventory(),
221
 
                                   ['a', 'a/hello', 'b'])
 
317
        wt.lock_read()
 
318
        try:
 
319
            self.check_tree_shape(wt, ['a/', 'a/hello', 'b/'])
 
320
        finally:
 
321
            wt.unlock()
222
322
 
223
323
        wt.move(['b'], 'a')
224
324
        r3 = 'test@rev-3'
225
325
        wt.commit('three', rev_id=r3, allow_pointless=False)
226
 
        self.check_inventory_shape(wt.read_working_inventory(),
227
 
                                   ['a', 'a/hello', 'a/b'])
228
 
        self.check_inventory_shape(b.repository.get_revision_inventory(r3),
229
 
                                   ['a', 'a/hello', 'a/b'])
 
326
        wt.lock_read()
 
327
        try:
 
328
            self.check_tree_shape(wt,
 
329
                                       ['a/', 'a/hello', 'a/b/'])
 
330
            self.check_tree_shape(b.repository.revision_tree(r3),
 
331
                                       ['a/', 'a/hello', 'a/b/'])
 
332
        finally:
 
333
            wt.unlock()
230
334
 
231
335
        wt.move(['a/hello'], 'a/b')
232
336
        r4 = 'test@rev-4'
233
337
        wt.commit('four', rev_id=r4, allow_pointless=False)
234
 
        self.check_inventory_shape(wt.read_working_inventory(),
235
 
                                   ['a', 'a/b/hello', 'a/b'])
 
338
        wt.lock_read()
 
339
        try:
 
340
            self.check_tree_shape(wt, ['a/', 'a/b/hello', 'a/b/'])
 
341
        finally:
 
342
            wt.unlock()
236
343
 
237
 
        inv = b.repository.get_revision_inventory(r4)
 
344
        inv = b.repository.get_inventory(r4)
238
345
        eq(inv['hello-id'].revision, r4)
239
346
        eq(inv['a-id'].revision, r1)
240
347
        eq(inv['b-id'].revision, r3)
241
 
        
 
348
 
242
349
    def test_removed_commit(self):
243
350
        """Commit with a removed file"""
244
351
        wt = self.make_branch_and_tree('.')
265
372
            rev_ids.append(rev_id)
266
373
            wt.commit(message='rev %d' % (i+1),
267
374
                     rev_id=rev_id)
268
 
        eq = self.assertEquals
269
 
        eq(b.revision_history(), rev_ids)
270
375
        for i in range(4):
271
 
            anc = b.repository.get_ancestry(rev_ids[i])
272
 
            eq(anc, [None] + rev_ids[:i+1])
 
376
            self.assertThat(rev_ids[:i+1],
 
377
                MatchesAncestry(b.repository, rev_ids[i]))
273
378
 
274
379
    def test_commit_new_subdir_child_selective(self):
275
380
        wt = self.make_branch_and_tree('.')
298
403
    def test_strict_commit_without_unknowns(self):
299
404
        """Try and commit with no unknown files and strict = True,
300
405
        should work."""
301
 
        from bzrlib.errors import StrictCommitFailed
302
406
        wt = self.make_branch_and_tree('.')
303
407
        b = wt.branch
304
408
        file('hello', 'w').write('hello world')
330
434
        wt = self.make_branch_and_tree('.')
331
435
        branch = wt.branch
332
436
        wt.commit("base", allow_pointless=True, rev_id='A')
333
 
        self.failIf(branch.repository.has_signature_for_revision_id('A'))
 
437
        self.assertFalse(branch.repository.has_signature_for_revision_id('A'))
334
438
        try:
335
439
            from bzrlib.testament import Testament
336
440
            # monkey patch gpg signing mechanism
337
441
            bzrlib.gpg.GPGStrategy = bzrlib.gpg.LoopbackGPGStrategy
338
 
            commit.Commit(config=MustSignConfig(branch)).commit(message="base",
339
 
                                                      allow_pointless=True,
340
 
                                                      rev_id='B',
341
 
                                                      working_tree=wt)
342
 
            self.assertEqual(Testament.from_revision(branch.repository,
343
 
                             'B').as_short_text(),
 
442
            commit.Commit(config_stack=MustSignConfig(branch)
 
443
                          ).commit(message="base", allow_pointless=True,
 
444
                                   rev_id='B', working_tree=wt)
 
445
            def sign(text):
 
446
                return bzrlib.gpg.LoopbackGPGStrategy(None).sign(text)
 
447
            self.assertEqual(sign(Testament.from_revision(branch.repository,
 
448
                                                          'B').as_short_text()),
344
449
                             branch.repository.get_signature_text('B'))
345
450
        finally:
346
451
            bzrlib.gpg.GPGStrategy = oldstrategy
352
457
        wt = self.make_branch_and_tree('.')
353
458
        branch = wt.branch
354
459
        wt.commit("base", allow_pointless=True, rev_id='A')
355
 
        self.failIf(branch.repository.has_signature_for_revision_id('A'))
 
460
        self.assertFalse(branch.repository.has_signature_for_revision_id('A'))
356
461
        try:
357
 
            from bzrlib.testament import Testament
358
462
            # monkey patch gpg signing mechanism
359
463
            bzrlib.gpg.GPGStrategy = bzrlib.gpg.DisabledGPGStrategy
360
464
            config = MustSignConfig(branch)
361
465
            self.assertRaises(SigningFailed,
362
 
                              commit.Commit(config=config).commit,
 
466
                              commit.Commit(config_stack=config).commit,
363
467
                              message="base",
364
468
                              allow_pointless=True,
365
469
                              rev_id='B',
366
470
                              working_tree=wt)
367
471
            branch = Branch.open(self.get_url('.'))
368
 
            self.assertEqual(branch.revision_history(), ['A'])
369
 
            self.failIf(branch.repository.has_revision('B'))
 
472
            self.assertEqual(branch.last_revision(), 'A')
 
473
            self.assertFalse(branch.repository.has_revision('B'))
370
474
        finally:
371
475
            bzrlib.gpg.GPGStrategy = oldstrategy
372
476
 
380
484
        bzrlib.ahook = called
381
485
        try:
382
486
            config = BranchWithHooks(branch)
383
 
            commit.Commit(config=config).commit(
 
487
            commit.Commit(config_stack=config).commit(
384
488
                            message = "base",
385
489
                            allow_pointless=True,
386
490
                            rev_id='A', working_tree = wt)
427
531
        other_bzrdir = master_branch.bzrdir.sprout('other')
428
532
        other_tree = other_bzrdir.open_workingtree()
429
533
 
430
 
        # do a commit to the the other branch changing the content file so
 
534
        # do a commit to the other branch changing the content file so
431
535
        # that our commit after merging will have a merged revision in the
432
536
        # content file history.
433
537
        self.build_tree_contents([('other/content_file', 'change in other\n')])
436
540
        # do a merge into the bound branch from other, and then change the
437
541
        # content file locally to force a new revision (rather than using the
438
542
        # revision from other). This forces extra processing in commit.
439
 
        self.merge(other_tree.branch, bound_tree)
 
543
        bound_tree.merge_from_branch(other_tree.branch)
440
544
        self.build_tree_contents([('bound/content_file', 'change in bound\n')])
441
545
 
442
546
        # before #34959 was fixed, this failed with 'revision not present in
444
548
        bound_tree.commit(message='commit of merge in bound tree')
445
549
 
446
550
    def test_commit_reporting_after_merge(self):
447
 
        # when doing a commit of a merge, the reporter needs to still 
 
551
        # when doing a commit of a merge, the reporter needs to still
448
552
        # be called for each item that is added/removed/deleted.
449
553
        this_tree = self.make_branch_and_tree('this')
450
554
        # we need a bunch of files and dirs, to perform one action on each.
490
594
            other_tree.commit('modify all sample files and dirs.')
491
595
        finally:
492
596
            other_tree.unlock()
493
 
        self.merge(other_tree.branch, this_tree)
 
597
        this_tree.merge_from_branch(other_tree.branch)
494
598
        reporter = CapturingReporter()
495
599
        this_tree.commit('do the commit', reporter=reporter)
496
 
        self.assertEqual([
497
 
            ('change', 'unchanged', 'dirtoleave'),
498
 
            ('change', 'unchanged', 'filetoleave'),
 
600
        expected = set([
499
601
            ('change', 'modified', 'filetomodify'),
500
602
            ('change', 'added', 'newdir'),
501
603
            ('change', 'added', 'newfile'),
502
604
            ('renamed', 'renamed', 'dirtorename', 'renameddir'),
 
605
            ('renamed', 'renamed', 'filetorename', 'renamedfile'),
503
606
            ('renamed', 'renamed', 'dirtoreparent', 'renameddir/reparenteddir'),
504
607
            ('renamed', 'renamed', 'filetoreparent', 'renameddir/reparentedfile'),
505
 
            ('renamed', 'renamed', 'filetorename', 'renamedfile'),
506
608
            ('deleted', 'dirtoremove'),
507
609
            ('deleted', 'filetoremove'),
508
 
            ],
509
 
            reporter.calls)
 
610
            ])
 
611
        result = set(reporter.calls)
 
612
        missing = expected - result
 
613
        new = result - expected
 
614
        self.assertEqual((set(), set()), (missing, new))
510
615
 
511
616
    def test_commit_removals_respects_filespec(self):
512
617
        """Commit respects the specified_files for removals."""
516
621
        tree.commit('added a, b')
517
622
        tree.remove(['a', 'b'])
518
623
        tree.commit('removed a', specific_files='a')
519
 
        basis = tree.basis_tree().inventory
520
 
        self.assertIs(None, basis.path2id('a'))
521
 
        self.assertFalse(basis.path2id('b') is None)
 
624
        basis = tree.basis_tree()
 
625
        tree.lock_read()
 
626
        try:
 
627
            self.assertIs(None, basis.path2id('a'))
 
628
            self.assertFalse(basis.path2id('b') is None)
 
629
        finally:
 
630
            tree.unlock()
522
631
 
523
632
    def test_commit_saves_1ms_timestamp(self):
524
633
        """Passing in a timestamp is saved with 1ms resolution"""
542
651
        timestamp = rev.timestamp
543
652
        timestamp_1ms = round(timestamp, 3)
544
653
        self.assertEqual(timestamp_1ms, timestamp)
 
654
 
 
655
    def assertBasisTreeKind(self, kind, tree, file_id):
 
656
        basis = tree.basis_tree()
 
657
        basis.lock_read()
 
658
        try:
 
659
            self.assertEqual(kind, basis.kind(file_id))
 
660
        finally:
 
661
            basis.unlock()
 
662
 
 
663
    def test_commit_kind_changes(self):
 
664
        self.requireFeature(SymlinkFeature)
 
665
        tree = self.make_branch_and_tree('.')
 
666
        os.symlink('target', 'name')
 
667
        tree.add('name', 'a-file-id')
 
668
        tree.commit('Added a symlink')
 
669
        self.assertBasisTreeKind('symlink', tree, 'a-file-id')
 
670
 
 
671
        os.unlink('name')
 
672
        self.build_tree(['name'])
 
673
        tree.commit('Changed symlink to file')
 
674
        self.assertBasisTreeKind('file', tree, 'a-file-id')
 
675
 
 
676
        os.unlink('name')
 
677
        os.symlink('target', 'name')
 
678
        tree.commit('file to symlink')
 
679
        self.assertBasisTreeKind('symlink', tree, 'a-file-id')
 
680
 
 
681
        os.unlink('name')
 
682
        os.mkdir('name')
 
683
        tree.commit('symlink to directory')
 
684
        self.assertBasisTreeKind('directory', tree, 'a-file-id')
 
685
 
 
686
        os.rmdir('name')
 
687
        os.symlink('target', 'name')
 
688
        tree.commit('directory to symlink')
 
689
        self.assertBasisTreeKind('symlink', tree, 'a-file-id')
 
690
 
 
691
        # prepare for directory <-> file tests
 
692
        os.unlink('name')
 
693
        os.mkdir('name')
 
694
        tree.commit('symlink to directory')
 
695
        self.assertBasisTreeKind('directory', tree, 'a-file-id')
 
696
 
 
697
        os.rmdir('name')
 
698
        self.build_tree(['name'])
 
699
        tree.commit('Changed directory to file')
 
700
        self.assertBasisTreeKind('file', tree, 'a-file-id')
 
701
 
 
702
        os.unlink('name')
 
703
        os.mkdir('name')
 
704
        tree.commit('file to directory')
 
705
        self.assertBasisTreeKind('directory', tree, 'a-file-id')
 
706
 
 
707
    def test_commit_unversioned_specified(self):
 
708
        """Commit should raise if specified files isn't in basis or worktree"""
 
709
        tree = self.make_branch_and_tree('.')
 
710
        self.assertRaises(errors.PathsNotVersionedError, tree.commit,
 
711
                          'message', specific_files=['bogus'])
 
712
 
 
713
    class Callback(object):
 
714
 
 
715
        def __init__(self, message, testcase):
 
716
            self.called = False
 
717
            self.message = message
 
718
            self.testcase = testcase
 
719
 
 
720
        def __call__(self, commit_obj):
 
721
            self.called = True
 
722
            self.testcase.assertTrue(isinstance(commit_obj, Commit))
 
723
            return self.message
 
724
 
 
725
    def test_commit_callback(self):
 
726
        """Commit should invoke a callback to get the message"""
 
727
 
 
728
        tree = self.make_branch_and_tree('.')
 
729
        try:
 
730
            tree.commit()
 
731
        except Exception, e:
 
732
            self.assertTrue(isinstance(e, BzrError))
 
733
            self.assertEqual('The message or message_callback keyword'
 
734
                             ' parameter is required for commit().', str(e))
 
735
        else:
 
736
            self.fail('exception not raised')
 
737
        cb = self.Callback(u'commit 1', self)
 
738
        tree.commit(message_callback=cb)
 
739
        self.assertTrue(cb.called)
 
740
        repository = tree.branch.repository
 
741
        message = repository.get_revision(tree.last_revision()).message
 
742
        self.assertEqual('commit 1', message)
 
743
 
 
744
    def test_no_callback_pointless(self):
 
745
        """Callback should not be invoked for pointless commit"""
 
746
        tree = self.make_branch_and_tree('.')
 
747
        cb = self.Callback(u'commit 2', self)
 
748
        self.assertRaises(PointlessCommit, tree.commit, message_callback=cb,
 
749
                          allow_pointless=False)
 
750
        self.assertFalse(cb.called)
 
751
 
 
752
    def test_no_callback_netfailure(self):
 
753
        """Callback should not be invoked if connectivity fails"""
 
754
        tree = self.make_branch_and_tree('.')
 
755
        cb = self.Callback(u'commit 2', self)
 
756
        repository = tree.branch.repository
 
757
        # simulate network failure
 
758
        def raise_(self, arg, arg2, arg3=None, arg4=None):
 
759
            raise errors.NoSuchFile('foo')
 
760
        repository.add_inventory = raise_
 
761
        repository.add_inventory_by_delta = raise_
 
762
        self.assertRaises(errors.NoSuchFile, tree.commit, message_callback=cb)
 
763
        self.assertFalse(cb.called)
 
764
 
 
765
    def test_selected_file_merge_commit(self):
 
766
        """Ensure the correct error is raised"""
 
767
        tree = self.make_branch_and_tree('foo')
 
768
        # pending merge would turn into a left parent
 
769
        tree.commit('commit 1')
 
770
        tree.add_parent_tree_id('example')
 
771
        self.build_tree(['foo/bar', 'foo/baz'])
 
772
        tree.add(['bar', 'baz'])
 
773
        err = self.assertRaises(errors.CannotCommitSelectedFileMerge,
 
774
            tree.commit, 'commit 2', specific_files=['bar', 'baz'])
 
775
        self.assertEqual(['bar', 'baz'], err.files)
 
776
        self.assertEqual('Selected-file commit of merges is not supported'
 
777
                         ' yet: files bar, baz', str(err))
 
778
 
 
779
    def test_commit_ordering(self):
 
780
        """Test of corner-case commit ordering error"""
 
781
        tree = self.make_branch_and_tree('.')
 
782
        self.build_tree(['a/', 'a/z/', 'a/c/', 'a/z/x', 'a/z/y'])
 
783
        tree.add(['a/', 'a/z/', 'a/c/', 'a/z/x', 'a/z/y'])
 
784
        tree.commit('setup')
 
785
        self.build_tree(['a/c/d/'])
 
786
        tree.add('a/c/d')
 
787
        tree.rename_one('a/z/x', 'a/c/d/x')
 
788
        tree.commit('test', specific_files=['a/z/y'])
 
789
 
 
790
    def test_commit_no_author(self):
 
791
        """The default kwarg author in MutableTree.commit should not add
 
792
        the 'author' revision property.
 
793
        """
 
794
        tree = self.make_branch_and_tree('foo')
 
795
        rev_id = tree.commit('commit 1')
 
796
        rev = tree.branch.repository.get_revision(rev_id)
 
797
        self.assertFalse('author' in rev.properties)
 
798
        self.assertFalse('authors' in rev.properties)
 
799
 
 
800
    def test_commit_author(self):
 
801
        """Passing a non-empty author kwarg to MutableTree.commit should add
 
802
        the 'author' revision property.
 
803
        """
 
804
        tree = self.make_branch_and_tree('foo')
 
805
        rev_id = self.callDeprecated(['The parameter author was '
 
806
                'deprecated in version 1.13. Use authors instead'],
 
807
                tree.commit, 'commit 1', author='John Doe <jdoe@example.com>')
 
808
        rev = tree.branch.repository.get_revision(rev_id)
 
809
        self.assertEqual('John Doe <jdoe@example.com>',
 
810
                         rev.properties['authors'])
 
811
        self.assertFalse('author' in rev.properties)
 
812
 
 
813
    def test_commit_empty_authors_list(self):
 
814
        """Passing an empty list to authors shouldn't add the property."""
 
815
        tree = self.make_branch_and_tree('foo')
 
816
        rev_id = tree.commit('commit 1', authors=[])
 
817
        rev = tree.branch.repository.get_revision(rev_id)
 
818
        self.assertFalse('author' in rev.properties)
 
819
        self.assertFalse('authors' in rev.properties)
 
820
 
 
821
    def test_multiple_authors(self):
 
822
        tree = self.make_branch_and_tree('foo')
 
823
        rev_id = tree.commit('commit 1',
 
824
                authors=['John Doe <jdoe@example.com>',
 
825
                         'Jane Rey <jrey@example.com>'])
 
826
        rev = tree.branch.repository.get_revision(rev_id)
 
827
        self.assertEqual('John Doe <jdoe@example.com>\n'
 
828
                'Jane Rey <jrey@example.com>', rev.properties['authors'])
 
829
        self.assertFalse('author' in rev.properties)
 
830
 
 
831
    def test_author_and_authors_incompatible(self):
 
832
        tree = self.make_branch_and_tree('foo')
 
833
        self.assertRaises(AssertionError, tree.commit, 'commit 1',
 
834
                authors=['John Doe <jdoe@example.com>',
 
835
                         'Jane Rey <jrey@example.com>'],
 
836
                author="Jack Me <jme@example.com>")
 
837
 
 
838
    def test_author_with_newline_rejected(self):
 
839
        tree = self.make_branch_and_tree('foo')
 
840
        self.assertRaises(AssertionError, tree.commit, 'commit 1',
 
841
                authors=['John\nDoe <jdoe@example.com>'])
 
842
 
 
843
    def test_commit_with_checkout_and_branch_sharing_repo(self):
 
844
        repo = self.make_repository('repo', shared=True)
 
845
        # make_branch_and_tree ignores shared repos
 
846
        branch = bzrdir.BzrDir.create_branch_convenience('repo/branch')
 
847
        tree2 = branch.create_checkout('repo/tree2')
 
848
        tree2.commit('message', rev_id='rev1')
 
849
        self.assertTrue(tree2.branch.repository.has_revision('rev1'))