1# Copyright (C) 2007-2010 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 fetch between repositories of the same type."""
18
19from breezy import (
20    controldir,
21    errors,
22    gpg,
23    repository,
24    )
25from breezy.bzr import (
26    remote,
27    )
28from breezy.bzr.inventory import ROOT_ID
29from breezy.tests import (
30    TestNotApplicable,
31    TestSkipped,
32    )
33from breezy.tests.per_repository import TestCaseWithRepository
34
35
36class TestFetchSameRepository(TestCaseWithRepository):
37
38    def test_fetch(self):
39        # smoke test fetch to ensure that the convenience function works.
40        # it is defined as a convenience function with the underlying
41        # functionality provided by an InterRepository
42        tree_a = self.make_branch_and_tree('a')
43        self.build_tree(['a/foo'])
44        tree_a.add('foo')
45        rev1 = tree_a.commit('rev1')
46        # fetch with a default limit (grab everything)
47        repo = self.make_repository('b')
48        if (tree_a.branch.repository.supports_rich_root() and not
49                repo.supports_rich_root()):
50            raise TestSkipped('Cannot fetch from model2 to model1')
51        repo.fetch(tree_a.branch.repository,
52                   revision_id=None)
53
54    def test_fetch_fails_in_write_group(self):
55        # fetch() manages a write group itself, fetching within one isn't safe.
56        repo = self.make_repository('a')
57        repo.lock_write()
58        self.addCleanup(repo.unlock)
59        repo.start_write_group()
60        self.addCleanup(repo.abort_write_group)
61        # Don't need a specific class - not expecting flow control based on
62        # this.
63        self.assertRaises(errors.BzrError, repo.fetch, repo)
64
65    def test_fetch_to_knit3(self):
66        # create a repository of the sort we are testing.
67        tree_a = self.make_branch_and_tree('a')
68        self.build_tree(['a/foo'])
69        tree_a.add('foo')
70        rev1 = tree_a.commit('rev1')
71        # create a knit-3 based format to fetch into
72        f = controldir.format_registry.make_controldir('development-subtree')
73        try:
74            format = tree_a.branch.repository._format
75            format.check_conversion_target(f.repository_format)
76            # if we cannot convert data to knit3, skip the test.
77        except errors.BadConversionTarget as e:
78            raise TestSkipped(str(e))
79        self.get_transport().mkdir('b')
80        b_bzrdir = f.initialize(self.get_url('b'))
81        knit3_repo = b_bzrdir.create_repository()
82        # fetch with a default limit (grab everything)
83        knit3_repo.fetch(tree_a.branch.repository, revision_id=None)
84        # Reopen to avoid any in-memory caching - ensure its reading from
85        # disk.
86        knit3_repo = b_bzrdir.open_repository()
87        rev1_tree = knit3_repo.revision_tree(rev1)
88        with rev1_tree.lock_read():
89            lines = rev1_tree.get_file_lines(u'')
90        self.assertEqual([], lines)
91        b_branch = b_bzrdir.create_branch()
92        b_branch.pull(tree_a.branch)
93        try:
94            tree_b = b_bzrdir.create_workingtree()
95        except errors.NotLocalUrl:
96            try:
97                tree_b = b_branch.create_checkout('b', lightweight=True)
98            except errors.NotLocalUrl:
99                raise TestSkipped("cannot make working tree with transport %r"
100                                  % b_bzrdir.transport)
101        rev2 = tree_b.commit('no change')
102        rev2_tree = knit3_repo.revision_tree(rev2)
103        self.assertEqual(rev1, rev2_tree.get_file_revision(u''))
104
105    def do_test_fetch_to_rich_root_sets_parents_correctly(self, result,
106                                                          snapshots, root_id=ROOT_ID, allow_lefthand_ghost=False):
107        """Assert that result is the parents of b'tip' after fetching snapshots.
108
109        This helper constructs a 1.9 format source, and a test-format target
110        and fetches the result of building snapshots in the source, then
111        asserts that the parents of tip are result.
112
113        :param result: A parents list for the inventories.get_parent_map call.
114        :param snapshots: An iterable of snapshot parameters for
115            BranchBuilder.build_snapshot.
116        '"""
117        # This overlaps slightly with the tests for commit builder about graph
118        # consistency.
119        # Cases:
120        repo = self.make_repository('target')
121        remote_format = isinstance(repo, remote.RemoteRepository)
122        if not repo._format.rich_root_data and not remote_format:
123            return  # not relevant
124        if not repo._format.supports_full_versioned_files:
125            raise TestNotApplicable(
126                'format does not support full versioned files')
127        builder = self.make_branch_builder('source', format='1.9')
128        builder.start_series()
129        for revision_id, parent_ids, actions in snapshots:
130            builder.build_snapshot(parent_ids, actions,
131                                   allow_leftmost_as_ghost=allow_lefthand_ghost,
132                                   revision_id=revision_id)
133        builder.finish_series()
134        source = builder.get_branch()
135        if remote_format and not repo._format.rich_root_data:
136            # use a manual rich root format to ensure the code path is tested.
137            repo = self.make_repository('remote-target',
138                                        format='1.9-rich-root')
139        repo.lock_write()
140        self.addCleanup(repo.unlock)
141        repo.fetch(source.repository)
142        graph = repo.get_file_graph()
143        self.assertEqual(result,
144                         graph.get_parent_map([(root_id, b'tip')])[(root_id, b'tip')])
145
146    def test_fetch_to_rich_root_set_parent_no_parents(self):
147        # No parents rev -> No parents
148        self.do_test_fetch_to_rich_root_sets_parents_correctly((),
149                                                               [(b'tip', None, [('add', ('', ROOT_ID, 'directory', ''))]),
150                                                                ])
151
152    def test_fetch_to_rich_root_set_parent_1_parent(self):
153        # 1 parent rev -> 1 parent
154        self.do_test_fetch_to_rich_root_sets_parents_correctly(
155            ((ROOT_ID, b'base'),),
156            [(b'base', None, [('add', ('', ROOT_ID, 'directory', ''))]),
157             (b'tip', None, []),
158             ])
159
160    def test_fetch_to_rich_root_set_parent_1_ghost_parent(self):
161        # 1 ghost parent -> No parents
162        if not self.repository_format.supports_ghosts:
163            raise TestNotApplicable("repository format does not support "
164                                    "ghosts")
165        self.do_test_fetch_to_rich_root_sets_parents_correctly((),
166                                                               [(b'tip', [b'ghost'], [('add', ('', ROOT_ID, 'directory', ''))]),
167                                                                ], allow_lefthand_ghost=True)
168
169    def test_fetch_to_rich_root_set_parent_2_head_parents(self):
170        # 2 parents both heads -> 2 parents
171        self.do_test_fetch_to_rich_root_sets_parents_correctly(
172            ((ROOT_ID, b'left'), (ROOT_ID, b'right')),
173            [(b'base', None, [('add', ('', ROOT_ID, 'directory', ''))]),
174             (b'left', None, []),
175             (b'right', [b'base'], []),
176             (b'tip', [b'left', b'right'], []),
177             ])
178
179    def test_fetch_to_rich_root_set_parent_2_parents_1_head(self):
180        # 2 parents one head -> 1 parent
181        self.do_test_fetch_to_rich_root_sets_parents_correctly(
182            ((ROOT_ID, b'right'),),
183            [(b'left', None, [('add', ('', ROOT_ID, 'directory', ''))]),
184             (b'right', None, []),
185             (b'tip', [b'left', b'right'], []),
186             ])
187
188    def test_fetch_to_rich_root_set_parent_1_parent_different_id_gone(self):
189        # 1 parent different fileid, ours missing -> no parents
190        self.do_test_fetch_to_rich_root_sets_parents_correctly(
191            (),
192            [(b'base', None, [('add', ('', ROOT_ID, 'directory', ''))]),
193             (b'tip', None, [('unversion', ''),
194                             ('add', ('', b'my-root', 'directory', '')),
195                             ]),
196             ], root_id=b'my-root')
197
198    def test_fetch_to_rich_root_set_parent_1_parent_different_id_moved(self):
199        # 1 parent different fileid, ours moved -> 1 parent
200        # (and that parent honours the changing revid of the other location)
201        self.do_test_fetch_to_rich_root_sets_parents_correctly(
202            ((b'my-root', b'origin'),),
203            [(b'origin', None, [('add', ('', ROOT_ID, 'directory', '')),
204                                ('add', ('child', b'my-root', 'directory', ''))]),
205             (b'base', None, []),
206             (b'tip', None, [('unversion', 'child'),
207                             ('unversion', ''),
208                             ('flush', None),
209                             ('add', ('', b'my-root', 'directory', '')),
210                             ]),
211             ], root_id=b'my-root')
212
213    def test_fetch_to_rich_root_set_parent_2_parent_1_different_id_gone(self):
214        # 2 parents, 1 different fileid, our second missing -> 1 parent
215        self.do_test_fetch_to_rich_root_sets_parents_correctly(
216            ((b'my-root', b'right'),),
217            [(b'base', None, [('add', ('', ROOT_ID, 'directory', ''))]),
218             (b'right', None, [('unversion', ''),
219                               ('add', ('', b'my-root', 'directory', ''))]),
220             (b'tip', [b'base', b'right'], [('unversion', ''),
221                                            ('add', ('', b'my-root', 'directory', '')),
222                                            ]),
223             ], root_id=b'my-root')
224
225    def test_fetch_to_rich_root_set_parent_2_parent_2_different_id_moved(self):
226        # 2 parents, 1 different fileid, our second moved -> 2 parent
227        # (and that parent honours the changing revid of the other location)
228        self.do_test_fetch_to_rich_root_sets_parents_correctly(
229            ((b'my-root', b'right'),),
230            # b'my-root' at 'child'.
231            [(b'origin', None, [('add', ('', ROOT_ID, 'directory', '')),
232                                ('add', ('child', b'my-root', 'directory', ''))]),
233             (b'base', None, []),
234             # b'my-root' at root
235             (b'right', None, [('unversion', 'child'),
236                               ('unversion', ''),
237                               ('flush', None),
238                               ('add', ('', b'my-root', 'directory', ''))]),
239             (b'tip', [b'base', b'right'], [('unversion', ''),
240                                            ('unversion', 'child'),
241                                            ('flush', None),
242                                            ('add', ('', b'my-root', 'directory', '')),
243                                            ]),
244             ], root_id=b'my-root')
245
246    def test_fetch_all_from_self(self):
247        tree = self.make_branch_and_tree('.')
248        rev_id = tree.commit('one')
249        # This needs to be a new copy of the repository, if this changes, the
250        # test needs to be rewritten
251        repo = tree.branch.repository.controldir.open_repository()
252        # This fetch should be a no-op see bug #158333
253        tree.branch.repository.fetch(repo, None)
254
255    def test_fetch_from_self(self):
256        tree = self.make_branch_and_tree('.')
257        rev_id = tree.commit('one')
258        repo = tree.branch.repository.controldir.open_repository()
259        # This fetch should be a no-op see bug #158333
260        tree.branch.repository.fetch(repo, rev_id)
261
262    def test_fetch_missing_from_self(self):
263        tree = self.make_branch_and_tree('.')
264        rev_id = tree.commit('one')
265        # Even though the fetch() is a NO-OP it should assert the revision id
266        # is present
267        repo = tree.branch.repository.controldir.open_repository()
268        self.assertRaises(errors.NoSuchRevision, tree.branch.repository.fetch,
269                          repo, b'no-such-revision')
270
271    def makeARepoWithSignatures(self):
272        wt = self.make_branch_and_tree('a-repo-with-sigs')
273        rev1 = wt.commit('rev1', allow_pointless=True)
274        repo = wt.branch.repository
275        repo.lock_write()
276        repo.start_write_group()
277        try:
278            repo.sign_revision(rev1, gpg.LoopbackGPGStrategy(None))
279        except errors.UnsupportedOperation:
280            self.assertFalse(repo._format.supports_revision_signatures)
281            raise TestNotApplicable(
282                "repository format does not support signatures")
283        repo.commit_write_group()
284        repo.unlock()
285        return repo, rev1
286
287    def test_fetch_copies_signatures(self):
288        source_repo, rev1 = self.makeARepoWithSignatures()
289        target_repo = self.make_repository('target')
290        target_repo.fetch(source_repo, revision_id=None)
291        self.assertEqual(
292            source_repo.get_signature_text(rev1),
293            target_repo.get_signature_text(rev1))
294
295    def make_repository_with_one_revision(self):
296        wt = self.make_branch_and_tree('source')
297        rev1 = wt.commit('rev1', allow_pointless=True)
298        return wt.branch.repository, rev1
299
300    def test_fetch_revision_already_exists(self):
301        # Make a repository with one revision.
302        source_repo, rev1 = self.make_repository_with_one_revision()
303        # Fetch that revision into a second repository.
304        target_repo = self.make_repository('target')
305        target_repo.fetch(source_repo, revision_id=rev1)
306        # Now fetch again; there will be nothing to do.  This should work
307        # without causing any errors.
308        target_repo.fetch(source_repo, revision_id=rev1)
309
310    def test_fetch_all_same_revisions_twice(self):
311        # Blind-fetching all the same revisions twice should succeed and be a
312        # no-op the second time.
313        repo = self.make_repository('repo')
314        tree = self.make_branch_and_tree('tree')
315        revision_id = tree.commit('test')
316        repo.fetch(tree.branch.repository)
317        repo.fetch(tree.branch.repository)
318
319    def make_simple_branch_with_ghost(self):
320        if not self.repository_format.supports_ghosts:
321            raise TestNotApplicable("repository format does not support "
322                                    "ghosts")
323        builder = self.make_branch_builder('source')
324        builder.start_series()
325        a_revid = builder.build_snapshot(None, [
326            ('add', ('', b'root-id', 'directory', None)),
327            ('add', ('file', b'file-id', 'file', b'content\n'))])
328        b_revid = builder.build_snapshot([a_revid, b'ghost-id'], [])
329        builder.finish_series()
330        source_b = builder.get_branch()
331        source_b.lock_read()
332        self.addCleanup(source_b.unlock)
333        return source_b, b_revid
334
335    def test_fetch_with_ghost(self):
336        source_b, b_revid = self.make_simple_branch_with_ghost()
337        target = self.make_repository('target')
338        target.lock_write()
339        self.addCleanup(target.unlock)
340        target.fetch(source_b.repository, revision_id=b_revid)
341
342    def test_fetch_into_smart_with_ghost(self):
343        trans = self.make_smart_server('target')
344        source_b, b_revid = self.make_simple_branch_with_ghost()
345        if not source_b.controldir._format.supports_transport(trans):
346            raise TestNotApplicable("format does not support transport")
347        target = self.make_repository('target')
348        # Re-open the repository over the smart protocol
349        target = repository.Repository.open(trans.base)
350        target.lock_write()
351        self.addCleanup(target.unlock)
352        try:
353            target.fetch(source_b.repository, revision_id=b_revid)
354        except errors.TokenLockingNotSupported:
355            # The code inside fetch() that tries to lock and then fails, also
356            # causes weird problems with 'lock_not_held' later on...
357            target.lock_read()
358            self.knownFailure('some repositories fail to fetch'
359                              ' via the smart server because of locking issues.')
360
361    def test_fetch_from_smart_with_ghost(self):
362        trans = self.make_smart_server('source')
363        source_b, b_revid = self.make_simple_branch_with_ghost()
364        if not source_b.controldir._format.supports_transport(trans):
365            raise TestNotApplicable("format does not support transport")
366        target = self.make_repository('target')
367        target.lock_write()
368        self.addCleanup(target.unlock)
369        # Re-open the repository over the smart protocol
370        source = repository.Repository.open(trans.base)
371        source.lock_read()
372        self.addCleanup(source.unlock)
373        target.fetch(source, revision_id=b_revid)
374