1
# Copyright (C) 2009, 2010 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
22
from testtools.matchers import (
38
from bzrlib.branch import Branch
39
from bzrlib.bzrdir import BzrDir
40
from bzrlib.memorytree import MemoryTree
41
from bzrlib.revision import NULL_REVISION
42
from bzrlib.smart import client, server
43
from bzrlib.smart.repository import SmartServerRepositoryGetParentMap
44
from bzrlib.tests.per_interbranch import (
45
TestCaseWithInterBranch,
47
from bzrlib.tests import test_server
50
# These tests are based on similar tests in
51
# bzrlib.tests.per_branch.test_push.
54
class TestPush(TestCaseWithInterBranch):
56
def test_push_convergence_simple(self):
57
# when revisions are pushed, the left-most accessible parents must
58
# become the revision-history.
59
mine = self.make_from_branch_and_tree('mine')
60
mine.commit('1st post', rev_id='P1', allow_pointless=True)
61
other = self.sprout_to(mine.bzrdir, 'other').open_workingtree()
62
other.commit('my change', rev_id='M1', allow_pointless=True)
63
mine.merge_from_branch(other.branch)
64
mine.commit('merge my change', rev_id='P2')
65
result = mine.branch.push(other.branch)
66
self.assertEqual(['P1', 'P2'], other.branch.revision_history())
67
# result object contains some structured data
68
self.assertEqual(result.old_revid, 'M1')
69
self.assertEqual(result.new_revid, 'P2')
70
# and it can be treated as an integer for compatibility
71
self.assertEqual(int(result), 0)
73
def test_push_merged_indirect(self):
74
# it should be possible to do a push from one branch into another
75
# when the tip of the target was merged into the source branch
76
# via a third branch - so its buried in the ancestry and is not
77
# directly accessible.
78
mine = self.make_from_branch_and_tree('mine')
79
mine.commit('1st post', rev_id='P1', allow_pointless=True)
80
target = self.sprout_to(mine.bzrdir, 'target').open_workingtree()
81
target.commit('my change', rev_id='M1', allow_pointless=True)
82
other = self.sprout_to(mine.bzrdir, 'other').open_workingtree()
83
other.merge_from_branch(target.branch)
84
other.commit('merge my change', rev_id='O2')
85
mine.merge_from_branch(other.branch)
86
mine.commit('merge other', rev_id='P2')
87
mine.branch.push(target.branch)
88
self.assertEqual(['P1', 'P2'], target.branch.revision_history())
90
def test_push_to_checkout_updates_master(self):
91
"""Pushing into a checkout updates the checkout and the master branch"""
92
master_tree = self.make_to_branch_and_tree('master')
93
checkout = self.make_to_branch_and_tree('checkout')
95
checkout.branch.bind(master_tree.branch)
96
except errors.UpgradeRequired:
97
# cant bind this format, the test is irrelevant.
99
rev1 = checkout.commit('master')
101
other_bzrdir = self.sprout_from(master_tree.branch.bzrdir, 'other')
102
other = other_bzrdir.open_workingtree()
103
rev2 = other.commit('other commit')
104
# now push, which should update both checkout and master.
105
other.branch.push(checkout.branch)
106
self.assertEqual([rev1, rev2], checkout.branch.revision_history())
107
self.assertEqual([rev1, rev2], master_tree.branch.revision_history())
109
def test_push_raises_specific_error_on_master_connection_error(self):
110
master_tree = self.make_to_branch_and_tree('master')
111
checkout = self.make_to_branch_and_tree('checkout')
113
checkout.branch.bind(master_tree.branch)
114
except errors.UpgradeRequired:
115
# cant bind this format, the test is irrelevant.
117
other_bzrdir = self.sprout_from(master_tree.branch.bzrdir, 'other')
118
other = other_bzrdir.open_workingtree()
119
# move the branch out of the way on disk to cause a connection
121
os.rename('master', 'master_gone')
122
# try to push, which should raise a BoundBranchConnectionFailure.
123
self.assertRaises(errors.BoundBranchConnectionFailure,
124
other.branch.push, checkout.branch)
126
def test_push_uses_read_lock(self):
127
"""Push should only need a read lock on the source side."""
128
source = self.make_from_branch_and_tree('source')
129
target = self.make_to_branch('target')
131
self.build_tree(['source/a'])
135
source.branch.lock_read()
139
source.branch.push(target, stop_revision=source.last_revision())
143
source.branch.unlock()
145
def test_push_within_repository(self):
146
"""Push from one branch to another inside the same repository."""
148
repo = self.make_repository('repo', shared=True)
149
except (errors.IncompatibleFormat, errors.UninitializableFormat):
150
# This Branch format cannot create shared repositories
152
# This is a little bit trickier because make_from_branch_and_tree will not
153
# re-use a shared repository.
155
a_branch = self.make_from_branch('repo/tree')
156
except (errors.UninitializableFormat):
157
# Cannot create these branches
160
tree = a_branch.bzrdir.create_workingtree()
161
except errors.NotLocalUrl:
162
if self.vfs_transport_factory is test_server.LocalURLServer:
163
# the branch is colocated on disk, we cannot create a checkout.
164
# hopefully callers will expect this.
165
local_controldir = bzrdir.BzrDir.open(self.get_vfs_only_url('repo/tree'))
166
tree = local_controldir.create_workingtree()
168
tree = a_branch.create_checkout('repo/tree', lightweight=True)
169
self.build_tree(['repo/tree/a'])
173
to_branch = self.make_to_branch('repo/branch')
174
tree.branch.push(to_branch)
176
self.assertEqual(tree.branch.last_revision(),
177
to_branch.last_revision())
179
def test_push_overwrite_of_non_tip_with_stop_revision(self):
180
"""Combining the stop_revision and overwrite options works.
182
This was <https://bugs.launchpad.net/bzr/+bug/234229>.
184
source = self.make_from_branch_and_tree('source')
185
target = self.make_to_branch('target')
187
source.commit('1st commit')
188
source.branch.push(target)
189
source.commit('2nd commit', rev_id='rev-2')
190
source.commit('3rd commit')
192
source.branch.push(target, stop_revision='rev-2', overwrite=True)
193
self.assertEqual('rev-2', target.last_revision())
195
def test_push_with_default_stacking_does_not_create_broken_branch(self):
196
"""Pushing a new standalone branch works even when there's a default
197
stacking policy at the destination.
199
The new branch will preserve the repo format (even if it isn't the
200
default for the branch), and will be stacked when the repo format
201
allows (which means that the branch format isn't necessarly preserved).
203
if isinstance(self.branch_format_from, branch.BzrBranchFormat4):
204
raise tests.TestNotApplicable('Not a metadir format.')
205
if isinstance(self.branch_format_from, branch.BranchReferenceFormat):
206
# This test could in principle apply to BranchReferenceFormat, but
207
# make_branch_builder doesn't support it.
208
raise tests.TestSkipped(
209
"BranchBuilder can't make reference branches.")
210
# Make a branch called "local" in a stackable repository
211
# The branch has 3 revisions:
212
# - rev-1, adds a file
213
# - rev-2, no changes
214
# - rev-3, modifies the file.
215
repo = self.make_repository('repo', shared=True, format='1.6')
216
builder = self.make_from_branch_builder('repo/local')
217
builder.start_series()
218
builder.build_snapshot('rev-1', None, [
219
('add', ('', 'root-id', 'directory', '')),
220
('add', ('filename', 'f-id', 'file', 'content\n'))])
221
builder.build_snapshot('rev-2', ['rev-1'], [])
222
builder.build_snapshot('rev-3', ['rev-2'],
223
[('modify', ('f-id', 'new-content\n'))])
224
builder.finish_series()
225
trunk = builder.get_branch()
226
# Sprout rev-1 to "trunk", so that we can stack on it.
227
trunk.bzrdir.sprout(self.get_url('trunk'), revision_id='rev-1')
228
# Set a default stacking policy so that new branches will automatically
230
self.make_bzrdir('.').get_config().set_default_stack_on('trunk')
231
# Push rev-2 to a new branch "remote". It will be stacked on "trunk".
233
push._show_push_branch(trunk, 'rev-2', self.get_url('remote'), output)
234
# Push rev-3 onto "remote". If "remote" not stacked and is missing the
235
# fulltext record for f-id @ rev-1, then this will fail.
236
remote_branch = Branch.open(self.get_url('remote'))
237
trunk.push(remote_branch)
238
check.check_dwim(remote_branch.base, False, True, True)
240
def test_no_get_parent_map_after_insert_stream(self):
241
# Effort test for bug 331823
242
self.setup_smart_server_with_call_log()
243
# Make a local branch with four revisions. Four revisions because:
244
# one to push, one there for _walk_to_common_revisions to find, one we
245
# don't want to access, one for luck :)
246
if isinstance(self.branch_format_from, branch.BranchReferenceFormat):
247
# This test could in principle apply to BranchReferenceFormat, but
248
# make_branch_builder doesn't support it.
249
raise tests.TestSkipped(
250
"BranchBuilder can't make reference branches.")
252
builder = self.make_from_branch_builder('local')
253
except (errors.TransportNotPossible, errors.UninitializableFormat):
254
raise tests.TestNotApplicable('format not directly constructable')
255
builder.start_series()
256
builder.build_snapshot('first', None, [
257
('add', ('', 'root-id', 'directory', ''))])
258
builder.build_snapshot('second', ['first'], [])
259
builder.build_snapshot('third', ['second'], [])
260
builder.build_snapshot('fourth', ['third'], [])
261
builder.finish_series()
262
local = branch.Branch.open(self.get_vfs_only_url('local'))
263
# Initial push of three revisions
264
remote_bzrdir = local.bzrdir.sprout(
265
self.get_url('remote'), revision_id='third')
266
remote = remote_bzrdir.open_branch()
267
# Push fourth revision
268
self.reset_smart_call_log()
269
self.disableOptimisticGetParentMap()
270
self.assertFalse(local.is_locked())
272
hpss_call_names = [item.call.method for item in self.hpss_calls]
273
self.assertTrue('Repository.insert_stream_1.19' in hpss_call_names)
274
insert_stream_idx = hpss_call_names.index(
275
'Repository.insert_stream_1.19')
276
calls_after_insert_stream = hpss_call_names[insert_stream_idx:]
277
# After inserting the stream the client has no reason to query the
278
# remote graph any further.
279
bzr_core_trace = Equals(
280
['Repository.insert_stream_1.19', 'Repository.insert_stream_1.19',
281
'get', 'Branch.set_last_revision_info', 'Branch.unlock'])
282
bzr_loom_trace = Equals(
283
['Repository.insert_stream_1.19', 'Repository.insert_stream_1.19',
284
'get', 'Branch.set_last_revision_info', 'get', 'Branch.unlock'])
285
self.assertThat(calls_after_insert_stream,
286
MatchesAny(bzr_core_trace, bzr_loom_trace))
288
def disableOptimisticGetParentMap(self):
289
# Tweak some class variables to stop remote get_parent_map calls asking
290
# for or receiving more data than the caller asked for.
291
self.overrideAttr(repository.InterRepository,
292
'_walk_to_common_revisions_batch_size', 1)
293
self.overrideAttr(SmartServerRepositoryGetParentMap,
294
'no_extra_results', True)
297
class TestPushHook(TestCaseWithInterBranch):
301
TestCaseWithInterBranch.setUp(self)
303
def capture_post_push_hook(self, result):
304
"""Capture post push hook calls to self.hook_calls.
306
The call is logged, as is some state of the two branches.
308
if result.local_branch:
309
local_locked = result.local_branch.is_locked()
310
local_base = result.local_branch.base
314
self.hook_calls.append(
315
('post_push', result.source_branch, local_base,
316
result.master_branch.base,
317
result.old_revno, result.old_revid,
318
result.new_revno, result.new_revid,
319
result.source_branch.is_locked(), local_locked,
320
result.master_branch.is_locked()))
322
def test_post_push_empty_history(self):
323
target = self.make_to_branch('target')
324
source = self.make_from_branch('source')
325
Branch.hooks.install_named_hook('post_push',
326
self.capture_post_push_hook, None)
328
# with nothing there we should still get a notification, and
329
# have both branches locked at the notification time.
331
('post_push', source, None, target.base, 0, NULL_REVISION,
332
0, NULL_REVISION, True, None, True)
336
def test_post_push_bound_branch(self):
337
# pushing to a bound branch should pass in the master branch to the
338
# hook, allowing the correct number of emails to be sent, while still
339
# allowing hooks that want to modify the target to do so to both
341
target = self.make_to_branch('target')
342
local = self.make_from_branch('local')
345
except errors.UpgradeRequired:
346
# We can't bind this format to itself- typically it is the local
347
# branch that doesn't support binding. As of May 2007
348
# remotebranches can't be bound. Let's instead make a new local
349
# branch of the default type, which does allow binding.
350
# See https://bugs.launchpad.net/bzr/+bug/112020
351
local = BzrDir.create_branch_convenience('local2')
353
source = self.make_from_branch('source')
354
Branch.hooks.install_named_hook('post_push',
355
self.capture_post_push_hook, None)
357
# with nothing there we should still get a notification, and
358
# have both branches locked at the notification time.
360
('post_push', source, local.base, target.base, 0, NULL_REVISION,
361
0, NULL_REVISION, True, True, True)
365
def test_post_push_nonempty_history(self):
366
target = self.make_to_branch_and_tree('target')
369
rev1 = target.commit('rev 1')
371
sourcedir = target.bzrdir.clone(self.get_url('source'))
372
source = MemoryTree.create_on_branch(sourcedir.open_branch())
373
rev2 = source.commit('rev 2')
374
Branch.hooks.install_named_hook('post_push',
375
self.capture_post_push_hook, None)
376
source.branch.push(target.branch)
377
# with nothing there we should still get a notification, and
378
# have both branches locked at the notification time.
380
('post_push', source.branch, None, target.branch.base, 1, rev1,
381
2, rev2, True, None, True)