~bzr-pqm/bzr/bzr.dev

1773.4.1 by Martin Pool
Add pyflakes makefile target; fix many warnings
1
# Copyright (C) 2005, 2006 Canonical Ltd
1666.1.19 by Robert Collins
Introduce a progress bar during commit.
2
# Authors:  Robert Collins <robert.collins@canonical.com>
3
#
4
# This program is free software; you can redistribute it and/or modify
5
# it under the terms of the GNU General Public License as published by
6
# the Free Software Foundation; either version 2 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU General Public License for more details.
13
#
14
# You should have received a copy of the GNU General Public License
15
# along with this program; if not, write to the Free Software
16
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
17
18
from cStringIO import StringIO
19
import os
20
1773.4.1 by Martin Pool
Add pyflakes makefile target; fix many warnings
21
from bzrlib import branch, bzrdir, errors, ui, workingtree
1666.1.19 by Robert Collins
Introduce a progress bar during commit.
22
from bzrlib.errors import (NotBranchError, NotVersionedError, 
23
                           UnsupportedOperation)
24
from bzrlib.osutils import pathjoin, getcwd, has_symlinks
25
from bzrlib.tests import TestSkipped, TestCase
26
from bzrlib.tests.workingtree_implementations import TestCaseWithWorkingTree
27
from bzrlib.trace import mutter
28
from bzrlib.workingtree import (TreeEntry, TreeDirectory, TreeFile, TreeLink,
29
                                WorkingTree)
30
31
32
class CapturingUIFactory(ui.UIFactory):
33
    """A UI Factory for testing - capture the updates made through it."""
34
35
    def __init__(self):
36
        super(CapturingUIFactory, self).__init__()
37
        self._calls = []
38
        self.depth = 0
39
40
    def clear(self):
41
        """See progress.ProgressBar.clear()."""
42
43
    def clear_term(self):
44
        """See progress.ProgressBar.clear_term()."""
45
46
    def finished(self):
47
        """See progress.ProgressBar.finished()."""
48
        self.depth -= 1
49
50
    def note(self, fmt_string, *args, **kwargs):
51
        """See progress.ProgressBar.note()."""
52
53
    def progress_bar(self):
54
        return self
55
    
56
    def nested_progress_bar(self):
57
        self.depth += 1
58
        return self
59
60
    def update(self, message, count=None, total=None):
61
        """See progress.ProgressBar.update()."""
62
        if self.depth == 1:
63
            self._calls.append(("update", count, total))
64
65
66
class TestCapturingUI(TestCase):
67
68
    def test_nested_ignore_depth_beyond_one(self):
69
        # we only want to capture the first level out progress, not
70
        # want sub-components might do. So we have nested bars ignored.
71
        factory = CapturingUIFactory()
72
        pb1 = factory.nested_progress_bar()
73
        pb1.update('foo', 0, 1)
74
        pb2 = factory.nested_progress_bar()
75
        pb2.update('foo', 0, 1)
76
        pb2.finished()
77
        pb1.finished()
78
        self.assertEqual([("update", 0, 1)], factory._calls)
79
80
81
class TestCommit(TestCaseWithWorkingTree):
82
83
    def test_commit_sets_last_revision(self):
84
        tree = self.make_branch_and_tree('tree')
1773.1.1 by Robert Collins
Teach WorkingTree.commit to return the committed revision id.
85
        committed_id = tree.commit('foo', rev_id='foo', allow_pointless=True)
1908.7.6 by Robert Collins
Deprecate WorkingTree.last_revision.
86
        self.assertEqual(['foo'], tree.get_parent_ids())
1773.1.1 by Robert Collins
Teach WorkingTree.commit to return the committed revision id.
87
        # the commit should have returned the same id we asked for.
88
        self.assertEqual('foo', committed_id)
89
90
    def test_commit_returns_revision_id(self):
91
        tree = self.make_branch_and_tree('.')
92
        committed_id = tree.commit('message', allow_pointless=True)
93
        self.assertTrue(tree.branch.repository.has_revision(committed_id))
94
        self.assertNotEqual(None, committed_id)
