~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/tests/test_commit.py

  • Committer: Canonical.com Patch Queue Manager
  • Date: 2011-02-25 02:01:51 UTC
  • mfrom: (5676.1.10 per_interrepo-extra)
  • Revision ID: pqm@pqm.ubuntu.com-20110225020151-tlqdjbxfv5byh7l7
(jelmer) Allow repositories to provide extra combinations to run
 bzrlib.tests.per_interrepo with. (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
    errors,
 
24
    )
22
25
from bzrlib.branch import Branch
23
 
from bzrlib.bzrdir import BzrDir, BzrDirMetaFormat1
24
 
from bzrlib.workingtree import WorkingTree
 
26
from bzrlib.bzrdir import BzrDirMetaFormat1
25
27
from bzrlib.commit import Commit, NullCommitReporter
26
28
from bzrlib.config import BranchConfig
27
 
from bzrlib.errors import (PointlessCommit, BzrError, SigningFailed, 
 
29
from bzrlib.errors import (PointlessCommit, BzrError, SigningFailed,
28
30
                           LockContention)
 
31
from bzrlib.tests import SymlinkFeature, TestCaseWithTransport
29
32
 
30
33
 
31
34
# TODO: Test commit with some added, and added-but-missing files
64
67
    def renamed(self, change, old_path, new_path):
65
68
        self.calls.append(('renamed', change, old_path, new_path))
66
69
 
 
70
    def is_verbose(self):
 
71
        return True
 
72
 
67
73
 
68
74
class TestCommit(TestCaseWithTransport):
69
75
 
86
92
        eq(rev.message, 'add hello')
87
93
 
88
94
        tree1 = b.repository.revision_tree(rh[0])
 
95
        tree1.lock_read()
89
96
        text = tree1.get_file_text(file_id)
90
 
        eq(text, 'hello world')
 
97
        tree1.unlock()
 
98
        self.assertEqual('hello world', text)
91
99
 
92
100
        tree2 = b.repository.revision_tree(rh[1])
93
 
        eq(tree2.get_file_text(file_id), 'version 2')
 
101
        tree2.lock_read()
 
102
        text = tree2.get_file_text(file_id)
 
103
        tree2.unlock()
 
104
        self.assertEqual('version 2', text)
94
105
 
95
 
    def test_delete_commit(self):
96
 
        """Test a commit with a deleted file"""
 
106
    def test_missing_commit(self):
 
107
        """Test a commit with a missing file"""
97
108
        wt = self.make_branch_and_tree('.')
98
109
        b = wt.branch
99
110
        file('hello', 'w').write('hello world')
106
117
        tree = b.repository.revision_tree('rev2')
107
118
        self.assertFalse(tree.has_id('hello-id'))
108
119
 
 
120
    def test_partial_commit_move(self):
 
121
        """Test a partial commit where a file was renamed but not committed.
 
122
 
 
123
        https://bugs.launchpad.net/bzr/+bug/83039
 
124
 
 
125
        If not handled properly, commit will try to snapshot
 
126
        dialog.py with olive/ as a parent, while
 
127
        olive/ has not been snapshotted yet.
 
128
        """
 
129
        wt = self.make_branch_and_tree('.')
 
130
        b = wt.branch
 
131
        self.build_tree(['annotate/', 'annotate/foo.py',
 
132
                         'olive/', 'olive/dialog.py'
 
133
                        ])
 
134
        wt.add(['annotate', 'olive', 'annotate/foo.py', 'olive/dialog.py'])
 
135
        wt.commit(message='add files')
 
136
        wt.rename_one("olive/dialog.py", "aaa")
 
137
        self.build_tree_contents([('annotate/foo.py', 'modified\n')])
 
138
        wt.commit('renamed hello', specific_files=["annotate"])
 
139
 
109
140
    def test_pointless_commit(self):
110
141
        """Commit refuses unless there are changes or it's forced."""
111
142
        wt = self.make_branch_and_tree('.')
119
150
                          message='fails',
120
151
                          allow_pointless=False)
121
152
        self.assertEquals(b.revno(), 1)
122
 
        
 
153
 
123
154
    def test_commit_empty(self):
124
155
        """Commiting an empty tree works."""
125
156
        wt = self.make_branch_and_tree('.')
142
173
              ['hello-id', 'buongia-id'])
143
174
        wt.commit(message='add files',
144
175
                 rev_id='test@rev-1')
145
 
        
 
176
 
146
177
        os.remove('hello')
147
178
        file('buongia', 'w').write('new text')
