~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/tests/per_interbranch/test_push.py

  • Committer: Johan Walles
  • Date: 2009-05-06 05:36:28 UTC
  • mfrom: (4332 +trunk)
  • mto: This revision was merged to the branch mainline in revision 4343.
  • Revision ID: johan.walles@gmail.com-20090506053628-tbf1wz4a0m9t684g
MergeĀ fromĀ upstream.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2004, 2005, 2007, 2009 Canonical Ltd
 
2
#
 
3
# This program is free software; you can redistribute it and/or modify
 
4
# it under the terms of the GNU General Public License as published by
 
5
# the Free Software Foundation; either version 2 of the License, or
 
6
# (at your option) any later version.
 
7
#
 
8
# This program is distributed in the hope that it will be useful,
 
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
11
# GNU General Public License for more details.
 
12
#
 
13
# You should have received a copy of the GNU General Public License
 
14
# along with this program; if not, write to the Free Software
 
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 
16
 
 
17
"""Tests for branch.push behaviour."""
 
18
 
 
19
from cStringIO import StringIO
 
20
import os
 
21
 
 
22
from bzrlib import (
 
23
    branch,
 
24
    builtins,
 
25
    bzrdir,
 
26
    debug,
 
27
    errors,
 
28
    push,
 
29
    repository,
 
30
    tests,
 
31
    )
 
32
from bzrlib.branch import Branch
 
33
from bzrlib.bzrdir import BzrDir
 
34
from bzrlib.memorytree import MemoryTree
 
35
from bzrlib.revision import NULL_REVISION
 
36
from bzrlib.smart import client, server
 
37
from bzrlib.smart.repository import SmartServerRepositoryGetParentMap
 
38
from bzrlib.tests.per_interbranch import (
 
39
    TestCaseWithInterBranch,
 
40
    )
 
41
from bzrlib.transport import get_transport
 
42
from bzrlib.transport.local import LocalURLServer
 
43
 
 
44
 
 
45
# These tests are based on similar tests in 
 
46
# bzrlib.tests.branch_implementations.test_push.
 
47
 
 
48
 
 
49
class TestPush(TestCaseWithInterBranch):
 
50
 
 
51
    def test_push_convergence_simple(self):
 
52
        # when revisions are pushed, the left-most accessible parents must
 
53
        # become the revision-history.
 
54
        mine = self.make_from_branch_and_tree('mine')
 
55
        mine.commit('1st post', rev_id='P1', allow_pointless=True)
 
56
        other = self.sprout_to(mine.bzrdir, 'other').open_workingtree()
 
57
        other.commit('my change', rev_id='M1', allow_pointless=True)
 
58
        mine.merge_from_branch(other.branch)
 
59
        mine.commit('merge my change', rev_id='P2')
 
60
        result = mine.branch.push(other.branch)
 
61
        self.assertEqual(['P1', 'P2'], other.branch.revision_history())
 
62
        # result object contains some structured data
 
63
        self.assertEqual(result.old_revid, 'M1')
 
64
        self.assertEqual(result.new_revid, 'P2')
 
65
        # and it can be treated as an integer for compatibility
 
66
        self.assertEqual(int(result), 0)
 
67
 
 
68
    def test_push_merged_indirect(self):
 
69
        # it should be possible to do a push from one branch into another
 
70
        # when the tip of the target was merged into the source branch
 
71
        # via a third branch - so its buried in the ancestry and is not
 
72
        # directly accessible.
 
73
        mine = self.make_from_branch_and_tree('mine')
 
74
        mine.commit('1st post', rev_id='P1', allow_pointless=True)
 
75
        target = self.sprout_to(mine.bzrdir, 'target').open_workingtree()
 
76
        target.commit('my change', rev_id='M1', allow_pointless=True)
 
77
        other = self.sprout_to(mine.bzrdir, 'other').open_workingtree()
 
78
        other.merge_from_branch(target.branch)
 
