/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 bzrlib/tests/test_commit.py

(jelmer) Print the number of colocated branches in 'bzr info'. (Jelmer
 Vernooij)

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005 by Canonical Ltd
2
 
 
 
1
# Copyright (C) 2005-2011 Canonical Ltd
 
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
5
5
# the Free Software Foundation; either version 2 of the License, or
6
6
# (at your option) any later version.
7
 
 
 
7
#
8
8
# This program is distributed in the hope that it will be useful,
9
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11
11
# GNU General Public License for more details.
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
    errors,
 
24
    )
22
25
from bzrlib.branch import Branch
23
 
from bzrlib.bzrdir import BzrDir, BzrDirMetaFormat1
24
 
from bzrlib.workingtree import WorkingTree
25
 
from bzrlib.commit import Commit
 
26
from bzrlib.bzrdir import BzrDirMetaFormat1
 
27
from bzrlib.commit import Commit, NullCommitReporter
26
28
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
45
59
        return "bzrlib.ahook bzrlib.ahook"
46
60
 
47
61
 
 
62
class CapturingReporter(NullCommitReporter):
 
63
    """This reporter captures the calls made to it for evaluation later."""
 
64
 
 
65
    def __init__(self):
 
66
        # a list of the calls this received
 
67
        self.calls = []
 
68
 
 
69
    def snapshot_change(self, change, path):
 
70
        self.calls.append(('change', change, path))
 
71
 
 
72
    def deleted(self, file_id):
 
73
        self.calls.append(('deleted', file_id))
 
74
 
 
75
    def missing(self, path):
 
76
        self.calls.append(('missing', path))
 
77
 
 
78
    def renamed(self, change, old_path, new_path):
 
79
        self.calls.append(('renamed', change, old_path, new_path))
 
80
 
 
81
    def is_verbose(self):
 
82
        return True
 
83
 
 
84
 
48
85
class TestCommit(TestCaseWithTransport):
49
86
 
50
87
    def test_simple_commit(self):
53
90
        b = wt.branch
54
91
        file('hello', 'w').write('hello world')
55
92
        wt.add('hello')
56
 
        wt.commit(message='add hello')
 
93
        rev1 = wt.commit(message='add hello')
57
94
        file_id = wt.path2id('hello')
58
95
 
59
96
        file('hello', 'w').write('version 2')
60
 
        wt.commit(message='commit 2')
 
97
        rev2 = wt.commit(message='commit 2')
61
98
 
62
99
        eq = self.assertEquals
63
100
        eq(b.revno(), 2)
64
 
        rh = b.revision_history()
65
 
        rev = b.repository.get_revision(rh[0])
 
101
        rev = b.repository.get_revision(rev1)
66
102
        eq(rev.message, 'add hello')
67
103
 
68
 
        tree1 = b.repository.revision_tree(rh[0])
 
104
        tree1 = b.repository.revision_tree(rev1)
 
105
        tree1.lock_read()
69
106
        text = tree1.get_file_text(file_id)
70
 
        eq(text, 'hello world')
71
 
 
72
 
        tree2 = b.repository.revision_tree(rh[1])
73
 
        eq(tree2.get_file_text(file_id), 'version 2')
74
 
 
75
 
    def test_delete_commit(self):
76
 
        """Test a commit with a deleted file"""
 
107
        tree1.unlock()
 
108
        self.assertEqual('hello world', text)
 
109
 
 
110
        tree2 = b.repository.revision_tree(rev2)
 
111
        tree2.lock_read()
 
112
        text = tree2.get_file_text(file_id)
 
113
        tree2.unlock()
 
114
        self.assertEqual('version 2', text)
 
115
 
 
116
    def test_commit_lossy_native(self):
 
117
        """Attempt a lossy commit to a native branch."""
 
118
        wt = self.make_branch_and_tree('.')
 
119
        b = wt.branch
 
120
        file('hello', 'w').write('hello world')
 
121
        wt.add('hello')
 
122
        revid = wt.commit(message='add hello', rev_id='revid', lossy=True)
 