1666.1.19 by Robert Collins
Introduce a progress bar during commit.
95
96
    def test_commit_local_unbound(self):
97
        # using the library api to do a local commit on unbound branches is 
98
        # also an error
99
        tree = self.make_branch_and_tree('tree')
100
        self.assertRaises(errors.LocalRequiresBoundBranch,
101
                          tree.commit,
102
                          'foo',
103
                          local=True)
2374.2.1 by John Arbash Meinel
(broken) merge a test case showing that commiting a merge of a kind change fails.
104
105
    def test_commit_merged_kind_change(self):
106
        """Test merging a kind change.
107
108
        Test making a kind change in a working tree, and then merging that
109
        from another. When committed it should commit the new kind.
110
        """
111
        wt = self.make_branch_and_tree('.')
112
        self.build_tree(['a'])
113
        wt.add(['a'])
114
        wt.commit('commit one')
115
        wt2 = wt.bzrdir.sprout('to').open_workingtree()
116
        os.remove('a')
117
        os.mkdir('a')
118
        wt.commit('changed kind')
119
        wt2.merge_from_branch(wt.branch)
120
        wt2.commit('merged kind change')
121
1666.1.19 by Robert Collins
Introduce a progress bar during commit.
122
    def test_local_commit_ignores_master(self):
123
        # a --local commit does not require access to the master branch
124
        # at all, or even for it to exist.
125
        # we test this by setting up a bound branch and then corrupting
126
        # the master.
127
        master = self.make_branch('master')
128
        tree = self.make_branch_and_tree('tree')
129
        try:
130
            tree.branch.bind(master)
131
        except errors.UpgradeRequired:
132
            # older format.
133
            return
1955.3.14 by John Arbash Meinel
Correctly fix the workingtree put() test fixes
134
        master.bzrdir.transport.put_bytes('branch-format', 'garbage')
1666.1.19 by Robert Collins
Introduce a progress bar during commit.
135
        del master
136
        # check its corrupted.
137
        self.assertRaises(errors.UnknownFormatError,
138
                          bzrdir.BzrDir.open,
139
                          'master')
140
        tree.commit('foo', rev_id='foo', local=True)
141
 
142
    def test_local_commit_does_not_push_to_master(self):
143
        # a --local commit does not require access to the master branch
144
        # at all, or even for it to exist.
145
        # we test that even when its available it does not push to it.
146
        master = self.make_branch('master')
147
        tree = self.make_branch_and_tree('tree')
148
        try:
149
            tree.branch.bind(master)
150
        except errors.UpgradeRequired:
151
            # older format.
152
            return
153
        tree.commit('foo', rev_id='foo', local=True)
154
        self.failIf(master.repository.has_revision('foo'))
155
        self.assertEqual(None, master.last_revision())
1927.2.1 by Robert Collins
Alter set_pending_merges to shove the left most merge into the trees last-revision if that is not set. Related bugfixes include basis_tree handling ghosts, de-duping the merges with the last-revision and update changing where and how it adds its pending merge.
156
157
    def test_record_initial_ghost(self):
158
        """The working tree needs to record ghosts during commit."""
159
        wt = self.make_branch_and_tree('.')
1908.6.7 by Robert Collins
Remove all users of set_pending_merges and add_pending_merge except tests that they work correctly.
160
        wt.set_parent_ids(['non:existent@rev--ision--0--2'],
161
            allow_leftmost_as_ghost=True)
1927.2.1 by Robert Collins
Alter set_pending_merges to shove the left most merge into the trees last-revision if that is not set. Related bugfixes include basis_tree handling ghosts, de-duping the merges with the last-revision and update changing where and how it adds its pending merge.
162
        rev_id = wt.commit('commit against a ghost first parent.')
163
        rev = wt.branch.repository.get_revision(rev_id)
164
        self.assertEqual(rev.parent_ids, ['non:existent@rev--ision--0--2'])
165
        # parent_sha1s is not populated now, WTF. rbc 20051003
166
        self.assertEqual(len(rev.parent_sha1s), 0)
167
168
    def test_record_two_ghosts(self):
