Merge property changes from 0.4.
[jelmer/subvertpy.git] / fetch.py
1 # Copyright (C) 2005-2007 Jelmer Vernooij <jelmer@samba.org>
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 3 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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
16 """Fetching revisions from Subversion repositories in batches."""
17
18 import bzrlib
19 from bzrlib import osutils, ui, urlutils
20 from bzrlib.inventory import Inventory
21 from bzrlib.revision import Revision, NULL_REVISION
22 from bzrlib.repository import InterRepository
23 from bzrlib.trace import mutter
24
25 from cStringIO import StringIO
26 import md5
27
28 import constants
29
30 from bzrlib.plugins.svn.delta import apply_txdelta_handler
31 from bzrlib.plugins.svn import properties
32 from bzrlib.plugins.svn.errors import InvalidFileName
33 from bzrlib.plugins.svn.logwalker import lazy_dict
34 from bzrlib.plugins.svn.mapping import (SVN_PROP_BZR_MERGE, 
35                      SVN_PROP_BZR_PREFIX, SVN_PROP_BZR_REVISION_INFO, 
36                      SVN_PROP_BZR_REVISION_ID,
37                      SVN_PROP_BZR_FILEIDS, SVN_REVPROP_BZR_SIGNATURE,
38                      parse_merge_property,
39                      parse_revision_metadata)
40 from bzrlib.plugins.svn.repository import SvnRepository, SvnRepositoryFormat
41 from bzrlib.plugins.svn.svk import SVN_PROP_SVK_MERGE
42 from bzrlib.plugins.svn.tree import (parse_externals_description, 
43                   inventory_add_external)
44
45
46 def _escape_commit_message(message):
47     """Replace xml-incompatible control characters."""
48     if message is None:
49         return None
50     import re
51     # FIXME: RBC 20060419 this should be done by the revision
52     # serialiser not by commit. Then we can also add an unescaper
53     # in the deserializer and start roundtripping revision messages
54     # precisely. See repository_implementations/test_repository.py
55     
56     # Python strings can include characters that can't be
57     # represented in well-formed XML; escape characters that
58     # aren't listed in the XML specification
59     # (http://www.w3.org/TR/REC-xml/#NT-Char).
60     message, _ = re.subn(
61         u'[^\x09\x0A\x0D\u0020-\uD7FF\uE000-\uFFFD]+',
62         lambda match: match.group(0).encode('unicode_escape'),
63         message)
64     return message
65
66
67 def md5_strings(strings):
68     """Return the MD5sum of the concatenation of strings.
69
70     :param strings: Strings to find the MD5sum of.
71     :return: MD5sum
72     """
73     s = md5.new()
74     map(s.update, strings)
75     return s.hexdigest()
76
77
78 def check_filename(path):
79     """Check that a path does not contain invalid characters.
80
81     :param path: Path to check
82     :raises InvalidFileName:
83     """
84     assert isinstance(path, unicode)
85     if u"\\" in path:
86         raise InvalidFileName(path)
87
88
89 class RevisionBuildEditor:
90     """Implementation of the Subversion commit editor interface that builds a 
91     Bazaar revision.
92     """
93     def __init__(self, source, target):
94         self.target = target
95         self.source = source
96         self.transact = target.get_transaction()
97
98     def set_target_revision(self, target_revision):
99         pass
100
101     def start_revision(self, revid, prev_inventory, revmeta):
102         self.revid = revid
103         (self.branch_path, self.revnum, self.mapping) = self.source.lookup_revision_id(revid)
104         self.revmeta = revmeta
105         self._id_map = None
106         self.dir_baserev = {}
107         self._revinfo = None
108         self._premature_deletes = set()
109         self.old_inventory = prev_inventory
110         self.inventory = prev_inventory.copy()
111         self._start_revision()
112
113     def _get_id_map(self):
114         if self._id_map is not None:
115             return self._id_map
116
117         renames = self.mapping.import_fileid_map(self.revmeta.revprops, self.revmeta.fileprops)
118         self._id_map = self.source.transform_fileid_map(self.source.uuid, 
119                               self.revnum, self.branch_path, self.revmeta.paths, renames, 
120                               self.mapping)
121
122         return self._id_map
123
124     def _get_revision(self, revid):
125         """Creates the revision object.
126
127         :param revid: Revision id of the revision to create.
128         """
129
130         # Commit SVN revision properties to a Revision object
131         rev = Revision(revision_id=revid, parent_ids=self.revmeta.get_parent_ids(self.mapping))
132
133         self.mapping.import_revision(self.revmeta.revprops, self.revmeta.fileprops, rev)
134
135         signature = self.revmeta.revprops.get(SVN_REVPROP_BZR_SIGNATURE)
136
137         return (rev, signature)
138
139     def open_root(self, base_revnum):
140         if self.old_inventory.root is None:
141             # First time the root is set
142             old_file_id = None
143             file_id = self.mapping.generate_file_id(self.source.uuid, self.revnum, self.branch_path, u"")
144             file_parents = []
145         else:
146             assert self.old_inventory.root.revision is not None
147             old_file_id = self.old_inventory.root.file_id
148             file_id = self._get_id_map().get("", old_file_id)
149             file_parents = [self.old_inventory.root.revision]
150
151         if self.inventory.root is not None and \
152                 file_id == self.inventory.root.file_id:
153             ie = self.inventory.root
154         else:
155             ie = self.inventory.add_path("", 'directory', file_id)
156         ie.revision = self.revid
157         return DirectoryBuildEditor(self, old_file_id, file_id, file_parents)
158
159     def close(self):
160         pass
161
162     def _store_directory(self, file_id, parents):
163         raise NotImplementedError(self._store_directory)
164
165     def _get_file_data(self, file_id, revid):
166         raise NotImplementedError(self._get_file_data)
167
168     def _finish_commit(self):
169         raise NotImplementedError(self._finish_commit)
170
171     def abort(self):
172         pass
173
174     def _start_revision(self):
175         pass
176
177     def _store_file(self, file_id, lines, parents):
178         raise NotImplementedError(self._store_file)
179
180     def _get_existing_id(self, old_parent_id, new_parent_id, path):
181         assert isinstance(path, unicode)
182         assert isinstance(old_parent_id, str)
183         assert isinstance(new_parent_id, str)
184         ret = self._get_id_map().get(path)
185         if ret is not None:
186             return ret
187         return self.old_inventory[old_parent_id].children[urlutils.basename(path)].file_id
188
189     def _get_old_id(self, parent_id, old_path):
190         assert isinstance(old_path, unicode)
191         assert isinstance(parent_id, str)
192         return self.old_inventory[parent_id].children[urlutils.basename(old_path)].file_id
193
194     def _get_new_id(self, parent_id, new_path):
195         assert isinstance(new_path, unicode)
196         assert isinstance(parent_id, str)
197         ret = self._get_id_map().get(new_path)
198         if ret is not None:
199             return ret
200         return self.mapping.generate_file_id(self.source.uuid, self.revnum, 
201                                              self.branch_path, new_path)
202
203     def _rename(self, file_id, parent_id, path):
204         assert isinstance(path, unicode)
205         assert isinstance(parent_id, str)
206         # Only rename if not right yet
207         if (self.inventory[file_id].parent_id == parent_id and 
208             self.inventory[file_id].name == urlutils.basename(path)):
209             return
210         self.inventory.rename(file_id, parent_id, urlutils.basename(path))
211
212 class DirectoryBuildEditor:
213     def __init__(self, editor, old_id, new_id, parent_revids=[]):
214         self.editor = editor
215         self.old_id = old_id
216         self.new_id = new_id
217         self.parent_revids = parent_revids
218
219     def close(self):
220         self.editor.inventory[self.new_id].revision = self.editor.revid
221         self.editor._store_directory(self.new_id, self.parent_revids)
222
223         if self.new_id == self.editor.inventory.root.file_id:
224             assert len(self.editor._premature_deletes) == 0
225             self.editor._finish_commit()
226
227     def add_directory(self, path, copyfrom_path=None, copyfrom_revnum=-1):
228         assert isinstance(path, str)
229         path = path.decode("utf-8")
230         check_filename(path)
231         file_id = self.editor._get_new_id(self.new_id, path)
232
233         if file_id in self.editor.inventory:
234             # This directory was moved here from somewhere else, but the 
235             # other location hasn't been removed yet. 
236             if copyfrom_path is None:
237                 # This should ideally never happen!
238                 copyfrom_path = self.editor.old_inventory.id2path(file_id)
239                 mutter('no copyfrom path set, assuming %r' % copyfrom_path)
240             assert copyfrom_path == self.editor.old_inventory.id2path(file_id)
241             assert copyfrom_path not in self.editor._premature_deletes
242             self.editor._premature_deletes.add(copyfrom_path)
243             self.editor._rename(file_id, self.new_id, path)
244             ie = self.editor.inventory[file_id]
245             old_file_id = file_id
246         else:
247             old_file_id = None
248             ie = self.editor.inventory.add_path(path, 'directory', file_id)
249         ie.revision = self.editor.revid
250
251         return DirectoryBuildEditor(self.editor, old_file_id, file_id)
252
253     def open_directory(self, path, base_revnum):
254         assert isinstance(path, str)
255         path = path.decode("utf-8")
256         assert isinstance(base_revnum, int)
257         base_file_id = self.editor._get_old_id(self.old_id, path)
258         base_revid = self.editor.old_inventory[base_file_id].revision
259         file_id = self.editor._get_existing_id(self.old_id, self.new_id, path)
260         if file_id == base_file_id:
261             file_parents = [base_revid]
262             ie = self.editor.inventory[file_id]
263         else:
264             # Replace if original was inside this branch
265             # change id of base_file_id to file_id
266             ie = self.editor.inventory[base_file_id]
267             for name in ie.children:
268                 ie.children[name].parent_id = file_id
269             # FIXME: Don't touch inventory internals
270             del self.editor.inventory._byid[base_file_id]
271             self.editor.inventory._byid[file_id] = ie
272             ie.file_id = file_id
273             file_parents = []
274         ie.revision = self.editor.revid
275         return DirectoryBuildEditor(self.editor, base_file_id, file_id, 
276                                     file_parents)
277
278     def change_prop(self, name, value):
279         if self.new_id == self.editor.inventory.root.file_id:
280             # Replay lazy_dict, since it may be more expensive
281             if type(self.editor.revmeta.fileprops) != dict:
282                 self.editor.revmeta.fileprops = {}
283             self.editor.revmeta.fileprops[name] = value
284
285         if name in (properties.PROP_ENTRY_COMMITTED_DATE,
286                     properties.PROP_ENTRY_COMMITTED_REV,
287                     properties.PROP_ENTRY_LAST_AUTHOR,
288                     properties.PROP_ENTRY_LOCK_TOKEN,
289                     properties.PROP_ENTRY_UUID,
290                     properties.PROP_EXECUTABLE):
291             pass
292         elif (name.startswith(properties.PROP_WC_PREFIX)):
293             pass
294         elif name.startswith(properties.PROP_PREFIX):
295             mutter('unsupported dir property %r' % name)
296
297     def add_file(self, path, copyfrom_path=None, copyfrom_revnum=-1):
298         assert isinstance(path, str)
299         path = path.decode("utf-8")
300         check_filename(path)
301         file_id = self.editor._get_new_id(self.new_id, path)
302         if file_id in self.editor.inventory:
303             # This file was moved here from somewhere else, but the 
304             # other location hasn't been removed yet. 
305             if copyfrom_path is None:
306                 # This should ideally never happen
307                 copyfrom_path = self.editor.old_inventory.id2path(file_id)
308                 mutter('no copyfrom path set, assuming %r' % copyfrom_path)
309             assert copyfrom_path == self.editor.old_inventory.id2path(file_id)
310             assert copyfrom_path not in self.editor._premature_deletes
311             self.editor._premature_deletes.add(copyfrom_path)
312             # No need to rename if it's already in the right spot
313             self.editor._rename(file_id, self.new_id, path)
314         return FileBuildEditor(self.editor, path, file_id)
315
316     def open_file(self, path, base_revnum):
317         assert isinstance(path, str)
318         path = path.decode("utf-8")
319         base_file_id = self.editor._get_old_id(self.old_id, path)
320         base_revid = self.editor.old_inventory[base_file_id].revision
321         file_id = self.editor._get_existing_id(self.old_id, self.new_id, path)
322         is_symlink = (self.editor.inventory[base_file_id].kind == 'symlink')
323         file_data = self.editor._get_file_data(base_file_id, base_revid)
324         if file_id == base_file_id:
325             file_parents = [base_revid]
326         else:
327             # Replace
328             del self.editor.inventory[base_file_id]
329             file_parents = []
330         return FileBuildEditor(self.editor, path, file_id, 
331                                file_parents, file_data, is_symlink=is_symlink)
332
333     def delete_entry(self, path, revnum):
334         assert isinstance(path, str)
335         path = path.decode("utf-8")
336         if path in self.editor._premature_deletes:
337             # Delete recursively
338             self.editor._premature_deletes.remove(path)
339             for p in self.editor._premature_deletes.copy():
340                 if p.startswith("%s/" % path):
341                     self.editor._premature_deletes.remove(p)
342         else:
343             self.editor.inventory.remove_recursive_id(self.editor._get_old_id(self.old_id, path))
344
345 class FileBuildEditor:
346     def __init__(self, editor, path, file_id, file_parents=[], data="", 
347                  is_symlink=False):
348         self.path = path
349         self.editor = editor
350         self.file_id = file_id
351         self.file_data = data
352         self.is_symlink = is_symlink
353         self.file_parents = file_parents
354         self.is_executable = None
355         self.file_stream = None
356
357     def apply_textdelta(self, base_checksum=None):
358         actual_checksum = md5.new(self.file_data).hexdigest()
359         assert (base_checksum is None or base_checksum == actual_checksum,
360             "base checksum mismatch: %r != %r" % (base_checksum, 
361                                                   actual_checksum))
362         self.file_stream = StringIO()
363         return apply_txdelta_handler(self.file_data, self.file_stream)
364
365     def change_prop(self, name, value):
366         if name == constants.PROP_EXECUTABLE: 
367             # You'd expect executable to match 
368             # constants.PROP_EXECUTABLE_VALUE, but that's not 
369             # how SVN behaves. It appears to consider the presence 
370             # of the property sufficient to mark it executable.
371             self.is_executable = (value != None)
372         elif (name == properties.PROP_SPECIAL):
373             self.is_symlink = (value != None)
374         elif name == properties.PROP_ENTRY_COMMITTED_REV:
375             self.last_file_rev = int(value)
376         elif name in (properties.PROP_ENTRY_COMMITTED_DATE,
377                       properties.PROP_ENTRY_LAST_AUTHOR,
378                       properties.PROP_ENTRY_LOCK_TOKEN,
379                       properties.PROP_ENTRY_UUID,
380                       properties.PROP_MIME_TYPE):
381             pass
382         elif name.startswith(properties.PROP_WC_PREFIX):
383             pass
384         elif name == properties.PROP_EXTERNALS:
385             mutter('svn:externals property on file!')
386         elif (name.startswith(properties.PROP_PREFIX) or
387               name.startswith(SVN_PROP_BZR_PREFIX)):
388             mutter('unsupported file property %r' % name)
389
390     def close(self, checksum=None):
391         assert isinstance(self.path, unicode)
392         if self.file_stream is not None:
393             self.file_stream.seek(0)
394             lines = osutils.split_lines(self.file_stream.read())
395         else:
396             # Data didn't change or file is new
397             lines = osutils.split_lines(self.file_data)
398
399         actual_checksum = md5_strings(lines)
400         assert checksum is None or checksum == actual_checksum
401
402         self.editor._store_file(self.file_id, lines, self.file_parents)
403
404         assert self.is_symlink in (True, False)
405
406         if self.file_id in self.editor.inventory:
407             del self.editor.inventory[self.file_id]
408
409         if self.is_symlink:
410             ie = self.editor.inventory.add_path(self.path, 'symlink', self.file_id)
411             ie.symlink_target = lines[0][len("link "):]
412             ie.text_sha1 = None
413             ie.text_size = None
414             ie.executable = False
415             ie.revision = self.editor.revid
416         else:
417             ie = self.editor.inventory.add_path(self.path, 'file', self.file_id)
418             ie.revision = self.editor.revid
419             ie.kind = 'file'
420             ie.symlink_target = None
421             ie.text_sha1 = osutils.sha_strings(lines)
422             ie.text_size = sum(map(len, lines))
423             assert ie.text_size is not None
424             if self.is_executable is not None:
425                 ie.executable = self.is_executable
426
427         self.file_stream = None
428
429
430 class WeaveRevisionBuildEditor(RevisionBuildEditor):
431     """Subversion commit editor that can write to a weave-based repository.
432     """
433     def __init__(self, source, target):
434         RevisionBuildEditor.__init__(self, source, target)
435         self.weave_store = target.weave_store
436
437     def _start_revision(self):
438         self._write_group_active = True
439         self.target.start_write_group()
440
441     def _store_directory(self, file_id, parents):
442         file_weave = self.weave_store.get_weave_or_empty(file_id, self.transact)
443         if not file_weave.has_version(self.revid):
444             file_weave.add_lines(self.revid, parents, [])
445
446     def _get_file_data(self, file_id, revid):
447         file_weave = self.weave_store.get_weave_or_empty(file_id, self.transact)
448         return file_weave.get_text(revid)
449
450     def _store_file(self, file_id, lines, parents):
451         file_weave = self.weave_store.get_weave_or_empty(file_id, self.transact)
452         if not file_weave.has_version(self.revid):
453             file_weave.add_lines(self.revid, parents, lines)
454
455     def _finish_commit(self):
456         (rev, signature) = self._get_revision(self.revid)
457         self.inventory.revision_id = self.revid
458         # Escaping the commit message is really the task of the serialiser
459         rev.message = _escape_commit_message(rev.message)
460         rev.inventory_sha1 = None
461         self.target.add_revision(self.revid, rev, self.inventory)
462         if signature is not None:
463             self.target.add_signature_text(self.revid, signature)
464         self.target.commit_write_group()
465         self._write_group_active = False
466
467     def abort(self):
468         if self._write_group_active:
469             self.target.abort_write_group()
470             self._write_group_active = False
471
472
473 class PackRevisionBuildEditor(WeaveRevisionBuildEditor):
474     """Revision Build Editor for Subversion that is specific for the packs API.
475     """
476     def __init__(self, source, target):
477         WeaveRevisionBuildEditor.__init__(self, source, target)
478
479     def _add_text_to_weave(self, file_id, new_lines, parents):
480         return self.target._packs._add_text_to_weave(file_id,
481             self.revid, new_lines, parents, nostore_sha=None, 
482             random_revid=False)
483
484     def _store_directory(self, file_id, parents):
485         self._add_text_to_weave(file_id, [], parents)
486
487     def _store_file(self, file_id, lines, parents):
488         self._add_text_to_weave(file_id, lines, parents)
489
490
491 class CommitBuilderRevisionBuildEditor(RevisionBuildEditor):
492     """Revision Build Editor for Subversion that uses the CommitBuilder API.
493     """
494     def __init__(self, source, target):
495         RevisionBuildEditor.__init__(self, source, target)
496         raise NotImplementedError(self)
497
498
499 def get_revision_build_editor(repository):
500     """Obtain a RevisionBuildEditor for a particular target repository.
501     
502     :param repository: Repository to obtain the buildeditor for.
503     :return: Class object of class descending from RevisionBuildEditor
504     """
505     if hasattr(repository, '_packs'):
506         return PackRevisionBuildEditor
507     return WeaveRevisionBuildEditor
508
509
510 class InterFromSvnRepository(InterRepository):
511     """Svn to any repository actions."""
512
513     _matching_repo_format = SvnRepositoryFormat()
514
515     _supports_branches = True
516
517     @staticmethod
518     def _get_repo_format_to_test():
519         return None
520
521     def _find_all(self, mapping, pb=None):
522         """Find all revisions from the source repository that are not 
523         yet in the target repository.
524         """
525         parents = {}
526         meta_map = {}
527         graph = self.source.get_graph()
528         available_revs = set()
529         for revmeta in self.source.iter_all_changes(pb=pb):
530             revid = revmeta.get_revision_id(mapping)
531             available_revs.add(revid)
532             meta_map[revid] = revmeta
533         missing = available_revs.difference(self.target.has_revisions(available_revs))
534         needed = list(graph.iter_topo_order(missing))
535         parents = graph.get_parent_map(needed)
536         return [(revid, parents[revid][0], meta_map[revid]) for revid in needed]
537
538     def _find_branches(self, branches, find_ghosts=False, fetch_rhs_ancestry=False, pb=None):
539         set_needed = set()
540         ret_needed = list()
541         for revid in branches:
542             if pb:
543                 pb.update("determining revisions to fetch", branches.index(revid), len(branches))
544             try:
545                 nestedpb = ui.ui_factory.nested_progress_bar()
546                 for rev in self._find_until(revid, find_ghosts=find_ghosts, fetch_rhs_ancestry=False,
547                                             pb=nestedpb):
548                     if rev[0] not in set_needed:
549                         ret_needed.append(rev)
550                         set_needed.add(rev[0])
551             finally:
552                 nestedpb.finished()
553         return ret_needed
554
555     def _find_until(self, revision_id, find_ghosts=False, fetch_rhs_ancestry=False, pb=None):
556         """Find all missing revisions until revision_id
557
558         :param revision_id: Stop revision
559         :param find_ghosts: Find ghosts
560         :param fetch_rhs_ancestry: Fetch right hand side ancestors
561         :return: Tuple with revisions missing and a dictionary with 
562             parents for those revision.
563         """
564         extra = set()
565         needed = []
566         revs = []
567         meta_map = {}
568         lhs_parent = {}
569         def check_revid(revision_id):
570             prev = None
571             (branch_path, revnum, mapping) = self.source.lookup_revision_id(revision_id)
572             for revmeta in self.source.iter_reverse_branch_changes(branch_path, revnum, mapping):
573                 if pb:
574                     pb.update("determining revisions to fetch", revnum-revmeta.revnum, revnum)
575                 revid = revmeta.get_revision_id(mapping)
576                 lhs_parent[prev] = revid
577                 meta_map[revid] = revmeta
578                 if fetch_rhs_ancestry:
579                     extra.update(revmeta.get_rhs_parents(mapping))
580                 if not self.target.has_revision(revid):
581                     revs.append(revid)
582                 elif not find_ghosts:
583                     prev = None
584                     break
585                 prev = revid
586             lhs_parent[prev] = NULL_REVISION
587
588         check_revid(revision_id)
589
590         for revid in extra:
591             if revid not in revs:
592                 check_revid(revid)
593
594         needed = [(revid, lhs_parent[revid], meta_map[revid]) for revid in reversed(revs)]
595
596         return needed
597
598     def copy_content(self, revision_id=None, pb=None):
599         """See InterRepository.copy_content."""
600         self.fetch(revision_id, pb, find_ghosts=False)
601
602     def _fetch_switch(self, repos_root, revids, pb=None):
603         """Copy a set of related revisions using svn.ra.switch.
604
605         :param revids: List of revision ids of revisions to copy, 
606                        newest first.
607         :param pb: Optional progress bar.
608         """
609         prev_revid = None
610         if pb is None:
611             pb = ui.ui_factory.nested_progress_bar()
612             nested_pb = pb
613         else:
614             nested_pb = None
615         num = 0
616         prev_inv = None
617
618         self.target.lock_write()
619         revbuildklass = get_revision_build_editor(self.target)
620         editor = revbuildklass(self.source, self.target)
621
622         try:
623             for (revid, parent_revid, revmeta) in revids:
624                 pb.update('copying revision', num, len(revids))
625
626                 assert parent_revid is not None
627
628                 if parent_revid == NULL_REVISION:
629                     parent_inv = Inventory(root_id=None)
630                 elif prev_revid != parent_revid:
631                     parent_inv = self.target.get_inventory(parent_revid)
632                 else:
633                     parent_inv = prev_inv
634
635                 editor.start_revision(revid, parent_inv, revmeta)
636
637                 try:
638                     conn = None
639                     try:
640                         if parent_revid == NULL_REVISION:
641                             branch_url = urlutils.join(repos_root, 
642                                                        editor.branch_path)
643
644                             conn = self.source.transport.connections.get(branch_url)
645                             reporter = conn.do_update(editor.revnum, "", True, 
646                                                            editor)
647
648                             try:
649                                 # Report status of existing paths
650                                 reporter.set_path("", editor.revnum, True, None)
651                             except:
652                                 reporter.abort()
653                                 raise
654                         else:
655                             (parent_branch, parent_revnum, mapping) = \
656                                     self.source.lookup_revision_id(parent_revid)
657                             conn = self.source.transport.connections.get(urlutils.join(repos_root, parent_branch))
658
659                             if parent_branch != editor.branch_path:
660                                 reporter = conn.do_switch(editor.revnum, "", True, 
661                                     urlutils.join(repos_root, editor.branch_path), 
662                                     editor)
663                             else:
664                                 reporter = conn.do_update(editor.revnum, "", True, editor)
665
666                             try:
667                                 # Report status of existing paths
668                                 reporter.set_path("", parent_revnum, False, None)
669                             except:
670                                 reporter.abort()
671                                 raise
672
673                         reporter.finish()
674                     finally:
675                         if conn is not None:
676                             self.source.transport.add_connection(conn)
677                 except:
678                     editor.abort()
679                     raise
680
681                 prev_inv = editor.inventory
682                 prev_revid = revid
683                 num += 1
684         finally:
685             self.target.unlock()
686             if nested_pb is not None:
687                 nested_pb.finished()
688
689     def fetch(self, revision_id=None, pb=None, find_ghosts=False, 
690               branches=None, fetch_rhs_ancestry=False):
691         """Fetch revisions. """
692         if revision_id == NULL_REVISION:
693             return
694
695         self._supports_replay = True # assume replay supported by default
696         # Dictionary with paths as keys, revnums as values
697
698         if pb:
699             pb.update("determining revisions to fetch", 0, 2)
700
701         # Loop over all the revnums until revision_id
702         # (or youngest_revnum) and call self.target.add_revision() 
703         # or self.target.add_inventory() each time
704         self.target.lock_read()
705         try:
706             if branches is not None:
707                 needed = self._find_branches(branches, find_ghosts, fetch_rhs_ancestry, pb=pb)
708             elif revision_id is None:
709                 needed = self._find_all(self.source.get_mapping(), pb=pb)
710             else:
711                 needed = self._find_until(revision_id, find_ghosts, fetch_rhs_ancestry, pb=pb)
712         finally:
713             self.target.unlock()
714
715         if len(needed) == 0:
716             # Nothing to fetch
717             return
718
719         self._fetch_switch(self.source.transport.get_svn_repos_root(), needed, pb)
720
721     @staticmethod
722     def is_compatible(source, target):
723         """Be compatible with SvnRepository."""
724         # FIXME: Also check target uses VersionedFile
725         return isinstance(source, SvnRepository) and target.supports_rich_root()
726