123
        self.assertEquals('revid', revid)
 
124
 
 
125
    def test_commit_lossy_foreign(self):
 
126
        """Attempt a lossy commit to a foreign branch."""
 
127
        test_foreign.register_dummy_foreign_for_test(self)
 
128
        wt = self.make_branch_and_tree('.',
 
129
            format=test_foreign.DummyForeignVcsDirFormat())
 
130
        b = wt.branch
 
131
        file('hello', 'w').write('hello world')
 
132
        wt.add('hello')
 
133
        revid = wt.commit(message='add hello', lossy=True,
 
134
            timestamp=1302659388, timezone=0)
 
135
        self.assertEquals('dummy-v1:1302659388.0-0-UNKNOWN', revid)
 
136
 
 
137
    def test_commit_bound_lossy_foreign(self):
 
138
        """Attempt a lossy commit to a bzr branch bound to a foreign branch."""
 
139
        test_foreign.register_dummy_foreign_for_test(self)
 
140
        foreign_branch = self.make_branch('foreign',
 
141
            format=test_foreign.DummyForeignVcsDirFormat())
 
142
        wt = foreign_branch.create_checkout("local")
 
143
        b = wt.branch
 
144
        file('local/hello', 'w').write('hello world')
 
145
        wt.add('hello')
 
146
        revid = wt.commit(message='add hello', lossy=True,
 
147
            timestamp=1302659388, timezone=0)
 
148
        self.assertEquals('dummy-v1:1302659388.0-0-0', revid)
 
149
        self.assertEquals('dummy-v1:1302659388.0-0-0',
 
150
            foreign_branch.last_revision())
 
151
        self.assertEquals('dummy-v1:1302659388.0-0-0',
 
152
            wt.branch.last_revision())
 
153
 
 
154
    def test_missing_commit(self):
 
155
        """Test a commit with a missing file"""
77
156
        wt = self.make_branch_and_tree('.')
78
157
        b = wt.branch
79
158
        file('hello', 'w').write('hello world')
81
160
        wt.commit(message='add hello')
82
161
 
83
162
        os.remove('hello')
84
 
        wt.commit('removed hello', rev_id='rev2')
 
163
        reporter = CapturingReporter()
 
164
        wt.commit('removed hello', rev_id='rev2', reporter=reporter)
 
165
        self.assertEquals(
 
166
            [('missing', u'hello'), ('deleted', u'hello')],
 
167
            reporter.calls)
85
168
 
86
169
        tree = b.repository.revision_tree('rev2')
87
170
        self.assertFalse(tree.has_id('hello-id'))
88
171
 
 
172
    def test_partial_commit_move(self):
 
173
        """Test a partial commit where a file was renamed but not committed.
 
174
 
 
175
        https://bugs.launchpad.net/bzr/+bug/83039
 
176
 
 
177
        If not handled properly, commit will try to snapshot
 
178
        dialog.py with olive/ as a parent, while
 
179
        olive/ has not been snapshotted yet.
 
180
        """
 
181
        wt = self.make_branch_and_tree('.')
 
182
        b = wt.branch
 
183
        self.build_tree(['annotate/', 'annotate/foo.py',
 
184
                         'olive/', 'olive/dialog.py'
 
185
                        ])
 
186
        wt.add(['annotate', 'olive', 'annotate/foo.py', 'olive/dialog.py'])
 
187
        wt.commit(message='add files')
 
188
        wt.rename_one("olive/dialog.py", "aaa")
 
189
        self.build_tree_contents([('annotate/foo.py', 'modified\n')])
 
190
        wt.commit('renamed hello', specific_files=["annotate"])
 
191
 
89
192
    def test_pointless_commit(self):
90
193
        """Commit refuses unless there are changes or it's forced."""
91
194
        wt = self.make_branch_and_tree('.')
99
202
                          message='fails',
100
203
                          allow_pointless=False)
101
204
        self.assertEquals(b.revno(), 1)
102
 
        
 
205
 
103
206
    def test_commit_empty(self):
104
207
        """Commiting an empty tree works."""