79
        other.commit('merge my change', rev_id='O2')
 
80
        mine.merge_from_branch(other.branch)
 
81
        mine.commit('merge other', rev_id='P2')
 
82
        mine.branch.push(target.branch)
 
83
        self.assertEqual(['P1', 'P2'], target.branch.revision_history())
 
84
 
 
85
    def test_push_to_checkout_updates_master(self):
 
86
        """Pushing into a checkout updates the checkout and the master branch"""
 
87
        master_tree = self.make_to_branch_and_tree('master')
 
88
        checkout = self.make_to_branch_and_tree('checkout')
 
89
        try:
 
90
            checkout.branch.bind(master_tree.branch)
 
91
        except errors.UpgradeRequired:
 
92
            # cant bind this format, the test is irrelevant.
 
93
            return
 
94
        rev1 = checkout.commit('master')
 
95
 
 
96
        other_bzrdir = self.sprout_from(master_tree.branch.bzrdir, 'other')
 
97
        other = other_bzrdir.open_workingtree()
 
98
        rev2 = other.commit('other commit')
 
99
        # now push, which should update both checkout and master.
 
100
        other.branch.push(checkout.branch)
 
101
        self.assertEqual([rev1, rev2], checkout.branch.revision_history())
 
102
        self.assertEqual([rev1, rev2], master_tree.branch.revision_history())
 
103
 
 
104
    def test_push_raises_specific_error_on_master_connection_error(self):
 
105
        master_tree = self.make_to_branch_and_tree('master')
 
106
        checkout = self.make_to_branch_and_tree('checkout')
 
107
        try:
 
108
            checkout.branch.bind(master_tree.branch)
 
109
        except errors.UpgradeRequired:
 
110
            # cant bind this format, the test is irrelevant.
 
111
            return
 
112
        other_bzrdir = self.sprout_from(master_tree.branch.bzrdir, 'other')
 
113
        other = other_bzrdir.open_workingtree()
 
114
        # move the branch out of the way on disk to cause a connection
 
115
        # error.
 
116
        os.rename('master', 'master_gone')
 
117
        # try to push, which should raise a BoundBranchConnectionFailure.
 
118
        self.assertRaises(errors.BoundBranchConnectionFailure,
 
119
                other.branch.push, checkout.branch)
 
120
 
 
121
    def test_push_uses_read_lock(self):
 
122
        """Push should only need a read lock on the source side."""
 
123
        source = self.make_from_branch_and_tree('source')
 
124
        target = self.make_to_branch('target')
 
125
 
 
126
        self.build_tree(['source/a'])
 
127
        source.add(['a'])
 
128
        source.commit('a')
 
129
 
 
130
        source.branch.lock_read()
 
131
        try:
 
132
            target.lock_write()
 
133
            try:
 
134
                source.branch.push(target, stop_revision=source.last_revision())
 
135
            finally:
 
136
                target.unlock()
 
137
        finally:
 
138
            source.branch.unlock()
 
139
 
 
140
    def test_push_within_repository(self):
 
141
        """Push from one branch to another inside the same repository."""
 
142
        try:
 
143
            repo = self.make_repository('repo', shared=True)
 
144
        except (errors.IncompatibleFormat, errors.UninitializableFormat):
 
145
            # This Branch format cannot create shared repositories
 
146
            return
 
147
        # This is a little bit trickier because make_branch_and_tree will not
 
148
        # re-use a shared repository.
 
149
        try:
 
150
            a_branch = self.make_from_branch('repo/tree')
 
151
        except (errors.UninitializableFormat):
 
152
            # Cannot create these branches
 
153
            return
 
154
        try:
 
155
            tree = a_branch.bzrdir.create_workingtree()
 
156
        except errors.NotLocalUrl:
 
157
            if self.vfs_transport_factory is LocalURLServer:
 
158
                # the branch is colocated on disk, we cannot create a checkout.
 
