~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/tests/test_commit.py

  • Committer: Vincent Ladeuil
  • Date: 2010-02-03 07:18:36 UTC
  • mto: (5008.1.1 integration)
  • mto: This revision was merged to the branch mainline in revision 5009.
  • Revision ID: v.ladeuil+lp@free.fr-20100203071836-u9b86q68fr9ri5s6
Fix NEWS.

Show diffs side-by-side

added added

removed removed

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