105
208
        wt = self.make_branch_and_tree('.')
122
225
              ['hello-id', 'buongia-id'])
123
226
        wt.commit(message='add files',
124
227
                 rev_id='test@rev-1')
125
 
        
 
228
 
126
229
        os.remove('hello')
127
230
        file('buongia', 'w').write('new text')
128
231
        wt.commit(message='update text',
139
242
        eq(b.revno(), 3)
140
243
 
141
244
        tree2 = b.repository.revision_tree('test@rev-2')
 
245
        tree2.lock_read()
 
246
        self.addCleanup(tree2.unlock)
142
247
        self.assertTrue(tree2.has_filename('hello'))
143
248
        self.assertEquals(tree2.get_file_text('hello-id'), 'hello')
144
249
        self.assertEquals(tree2.get_file_text('buongia-id'), 'new text')
145
 
        
 
250
 
146
251
        tree3 = b.repository.revision_tree('test@rev-3')
 
252
        tree3.lock_read()
 
253
        self.addCleanup(tree3.unlock)
147
254
        self.assertFalse(tree3.has_filename('hello'))
148
255
        self.assertEquals(tree3.get_file_text('buongia-id'), 'new text')
149
256
 
160
267
 
161
268
        eq = self.assertEquals
162
269
        tree1 = b.repository.revision_tree('test@rev-1')
 
270
        tree1.lock_read()
 
271
        self.addCleanup(tree1.unlock)
163
272
        eq(tree1.id2path('hello-id'), 'hello')
164
273
        eq(tree1.get_file_text('hello-id'), 'contents of hello\n')
165
274
        self.assertFalse(tree1.has_filename('fruity'))
166
 
        self.check_inventory_shape(tree1.inventory, ['hello'])
167
 
        ie = tree1.inventory['hello-id']
168
 
        eq(ie.revision, 'test@rev-1')
 
275
        self.check_tree_shape(tree1, ['hello'])
 
276
        eq(tree1.get_file_revision('hello-id'), 'test@rev-1')
169
277
 
170
278
        tree2 = b.repository.revision_tree('test@rev-2')
 
279
        tree2.lock_read()
 
280
        self.addCleanup(tree2.unlock)
171
281
        eq(tree2.id2path('hello-id'), 'fruity')
172
282
        eq(tree2.get_file_text('hello-id'), 'contents of hello\n')
173
 
        self.check_inventory_shape(tree2.inventory, ['fruity'])
174
 
        ie = tree2.inventory['hello-id']
175
 
        eq(ie.revision, 'test@rev-2')
 
283
        self.check_tree_shape(tree2, ['fruity'])
 
284
        eq(tree2.get_file_revision('hello-id'), 'test@rev-2')
176
285
 
177
286
    def test_reused_rev_id(self):
178
287
        """Test that a revision id cannot be reused in a branch"""
197
306
        wt.move(['hello'], 'a')
198
307
        r2 = 'test@rev-2'
199
308
        wt.commit('two', rev_id=r2, allow_pointless=False)
200
 
        self.check_inventory_shape(wt.read_working_inventory(),
201
 
                                   ['a', 'a/hello', 'b'])
 
309
        wt.lock_read()
 
310
        try:
 
311
            self.check_tree_shape(wt, ['a/', 'a/hello', 'b/'])
 
312
        finally:
 
313
            wt.unlock()
202
314
 
203
315
        wt.move(['b'], 'a')
204
316
        r3 = 'test@rev-3'
205
317
        wt.commit('three', rev_id=r3, allow_pointless=False)
206
 
        self.check_inventory_shape(wt.read_working_inventory(),
207
 
                                   ['a', 'a/hello', 'a/b'])
208
 
        self.check_inventory_shape(b.repository.get_revision_inventory(r3),
209
 
                                   ['a', 'a/hello', 'a/b'])
 
318
        wt.lock_read()
 
319
        try:
 
320
            self.check_tree_shape(wt,
 
321
                                       ['a/', 'a/hello', 'a/b/'])
 
322
            self.check_tree_shape(b.repository.revision_tree(r3),
 
323
                                       ['a/', 'a/hello', 'a/b/'])
 
324
        finally:
 
325
            wt.unlock()
210
326
 
211
327
        wt.move(['a/hello'], 'a/b')
212
328
        r4 = 'test@rev-4'
213
329
        wt.commit('four', rev_id=r4, allow_pointless=False)
214
 
        self.check_inventory_shape(wt.read_working_inventory(),
215
 
                                   ['a', 'a/b/hello', 'a/b'])
 
330
        wt.lock_read()
 
331
        try:
 
332
            self.check_tree_shape(wt, ['a/', 'a/b/hello', 'a/b/'])
 
333
        finally:
 
334
            wt.unlock()
216
335
 
217
 
        inv = b.repository.get_revision_inventory(r4)
 
336
        inv = b.repository.get_inventory(r4)
218
337
        eq(inv['hello-id'].revision, r4)
219
338
        eq(inv['a-id'].revision, r1)
220
339
        eq(inv['b-id'].revision, r3)
221
 
        
 
340
 
222
341
    def test_removed_commit(self):
223
342
        """Commit with a removed file"""
224
343
        wt = self.make_branch_and_tree('.')
245
364
            rev_ids.append(rev_id)
246
365
            wt.commit(message='rev %d' % (i+1),
247
366
                     rev_id=rev_id)
248
 
        eq = self.assertEquals
249
 
        eq(b.revision_history(), rev_ids)
250
367
        for i in range(4):
251
 
            anc = b.repository.get_ancestry(rev_ids[i])
252
 
            eq(anc, [None] + rev_ids[:i+1])
 
368
            self.assertThat(rev_ids[:i+1],
 
369
                MatchesAncestry(b.repository, rev_ids[i]))
253
370
 
254
371
    def test_commit_new_subdir_child_selective(self):
255
372
        wt = self.make_branch_and_tree('.')
278
395
    def test_strict_commit_without_unknowns(self):
279
396
        """Try and commit with no unknown files and strict = True,
280
397
        should work."""
281
 
        from bzrlib.errors import StrictCommitFailed
282
398
        wt = self.make_branch_and_tree('.')
283
399
        b = wt.branch
284
400
        file('hello', 'w').write('hello world')
310
426
        wt = self.make_branch_and_tree('.')
311
427
        branch = wt.branch
312
428
        wt.commit("base", allow_pointless=True, rev_id='A')
313
 
        self.failIf(branch.repository.has_signature_for_revision_id('A'))
 
429
        self.assertFalse(branch.repository.has_signature_for_revision_id('A'))
314
430
        try:
315
431
            from bzrlib.testament import Testament
316
432
            # monkey patch gpg signing mechanism
319
435
                                                      allow_pointless=True,
320
436
                                                      rev_id='B',
321
437
                                                      working_tree=wt)