159
                # hopefully callers will expect this.
 
160
                local_controldir = bzrdir.BzrDir.open(self.get_vfs_only_url('repo/tree'))
 
161
                tree = local_controldir.create_workingtree()
 
162
            else:
 
163
                tree = a_branch.create_checkout('repo/tree', lightweight=True)
 
164
        self.build_tree(['repo/tree/a'])
 
165
        tree.add(['a'])
 
166
        tree.commit('a')
 
167
 
 
168
        to_branch = self.make_to_branch('repo/branch')
 
169
        tree.branch.push(to_branch)
 
170
 
 
171
        self.assertEqual(tree.branch.last_revision(),
 
172
                         to_branch.last_revision())
 
173
 
 
174
    def test_push_overwrite_of_non_tip_with_stop_revision(self):
 
175
        """Combining the stop_revision and overwrite options works.
 
176
 
 
177
        This was <https://bugs.launchpad.net/bzr/+bug/234229>.
 
178
        """
 
179
        source = self.make_from_branch_and_tree('source')
 
180
        target = self.make_to_branch('target')
 
181
 
 
182
        source.commit('1st commit')
 
183
        source.branch.push(target)
 
184
        source.commit('2nd commit', rev_id='rev-2')
 
185
        source.commit('3rd commit')
 
186
 
 
187
        source.branch.push(target, stop_revision='rev-2', overwrite=True)
 
188
        self.assertEqual('rev-2', target.last_revision())
 
189
 
 
190
    def test_push_with_default_stacking_does_not_create_broken_branch(self):
 
191
        """Pushing a new standalone branch works even when there's a default
 
192
        stacking policy at the destination.
 
193
 
 
194
        The new branch will preserve the repo format (even if it isn't the
 
195
        default for the branch), and will be stacked when the repo format
 
196
        allows (which means that the branch format isn't necessarly preserved).
 
197
        """
 
198
        if isinstance(self.branch_format_from, branch.BzrBranchFormat4):
 
199
            raise tests.TestNotApplicable('Not a metadir format.')
 
200
        if isinstance(self.branch_format_from, branch.BranchReferenceFormat):
 
201
            # This test could in principle apply to BranchReferenceFormat, but
 
202
            # make_branch_builder doesn't support it.
 
203
            raise tests.TestSkipped(
 
204
                "BranchBuilder can't make reference branches.")
 
205
        # Make a branch called "local" in a stackable repository
 
206
        # The branch has 3 revisions:
 
207
        #   - rev-1, adds a file
 
208
        #   - rev-2, no changes
 
209
        #   - rev-3, modifies the file.
 
210
        repo = self.make_repository('repo', shared=True, format='1.6')
 
211
        builder = self.make_from_branch_builder('repo/local')
 
212
        builder.start_series()
 
213
        builder.build_snapshot('rev-1', None, [
 
214
            ('add', ('', 'root-id', 'directory', '')),
 
215
            ('add', ('filename', 'f-id', 'file', 'content\n'))])
 
216
        builder.build_snapshot('rev-2', ['rev-1'], [])
 
217
        builder.build_snapshot('rev-3', ['rev-2'],
 
218
            [('modify', ('f-id', 'new-content\n'))])
 
219
        builder.finish_series()
 
220
        trunk = builder.get_branch()
 
221
        # Sprout rev-1 to "trunk", so that we can stack on it.
 
222
        trunk.bzrdir.sprout(self.get_url('trunk'), revision_id='rev-1')
 
223
        # Set a default stacking policy so that new branches will automatically
 
224
        # stack on trunk.
 
225
        self.make_bzrdir('.').get_config().set_default_stack_on('trunk')
 
226
        # Push rev-2 to a new branch "remote".  It will be stacked on "trunk".
 
227
        output = StringIO()
 
228
        push._show_push_branch(trunk, 'rev-2', self.get_url('remote'), output)
 
