1 # Copyright (C) 2005-2007 Jelmer Vernooij <jelmer@samba.org>
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 bzrlib.inventory import Inventory
19 import bzrlib.osutils as osutils
20 from bzrlib.revision import Revision
21 from bzrlib.repository import InterRepository
22 from bzrlib.trace import mutter
23 import bzrlib.ui as ui
26 from cStringIO import StringIO
30 from svn.core import SubversionException, Pool
33 from fileids import generate_file_id
34 from repository import (SvnRepository, SVN_PROP_BZR_MERGE, SVN_PROP_SVK_MERGE,
35 SVN_PROP_BZR_PREFIX, SVN_PROP_BZR_REVPROP_PREFIX,
37 from tree import apply_txdelta_handler
40 def md5_strings(strings):
42 map(s.update, strings)
46 class RevisionBuildEditor(svn.delta.Editor):
47 """Implementation of the Subversion commit editor interface that builds a
50 def __init__(self, source, target, branch_path, prev_inventory, revid,
51 svn_revprops, id_map):
52 self.branch_path = branch_path
53 self.old_inventory = prev_inventory
54 self.inventory = copy(prev_inventory)
59 self.transact = target.get_transaction()
60 self.weave_store = target.weave_store
62 self._parent_ids = None
64 self._svn_revprops = svn_revprops
67 def _get_revision(self, revid):
68 """Creates the revision object.
70 :param revid: Revision id of the revision to create.
72 if self._parent_ids is None:
75 parent_ids = self.source.revision_parents(revid, self._parent_ids)
77 # Commit SVN revision properties to a Revision object
78 rev = Revision(revision_id=revid, parent_ids=parent_ids)
80 if self._svn_revprops[2] is not None:
81 rev.timestamp = 1.0 * svn.core.secs_from_timestr(
82 self._svn_revprops[2], None) #date
84 rev.timestamp = 0 # FIXME: Obtain repository creation time
87 rev.committer = self._svn_revprops[0] # author
88 if rev.committer is None:
90 rev.message = self._svn_revprops[1] # message
92 rev.properties = self._revprops
95 def open_root(self, base_revnum, baton):
96 if self.old_inventory.root is None:
97 # First time the root is set
98 file_id = generate_file_id(self.revid, "")
99 self.dir_baserev[file_id] = []
101 assert self.old_inventory.root.revision is not None
102 if self.id_map.has_key(""):
103 file_id = self.id_map[""]
105 file_id = self.old_inventory.root.file_id
106 self.dir_baserev[file_id] = [self.old_inventory.root.revision]
108 if self.inventory.root is not None and \
109 file_id == self.inventory.root.file_id:
110 ie = self.inventory.root
112 ie = self.inventory.add_path("", 'directory', file_id)
113 ie.revision = self.revid
116 def _get_existing_id(self, parent_id, path):
117 if self.id_map.has_key(path):
118 return self.id_map[path]
119 return self._get_old_id(parent_id, path)
121 def _get_old_id(self, parent_id, old_path):
122 return self.old_inventory[parent_id].children[os.path.basename(old_path)].file_id
124 def _get_new_id(self, parent_id, new_path):
125 if self.id_map.has_key(new_path):
126 return self.id_map[new_path]
127 return generate_file_id(self.revid, new_path)
129 def delete_entry(self, path, revnum, parent_id, pool):
130 path = path.decode("utf-8")
131 del self.inventory[self._get_old_id(parent_id, path)]
133 def close_directory(self, id):
134 self.inventory[id].revision = self.revid
136 file_weave = self.weave_store.get_weave_or_empty(id, self.transact)
137 if not file_weave.has_version(self.revid):
138 file_weave.add_lines(self.revid, self.dir_baserev[id], [])
140 def add_directory(self, path, parent_id, copyfrom_path, copyfrom_revnum,
142 path = path.decode("utf-8")
143 file_id = self._get_new_id(parent_id, path)
145 self.dir_baserev[file_id] = []
146 ie = self.inventory.add_path(path, 'directory', file_id)
147 ie.revision = self.revid
151 def open_directory(self, path, parent_id, base_revnum, pool):
152 assert base_revnum >= 0
153 base_file_id = self._get_old_id(parent_id, path)
154 base_revid = self.old_inventory[base_file_id].revision
155 file_id = self._get_existing_id(parent_id, path)
156 if file_id == base_file_id:
157 self.dir_baserev[file_id] = [base_revid]
158 ie = self.inventory[file_id]
160 # Replace if original was inside this branch
161 # change id of base_file_id to file_id
162 ie = self.inventory[base_file_id]
163 for name in ie.children:
164 ie.children[name].parent_id = file_id
165 # FIXME: Don't touch inventory internals
166 del self.inventory._byid[base_file_id]
167 self.inventory._byid[file_id] = ie
169 self.dir_baserev[file_id] = []
170 ie.revision = self.revid
173 def change_dir_prop(self, id, name, value, pool):
174 if name == SVN_PROP_BZR_MERGE:
175 if id != self.inventory.root.file_id:
176 mutter('rogue %r on non-root directory' % SVN_PROP_BZR_MERGE)
179 self._parent_ids = value.splitlines()[-1]
180 elif name == SVN_PROP_SVK_MERGE:
181 if self._parent_ids is None:
182 # Only set parents using svk:merge if no
185 elif name.startswith(SVN_PROP_BZR_REVPROP_PREFIX):
186 self._revprops[name[len(SVN_PROP_BZR_REVPROP_PREFIX):]] = value
187 elif name in (svn.core.SVN_PROP_ENTRY_COMMITTED_DATE,
188 svn.core.SVN_PROP_ENTRY_COMMITTED_REV,
189 svn.core.SVN_PROP_ENTRY_LAST_AUTHOR,
190 svn.core.SVN_PROP_ENTRY_LOCK_TOKEN,
191 svn.core.SVN_PROP_ENTRY_UUID,
192 svn.core.SVN_PROP_EXECUTABLE):
194 elif name.startswith(svn.core.SVN_PROP_WC_PREFIX):
196 elif (name.startswith(svn.core.SVN_PROP_PREFIX) or
197 name.startswith(SVN_PROP_BZR_PREFIX)):
198 mutter('unsupported file property %r' % name)
200 def change_file_prop(self, id, name, value, pool):
201 if name == svn.core.SVN_PROP_EXECUTABLE:
202 # You'd expect executable to match
203 # svn.core.SVN_PROP_EXECUTABLE_VALUE, but that's not
204 # how SVN behaves. It appears to consider the presence
205 # of the property sufficient to mark it executable.
206 self.is_executable = (value != None)
207 elif (name == svn.core.SVN_PROP_SPECIAL):
208 self.is_symlink = (value != None)
209 elif name == svn.core.SVN_PROP_ENTRY_COMMITTED_REV:
210 self.last_file_rev = int(value)
211 elif name in (svn.core.SVN_PROP_ENTRY_COMMITTED_DATE,
212 svn.core.SVN_PROP_ENTRY_LAST_AUTHOR,
213 svn.core.SVN_PROP_ENTRY_LOCK_TOKEN,
214 svn.core.SVN_PROP_ENTRY_UUID,
215 svn.core.SVN_PROP_MIME_TYPE):
217 elif name.startswith(svn.core.SVN_PROP_WC_PREFIX):
219 elif (name.startswith(svn.core.SVN_PROP_PREFIX) or
220 name.startswith(SVN_PROP_BZR_PREFIX)):
221 mutter('unsupported file property %r' % name)
223 def add_file(self, path, parent_id, copyfrom_path, copyfrom_revnum, baton):
224 path = path.decode("utf-8")
225 self.is_symlink = False
226 self.is_executable = None
228 self.file_parents = []
229 self.file_stream = None
230 self.file_id = self._get_new_id(parent_id, path)
233 def open_file(self, path, parent_id, base_revnum, pool):
234 base_file_id = self._get_old_id(parent_id, path)
235 base_revid = self.old_inventory[base_file_id].revision
236 self.file_id = self._get_existing_id(parent_id, path)
237 self.is_executable = None
238 self.is_symlink = (self.inventory[base_file_id].kind == 'symlink')
239 file_weave = self.weave_store.get_weave_or_empty(base_file_id,
241 self.file_data = file_weave.get_text(base_revid)
242 self.file_stream = None
243 if self.file_id == base_file_id:
244 self.file_parents = [base_revid]
247 del self.inventory[base_file_id]
248 self.file_parents = []
251 def close_file(self, path, checksum):
252 if self.file_stream is not None:
253 self.file_stream.seek(0)
254 lines = osutils.split_lines(self.file_stream.read())
256 # Data didn't change or file is new
257 lines = osutils.split_lines(self.file_data)
259 actual_checksum = md5_strings(lines)
260 assert checksum is None or checksum == actual_checksum
262 file_weave = self.weave_store.get_weave_or_empty(self.file_id,
264 if not file_weave.has_version(self.revid):
265 file_weave.add_lines(self.revid, self.file_parents, lines)
267 if self.file_id in self.inventory:
268 ie = self.inventory[self.file_id]
269 elif self.is_symlink:
270 ie = self.inventory.add_path(path, 'symlink', self.file_id)
272 ie = self.inventory.add_path(path, 'file', self.file_id)
273 ie.revision = self.revid
276 ie.symlink_target = lines[0][len("link "):]
281 ie.text_sha1 = osutils.sha_strings(lines)
282 ie.text_size = sum(map(len, lines))
283 if self.is_executable is not None:
284 ie.executable = self.is_executable
286 self.file_stream = None
288 def close_edit(self):
289 rev = self._get_revision(self.revid)
290 self.inventory.revision_id = self.revid
291 rev.inventory_sha1 = osutils.sha_string(
292 bzrlib.xml5.serializer_v5.write_inventory_to_string(
294 self.target.add_revision(self.revid, rev, self.inventory)
297 def abort_edit(self):
300 def apply_textdelta(self, file_id, base_checksum):
301 actual_checksum = md5.new(self.file_data).hexdigest(),
302 assert (base_checksum is None or base_checksum == actual_checksum,
303 "base checksum mismatch: %r != %r" % (base_checksum,
305 self.file_stream = StringIO()
306 return apply_txdelta_handler(StringIO(self.file_data),
307 self.file_stream, self.pool)
310 class InterSvnRepository(InterRepository):
311 """Svn to any repository actions."""
313 _matching_repo_format = SvnRepositoryFormat()
316 def _get_repo_format_to_test():
322 for (branch, revnum) in self.source.follow_history(
323 self.source._latest_revnum):
324 revid = self.source.generate_revision_id(revnum, branch)
325 parents[revid] = self.source._mainline_revision_parent(branch,
328 if not self.target.has_revision(revid):
330 return (needed, parents)
332 def _find_until(self, revision_id):
335 (path, until_revnum) = self.source.parse_revision_id(revision_id)
338 for (branch, revnum) in self.source.follow_branch(path,
340 revid = self.source.generate_revision_id(revnum, branch)
342 if prev_revid is not None:
343 parents[prev_revid] = revid
347 if not self.target.has_revision(revid):
350 parents[prev_revid] = None
351 return (needed, parents)
353 def copy_content(self, revision_id=None, basis=None, pb=None):
354 """See InterRepository.copy_content."""
355 # Dictionary with paths as keys, revnums as values
357 # Loop over all the revnums until revision_id
358 # (or youngest_revnum) and call self.target.add_revision()
359 # or self.target.add_inventory() each time
362 self.target.lock_read()
364 if revision_id is None:
365 (needed, parents) = self._find_all()
367 (needed, parents) = self._find_until(revision_id)
375 repos_root = self.source.transport.get_repos_root()
379 transport = self.source.transport
380 self.target.lock_write()
382 pb = ui.ui_factory.nested_progress_bar()
390 (branch, revnum) = self.source.parse_revision_id(revid)
391 pb.update('copying revision', num, len(needed))
393 parent_revid = parents[revid]
395 if parent_revid is None:
396 parent_inv = Inventory(root_id=None)
397 elif prev_revid != parent_revid:
398 parent_inv = self.target.get_inventory(parent_revid)
400 assert prev_inv is not None
401 parent_inv = prev_inv
403 changes = self.source._log.get_revision_paths(revnum, branch)
404 renames = self.source.revision_fileid_renames(revid)
405 id_map = self.source.transform_fileid_map(self.source.uuid,
406 revnum, branch, changes, renames)
408 editor = RevisionBuildEditor(self.source, self.target, branch,
410 self.source._log.get_revision_info(revnum),
414 edit, edit_baton = svn.delta.make_editor(editor, pool)
416 if parent_revid is None:
417 transport.reparent("%s/%s" % (repos_root, branch))
418 reporter = transport.do_update(
419 revnum, "", True, edit, edit_baton, pool)
421 # Report status of existing paths
422 reporter.set_path("", revnum, True, None, pool)
424 (parent_branch, parent_revnum) = \
425 self.source.parse_revision_id(parent_revid)
426 transport.reparent("%s/%s" % (repos_root, parent_branch))
428 if parent_branch != branch:
429 switch_url = "%s/%s" % (repos_root, branch)
430 reporter = transport.do_switch(
432 switch_url, edit, edit_baton, pool)
434 reporter = transport.do_update(
435 revnum, "", True, edit, edit_baton, pool)
437 # Report status of existing paths
438 reporter.set_path("", parent_revnum, False, None, pool)
441 reporter.finish_report(pool)
444 prev_inv = editor.inventory
450 if nested_pb is not None:
452 self.source.transport.reparent(repos_root)
454 def fetch(self, revision_id=None, pb=None):
455 """Fetch revisions. """
456 self.copy_content(revision_id=revision_id, pb=pb)
459 def is_compatible(source, target):
460 """Be compatible with SvnRepository."""
461 # FIXME: Also check target uses VersionedFile
462 return isinstance(source, SvnRepository)