322
 
            self.assertEqual(Testament.from_revision(branch.repository,
323
 
                             'B').as_short_text(),
 
438
            def sign(text):
 
439
                return bzrlib.gpg.LoopbackGPGStrategy(None).sign(text)
 
440
            self.assertEqual(sign(Testament.from_revision(branch.repository,
 
441
                             'B').as_short_text()),
324
442
                             branch.repository.get_signature_text('B'))
325
443
        finally:
326
444
            bzrlib.gpg.GPGStrategy = oldstrategy
332
450
        wt = self.make_branch_and_tree('.')
333
451
        branch = wt.branch
334
452
        wt.commit("base", allow_pointless=True, rev_id='A')
335
 
        self.failIf(branch.repository.has_signature_for_revision_id('A'))
 
453
        self.assertFalse(branch.repository.has_signature_for_revision_id('A'))
336
454
        try:
337
 
            from bzrlib.testament import Testament
338
455
            # monkey patch gpg signing mechanism
339
456
            bzrlib.gpg.GPGStrategy = bzrlib.gpg.DisabledGPGStrategy
340
457
            config = MustSignConfig(branch)
345
462
                              rev_id='B',
346
463
                              working_tree=wt)
347
464
            branch = Branch.open(self.get_url('.'))
348
 
            self.assertEqual(branch.revision_history(), ['A'])