229
        # Push rev-3 onto "remote".  If "remote" not stacked and is missing the
 
230
        # fulltext record for f-id @ rev-1, then this will fail.
 
231
        remote_branch = Branch.open(self.get_url('remote'))
 
232
        trunk.push(remote_branch)
 
233
        remote_branch.check()
 
234
 
 
235
    def test_no_get_parent_map_after_insert_stream(self):
 
236
        # Effort test for bug 331823
 
237
        self.setup_smart_server_with_call_log()
 
238
        # Make a local branch with four revisions.  Four revisions because:
 
239
        # one to push, one there for _walk_to_common_revisions to find, one we
 
240
        # don't want to access, one for luck :)
 
241
        if isinstance(self.branch_format_from, branch.BranchReferenceFormat):
 
242
            # This test could in principle apply to BranchReferenceFormat, but
 
243
            # make_branch_builder doesn't support it.
 
244
            raise tests.TestSkipped(
 
245
                "BranchBuilder can't make reference branches.")
 
246
        try:
 
247
            builder = self.make_from_branch_builder('local')
 
248
        except (errors.TransportNotPossible, errors.UninitializableFormat):
 
249
            raise tests.TestNotApplicable('format not directly constructable')
 
250
        builder.start_series()
 
251
        builder.build_snapshot('first', None, [
 
252
            ('add', ('', 'root-id', 'directory', ''))])
 
253
        builder.build_snapshot('second', ['first'], [])
 
254
        builder.build_snapshot('third', ['second'], [])
 
255
        builder.build_snapshot('fourth', ['third'], [])
 
256
        builder.finish_series()
 
257
        local = builder.get_branch()
 
258
        local = branch.Branch.open(self.get_vfs_only_url('local'))
 
259
        # Initial push of three revisions
 
260
        remote_bzrdir = local.bzrdir.sprout(
 
261
            self.get_url('remote'), revision_id='third')
 
262
        remote = remote_bzrdir.open_branch()
 
263
        # Push fourth revision
 
264
        self.reset_smart_call_log()
 
265
        self.disableOptimisticGetParentMap()
 
266
        self.assertFalse(local.is_locked())
 
267
        local.push(remote)
 
268
        hpss_call_names = [item.call.method for item in self.hpss_calls]
 
269
        self.assertTrue('Repository.insert_stream' in hpss_call_names)
 
270
        insert_stream_idx = hpss_call_names.index('Repository.insert_stream')
 
271
        calls_after_insert_stream = hpss_call_names[insert_stream_idx:]
 
272
        # After inserting the stream the client has no reason to query the
 
273
        # remote graph any further.
 
274
        self.assertEqual(
 
275
            ['Repository.insert_stream', 'Repository.insert_stream', 'get',
 
276
             'Branch.set_last_revision_info', 'Branch.unlock'],
 
277
            calls_after_insert_stream)
 
278
 
 
279
    def disableOptimisticGetParentMap(self):
 
280
        # Tweak some class variables to stop remote get_parent_map calls asking
 
281
        # for or receiving more data than the caller asked for.
 
282
        old_flag = SmartServerRepositoryGetParentMap.no_extra_results
 
283
        inter_class = repository.InterRepository
 
284
        old_batch_size = inter_class._walk_to_common_revisions_batch_size
 
285
        inter_class._walk_to_common_revisions_batch_size = 1
 
286
        SmartServerRepositoryGetParentMap.no_extra_results = True
 
287
        def reset_values():
 
288
            SmartServerRepositoryGetParentMap.no_extra_results = old_flag
 
289
            inter_class._walk_to_common_revisions_batch_size = old_batch_size
 
290
        self.addCleanup(reset_values)
 
291
 
 
292
 
 
293
class TestPushHook(TestCaseWithInterBranch):
 
294
 
 
295
    def setUp(self):
 
296
        self.hook_calls = []
 
