1
# Copyright (C) 2005 by Canonical Ltd
1
# Copyright (C) 2005-2011 Canonical Ltd
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.
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.
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
21
from bzrlib.tests import TestCaseWithTransport
22
26
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.config import BranchConfig
27
from bzrlib.errors import (PointlessCommit, BzrError, SigningFailed,
27
from bzrlib.bzrdir import BzrDirMetaFormat1
28
from bzrlib.commit import Commit, NullCommitReporter
29
from bzrlib.errors import (
35
from bzrlib.tests import (
36
TestCaseWithTransport,
39
from bzrlib.tests.features import (
42
from bzrlib.tests.matchers import MatchesAncestry
31
45
# TODO: Test commit with some added, and added-but-missing files
33
class MustSignConfig(BranchConfig):
35
def signature_needed(self):
47
class MustSignConfig(config.MemoryStack):
50
super(MustSignConfig, self).__init__('''
51
gpg_signing_command=cat -
52
create_signatures=always
56
class CapturingReporter(NullCommitReporter):
57
"""This reporter captures the calls made to it for evaluation later."""
60
# a list of the calls this received
63
def snapshot_change(self, change, path):
64
self.calls.append(('change', change, path))
66
def deleted(self, file_id):
67
self.calls.append(('deleted', file_id))
69
def missing(self, path):
70
self.calls.append(('missing', path))
72
def renamed(self, change, old_path, new_path):
73
self.calls.append(('renamed', change, old_path, new_path))
38
def gpg_signing_command(self):
42
class BranchWithHooks(BranchConfig):
44
def post_commit(self):
45
return "bzrlib.ahook bzrlib.ahook"
48
79
class TestCommit(TestCaseWithTransport):
51
82
"""Commit and check two versions of a single file."""
52
83
wt = self.make_branch_and_tree('.')
54
file('hello', 'w').write('hello world')
85
with file('hello', 'w') as f: f.write('hello world')
56
wt.commit(message='add hello')
87
rev1 = wt.commit(message='add hello')
57
88
file_id = wt.path2id('hello')
59
file('hello', 'w').write('version 2')
60
wt.commit(message='commit 2')
90
with file('hello', 'w') as f: f.write('version 2')
91
rev2 = wt.commit(message='commit 2')
62
93
eq = self.assertEquals
64
rh = b.revision_history()
65
rev = b.repository.get_revision(rh[0])
95
rev = b.repository.get_revision(rev1)
66
96
eq(rev.message, 'add hello')
68
tree1 = b.repository.revision_tree(rh[0])
98
tree1 = b.repository.revision_tree(rev1)
69
100
text = tree1.get_file_text(file_id)
70
eq(text, 'hello world')
72
tree2 = b.repository.revision_tree(rh[1])
73
eq(tree2.get_file_text(file_id), 'version 2')
75
def test_delete_commit(self):
76
"""Test a commit with a deleted file"""
77
wt = self.make_branch_and_tree('.')
79
file('hello', 'w').write('hello world')
102
self.assertEqual('hello world', text)
104
tree2 = b.repository.revision_tree(rev2)
106
text = tree2.get_file_text(file_id)
108
self.assertEqual('version 2', text)
110
def test_commit_lossy_native(self):
111
"""Attempt a lossy commit to a native branch."""
112
wt = self.make_branch_and_tree('.')
114
with file('hello', 'w') as f: f.write('hello world')
116
revid = wt.commit(message='add hello', rev_id='revid', lossy=True)
117
self.assertEquals('revid', revid)
119
def test_commit_lossy_foreign(self):
120
"""Attempt a lossy commit to a foreign branch."""
121
test_foreign.register_dummy_foreign_for_test(self)
122
wt = self.make_branch_and_tree('.',
123
format=test_foreign.DummyForeignVcsDirFormat())
125
with file('hello', 'w') as f: f.write('hello world')
127
revid = wt.commit(message='add hello', lossy=True,
128
timestamp=1302659388, timezone=0)
129
self.assertEquals('dummy-v1:1302659388.0-0-UNKNOWN', revid)
131
def test_commit_bound_lossy_foreign(self):
132
"""Attempt a lossy commit to a bzr branch bound to a foreign branch."""
133
test_foreign.register_dummy_foreign_for_test(self)
134
foreign_branch = self.make_branch('foreign',
135
format=test_foreign.DummyForeignVcsDirFormat())
136
wt = foreign_branch.create_checkout("local")
138
with file('local/hello', 'w') as f: f.write('hello world')
140
revid = wt.commit(message='add hello', lossy=True,
141
timestamp=1302659388, timezone=0)
142
self.assertEquals('dummy-v1:1302659388.0-0-0', revid)
143
self.assertEquals('dummy-v1:1302659388.0-0-0',
144
foreign_branch.last_revision())
145
self.assertEquals('dummy-v1:1302659388.0-0-0',
146
wt.branch.last_revision())
148
def test_missing_commit(self):
149
"""Test a commit with a missing file"""
150
wt = self.make_branch_and_tree('.')
152
with file('hello', 'w') as f: f.write('hello world')
80
153
wt.add(['hello'], ['hello-id'])
81
154
wt.commit(message='add hello')
83
156
os.remove('hello')
84
wt.commit('removed hello', rev_id='rev2')
157
reporter = CapturingReporter()
158
wt.commit('removed hello', rev_id='rev2', reporter=reporter)
160
[('missing', u'hello'), ('deleted', u'hello')],
86
163
tree = b.repository.revision_tree('rev2')
87
164
self.assertFalse(tree.has_id('hello-id'))
166
def test_partial_commit_move(self):
167
"""Test a partial commit where a file was renamed but not committed.
169
https://bugs.launchpad.net/bzr/+bug/83039
171
If not handled properly, commit will try to snapshot
172
dialog.py with olive/ as a parent, while
173
olive/ has not been snapshotted yet.
175
wt = self.make_branch_and_tree('.')
177
self.build_tree(['annotate/', 'annotate/foo.py',
178
'olive/', 'olive/dialog.py'
180
wt.add(['annotate', 'olive', 'annotate/foo.py', 'olive/dialog.py'])
181
wt.commit(message='add files')
182
wt.rename_one("olive/dialog.py", "aaa")
183
self.build_tree_contents([('annotate/foo.py', 'modified\n')])
184
wt.commit('renamed hello', specific_files=["annotate"])
89
186
def test_pointless_commit(self):
90
187
"""Commit refuses unless there are changes or it's forced."""
91
188
wt = self.make_branch_and_tree('.')
93
file('hello', 'w').write('hello')
190
with file('hello', 'w') as f: f.write('hello')
95
192
wt.commit(message='add hello')
96
193
self.assertEquals(b.revno(), 1)
388
504
wt = bound.open_workingtree()
389
505
wt.branch.set_bound_location(os.path.realpath('master'))
390
506
master_branch.lock_write()
391
self.assertRaises(LockContention, wt.commit, 'silly')
508
self.assertRaises(LockContention, wt.commit, 'silly')
510
master_branch.unlock()
512
def test_commit_bound_merge(self):
513
# see bug #43959; commit of a merge in a bound branch fails to push
514
# the new commit into the master
515
master_branch = self.make_branch('master')
516
bound_tree = self.make_branch_and_tree('bound')
517
bound_tree.branch.bind(master_branch)
519
self.build_tree_contents([('bound/content_file', 'initial contents\n')])
520
bound_tree.add(['content_file'])
521
bound_tree.commit(message='woo!')
523
other_bzrdir = master_branch.bzrdir.sprout('other')
524
other_tree = other_bzrdir.open_workingtree()
526
# do a commit to the other branch changing the content file so
527
# that our commit after merging will have a merged revision in the
528
# content file history.
529
self.build_tree_contents([('other/content_file', 'change in other\n')])
530
other_tree.commit('change in other')
532
# do a merge into the bound branch from other, and then change the
533
# content file locally to force a new revision (rather than using the
534
# revision from other). This forces extra processing in commit.
535
bound_tree.merge_from_branch(other_tree.branch)
536
self.build_tree_contents([('bound/content_file', 'change in bound\n')])
538
# before #34959 was fixed, this failed with 'revision not present in
539
# weave' when trying to implicitly push from the bound branch to the master
540
bound_tree.commit(message='commit of merge in bound tree')
542
def test_commit_reporting_after_merge(self):
543
# when doing a commit of a merge, the reporter needs to still
544
# be called for each item that is added/removed/deleted.
545
this_tree = self.make_branch_and_tree('this')
546
# we need a bunch of files and dirs, to perform one action on each.
549
'this/dirtoreparent/',
552
'this/filetoreparent',
569
this_tree.commit('create_files')
570
other_dir = this_tree.bzrdir.sprout('other')
571
other_tree = other_dir.open_workingtree()
572
other_tree.lock_write()
573
# perform the needed actions on the files and dirs.
575
other_tree.rename_one('dirtorename', 'renameddir')
576
other_tree.rename_one('dirtoreparent', 'renameddir/reparenteddir')
577
other_tree.rename_one('filetorename', 'renamedfile')
578
other_tree.rename_one('filetoreparent', 'renameddir/reparentedfile')
579
other_tree.remove(['dirtoremove', 'filetoremove'])
580
self.build_tree_contents([
582
('other/filetomodify', 'new content'),
583
('other/newfile', 'new file content')])
584
other_tree.add('newfile')
585
other_tree.add('newdir/')
586
other_tree.commit('modify all sample files and dirs.')
589
this_tree.merge_from_branch(other_tree.branch)
590
reporter = CapturingReporter()
591
this_tree.commit('do the commit', reporter=reporter)
593
('change', 'modified', 'filetomodify'),
594
('change', 'added', 'newdir'),
595
('change', 'added', 'newfile'),
596
('renamed', 'renamed', 'dirtorename', 'renameddir'),
597
('renamed', 'renamed', 'filetorename', 'renamedfile'),
598
('renamed', 'renamed', 'dirtoreparent', 'renameddir/reparenteddir'),
599
('renamed', 'renamed', 'filetoreparent', 'renameddir/reparentedfile'),
600
('deleted', 'dirtoremove'),
601
('deleted', 'filetoremove'),
603
result = set(reporter.calls)
604
missing = expected - result
605
new = result - expected
606
self.assertEqual((set(), set()), (missing, new))
608
def test_commit_removals_respects_filespec(self):
609
"""Commit respects the specified_files for removals."""
610
tree = self.make_branch_and_tree('.')
611
self.build_tree(['a', 'b'])
613
tree.commit('added a, b')
614
tree.remove(['a', 'b'])
615
tree.commit('removed a', specific_files='a')
616
basis = tree.basis_tree()
619
self.assertIs(None, basis.path2id('a'))
620
self.assertFalse(basis.path2id('b') is None)
624
def test_commit_saves_1ms_timestamp(self):
625
"""Passing in a timestamp is saved with 1ms resolution"""
626
tree = self.make_branch_and_tree('.')
627
self.build_tree(['a'])
629
tree.commit('added a', timestamp=1153248633.4186721, timezone=0,
632
rev = tree.branch.repository.get_revision('a1')
633
self.assertEqual(1153248633.419, rev.timestamp)
635
def test_commit_has_1ms_resolution(self):
636
"""Allowing commit to generate the timestamp also has 1ms resolution"""
637
tree = self.make_branch_and_tree('.')
638
self.build_tree(['a'])
640
tree.commit('added a', rev_id='a1')
642
rev = tree.branch.repository.get_revision('a1')
643
timestamp = rev.timestamp
644
timestamp_1ms = round(timestamp, 3)
645
self.assertEqual(timestamp_1ms, timestamp)
647
def assertBasisTreeKind(self, kind, tree, file_id):
648
basis = tree.basis_tree()
651
self.assertEqual(kind, basis.kind(file_id))
655
def test_commit_kind_changes(self):
656
self.requireFeature(SymlinkFeature)
657
tree = self.make_branch_and_tree('.')
658
os.symlink('target', 'name')
659
tree.add('name', 'a-file-id')
660
tree.commit('Added a symlink')
661
self.assertBasisTreeKind('symlink', tree, 'a-file-id')
664
self.build_tree(['name'])
665
tree.commit('Changed symlink to file')
666
self.assertBasisTreeKind('file', tree, 'a-file-id')
669
os.symlink('target', 'name')
670
tree.commit('file to symlink')
671
self.assertBasisTreeKind('symlink', tree, 'a-file-id')
675
tree.commit('symlink to directory')
676
self.assertBasisTreeKind('directory', tree, 'a-file-id')
679
os.symlink('target', 'name')
680
tree.commit('directory to symlink')
681
self.assertBasisTreeKind('symlink', tree, 'a-file-id')
683
# prepare for directory <-> file tests
686
tree.commit('symlink to directory')
687
self.assertBasisTreeKind('directory', tree, 'a-file-id')
690
self.build_tree(['name'])
691
tree.commit('Changed directory to file')
692
self.assertBasisTreeKind('file', tree, 'a-file-id')
696
tree.commit('file to directory')
697
self.assertBasisTreeKind('directory', tree, 'a-file-id')
699
def test_commit_unversioned_specified(self):
700
"""Commit should raise if specified files isn't in basis or worktree"""
701
tree = self.make_branch_and_tree('.')
702
self.assertRaises(errors.PathsNotVersionedError, tree.commit,
703
'message', specific_files=['bogus'])
705
class Callback(object):
707
def __init__(self, message, testcase):
709
self.message = message
710
self.testcase = testcase
712
def __call__(self, commit_obj):
714
self.testcase.assertTrue(isinstance(commit_obj, Commit))
717
def test_commit_callback(self):
718
"""Commit should invoke a callback to get the message"""
720
tree = self.make_branch_and_tree('.')
724
self.assertTrue(isinstance(e, BzrError))
725
self.assertEqual('The message or message_callback keyword'
726
' parameter is required for commit().', str(e))
728
self.fail('exception not raised')
729
cb = self.Callback(u'commit 1', self)
730
tree.commit(message_callback=cb)
731
self.assertTrue(cb.called)
732
repository = tree.branch.repository
733
message = repository.get_revision(tree.last_revision()).message
734
self.assertEqual('commit 1', message)
736
def test_no_callback_pointless(self):
737
"""Callback should not be invoked for pointless commit"""
738
tree = self.make_branch_and_tree('.')
739
cb = self.Callback(u'commit 2', self)
740
self.assertRaises(PointlessCommit, tree.commit, message_callback=cb,
741
allow_pointless=False)
742
self.assertFalse(cb.called)
744
def test_no_callback_netfailure(self):
745
"""Callback should not be invoked if connectivity fails"""
746
tree = self.make_branch_and_tree('.')
747
cb = self.Callback(u'commit 2', self)
748
repository = tree.branch.repository
749
# simulate network failure
750
def raise_(self, arg, arg2, arg3=None, arg4=None):
751
raise errors.NoSuchFile('foo')
752
repository.add_inventory = raise_
753
repository.add_inventory_by_delta = raise_
754
self.assertRaises(errors.NoSuchFile, tree.commit, message_callback=cb)
755
self.assertFalse(cb.called)
757
def test_selected_file_merge_commit(self):
758
"""Ensure the correct error is raised"""
759
tree = self.make_branch_and_tree('foo')
760
# pending merge would turn into a left parent
761
tree.commit('commit 1')
762
tree.add_parent_tree_id('example')
763
self.build_tree(['foo/bar', 'foo/baz'])
764
tree.add(['bar', 'baz'])
765
err = self.assertRaises(errors.CannotCommitSelectedFileMerge,
766
tree.commit, 'commit 2', specific_files=['bar', 'baz'])
767
self.assertEqual(['bar', 'baz'], err.files)
768
self.assertEqual('Selected-file commit of merges is not supported'
769
' yet: files bar, baz', str(err))
771
def test_commit_ordering(self):
772
"""Test of corner-case commit ordering error"""
773
tree = self.make_branch_and_tree('.')
774
self.build_tree(['a/', 'a/z/', 'a/c/', 'a/z/x', 'a/z/y'])
775
tree.add(['a/', 'a/z/', 'a/c/', 'a/z/x', 'a/z/y'])
777
self.build_tree(['a/c/d/'])
779
tree.rename_one('a/z/x', 'a/c/d/x')
780
tree.commit('test', specific_files=['a/z/y'])
782
def test_commit_no_author(self):
783
"""The default kwarg author in MutableTree.commit should not add
784
the 'author' revision property.
786
tree = self.make_branch_and_tree('foo')
787
rev_id = tree.commit('commit 1')
788
rev = tree.branch.repository.get_revision(rev_id)
789
self.assertFalse('author' in rev.properties)
790
self.assertFalse('authors' in rev.properties)
792
def test_commit_author(self):
793
"""Passing a non-empty author kwarg to MutableTree.commit should add
794
the 'author' revision property.
796
tree = self.make_branch_and_tree('foo')
797
rev_id = self.callDeprecated(['The parameter author was '
798
'deprecated in version 1.13. Use authors instead'],
799
tree.commit, 'commit 1', author='John Doe <jdoe@example.com>')
800
rev = tree.branch.repository.get_revision(rev_id)
801
self.assertEqual('John Doe <jdoe@example.com>',
802
rev.properties['authors'])
803
self.assertFalse('author' in rev.properties)
805
def test_commit_empty_authors_list(self):
806
"""Passing an empty list to authors shouldn't add the property."""
807
tree = self.make_branch_and_tree('foo')
808
rev_id = tree.commit('commit 1', authors=[])
809
rev = tree.branch.repository.get_revision(rev_id)
810
self.assertFalse('author' in rev.properties)
811
self.assertFalse('authors' in rev.properties)
813
def test_multiple_authors(self):
814
tree = self.make_branch_and_tree('foo')
815
rev_id = tree.commit('commit 1',
816
authors=['John Doe <jdoe@example.com>',
817
'Jane Rey <jrey@example.com>'])
818
rev = tree.branch.repository.get_revision(rev_id)
819
self.assertEqual('John Doe <jdoe@example.com>\n'
820
'Jane Rey <jrey@example.com>', rev.properties['authors'])
821
self.assertFalse('author' in rev.properties)
823
def test_author_and_authors_incompatible(self):
824
tree = self.make_branch_and_tree('foo')
825
self.assertRaises(AssertionError, tree.commit, 'commit 1',
826
authors=['John Doe <jdoe@example.com>',
827
'Jane Rey <jrey@example.com>'],
828
author="Jack Me <jme@example.com>")
830
def test_author_with_newline_rejected(self):
831
tree = self.make_branch_and_tree('foo')
832
self.assertRaises(AssertionError, tree.commit, 'commit 1',
833
authors=['John\nDoe <jdoe@example.com>'])
835
def test_commit_with_checkout_and_branch_sharing_repo(self):
836
repo = self.make_repository('repo', shared=True)
837
# make_branch_and_tree ignores shared repos
838
branch = controldir.ControlDir.create_branch_convenience('repo/branch')
839
tree2 = branch.create_checkout('repo/tree2')
840
tree2.commit('message', rev_id='rev1')
841
self.assertTrue(tree2.branch.repository.has_revision('rev1'))