349
 
            self.failIf(branch.repository.has_revision('B'))
 
465
            self.assertEqual(branch.last_revision(), 'A')
 
466
            self.assertFalse(branch.repository.has_revision('B'))
350
467
        finally:
351
468
            bzrlib.gpg.GPGStrategy = oldstrategy
352
469
 
392
509
            self.assertRaises(LockContention, wt.commit, 'silly')
393
510
        finally:
394
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.bzrdir.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.bzrdir.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 = set([
 
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
        tree.lock_read()
 
619
        try:
 
620
            self.assertIs(None, basis.path2id('a'))
 
621
            self.assertFalse(basis.path2id('b') is None)
 
622
        finally:
 
623
            tree.unlock()
 
624
 
 
625
    def test_commit_saves_1ms_timestamp(self):
 
626
        """Passing in a timestamp is saved with 1ms resolution"""
 
627
        tree = self.make_branch_and_tree('.')
 
628
        self.build_tree(['a'])
 
629
        tree.add('a')
 
630
        tree.commit('added a', timestamp=1153248633.4186721, timezone=0,
 
631
                    rev_id='a1')
 
632
 
 
633
        rev = tree.branch.repository.get_revision('a1')
 
634
        self.assertEqual(1153248633.419, rev.timestamp)
 
635
 
 
636
    def test_commit_has_1ms_resolution(self):
 
637
        """Allowing commit to generate the timestamp also has 1ms resolution"""
 
638
        tree = self.make_branch_and_tree('.')
 
639
        self.build_tree(['a'])
 
640
        tree.add('a')
 
641
        tree.commit('added a', rev_id='a1')
 
642
 
 
643
        rev = tree.branch.repository.get_revision('a1')
 
644
        timestamp = rev.timestamp
 
645
        timestamp_1ms = round(timestamp, 3)
 
646
        self.assertEqual(timestamp_1ms, timestamp)
 
647
 
 
648
    def assertBasisTreeKind(self, kind, tree, file_id):
 
649
        basis = tree.basis_tree()
 
650
        basis.lock_read()
 
651
        try:
 
652
            self.assertEqual(kind, basis.kind(file_id))
 
653
        finally:
 
654
            basis.unlock()
 
655
 
 
656
    def test_commit_kind_changes(self):
 
657
        self.requireFeature(SymlinkFeature)
 
658
        tree = self.make_branch_and_tree('.')
 
659
        os.symlink('target', 'name')
 
660
        tree.add('name', 'a-file-id')
 
661
        tree.commit('Added a symlink')
 
662
        self.assertBasisTreeKind('symlink', tree, 'a-file-id')
 
663
 
 
664
        os.unlink('name')
 
665
        self.build_tree(['name'])
 
666
        tree.commit('Changed symlink to file')
 
667
        self.assertBasisTreeKind('file', tree, 'a-file-id')
 
668
 
 
669
        os.unlink('name')
 
670
        os.symlink('target', 'name')
 
671
        tree.commit('file to symlink')
 
672
        self.assertBasisTreeKind('symlink', tree, 'a-file-id')
 
673
 
 
674
        os.unlink('name')
 
675
        os.mkdir('name')
 
676
        tree.commit('symlink to directory')
 
677
        self.assertBasisTreeKind('directory', tree, 'a-file-id')
 
678
 
 
679
        os.rmdir('name')
 
680
        os.symlink('target', 'name')
 
681
        tree.commit('directory to symlink')
 
682
        self.assertBasisTreeKind('symlink', tree, 'a-file-id')
 
683
 
 
684
        # prepare for directory <-> file tests
 
685
        os.unlink('name')
 
686
        os.mkdir('name')
 
687
        tree.commit('symlink to directory')
 
688
        self.assertBasisTreeKind('directory', tree, 'a-file-id')
 
689
 
 
690
        os.rmdir('name')
 
691
        self.build_tree(['name'])
 
692
        tree.commit('Changed directory to file')
 
693
        self.assertBasisTreeKind('file', tree, 'a-file-id')
 
694
 
 
695
        os.unlink('name')
 
696
        os.mkdir('name')
 
697
        tree.commit('file to directory')
 
698
        self.assertBasisTreeKind('directory', tree, 'a-file-id')
 
699
 
 
700
    def test_commit_unversioned_specified(self):
 
701
        """Commit should raise if specified files isn't in basis or worktree"""
 
702
        tree = self.make_branch_and_tree('.')
 
703
        self.assertRaises(errors.PathsNotVersionedError, tree.commit,
 
704
                          'message', specific_files=['bogus'])
 
705
 
 
706
    class Callback(object):
 
707
 
 
708
        def __init__(self, message, testcase):
 
709
            self.called = False
 
710
            self.message = message
 
711
            self.testcase = testcase
 
712
 
 
713
        def __call__(self, commit_obj):
 
714
            self.called = True
 
715
            self.testcase.assertTrue(isinstance(commit_obj, Commit))
 
716
            return self.message
 
717
 
 
718
    def test_commit_callback(self):
 
719
        """Commit should invoke a callback to get the message"""
 
720
 
 
721
        tree = self.make_branch_and_tree('.')
 
722
        try:
 
723
            tree.commit()
 
724
        except Exception, e:
 
725
            self.assertTrue(isinstance(e, BzrError))
 
726
            self.assertEqual('The message or message_callback keyword'
 
727
                             ' parameter is required for commit().', str(e))
 
728
        else:
 
729
            self.fail('exception not raised')
 
730
        cb = self.Callback(u'commit 1', self)
 
731
        tree.commit(message_callback=cb)
 
732
        self.assertTrue(cb.called)
 
733
        repository = tree.branch.repository
 
734
        message = repository.get_revision(tree.last_revision()).message
 
735
        self.assertEqual('commit 1', message)
 
736
 
 
737
    def test_no_callback_pointless(self):
 
738
        """Callback should not be invoked for pointless commit"""
 
739
        tree = self.make_branch_and_tree('.')
 
740
        cb = self.Callback(u'commit 2', self)
 
741
        self.assertRaises(PointlessCommit, tree.commit, message_callback=cb,
 
742
                          allow_pointless=False)
 
743
        self.assertFalse(cb.called)
 
744
 
 
745
    def test_no_callback_netfailure(self):
 
746
        """Callback should not be invoked if connectivity fails"""
 
747
        tree = self.make_branch_and_tree('.')
 
748
        cb = self.Callback(u'commit 2', self)
 
749
        repository = tree.branch.repository
 
750
        # simulate network failure
 
751
        def raise_(self, arg, arg2, arg3=None, arg4=None):
 
752
            raise errors.NoSuchFile('foo')
 
753
        repository.add_inventory = raise_
 
754
        repository.add_inventory_by_delta = raise_
 
755
        self.assertRaises(errors.NoSuchFile, tree.commit, message_callback=cb)
 
756
        self.assertFalse(cb.called)
 
757
 
 
758
    def test_selected_file_merge_commit(self):
 
759
        """Ensure the correct error is raised"""
 
760
        tree = self.make_branch_and_tree('foo')
 
761
        # pending merge would turn into a left parent
 
762
        tree.commit('commit 1')
 
763
        tree.add_parent_tree_id('example')
 
764
        self.build_tree(['foo/bar', 'foo/baz'])
 
765
        tree.add(['bar', 'baz'])
 
766
        err = self.assertRaises(errors.CannotCommitSelectedFileMerge,
 
767
            tree.commit, 'commit 2', specific_files=['bar', 'baz'])
 
768
        self.assertEqual(['bar', 'baz'], err.files)
 
769
        self.assertEqual('Selected-file commit of merges is not supported'
 
770
                         ' yet: files bar, baz', str(err))
 
771
 
 
772
    def test_commit_ordering(self):
 
773
        """Test of corner-case commit ordering error"""
 
774
        tree = self.make_branch_and_tree('.')
 
775
        self.build_tree(['a/', 'a/z/', 'a/c/', 'a/z/x', 'a/z/y'])
 
776
        tree.add(['a/', 'a/z/', 'a/c/', 'a/z/x', 'a/z/y'])
 
777
        tree.commit('setup')
 
778
        self.build_tree(['a/c/d/'])
 
779
        tree.add('a/c/d')
 
780
        tree.rename_one('a/z/x', 'a/c/d/x')
 
781
        tree.commit('test', specific_files=['a/z/y'])
 
782
 
 
783
    def test_commit_no_author(self):
 
784
        """The default kwarg author in MutableTree.commit should not add
 
785
        the 'author' revision property.
 
786
        """
 
787
        tree = self.make_branch_and_tree('foo')
 
788
        rev_id = tree.commit('commit 1')
 
789
        rev = tree.branch.repository.get_revision(rev_id)
 
790
        self.assertFalse('author' in rev.properties)
 
791
        self.assertFalse('authors' in rev.properties)
 
792
 
 
793
    def test_commit_author(self):
 
794
        """Passing a non-empty author kwarg to MutableTree.commit should add
 
795
        the 'author' revision property.
 
796
        """
 
797
        tree = self.make_branch_and_tree('foo')
 
798
        rev_id = self.callDeprecated(['The parameter author was '
 
799
                'deprecated in version 1.13. Use authors instead'],
 
800
                tree.commit, 'commit 1', author='John Doe <jdoe@example.com>')
 
801
        rev = tree.branch.repository.get_revision(rev_id)
 
802
        self.assertEqual('John Doe <jdoe@example.com>',
 
803
                         rev.properties['authors'])
 
804
        self.assertFalse('author' in rev.properties)
 
805
 
 
806
    def test_commit_empty_authors_list(self):
 
807
        """Passing an empty list to authors shouldn't add the property."""
 
808
        tree = self.make_branch_and_tree('foo')
 
809
        rev_id = tree.commit('commit 1', authors=[])
 
810
        rev = tree.branch.repository.get_revision(rev_id)
 
811
        self.assertFalse('author' in rev.properties)
 
812
        self.assertFalse('authors' in rev.properties)
 
813
 
 
814
    def test_multiple_authors(self):
 
815
        tree = self.make_branch_and_tree('foo')
 
816
        rev_id = tree.commit('commit 1',
 
817
                authors=['John Doe <jdoe@example.com>',
 
818
                         'Jane Rey <jrey@example.com>'])
 
819
        rev = tree.branch.repository.get_revision(rev_id)
 
820
        self.assertEqual('John Doe <jdoe@example.com>\n'
 
821
                'Jane Rey <jrey@example.com>', rev.properties['authors'])
 
822
        self.assertFalse('author' in rev.properties)
 
823
 
 
824
    def test_author_and_authors_incompatible(self):
 
825
        tree = self.make_branch_and_tree('foo')
 
826
        self.assertRaises(AssertionError, tree.commit, 'commit 1',
 
827
                authors=['John Doe <jdoe@example.com>',
 
828
                         'Jane Rey <jrey@example.com>'],
 
829
                author="Jack Me <jme@example.com>")
 
830
 
 
831
    def test_author_with_newline_rejected(self):
 
832
        tree = self.make_branch_and_tree('foo')
 
833
        self.assertRaises(AssertionError, tree.commit, 'commit 1',
 
834
                authors=['John\nDoe <jdoe@example.com>'])
 
835
 
 
836
    def test_commit_with_checkout_and_branch_sharing_repo(self):
 
837
        repo = self.make_repository('repo', shared=True)
 
838
        # make_branch_and_tree ignores shared repos
 
839
        branch = bzrdir.BzrDir.create_branch_convenience('repo/branch')
 
840
        tree2 = branch.create_checkout('repo/tree2')
 
841
        tree2.commit('message', rev_id='rev1')
 
842
        self.assertTrue(tree2.branch.repository.has_revision('rev1'))