169
        """The working tree should preserve all the parents during commit."""
170
        wt = self.make_branch_and_tree('.')
1908.6.7 by Robert Collins
Remove all users of set_pending_merges and add_pending_merge except tests that they work correctly.
171
        wt.set_parent_ids([
172
                'foo@azkhazan-123123-abcabc',
173
                'wibble@fofof--20050401--1928390812',
174
            ],
175
            allow_leftmost_as_ghost=True)
1927.2.1 by Robert Collins
Alter set_pending_merges to shove the left most merge into the trees last-revision if that is not set. Related bugfixes include basis_tree handling ghosts, de-duping the merges with the last-revision and update changing where and how it adds its pending merge.
176
        rev_id = wt.commit("commit from ghost base with one merge")
177
        # the revision should have been committed with two parents
178
        rev = wt.branch.repository.get_revision(rev_id)
179
        self.assertEqual(['foo@azkhazan-123123-abcabc',
180
            'wibble@fofof--20050401--1928390812'],
181
            rev.parent_ids)
1666.1.19 by Robert Collins
Introduce a progress bar during commit.
182
1988.3.1 by Robert Collins
Add test case to ensure that the working tree inventory and disk state is correctly update when commit is removing directory entries.
183
    def test_commit_deleted_subtree_and_files_updates_workingtree(self):
184
        """The working trees inventory may be adjusted by commit."""
185
        wt = self.make_branch_and_tree('.')
186
        wt.lock_write()
187
        self.build_tree(['a', 'b/', 'b/c', 'd'])
188
        wt.add(['a', 'b', 'b/c', 'd'], ['a-id', 'b-id', 'c-id', 'd-id'])
189
        this_dir = self.get_transport()
190
        this_dir.delete_tree('b')
191
        this_dir.delete('d')
192
        # now we have a tree with a through d in the inventory, but only
193
        # a present on disk. After commit b-id, c-id and d-id should be
194
        # missing from the inventory, within the same tree transaction.
195
        wt.commit('commit stuff')
196
        self.assertTrue(wt.has_id('a-id'))
197
        self.assertFalse(wt.has_or_had_id('b-id'))
198
        self.assertFalse(wt.has_or_had_id('c-id'))
199
        self.assertFalse(wt.has_or_had_id('d-id'))
200
        self.assertTrue(wt.has_filename('a'))
201
        self.assertFalse(wt.has_filename('b'))
202
        self.assertFalse(wt.has_filename('b/c'))
203
        self.assertFalse(wt.has_filename('d'))
204
        wt.unlock()
205
        # the changes should have persisted to disk - reopen the workingtree
206
        # to be sure.
207
        wt = wt.bzrdir.open_workingtree()
208
        wt.lock_read()
209
        self.assertTrue(wt.has_id('a-id'))
210
        self.assertFalse(wt.has_or_had_id('b-id'))
211
        self.assertFalse(wt.has_or_had_id('c-id'))
212
        self.assertFalse(wt.has_or_had_id('d-id'))
213
        self.assertTrue(wt.has_filename('a'))
214
        self.assertFalse(wt.has_filename('b'))
215
        self.assertFalse(wt.has_filename('b/c'))
216
        self.assertFalse(wt.has_filename('d'))
217
        wt.unlock()
1731.2.4 by Aaron Bentley
Ensure subsume works with Knit2 repos
218
2363.2.2 by John Arbash Meinel
Simplify the test even further....
219
    def test_commit_deleted_subtree_with_removed(self):
2363.2.1 by John Arbash Meinel
(broken) Add a simplified test which exposes the bug.
220
        wt = self.make_branch_and_tree('.')
221
        self.build_tree(['a', 'b/', 'b/c', 'd'])
222
        wt.add(['a', 'b', 'b/c'], ['a-id', 'b-id', 'c-id'])
223
        wt.commit('first')
2363.2.2 by John Arbash Meinel
Simplify the test even further....
224
        wt.remove('b/c')
2363.2.1 by John Arbash Meinel
(broken) Add a simplified test which exposes the bug.
225
        this_dir = self.get_transport()