148
179
        wt.commit(message='update text',
159
190
        eq(b.revno(), 3)
160
191
 
161
192
        tree2 = b.repository.revision_tree('test@rev-2')
 
193
        tree2.lock_read()
 
194
        self.addCleanup(tree2.unlock)
162
195
        self.assertTrue(tree2.has_filename('hello'))
163
196
        self.assertEquals(tree2.get_file_text('hello-id'), 'hello')
164
197
        self.assertEquals(tree2.get_file_text('buongia-id'), 'new text')
165
 
        
 
198
 
166
199
        tree3 = b.repository.revision_tree('test@rev-3')
 
200
        tree3.lock_read()
 
201
        self.addCleanup(tree3.unlock)
167
202
        self.assertFalse(tree3.has_filename('hello'))
168
203
        self.assertEquals(tree3.get_file_text('buongia-id'), 'new text')
169
204
 
180
215
 
181
216
        eq = self.assertEquals
182
217
        tree1 = b.repository.revision_tree('test@rev-1')
 
218
        tree1.lock_read()
 
219
        self.addCleanup(tree1.unlock)
183
220
        eq(tree1.id2path('hello-id'), 'hello')
184
221
        eq(tree1.get_file_text('hello-id'), 'contents of hello\n')
185
222
        self.assertFalse(tree1.has_filename('fruity'))
188
225
        eq(ie.revision, 'test@rev-1')
189
226
 
190
227
        tree2 = b.repository.revision_tree('test@rev-2')
 
228
        tree2.lock_read()
 
229
        self.addCleanup(tree2.unlock)
191
230
        eq(tree2.id2path('hello-id'), 'fruity')
192
231
        eq(tree2.get_file_text('hello-id'), 'contents of hello\n')
193
232
        self.check_inventory_shape(tree2.inventory, ['fruity'])
217
256
        wt.move(['hello'], 'a')
218
257
        r2 = 'test@rev-2'
219
258
        wt.commit('two', rev_id=r2, allow_pointless=False)
220
 
        self.check_inventory_shape(wt.read_working_inventory(),
221
 
                                   ['a', 'a/hello', 'b'])
 
259
        wt.lock_read()
 
260
        try:
 
261
            self.check_inventory_shape(wt.read_working_inventory(),
 
262
                                       ['a/', 'a/hello', 'b/'])
 
263
        finally:
 
264
            wt.unlock()
222
265
 
223
266
        wt.move(['b'], 'a')
224
267
        r3 = 'test@rev-3'
225
268
        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'])
 
269
        wt.lock_read()
 
270
        try:
 
271
            self.check_inventory_shape(wt.read_working_inventory(),
 
272
                                       ['a/', 'a/hello', 'a/b/'])
 
273
            self.check_inventory_shape(b.repository.get_inventory(r3),
 
274
                                       ['a/', 'a/hello', 'a/b/'])
 
275
        finally:
 
276
            wt.unlock()
230
277
 
231
278
        wt.move(['a/hello'], 'a/b')
232
279
        r4 = 'test@rev-4'
233
280
        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'])
 
281
        wt.lock_read()
 
282
        try:
 
283
            self.check_inventory_shape(wt.read_working_inventory(),
 
284
                                       ['a/', 'a/b/hello', 'a/b/'])
 
285
        finally:
 
286
            wt.unlock()
236
287
 
237
 
        inv = b.repository.get_revision_inventory(r4)
 
288
        inv = b.repository.get_inventory(r4)
238
289
        eq(inv['hello-id'].revision, r4)
239
290
        eq(inv['a-id'].revision, r1)
240
291
        eq(inv['b-id'].revision, r3)
241
 
        
 
292
 
242
293
    def test_removed_commit(self):
243
294
        """Commit with a removed file"""
244
295
        wt = self.make_branch_and_tree('.')
339
390
                                                      allow_pointless=True,
340
391
                                                      rev_id='B',
341
392
                                                      working_tree=wt)
