1
# Copyright (C) 2005 by 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
18
from cStringIO import StringIO
21
from bzrlib.trace import mutter, note, warning
22
from bzrlib.branch import Branch, INVENTORY_FILEID, ANCESTRY_FILEID
23
from bzrlib.progress import ProgressBar
24
from bzrlib.xml5 import serializer_v5
25
from bzrlib.osutils import sha_string, split_lines
26
from bzrlib.errors import NoSuchRevision
28
"""Copying of history from one branch to another.
30
The basic plan is that every branch knows the history of everything
31
that has merged into it. As the first step of a merge, pull, or
32
branch operation we copy history from the source into the destination
35
The copying is done in a slightly complicated order. We don't want to
36
add a revision to the store until everything it refers to is also
37
stored, so that if a revision is present we can totally recreate it.
38
However, we can't know what files are included in a revision until we
39
read its inventory. Therefore, we first pull the XML and hold it in
40
memory until we've updated all of the files referenced.
43
# TODO: Avoid repeatedly opening weaves so many times.
45
# XXX: This doesn't handle ghost (not present in branch) revisions at
46
# all yet. I'm not sure they really should be supported.
48
# NOTE: This doesn't copy revisions which may be present but not
49
# merged into the last revision. I'm not sure we want to do that.
51
# - get a list of revisions that need to be pulled in
52
# - for each one, pull in that revision file
53
# and get the inventory, and store the inventory with right
55
# - and get the ancestry, and store that with right parents too
56
# - and keep a note of all file ids and version seen
57
# - then go through all files; for each one get the weave,
58
# and add in all file versions
62
def greedy_fetch(to_branch, from_branch, revision=None, pb=None):
63
f = Fetcher(to_branch, from_branch, revision, pb)
64
return f.count_copied, f.failed_revisions
67
class Fetcher(object):
68
"""Pull revisions and texts from one branch to another.
70
This doesn't update the destination's history; that can be done
71
separately if desired.
74
If set, pull only up to this revision_id.
78
last_revision -- if last_revision
79
is given it will be that, otherwise the last revision of
82
count_copied -- number of revisions copied
84
count_texts -- number of file texts copied
86
def __init__(self, to_branch, from_branch, last_revision=None, pb=None):
87
self.to_branch = to_branch
88
self.to_weaves = to_branch.weave_store
89
self.from_branch = from_branch
90
self.from_weaves = from_branch.weave_store
91
self.failed_revisions = []
96
self.pb = bzrlib.ui.ui_factory.progress_bar()
99
self.last_revision = self._find_last_revision(last_revision)
100
mutter('fetch up to rev {%s}', self.last_revision)
101
revs_to_fetch = self._compare_ancestries()
102
self._copy_revisions(revs_to_fetch)
103
self.new_ancestry = revs_to_fetch
107
def _find_last_revision(self, last_revision):
108
"""Find the limiting source revision.
110
Every ancestor of that revision will be merged across.
112
Returns the revision_id, or returns None if there's no history
113
in the source branch."""
114
self.pb.update('get source history')
115
from_history = self.from_branch.revision_history()
116
self.pb.update('get destination history')
118
if last_revision not in from_history:
119
raise NoSuchRevision(self.from_branch, last_revision)
123
return from_history[-1]
125
return None # no history in the source branch
128
def _compare_ancestries(self):
129
"""Get a list of revisions that must be copied.
131
That is, every revision that's in the ancestry of the source
132
branch and not in the destination branch."""
133
self.pb.update('get source ancestry')
134
self.from_ancestry = self.from_branch.get_ancestry(self.last_revision)
136
dest_last_rev = self.to_branch.last_revision()
137
self.pb.update('get destination ancestry')
139
dest_ancestry = self.to_branch.get_ancestry(dest_last_rev)
142
ss = set(dest_ancestry)
144
for rev_id in self.from_ancestry:
146
to_fetch.append(rev_id)
147
mutter('need to get revision {%s}', rev_id)
148
mutter('need to get %d revisions in total', len(to_fetch))
149
self.count_total = len(to_fetch)
154
def _copy_revisions(self, revs_to_fetch):
156
for rev_id in revs_to_fetch:
158
if self.to_branch.has_revision(rev_id):
160
self.pb.update('fetch revision', i, self.count_total)
161
self._copy_one_revision(rev_id)
162
self.count_copied += 1
165
def _copy_one_revision(self, rev_id):
166
"""Copy revision and everything referenced by it."""
167
mutter('copying revision {%s}', rev_id)
168
rev_xml = self.from_branch.get_revision_xml(rev_id)
169
inv_xml = self.from_branch.get_inventory_xml(rev_id)
170
rev = serializer_v5.read_revision_from_string(rev_xml)
171
inv = serializer_v5.read_inventory_from_string(inv_xml)
172
assert rev.revision_id == rev_id
173
assert rev.inventory_sha1 == sha_string(inv_xml)
174
mutter(' commiter %s, %d parents',
177
self._copy_new_texts(rev_id, inv)
178
parent_ids = [x.revision_id for x in rev.parents]
179
self._copy_inventory(rev_id, inv_xml, parent_ids)
180
self._copy_ancestry(rev_id, parent_ids)
181
self.to_branch.revision_store.add(StringIO(rev_xml), rev_id)
184
def _copy_inventory(self, rev_id, inv_xml, parent_ids):
185
self.to_weaves.add_text(INVENTORY_FILEID, rev_id,
186
split_lines(inv_xml), parent_ids)
189
def _copy_ancestry(self, rev_id, parent_ids):
190
ancestry_lines = self.from_weaves.get_lines(ANCESTRY_FILEID, rev_id)
191
self.to_weaves.add_text(ANCESTRY_FILEID, rev_id, ancestry_lines,
195
def _copy_new_texts(self, rev_id, inv):
196
"""Copy any new texts occuring in this revision."""
197
# TODO: Rather than writing out weaves every time, hold them
198
# in memory until everything's done? But this way is nicer
199
# if it's interrupted.
200
for path, ie in inv.iter_entries():
201
if ie.kind != 'file':
203
if ie.text_version != rev_id:
205
mutter('%s {%s} is changed in this revision',
207
self._copy_one_text(rev_id, ie.file_id)
210
def _copy_one_text(self, rev_id, file_id):
211
"""Copy one file text."""
212
mutter('copy text version {%s} of file {%s}',
214
from_weave = self.from_weaves.get_weave(file_id)
215
from_idx = from_weave.lookup(rev_id)
216
from_parents = map(from_weave.idx_to_name, from_weave.parents(from_idx))
217
text_lines = from_weave.get(from_idx)
218
to_weave = self.to_weaves.get_weave_or_empty(file_id)
219
to_parents = map(to_weave.lookup, from_parents)
220
# it's ok to add even if the text is already there
221
to_weave.add(rev_id, to_parents, text_lines)
222
self.to_weaves.put_weave(file_id, to_weave)
223
self.count_texts += 1