226
        this_dir.delete_tree('b')
227
        wt.lock_write()
228
        wt.commit('commit deleted rename')
229
        self.assertTrue(wt.has_id('a-id'))
230
        self.assertFalse(wt.has_or_had_id('b-id'))
231
        self.assertFalse(wt.has_or_had_id('c-id'))
232
        self.assertTrue(wt.has_filename('a'))
233
        self.assertFalse(wt.has_filename('b'))
234
        self.assertFalse(wt.has_filename('b/c'))
235
        wt.unlock()
236
1731.2.4 by Aaron Bentley
Ensure subsume works with Knit2 repos
237
    def test_commit_move_new(self):
238
        wt = self.make_branch_and_tree('first')
239
        wt.commit('first')
240
        wt2 = wt.bzrdir.sprout('second').open_workingtree()
241
        self.build_tree(['second/name1'])
242
        wt2.add('name1', 'name1-id')
243
        wt2.commit('second')
244
        wt.merge_from_branch(wt2.branch)
245
        wt.rename_one('name1', 'name2')
246
        wt.commit('third')
247
        wt.path2id('name1-id')
2255.2.218 by Robert Collins
Make the nested tree commit smoke test be more rigourous.
248
249
    def test_nested_commit(self):
250
        """Commit in multiply-nested trees"""
251
        tree = self.make_branch_and_tree('.')
252
        if not tree.supports_tree_reference():
253
            # inapplicable test.
254
            return
255
        subtree = self.make_branch_and_tree('subtree')
256
        subsubtree = self.make_branch_and_tree('subtree/subtree')
257
        subtree.add(['subtree'])
258
        tree.add(['subtree'])
259
        # use allow_pointless=False to ensure that the deepest tree, which
260
        # has no commits made to it, does not get a pointless commit.
261
        rev_id = tree.commit('added reference', allow_pointless=False)
262
        tree.lock_read()
263
        self.addCleanup(tree.unlock)
264
        # the deepest subtree has not changed, so no commit should take place.
265
        self.assertEqual(None, subsubtree.last_revision())
266
        # the intermediate tree should have committed a pointer to the current
267
        # subtree revision.
268
        sub_basis = subtree.basis_tree()
269
        sub_basis.lock_read()
270
        self.addCleanup(sub_basis.unlock)
271
        self.assertEqual(subsubtree.last_revision(),
2255.2.227 by Robert Collins
Make all test_commit tests pass.
272
            sub_basis.get_reference_revision(sub_basis.path2id('subtree')))
2255.2.218 by Robert Collins
Make the nested tree commit smoke test be more rigourous.
273
        # the intermediate tree has changed, so should have had a commit
274
        # take place.
275
        self.assertNotEqual(None, subtree.last_revision())
276
        # the outer tree should have committed a pointer to the current
277
        # subtree revision.
278
        basis = tree.basis_tree()
279
        basis.lock_read()
280
        self.addCleanup(basis.unlock)
281
        self.assertEqual(subtree.last_revision(),
2255.2.226 by Robert Collins
Get merge_nested finally working: change nested tree iterators to take file_ids, and ensure the right branch is connected to in the merge logic. May not be suitable for shared repositories yet.
282
            basis.get_reference_revision(basis.path2id('subtree')))
2255.2.218 by Robert Collins
Make the nested tree commit smoke test be more rigourous.
283
        # the outer tree must have have changed too.
284
        self.assertNotEqual(None, rev_id)
1988.3.1 by Robert Collins
Add test case to ensure that the working tree inventory and disk state is correctly update when commit is removing directory entries.
285
        
2255.2.220 by Robert Collins
Fix failing detection of changes restricted to subtrees causing spurious pointless commit errors.
286
    def test_nested_commit_second_commit_detects_changes(self):
287
        """Commit with a nested tree picks up the correct child revid."""
288
        tree = self.make_branch_and_tree('.')
289
        if not tree.supports_tree_reference():
290
            # inapplicable test.
291
            return
292
        subtree = self.make_branch_and_tree('subtree')
293
        tree.add(['subtree'])