297
        TestCaseWithInterBranch.setUp(self)
 
298
 
 
299
    def capture_post_push_hook(self, result):
 
300
        """Capture post push hook calls to self.hook_calls.
 
301
 
 
302
        The call is logged, as is some state of the two branches.
 
303
        """
 
304
        if result.local_branch:
 
305
            local_locked = result.local_branch.is_locked()
 
306
            local_base = result.local_branch.base
 
307
        else:
 
308
            local_locked = None
 
309
            local_base = None
 
310
        self.hook_calls.append(
 
311
            ('post_push', result.source_branch, local_base,
 
312
             result.master_branch.base,
 
313
             result.old_revno, result.old_revid,
 
314
             result.new_revno, result.new_revid,
 
315
             result.source_branch.is_locked(), local_locked,
 
316
             result.master_branch.is_locked()))
 
317
 
 
318
    def test_post_push_empty_history(self):
 
319
        target = self.make_to_branch('target')
 
320
        source = self.make_from_branch('source')
 
321
        Branch.hooks.install_named_hook('post_push',
 
322
                                        self.capture_post_push_hook, None)
 
323
        source.push(target)
 
324
        # with nothing there we should still get a notification, and
 
325
        # have both branches locked at the notification time.
 
326
        self.assertEqual([
 
327
            ('post_push', source, None, target.base, 0, NULL_REVISION,
 
328
             0, NULL_REVISION, True, None, True)
 
329
            ],
 
330
            self.hook_calls)
 
331
 
 
332
    def test_post_push_bound_branch(self):
 
333
        # pushing to a bound branch should pass in the master branch to the
 
334
        # hook, allowing the correct number of emails to be sent, while still
 
335
        # allowing hooks that want to modify the target to do so to both
 
336
        # instances.
 
337
        target = self.make_to_branch('target')
 
338
        local = self.make_from_branch('local')
 
339
        try:
 
340
            local.bind(target)
 
341
        except errors.UpgradeRequired:
 
342
            # We can't bind this format to itself- typically it is the local
 
343
            # branch that doesn't support binding.  As of May 2007
 
344
            # remotebranches can't be bound.  Let's instead make a new local
 
345
            # branch of the default type, which does allow binding.
 
346
            # See https://bugs.launchpad.net/bzr/+bug/112020
 
347
            local = BzrDir.create_branch_convenience('local2')
 
348
            local.bind(target)
 
349
        source = self.make_from_branch('source')
 
350
        Branch.hooks.install_named_hook('post_push',
 
351
                                        self.capture_post_push_hook, None)
 
352
        source.push(local)
 
353
        # with nothing there we should still get a notification, and
 
354
        # have both branches locked at the notification time.
 
355
        self.assertEqual([
 
356
            ('post_push', source, local.base, target.base, 0, NULL_REVISION,
 
357
             0, NULL_REVISION, True, True, True)
 
358
            ],
 
359
            self.hook_calls)
 
360
 
 
361
    def test_post_push_nonempty_history(self):
 
362
        target = self.make_to_branch_and_tree('target')
 
363
        target.lock_write()
 
364
        target.add('')
 
365
        rev1 = target.commit('rev 1')
 
366
        target.unlock()
 
367
        sourcedir = target.bzrdir.clone(self.get_url('source'))
 
368
        source = MemoryTree.create_on_branch(sourcedir.open_branch())
 
369
        rev2 = source.commit('rev 2')
 
370
        Branch.hooks.install_named_hook('post_push',
 
371
                                        self.capture_post_push_hook, None)
 
372
        source.branch.push(target.branch)
 
373
        # with nothing there we should still get a notification, and
 
374
        # have both branches locked at the notification time.
 
375
        self.assertEqual([
 
376
            ('post_push', source.branch, None, target.branch.base, 1, rev1,
 
377
             2, rev2, True, None, True)
 
378
            ],
 
379
            self.hook_calls)