1
# Copyright (C) 2004, 2005, 2007, 2009 Canonical Ltd
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.
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.
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
17
"""Tests for branch.push behaviour."""
19
from cStringIO import StringIO
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,
41
from bzrlib.transport import get_transport
42
from bzrlib.transport.local import LocalURLServer
45
# These tests are based on similar tests in
46
# bzrlib.tests.branch_implementations.test_push.
49
class TestPush(TestCaseWithInterBranch):
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)
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())
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')
90
checkout.branch.bind(master_tree.branch)
91
except errors.UpgradeRequired:
92
# cant bind this format, the test is irrelevant.
94
rev1 = checkout.commit('master')
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())
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')
108
checkout.branch.bind(master_tree.branch)
109
except errors.UpgradeRequired:
110
# cant bind this format, the test is irrelevant.
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
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)
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')
126
self.build_tree(['source/a'])
130
source.branch.lock_read()
134
source.branch.push(target, stop_revision=source.last_revision())
138
source.branch.unlock()
140
def test_push_within_repository(self):
141
"""Push from one branch to another inside the same repository."""
143
repo = self.make_repository('repo', shared=True)
144
except (errors.IncompatibleFormat, errors.UninitializableFormat):
145
# This Branch format cannot create shared repositories
147
# This is a little bit trickier because make_branch_and_tree will not
148
# re-use a shared repository.
150
a_branch = self.make_from_branch('repo/tree')
151
except (errors.UninitializableFormat):
152
# Cannot create these branches
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()
163
tree = a_branch.create_checkout('repo/tree', lightweight=True)
164
self.build_tree(['repo/tree/a'])
168
to_branch = self.make_to_branch('repo/branch')
169
tree.branch.push(to_branch)
171
self.assertEqual(tree.branch.last_revision(),
172
to_branch.last_revision())
174
def test_push_overwrite_of_non_tip_with_stop_revision(self):
175
"""Combining the stop_revision and overwrite options works.
177
This was <https://bugs.launchpad.net/bzr/+bug/234229>.
179
source = self.make_from_branch_and_tree('source')
180
target = self.make_to_branch('target')
182
source.commit('1st commit')
183
source.branch.push(target)
184
source.commit('2nd commit', rev_id='rev-2')
185
source.commit('3rd commit')
187
source.branch.push(target, stop_revision='rev-2', overwrite=True)
188
self.assertEqual('rev-2', target.last_revision())
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.
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).
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
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".
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()
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.")
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())
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.
275
['Repository.insert_stream', 'Repository.insert_stream', 'get',
276
'Branch.set_last_revision_info', 'Branch.unlock'],
277
calls_after_insert_stream)
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
288
SmartServerRepositoryGetParentMap.no_extra_results = old_flag
289
inter_class._walk_to_common_revisions_batch_size = old_batch_size
290
self.addCleanup(reset_values)
293
class TestPushHook(TestCaseWithInterBranch):
297
TestCaseWithInterBranch.setUp(self)
299
def capture_post_push_hook(self, result):
300
"""Capture post push hook calls to self.hook_calls.
302
The call is logged, as is some state of the two branches.
304
if result.local_branch:
305
local_locked = result.local_branch.is_locked()
306
local_base = result.local_branch.base
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()))
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)
324
# with nothing there we should still get a notification, and
325
# have both branches locked at the notification time.
327
('post_push', source, None, target.base, 0, NULL_REVISION,
328
0, NULL_REVISION, True, None, True)
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
337
target = self.make_to_branch('target')
338
local = self.make_from_branch('local')
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')
349
source = self.make_from_branch('source')
350
Branch.hooks.install_named_hook('post_push',
351
self.capture_post_push_hook, None)
353
# with nothing there we should still get a notification, and
354
# have both branches locked at the notification time.
356
('post_push', source, local.base, target.base, 0, NULL_REVISION,
357
0, NULL_REVISION, True, True, True)
361
def test_post_push_nonempty_history(self):
362
target = self.make_to_branch_and_tree('target')
365
rev1 = target.commit('rev 1')
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.
376
('post_push', source.branch, None, target.branch.base, 1, rev1,
377
2, rev2, True, None, True)