~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/fetch.py

  • Committer: Jelmer Vernooij
  • Date: 2011-12-19 10:58:39 UTC
  • mfrom: (6383 +trunk)
  • mto: This revision was merged to the branch mainline in revision 6386.
  • Revision ID: jelmer@canonical.com-20111219105839-uji05ck4rkm1mj4j
Merge bzr.dev.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005, 2006, 2008, 2009 Canonical Ltd
 
1
# Copyright (C) 2005-2011 Canonical Ltd
2
2
#
3
3
# This program is free software; you can redistribute it and/or modify
4
4
# it under the terms of the GNU General Public License as published by
14
14
# along with this program; if not, write to the Free Software
15
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16
16
 
 
17
from __future__ import absolute_import
17
18
 
18
19
"""Copying of history from one branch to another.
19
20
 
25
26
 
26
27
import operator
27
28
 
28
 
import bzrlib
 
29
from bzrlib.lazy_import import lazy_import
 
30
lazy_import(globals(), """
 
31
from bzrlib import (
 
32
    tsort,
 
33
    versionedfile,
 
34
    vf_search,
 
35
    )
 
36
""")
29
37
from bzrlib import (
30
38
    errors,
31
 
    symbol_versioning,
 
39
    ui,
32
40
    )
 
41
from bzrlib.i18n import gettext
33
42
from bzrlib.revision import NULL_REVISION
34
 
from bzrlib.tsort import topo_sort
35
43
from bzrlib.trace import mutter
36
 
import bzrlib.ui
37
 
from bzrlib.versionedfile import FulltextContentFactory
38
44
 
39
45
 
40
46
class RepoFetcher(object):
45
51
    """
46
52
 
47
53
    def __init__(self, to_repository, from_repository, last_revision=None,
48
 
        pb=None, find_ghosts=True, fetch_spec=None):
 
54
        find_ghosts=True, fetch_spec=None):
49
55
        """Create a repo fetcher.
50
56
 
51
57
        :param last_revision: If set, try to limit to the data this revision
52
58
            references.
 
59
        :param fetch_spec: A SearchResult specifying which revisions to fetch.
 
60
            If set, this overrides last_revision.
53
61
        :param find_ghosts: If True search the entire history for ghosts.
54
 
        :param _write_group_acquired_callable: Don't use; this parameter only
55
 
            exists to facilitate a hack done in InterPackRepo.fetch.  We would
56
 
            like to remove this parameter.
57
 
        :param pb: ProgressBar object to use; deprecated and ignored.
58
 
            This method will just create one on top of the stack.
59
62
        """
60
 
        if pb is not None:
61
 
            symbol_versioning.warn(
62
 
                symbol_versioning.deprecated_in((1, 14, 0))
63
 
                % "pb parameter to RepoFetcher.__init__")
64
 
            # and for simplicity it is in fact ignored
65
 
        if to_repository.has_same_location(from_repository):
66
 
            # repository.fetch should be taking care of this case.
67
 
            raise errors.BzrError('RepoFetcher run '
68
 
                    'between two objects at the same location: '
69
 
                    '%r and %r' % (to_repository, from_repository))
 
63
        # repository.fetch has the responsibility for short-circuiting
 
64
        # attempts to copy between a repository and itself.
70
65
        self.to_repository = to_repository
71
66
        self.from_repository = from_repository
72
67
        self.sink = to_repository._get_sink()
97
92
        # assert not missing
98
93
        self.count_total = 0
99
94
        self.file_ids_names = {}
100
 
        pb = bzrlib.ui.ui_factory.nested_progress_bar()
 
95
        pb = ui.ui_factory.nested_progress_bar()
101
96
        pb.show_pct = pb.show_count = False
102
97
        try:
103
 
            pb.update("Finding revisions", 0, 2)
104
 
            search = self._revids_to_fetch()
105
 
            if search is None:
 
98
            pb.update(gettext("Finding revisions"), 0, 2)
 