294
        self.build_tree(['subtree/file'])
295
        subtree.add(['file'], ['file-id'])
296
        rev_id = tree.commit('added reference', allow_pointless=False)
297
        child_revid = subtree.last_revision()
298
        # now change the child tree
299
        self.build_tree_contents([('subtree/file', 'new-content')])
300
        # and commit in the parent should commit the child and grab its revid,
301
        # we test with allow_pointless=False here so that we are simulating
302
        # what users will see.
303
        rev_id2 = tree.commit('changed subtree only', allow_pointless=False)
304
        # the child tree has changed, so should have had a commit
305
        # take place.
306
        self.assertNotEqual(None, subtree.last_revision())
307
        self.assertNotEqual(child_revid, subtree.last_revision())
308
        # the outer tree should have committed a pointer to the current
309
        # subtree revision.
310
        basis = tree.basis_tree()
311
        basis.lock_read()
312
        self.addCleanup(basis.unlock)
313
        self.assertEqual(subtree.last_revision(),
2255.2.226 by Robert Collins
Get merge_nested finally working: change nested tree iterators to take file_ids, and ensure the right branch is connected to in the merge logic. May not be suitable for shared repositories yet.
314
            basis.get_reference_revision(basis.path2id('subtree')))
2255.2.220 by Robert Collins
Fix failing detection of changes restricted to subtrees causing spurious pointless commit errors.
315
        self.assertNotEqual(rev_id, rev_id2)
316
1988.3.1 by Robert Collins
Add test case to ensure that the working tree inventory and disk state is correctly update when commit is removing directory entries.
317
1740.3.10 by Jelmer Vernooij
Fix some minor issues pointed out by j-a-m.
318
class TestCommitProgress(TestCaseWithWorkingTree):
1666.1.19 by Robert Collins
Introduce a progress bar during commit.
319
    
320
    def restoreDefaults(self):
321
        ui.ui_factory = self.old_ui_factory
322
323
    def test_commit_progress_steps(self):
324
        # during commit we one progress update for every entry in the 
325
        # inventory, and then one for the inventory, and one for the
326
        # inventory, and one for the revision insertions.
327
        # first we need a test commit to do. Lets setup a branch with 
328
        # 3 files, and alter one in a selected-file commit. This exercises
329
        # a number of cases quickly. We should also test things like 
330
        # selective commits which excludes newly added files.
331
        tree = self.make_branch_and_tree('.')
332
        self.build_tree(['a', 'b', 'c'])
333
        tree.add(['a', 'b', 'c'])
334
        tree.commit('first post')
335
        f = file('b', 'wt')
336
        f.write('new content')
337
        f.close()
338
        # set a progress bar that captures the calls so we can see what is 
339
        # emitted
340
        self.old_ui_factory = ui.ui_factory
341
        self.addCleanup(self.restoreDefaults)
342
        factory = CapturingUIFactory()
343
        ui.ui_factory = factory
344
        # TODO RBC 20060421 it would be nice to merge the reporter output
345
        # into the factory for this test - just make the test ui factory
346
        # pun as a reporter. Then we can check the ordering is right.
347
        tree.commit('second post', specific_files=['b'])
1740.3.10 by Jelmer Vernooij
Fix some minor issues pointed out by j-a-m.
348
        # 9 steps: 1 for rev, 2 for inventory, 1 for finishing. 2 for root
349
        # and 6 for inventory files.
350
        # 2 steps don't trigger an update, as 'a' and 'c' are not 
351
        # committed.
1666.1.19 by Robert Collins
Introduce a progress bar during commit.
352
        self.assertEqual(
1740.3.10 by Jelmer Vernooij
Fix some minor issues pointed out by j-a-m.
353
            [("update", 0, 9),
354
             ("update", 1, 9),
355
             ("update", 2, 9),
356
             ("update", 3, 9),
357
             ("update", 4, 9),
358
             ("update", 5, 9),
359
             ("update", 6, 9),
360
             ("update", 7, 9)],
1666.1.19 by Robert Collins
Introduce a progress bar during commit.
361
            factory._calls
362
           )