342
 
            self.assertEqual(Testament.from_revision(branch.repository,
343
 
                             'B').as_short_text(),
 
393
            def sign(text):
 
394
                return bzrlib.gpg.LoopbackGPGStrategy(None).sign(text)
 
395
            self.assertEqual(sign(Testament.from_revision(branch.repository,
 
396
                             'B').as_short_text()),
344
397
                             branch.repository.get_signature_text('B'))
345
398
        finally:
346
399
            bzrlib.gpg.GPGStrategy = oldstrategy
427
480
        other_bzrdir = master_branch.bzrdir.sprout('other')
428
481
        other_tree = other_bzrdir.open_workingtree()
429
482
 
430
 
        # do a commit to the the other branch changing the content file so
 
483
        # do a commit to the other branch changing the content file so
431
484
        # that our commit after merging will have a merged revision in the
432
485
        # content file history.
433
486
        self.build_tree_contents([('other/content_file', 'change in other\n')])
436
489
        # do a merge into the bound branch from other, and then change the
437
490
        # content file locally to force a new revision (rather than using the
438
491
        # revision from other). This forces extra processing in commit.
439
 
        self.merge(other_tree.branch, bound_tree)
 
492
        bound_tree.merge_from_branch(other_tree.branch)
440
493
        self.build_tree_contents([('bound/content_file', 'change in bound\n')])
441
494
 
442
495
        # before #34959 was fixed, this failed with 'revision not present in
444
497
        bound_tree.commit(message='commit of merge in bound tree')
445
498
 
446
499
    def test_commit_reporting_after_merge(self):
447
 
        # when doing a commit of a merge, the reporter needs to still 
 
500
        # when doing a commit of a merge, the reporter needs to still
448
501
        # be called for each item that is added/removed/deleted.
449
502
        this_tree = self.make_branch_and_tree('this')
450
503
        # we need a bunch of files and dirs, to perform one action on each.
490
543
            other_tree.commit('modify all sample files and dirs.')
491
544
        finally:
492
545
            other_tree.unlock()
493
 
        self.merge(other_tree.branch, this_tree)
 
546
        this_tree.merge_from_branch(other_tree.branch)
494
547
        reporter = CapturingReporter()
495
548
        this_tree.commit('do the commit', reporter=reporter)
496
 
        self.assertEqual([
497
 
            ('change', 'unchanged', ''),
498
 
            ('change', 'unchanged', 'dirtoleave'),
499
 
            ('change', 'unchanged', 'filetoleave'),
 
549
        expected = set([
500
550
            ('change', 'modified', 'filetomodify'),
501
551
            ('change', 'added', 'newdir'),
502
552
            ('change', 'added', 'newfile'),
503
553
            ('renamed', 'renamed', 'dirtorename', 'renameddir'),
 
554
            ('renamed', 'renamed', 'filetorename', 'renamedfile'),
504
555
            ('renamed', 'renamed', 'dirtoreparent', 'renameddir/reparenteddir'),
505
556
            ('renamed', 'renamed', 'filetoreparent', 'renameddir/reparentedfile'),
506
 
            ('renamed', 'renamed', 'filetorename', 'renamedfile'),
507
557
            ('deleted', 'dirtoremove'),
508
558
            ('deleted', 'filetoremove'),
509
 
            ],
510
 
            reporter.calls)
 
559
            ])
 
560
        result = set(reporter.calls)
 
561
        missing = expected - result
 
562
        new = result - expected
 
563
        self.assertEqual((set(), set()), (missing, new))
511
564
 
512
565
    def test_commit_removals_respects_filespec(self):
513
566
        """Commit respects the specified_files for removals."""
517
570
        tree.commit('added a, b')
518
571
        tree.remove(['a', 'b'])
519
572
        tree.commit('removed a', specific_files='a')
520
 
        basis = tree.basis_tree().inventory
521
 
        self.assertIs(None, basis.path2id('a'))
522
 
        self.assertFalse(basis.path2id('b') is None)
 
573
        basis = tree.basis_tree()
 
574
        tree.lock_read()
 
575
        try:
 
576
            self.assertIs(None, basis.path2id('a'))
 
577
            self.assertFalse(basis.path2id('b') is None)
 
578
        finally:
 
579
            tree.unlock()
523
580
 
524
581
    def test_commit_saves_1ms_timestamp(self):
525
582
        """Passing in a timestamp is saved with 1ms resolution"""
543
600
        timestamp = rev.timestamp
544
601
        timestamp_1ms = round(timestamp, 3)
545
602
        self.assertEqual(timestamp_1ms, timestamp)
 
603
 
 
604
    def assertBasisTreeKind(self, kind, tree, file_id):
 
605
        basis = tree.basis_tree()
 
606
        basis.lock_read()
 
607
        try:
 
608
            self.assertEqual(kind, basis.kind(file_id))
 
609
        finally:
 
610
            basis.unlock()
 
611
 
 
612
    def test_commit_kind_changes(self):
 
613
        self.requireFeature(SymlinkFeature)
 
614
        tree = self.make_branch_and_tree('.')
 
615
        os.symlink('target', 'name')
 
616
        tree.add('name', 'a-file-id')
 
617
        tree.commit('Added a symlink')
 
618
        self.assertBasisTreeKind('symlink', tree, 'a-file-id')
 
619
 
 
620
        os.unlink('name')
 
621
        self.build_tree(['name'])
 
622
        tree.commit('Changed symlink to file')
 
623
        self.assertBasisTreeKind('file', tree, 'a-file-id')
 
624
 
 
625
        os.unlink('name')
 
626
        os.symlink('target', 'name')
 
627
        tree.commit('file to symlink')
 
628
        self.assertBasisTreeKind('symlink', tree, 'a-file-id')
 
629
 
 
630
        os.unlink('name')
 
631
        os.mkdir('name')
 
632
        tree.commit('symlink to directory')
 
633
        self.assertBasisTreeKind('directory', tree, 'a-file-id')
 
634
 
 
635
        os.rmdir('name')
 
636
        os.symlink('target', 'name')
 
637
        tree.commit('directory to symlink')
 
638
        self.assertBasisTreeKind('symlink', tree, 'a-file-id')
 
639
 
 
640
        # prepare for directory <-> file tests
 
641
        os.unlink('name')
 
642
        os.mkdir('name')
 
643
        tree.commit('symlink to directory')
 
644
        self.assertBasisTreeKind('directory', tree, 'a-file-id')
 
645
 
 
646
        os.rmdir('name')
 
647
        self.build_tree(['name'])
 
648
        tree.commit('Changed directory to file')
 
649
        self.assertBasisTreeKind('file', tree, 'a-file-id')
 
650
 
 
651
        os.unlink('name')
 
652
        os.mkdir('name')
 
653
        tree.commit('file to directory')
 
654
        self.assertBasisTreeKind('directory', tree, 'a-file-id')
 
655
 
 
656
    def test_commit_unversioned_specified(self):
 
657
        """Commit should raise if specified files isn't in basis or worktree"""
 
658
        tree = self.make_branch_and_tree('.')
 
659
        self.assertRaises(errors.PathsNotVersionedError, tree.commit,
 
660
                          'message', specific_files=['bogus'])
 
661
 
 
662
    class Callback(object):
 
663
 
 
664
        def __init__(self, message, testcase):
 
665
            self.called = False
 
666
            self.message = message
 
667
            self.testcase = testcase
 
668
 
 
669
        def __call__(self, commit_obj):
 
670
            self.called = True
 
671
            self.testcase.assertTrue(isinstance(commit_obj, Commit))
 
672
            return self.message
 
673
 
 
674
    def test_commit_callback(self):
 
675
        """Commit should invoke a callback to get the message"""
 
676
 
 
677
        tree = self.make_branch_and_tree('.')
 
678
        try:
 
679
            tree.commit()
 
680
        except Exception, e:
 
681
            self.assertTrue(isinstance(e, BzrError))
 
682
            self.assertEqual('The message or message_callback keyword'
 
683
                             ' parameter is required for commit().', str(e))
 
684
        else:
 
685
            self.fail('exception not raised')
 
686
        cb = self.Callback(u'commit 1', self)
 
687
        tree.commit(message_callback=cb)
 
688
        self.assertTrue(cb.called)
 
689
        repository = tree.branch.repository
 
690
        message = repository.get_revision(tree.last_revision()).message
 
691
        self.assertEqual('commit 1', message)
 
692
 
 
693
    def test_no_callback_pointless(self):
 
694
        """Callback should not be invoked for pointless commit"""
 
695
        tree = self.make_branch_and_tree('.')
 
696
        cb = self.Callback(u'commit 2', self)
 
697
        self.assertRaises(PointlessCommit, tree.commit, message_callback=cb,
 
698
                          allow_pointless=False)
 
699
        self.assertFalse(cb.called)
 
700
 
 
701
    def test_no_callback_netfailure(self):
 
702
        """Callback should not be invoked if connectivity fails"""
 
703
        tree = self.make_branch_and_tree('.')
 
704
        cb = self.Callback(u'commit 2', self)
 
705
        repository = tree.branch.repository
 
706
        # simulate network failure
 
707
        def raise_(self, arg, arg2, arg3=None, arg4=None):
 
708
            raise errors.NoSuchFile('foo')
 
709
        repository.add_inventory = raise_
 
710
        repository.add_inventory_by_delta = raise_
 
711
        self.assertRaises(errors.NoSuchFile, tree.commit, message_callback=cb)
 
712
        self.assertFalse(cb.called)
 
713
 
 
714
    def test_selected_file_merge_commit(self):
 
715
        """Ensure the correct error is raised"""
 
716
        tree = self.make_branch_and_tree('foo')
 
717
        # pending merge would turn into a left parent
 
718
        tree.commit('commit 1')
 
719
        tree.add_parent_tree_id('example')
 
720
        self.build_tree(['foo/bar', 'foo/baz'])
 
721
        tree.add(['bar', 'baz'])
 
722
        err = self.assertRaises(errors.CannotCommitSelectedFileMerge,
 
723
            tree.commit, 'commit 2', specific_files=['bar', 'baz'])
 
724
        self.assertEqual(['bar', 'baz'], err.files)
 
725
        self.assertEqual('Selected-file commit of merges is not supported'
 
726
                         ' yet: files bar, baz', str(err))
 
727
 
 
728
    def test_commit_ordering(self):
 
729
        """Test of corner-case commit ordering error"""
 
730
        tree = self.make_branch_and_tree('.')
 
731
        self.build_tree(['a/', 'a/z/', 'a/c/', 'a/z/x', 'a/z/y'])
 
732
        tree.add(['a/', 'a/z/', 'a/c/', 'a/z/x', 'a/z/y'])
 
733
        tree.commit('setup')
 
734
        self.build_tree(['a/c/d/'])
 
735
        tree.add('a/c/d')
 
736
        tree.rename_one('a/z/x', 'a/c/d/x')
 
737
        tree.commit('test', specific_files=['a/z/y'])
 
738
 
 
739
    def test_commit_no_author(self):
 
740
        """The default kwarg author in MutableTree.commit should not add
 
741
        the 'author' revision property.
 
742
        """
 
743
        tree = self.make_branch_and_tree('foo')
 
744
        rev_id = tree.commit('commit 1')
 
745
        rev = tree.branch.repository.get_revision(rev_id)
 
746
        self.assertFalse('author' in rev.properties)
 
747
        self.assertFalse('authors' in rev.properties)
 
748
 
 
749
    def test_commit_author(self):
 
750
        """Passing a non-empty author kwarg to MutableTree.commit should add
 
751
        the 'author' revision property.
 
752
        """
 
753
        tree = self.make_branch_and_tree('foo')
 
754
        rev_id = self.callDeprecated(['The parameter author was '
 
755
                'deprecated in version 1.13. Use authors instead'],
 
756
                tree.commit, 'commit 1', author='John Doe <jdoe@example.com>')
 
757
        rev = tree.branch.repository.get_revision(rev_id)
 
758
        self.assertEqual('John Doe <jdoe@example.com>',
 
759
                         rev.properties['authors'])
 
760
        self.assertFalse('author' in rev.properties)
 
761
 
 
762
    def test_commit_empty_authors_list(self):
 
763
        """Passing an empty list to authors shouldn't add the property."""
 
764
        tree = self.make_branch_and_tree('foo')
 
765
        rev_id = tree.commit('commit 1', authors=[])
 
766
        rev = tree.branch.repository.get_revision(rev_id)
 
767
        self.assertFalse('author' in rev.properties)
 
768
        self.assertFalse('authors' in rev.properties)
 
769
 
 
770
    def test_multiple_authors(self):
 
771
        tree = self.make_branch_and_tree('foo')
 
772
        rev_id = tree.commit('commit 1',
 
773
                authors=['John Doe <jdoe@example.com>',
 
774
                         'Jane Rey <jrey@example.com>'])
 
775
        rev = tree.branch.repository.get_revision(rev_id)
 
776
        self.assertEqual('John Doe <jdoe@example.com>\n'
 
777
                'Jane Rey <jrey@example.com>', rev.properties['authors'])
 
778
        self.assertFalse('author' in rev.properties)
 
779
 
 
780
    def test_author_and_authors_incompatible(self):
 
781
        tree = self.make_branch_and_tree('foo')
 
782
        self.assertRaises(AssertionError, tree.commit, 'commit 1',
 
783
                authors=['John Doe <jdoe@example.com>',
 
784
                         'Jane Rey <jrey@example.com>'],
 
785
                author="Jack Me <jme@example.com>")
 
786
 
 
787
    def test_author_with_newline_rejected(self):
 
788
        tree = self.make_branch_and_tree('foo')
 
789
        self.assertRaises(AssertionError, tree.commit, 'commit 1',
 
790
                authors=['John\nDoe <jdoe@example.com>'])
 
791
 
 
792
    def test_commit_with_checkout_and_branch_sharing_repo(self):
 
793
        repo = self.make_repository('repo', shared=True)
 
794
        # make_branch_and_tree ignores shared repos
 
795
        branch = bzrdir.BzrDir.create_branch_convenience('repo/branch')
 
796
        tree2 = branch.create_checkout('repo/tree2')
 
797
        tree2.commit('message', rev_id='rev1')
 
798
        self.assertTrue(tree2.branch.repository.has_revision('rev1'))