99
            search_result = self._revids_to_fetch()
 
100
            mutter('fetching: %s', search_result)
 
101
            if search_result.is_empty():
106
102
                return
107
 
            pb.update("Fetching revisions", 1, 2)
108
 
            self._fetch_everything_for_search(search)
 
103
            pb.update(gettext("Fetching revisions"), 1, 2)
 
104
            self._fetch_everything_for_search(search_result)
109
105
        finally:
110
106
            pb.finished()
111
107
 
124
120
            raise errors.IncompatibleRepositories(
125
121
                self.from_repository, self.to_repository,
126
122
                "different rich-root support")
127
 
        pb = bzrlib.ui.ui_factory.nested_progress_bar()
 
123
        pb = ui.ui_factory.nested_progress_bar()
128
124
        try:
129
125
            pb.update("Get stream source")
130
126
            source = self.from_repository._get_source(
134
130
            pb.update("Inserting stream")
135
131
            resume_tokens, missing_keys = self.sink.insert_stream(
136
132
                stream, from_format, [])
137
 
            if self.to_repository._fallback_repositories:
138
 
                missing_keys.update(
139
 
                    self._parent_inventories(search.get_keys()))
140
133
            if missing_keys:
141
134
                pb.update("Missing keys")
142
135
                stream = source.get_stream_for_missing_keys(missing_keys)
160
153
        """Determines the exact revisions needed from self.from_repository to
161
154
        install self._last_revision in self.to_repository.
162
155
 
163
 
        If no revisions need to be fetched, then this just returns None.
 
156
        :returns: A SearchResult of some sort.  (Possibly a
 
157
            PendingAncestryResult, EmptySearchResult, etc.)
164
158
        """
165
159
        if self._fetch_spec is not None:
 
160
            # The fetch spec is already a concrete search result.
166
161
            return self._fetch_spec
167
 
        mutter('fetch up to rev {%s}', self._last_revision)
168
 
        if self._last_revision is NULL_REVISION:
 
162
        elif self._last_revision == NULL_REVISION:
 
163
            # fetch_spec is None + last_revision is null => empty fetch.
169
164
            # explicit limit of no revisions needed
170
 
            return None
171
 
        return self.to_repository.search_missing_revision_ids(
172
 
            self.from_repository, self._last_revision,
173
 
            find_ghosts=self.find_ghosts)
174
 
 
175
 
    def _parent_inventories(self, revision_ids):
176
 
        # Find all the parent revisions referenced by the stream, but
177
 
        # not present in the stream, and make sure we send their
178
 
        # inventories.
179
 
        parent_maps = self.to_repository.get_parent_map(revision_ids)
180
 
        parents = set()
181
 
        map(parents.update, parent_maps.itervalues())
182
 
        parents.discard(NULL_REVISION)
183
 
        parents.difference_update(revision_ids)
184
 
        missing_keys = set(('inventories', rev_id) for rev_id in parents)
185
 
        return missing_keys
 
165
            return vf_search.EmptySearchResult()
 
166
        elif self._last_revision is not None:
 
167
            return vf_search.NotInOtherForRevs(self.to_repository,
 
168
                self.from_repository, [self._last_revision],
 
169
                find_ghosts=self.find_ghosts).execute()
 
170
        else: # self._last_revision is None:
 
171
            return vf_search.EverythingNotInOther(self.to_repository,
 
172
                self.from_repository,
 
173
                find_ghosts=self.find_ghosts).execute()
186
174
 
187
175
 
188
176
class Inter1and2Helper(object):
191
179
    This is for use by fetchers and converters.
192
180
    """
193
181
 
 
182
    # This is a class variable so that the test suite can override it.
 
183
    known_graph_threshold = 100
 
184
 
194
185
    def __init__(self, source):
195
186
        """Constructor.
196
187
 
219
210
 
220
211
    def _find_root_ids(self, revs, parent_map, graph):
221
212
        revision_root = {}
222
 
        planned_versions = {}
223
213
        for tree in self.iter_rev_trees(revs):
224
214
            revision_id = tree.inventory.root.revision
225
215
            root_id = tree.get_root_id()
226
 
            planned_versions.setdefault(root_id, []).append(revision_id)
227
216
            revision_root[revision_id] = root_id
228
217
        # Find out which parents we don't already know root ids for
229
218
        parents = set()
235
224
        for tree in self.iter_rev_trees(parents):
236
225
            root_id = tree.get_root_id()
237
226
            revision_root[tree.get_revision_id()] = root_id
238
 
        return revision_root, planned_versions
 
227
        return revision_root
239
228
 
240
229
    def generate_root_texts(self, revs):
241
230
        """Generate VersionedFiles for all root ids.
244
233
        """
245
234
        graph = self.source.get_graph()
246
235
        parent_map = graph.get_parent_map(revs)
247
 
        rev_order = topo_sort(parent_map)
248
 
        rev_id_to_root_id, root_id_to_rev_ids = self._find_root_ids(
249
 
            revs, parent_map, graph)
 
236
        rev_order = tsort.topo_sort(parent_map)
 
237
        rev_id_to_root_id = self._find_root_ids(revs, parent_map, graph)
250
238
        root_id_order = [(rev_id_to_root_id[rev_id], rev_id) for rev_id in
251
239
            rev_order]
252
240
        # Guaranteed stable, this groups all the file id operations together
255
243
        # yet, and are unlikely to in non-rich-root environments anyway.
256
244
        root_id_order.sort(key=operator.itemgetter(0))
257
245
        # Create a record stream containing the roots to create.
258
 
        def yield_roots():
259
 
            for key in root_id_order:
260
 
                root_id, rev_id = key
261
 
                rev_parents = parent_map[rev_id]
262
 
                # We drop revision parents with different file-ids, because
263
 
                # that represents a rename of the root to a different location
264
 
                # - its not actually a parent for us. (We could look for that
265
 
                # file id in the revision tree at considerably more expense,
266
 
                # but for now this is sufficient (and reconcile will catch and
267
 
                # correct this anyway).
268
 
                # When a parent revision is a ghost, we guess that its root id
269
 
                # was unchanged (rather than trimming it from the parent list).
270
 
                parent_keys = tuple((root_id, parent) for parent in rev_parents
271
 
                    if parent != NULL_REVISION and
272
 
                        rev_id_to_root_id.get(parent, root_id) == root_id)
273
 
                yield FulltextContentFactory(key, parent_keys, None, '')
274
 
        return [('texts', yield_roots())]
 
246
        if len(revs) > self.known_graph_threshold:
 
247
            graph = self.source.get_known_graph_ancestry(revs)
 
248
        new_roots_stream = _new_root_data_stream(
 
249
            root_id_order, rev_id_to_root_id, parent_map, self.source, graph)
 
250
        return [('texts', new_roots_stream)]
 
251
 
 
252
 
 
253
def _new_root_data_stream(
 
254
    root_keys_to_create, rev_id_to_root_id_map, parent_map, repo, graph=None):
 
255
    """Generate a texts substream of synthesised root entries.
 
256
 
 
257
    Used in fetches that do rich-root upgrades.
 
258
    
 
259
    :param root_keys_to_create: iterable of (root_id, rev_id) pairs describing
 
260
        the root entries to create.
 
261
    :param rev_id_to_root_id_map: dict of known rev_id -> root_id mappings for
 
262
        calculating the parents.  If a parent rev_id is not found here then it
 
263
        will be recalculated.
 
264
    :param parent_map: a parent map for all the revisions in
 
265
        root_keys_to_create.
 
266
    :param graph: a graph to use instead of repo.get_graph().
 
267
    """
 
268
    for root_key in root_keys_to_create:
 
269
        root_id, rev_id = root_key
 
270
        parent_keys = _parent_keys_for_root_version(
 
271
            root_id, rev_id, rev_id_to_root_id_map, parent_map, repo, graph)
 
272
        yield versionedfile.FulltextContentFactory(
 
273
            root_key, parent_keys, None, '')
 
274
 
 
275
 
 
276
def _parent_keys_for_root_version(
 
277
    root_id, rev_id, rev_id_to_root_id_map, parent_map, repo, graph=None):
 
278
    """Get the parent keys for a given root id.
 
279
    
 
280
    A helper function for _new_root_data_stream.
 
281
    """
 
282
    # Include direct parents of the revision, but only if they used the same
 
283
    # root_id and are heads.
 
284
    rev_parents = parent_map[rev_id]
 
285
    parent_ids = []
 
286
    for parent_id in rev_parents:
 
287
        if parent_id == NULL_REVISION:
 
288
            continue
 
289
        if parent_id not in rev_id_to_root_id_map:
 
290
            # We probably didn't read this revision, go spend the extra effort
 
291
            # to actually check
 
292
            try:
 
293
                tree = repo.revision_tree(parent_id)
 
294
            except errors.NoSuchRevision:
 
295
                # Ghost, fill out rev_id_to_root_id in case we encounter this
 
296
                # again.
 
297
                # But set parent_root_id to None since we don't really know
 
298
                parent_root_id = None
 
299
            else:
 
300
                parent_root_id = tree.get_root_id()
 
301
            rev_id_to_root_id_map[parent_id] = None
 
302
            # XXX: why not:
 
303
            #   rev_id_to_root_id_map[parent_id] = parent_root_id
 
304
            # memory consumption maybe?
 
305
        else:
 
306
            parent_root_id = rev_id_to_root_id_map[parent_id]
 
307
        if root_id == parent_root_id:
 
308
            # With stacking we _might_ want to refer to a non-local revision,
 
309
            # but this code path only applies when we have the full content
 
310
            # available, so ghosts really are ghosts, not just the edge of
 
311
            # local data.
 
312
            parent_ids.append(parent_id)
 
313
        else:
 
314
            # root_id may be in the parent anyway.
 
315
            try:
 
316
                tree = repo.revision_tree(parent_id)
 
317
            except errors.NoSuchRevision:
 
318
                # ghost, can't refer to it.
 
319
                pass
 
320
            else:
 
321
                try:
 
322
                    parent_ids.append(tree.get_file_revision(root_id))
 
323
                except errors.NoSuchId:
 
324
                    # not in the tree
 
325
                    pass
 
326
    # Drop non-head parents
 
327
    if graph is None:
 
328
        graph = repo.get_graph()
 
329
    heads = graph.heads(parent_ids)
 
330
    selected_ids = []
 
331
    for parent_id in parent_ids:
 
332
        if parent_id in heads and parent_id not in selected_ids:
 
333
            selected_ids.append(parent_id)
 
334
    parent_keys = [(root_id, parent_id) for parent_id in selected_ids]
 
335
    return parent_keys
 
336
 
 
337
 
 
338
class TargetRepoKinds(object):
 
339
    """An enum-like set of constants.
 
340
    
 
341
    They are the possible values of FetchSpecFactory.target_repo_kinds.
 
342
    """
 
343
    
 
344
    PREEXISTING = 'preexisting'
 
345
    STACKED = 'stacked'
 
346
    EMPTY = 'empty'
 
347
 
 
348
 
 
349
class FetchSpecFactory(object):
 
350
    """A helper for building the best fetch spec for a sprout call.
 
351
 
 
352
    Factors that go into determining the sort of fetch to perform:
 
353
     * did the caller specify any revision IDs?
 
354
     * did the caller specify a source branch (need to fetch its
 
355
       heads_to_fetch(), usually the tip + tags)
 
356
     * is there an existing target repo (don't need to refetch revs it
 
357
       already has)
 
358
     * target is stacked?  (similar to pre-existing target repo: even if
 
359
       the target itself is new don't want to refetch existing revs)
 
360
 
 
361
    :ivar source_branch: the source branch if one specified, else None.
 
362
    :ivar source_branch_stop_revision_id: fetch up to this revision of
 
363
        source_branch, rather than its tip.
 
364
    :ivar source_repo: the source repository if one found, else None.
 
365
    :ivar target_repo: the target repository acquired by sprout.
 
366
    :ivar target_repo_kind: one of the TargetRepoKinds constants.
 
367
    """
 
368
 
 
369
    def __init__(self):
 
370
        self._explicit_rev_ids = set()
 
371
        self.source_branch = None
 
372
        self.source_branch_stop_revision_id = None
 
373
        self.source_repo = None
 
374
        self.target_repo = None
 
375
        self.target_repo_kind = None
 
376
        self.limit = None
 
377
 
 
378
    def add_revision_ids(self, revision_ids):
 
379
        """Add revision_ids to the set of revision_ids to be fetched."""
 
380
        self._explicit_rev_ids.update(revision_ids)
 
381
 
 
382
    def make_fetch_spec(self):
 
383
        """Build a SearchResult or PendingAncestryResult or etc."""
 
384
        if self.target_repo_kind is None or self.source_repo is None:
 
385
            raise AssertionError(
 
386
                'Incomplete FetchSpecFactory: %r' % (self.__dict__,))
 
387
        if len(self._explicit_rev_ids) == 0 and self.source_branch is None:
 
388
            if self.limit is not None:
 
389
                raise NotImplementedError(
 
390
                    "limit is only supported with a source branch set")
 
391
            # Caller hasn't specified any revisions or source branch
 
392
            if self.target_repo_kind == TargetRepoKinds.EMPTY:
 
393
                return vf_search.EverythingResult(self.source_repo)
 
394
            else:
 
395
                # We want everything not already in the target (or target's
 
396
                # fallbacks).
 
397
                return vf_search.EverythingNotInOther(
 
398
                    self.target_repo, self.source_repo).execute()
 
399
        heads_to_fetch = set(self._explicit_rev_ids)
 
400
        if self.source_branch is not None:
 
401
            must_fetch, if_present_fetch = self.source_branch.heads_to_fetch()
 
402
            if self.source_branch_stop_revision_id is not None:
 
403
                # Replace the tip rev from must_fetch with the stop revision
 
404
                # XXX: this might be wrong if the tip rev is also in the
 
405
                # must_fetch set for other reasons (e.g. it's the tip of
 
406
                # multiple loom threads?), but then it's pretty unclear what it
 
407
                # should mean to specify a stop_revision in that case anyway.
 
408
                must_fetch.discard(self.source_branch.last_revision())
 
409
                must_fetch.add(self.source_branch_stop_revision_id)
 
410
            heads_to_fetch.update(must_fetch)
 
411
        else:
 
412
            if_present_fetch = set()
 
413
        if self.target_repo_kind == TargetRepoKinds.EMPTY:
 
414
            # PendingAncestryResult does not raise errors if a requested head
 
415
            # is absent.  Ideally it would support the
 
416
            # required_ids/if_present_ids distinction, but in practice
 
417
            # heads_to_fetch will almost certainly be present so this doesn't
 
418
            # matter much.
 
419
            all_heads = heads_to_fetch.union(if_present_fetch)
 
420
            ret = vf_search.PendingAncestryResult(all_heads, self.source_repo)
 
421
            if self.limit is not None:
 
422
                graph = self.source_repo.get_graph()
 
423
                topo_order = list(graph.iter_topo_order(ret.get_keys()))
 
424
                result_set = topo_order[:self.limit]
 
425
                ret = self.source_repo.revision_ids_to_search_result(result_set)
 
426
            return ret
 
427
        else:
 
428
            return vf_search.NotInOtherForRevs(self.target_repo, self.source_repo,
 
429
                required_ids=heads_to_fetch, if_present_ids=if_present_fetch,
 
430
                limit=self.limit).execute()