Handle another corner case: a file being removed from a directory that
[jelmer/subvertpy.git] / commit.py
1 # Copyright (C) 2006-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 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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
16 """Committing and pushing to Subversion repositories."""
17
18 import svn.delta
19 from svn.core import Pool, SubversionException
20
21 from bzrlib import osutils, urlutils
22 from bzrlib.branch import Branch
23 from bzrlib.errors import InvalidRevisionId, DivergedBranches
24 from bzrlib.inventory import Inventory
25 from bzrlib.repository import RootCommitBuilder, InterRepository
26 from bzrlib.revision import NULL_REVISION
27 from bzrlib.trace import mutter
28
29 import os
30 from repository import (SVN_PROP_BZR_ANCESTRY, SVN_PROP_BZR_FILEIDS,
31                         SVN_PROP_SVK_MERGE, SVN_PROP_BZR_REVISION_INFO, 
32                         SVN_PROP_BZR_REVISION_ID, revision_id_to_svk_feature,
33                         generate_revision_metadata, SvnRepositoryFormat, 
34                         SvnRepository)
35 from revids import escape_svn_path
36
37 from copy import deepcopy
38
39 def _check_dirs_exist(transport, bp_parts, base_rev):
40     for i in range(len(bp_parts), 0, -1):
41         current = bp_parts[:i]
42         if transport.check_path("/".join(current).strip("/"), base_rev) == svn.core.svn_node_dir:
43             return current
44     return []
45
46
47 class SvnCommitBuilder(RootCommitBuilder):
48     """Commit Builder implementation wrapped around svn_delta_editor. """
49
50     def __init__(self, repository, branch, parents, config, timestamp, 
51                  timezone, committer, revprops, revision_id, old_inv=None):
52         """Instantiate a new SvnCommitBuilder.
53
54         :param repository: SvnRepository to commit to.
55         :param branch: SvnBranch to commit to.
56         :param parents: List of parent revision ids.
57         :param config: Branch configuration to use.
58         :param timestamp: Optional timestamp recorded for commit.
59         :param timezone: Optional timezone for timestamp.
60         :param committer: Optional committer to set for commit.
61         :param revprops: Revision properties to set.
62         :param revision_id: Revision id for the new revision.
63         :param old_inv: Optional revision on top of which 
64             the commit is happening
65         """
66         super(SvnCommitBuilder, self).__init__(repository, parents, 
67             config, timestamp, timezone, committer, revprops, revision_id)
68         self.branch = branch
69         self.pool = Pool()
70
71         # Keep track of what Subversion properties to set later on
72         self._svnprops = {}
73         self._svnprops[SVN_PROP_BZR_REVISION_INFO] = generate_revision_metadata(
74             timestamp, timezone, committer, revprops)
75         self._svnprops[SVN_PROP_BZR_FILEIDS] = ""
76
77         # Gather information about revision on top of which the commit is 
78         # happening
79         (self.base_revno, self.base_revid) = self.branch.last_revision_info()
80         if self.base_revid is None:
81             self.base_revnum = -1
82             self.base_path = None
83             self.base_scheme = repository.get_scheme()
84         else:
85             (self.base_path, self.base_revnum, self.base_scheme) = \
86                 repository.lookup_revision_id(self.base_revid)
87
88         # Determine revisions merged in this one
89         merges = filter(lambda x: x != self.base_revid, parents)
90
91         if len(merges) > 0:
92             self._record_merges(merges)
93
94         # Set appropriate property if revision id was specified by 
95         # caller
96         if revision_id is not None:
97             self._record_revision_id(revision_id)
98
99         # At least one of the parents has to be the last revision on the 
100         # mainline in Subversion.
101         assert (self.base_revid is None or self.base_revid in parents)
102
103         if old_inv is None:
104             if self.base_revid is None:
105                 self.old_inv = Inventory(root_id=None)
106             else:
107                 self.old_inv = self.repository.get_inventory(self.base_revid)
108         else:
109             self.old_inv = old_inv
110             # Not all repositories appear to set Inventory.revision_id, 
111             # so allow None as well.
112             assert self.old_inv.revision_id in (None, self.base_revid)
113
114         self.modified_files = {}
115         self.modified_dirs = set()
116
117     def _record_revision_id(self, revid):
118         if self.base_revid is not None:
119             old = self.repository.branchprop_list.get_property(
120                     self.base_path, self.base_revnum, 
121                         SVN_PROP_BZR_REVISION_ID+str(self.base_scheme), "")
122         else:
123             old = ""
124
125         self._svnprops[SVN_PROP_BZR_REVISION_ID+str(self.base_scheme)] = \
126                 old + "%d %s\n" % (self.base_revno+1, revid)
127
128     def _record_merges(self, merges):
129         # Bazaar Parents
130         if self.base_revid is not None:
131             old = self.repository.branchprop_list.get_property(
132                   self.base_path, self.base_revnum, 
133                   SVN_PROP_BZR_ANCESTRY+str(self.base_scheme), "")
134         else:
135             old = ""
136         self._svnprops[SVN_PROP_BZR_ANCESTRY+str(self.base_scheme)] = old + "\t".join(merges) + "\n"
137
138         if self.base_revid is not None:
139             old = self.repository.branchprop_list.get_property(
140                 self.base_path, self.base_revnum, SVN_PROP_SVK_MERGE)
141         else:
142             old = ""
143
144         new = ""
145         # SVK compatibility
146         for p in merges:
147             try:
148                 new += "%s\n" % revision_id_to_svk_feature(p)
149             except InvalidRevisionId:
150                 pass
151
152         if new != "":
153             self._svnprops[SVN_PROP_SVK_MERGE] = old + new
154         
155     def _generate_revision_if_needed(self):
156         pass
157
158     def finish_inventory(self):
159         pass
160
161     def modified_file_text(self, file_id, file_parents,
162                            get_content_byte_lines, text_sha1=None,
163                            text_size=None):
164         new_lines = get_content_byte_lines()
165         self.modified_files[file_id] = "".join(new_lines)
166         return osutils.sha_strings(new_lines), sum(map(len, new_lines))
167
168     def modified_link(self, file_id, file_parents, link_target):
169         self.modified_files[file_id] = "link %s" % link_target
170
171     def modified_directory(self, file_id, file_parents):
172         self.modified_dirs.add(file_id)
173
174     def _file_process(self, file_id, contents, baton):
175         assert baton is not None
176         (txdelta, txbaton) = self.editor.apply_textdelta(baton, None, self.pool)
177         svn.delta.svn_txdelta_send_string(contents, txdelta, txbaton, self.pool)
178
179     def _dir_process(self, path, file_id, baton):
180         assert baton is not None
181         # Loop over entries of file_id in self.old_inv
182         # remove if they no longer exist with the same name
183         # or parents
184         if file_id in self.old_inv:
185             for child_name in self.old_inv[file_id].children:
186                 child_ie = self.old_inv.get_child(file_id, child_name)
187                 # remove if...
188                 if (
189                     # ... path no longer exists
190                     not child_ie.file_id in self.new_inventory or 
191                     # ... parent changed
192                     child_ie.parent_id != self.new_inventory[child_ie.file_id].parent_id or
193                     # ... name changed
194                     self.new_inventory[child_ie.file_id].name != child_name):
195                     mutter('removing %r' % child_ie.file_id)
196                     self.editor.delete_entry(
197                             urlutils.join(
198                                 self.branch.get_branch_path(), 
199                                 self.old_inv.id2path(child_ie.file_id)), 
200                             self.base_revnum, baton, self.pool)
201
202         # Loop over file children of file_id in self.new_inventory
203         for child_name in self.new_inventory[file_id].children:
204             child_ie = self.new_inventory.get_child(file_id, child_name)
205             assert child_ie is not None
206
207             if not (child_ie.kind in ('file', 'symlink')):
208                 continue
209
210             new_child_path = self.new_inventory.id2path(child_ie.file_id)
211             # add them if they didn't exist in old_inv 
212             if not child_ie.file_id in self.old_inv:
213                 mutter('adding %s %r' % (child_ie.kind, new_child_path))
214                 self._record_file_id(child_ie, new_child_path)
215                 child_baton = self.editor.add_file(
216                     urlutils.join(self.branch.get_branch_path(), 
217                                   new_child_path), baton, None, -1, self.pool)
218
219
220             # copy if they existed at different location
221             elif self.old_inv.id2path(child_ie.file_id) != new_child_path:
222                 mutter('copy %s %r -> %r' % (child_ie.kind, 
223                                   self.old_inv.id2path(child_ie.file_id), 
224                                   new_child_path))
225                 self._record_file_id(child_ie, new_child_path)
226                 child_baton = self.editor.add_file(
227                     urlutils.join(self.branch.get_branch_path(), new_child_path), baton, 
228                     urlutils.join(self.repository.transport.svn_url, self.base_path, self.old_inv.id2path(child_ie.file_id)),
229                     self.base_revnum, self.pool)
230
231             # open if they existed at the same location
232             elif child_ie.revision is None:
233                 mutter('open %s %r' % (child_ie.kind, new_child_path))
234
235                 child_baton = self.editor.open_file(
236                     urlutils.join(self.branch.get_branch_path(), 
237                         new_child_path), 
238                     baton, self.base_revnum, self.pool)
239
240             else:
241                 # Old copy of the file was retained. No need to send changes
242                 assert child_ie.file_id not in self.modified_files
243                 child_baton = None
244
245             if child_ie.file_id in self.old_inv:
246                 old_executable = self.old_inv[child_ie.file_id].executable
247                 old_special = (self.old_inv[child_ie.file_id].kind == 'symlink')
248             else:
249                 old_special = False
250                 old_executable = False
251
252             if child_baton is not None:
253                 if old_executable != child_ie.executable:
254                     if child_ie.executable:
255                         value = svn.core.SVN_PROP_EXECUTABLE_VALUE
256                     else:
257                         value = None
258                     self.editor.change_file_prop(child_baton, 
259                             svn.core.SVN_PROP_EXECUTABLE, value, self.pool)
260
261                 if old_special != (child_ie.kind == 'symlink'):
262                     if child_ie.kind == 'symlink':
263                         value = svn.core.SVN_PROP_SPECIAL_VALUE
264                     else:
265                         value = None
266
267                     self.editor.change_file_prop(child_baton, 
268                             svn.core.SVN_PROP_SPECIAL, value, self.pool)
269
270             # handle the file
271             if child_ie.file_id in self.modified_files:
272                 self._file_process(child_ie.file_id, 
273                     self.modified_files[child_ie.file_id], child_baton)
274
275             if child_baton is not None:
276                 self.editor.close_file(child_baton, None, self.pool)
277
278         # Loop over subdirectories of file_id in self.new_inventory
279         for child_name in self.new_inventory[file_id].children:
280             child_ie = self.new_inventory.get_child(file_id, child_name)
281             if child_ie.kind != 'directory':
282                 continue
283
284             new_child_path = self.new_inventory.id2path(child_ie.file_id)
285             # add them if they didn't exist in old_inv 
286             if not child_ie.file_id in self.old_inv:
287                 mutter('adding dir %r' % child_ie.name)
288                 self._record_file_id(child_ie, new_child_path)
289                 child_baton = self.editor.add_directory(
290                     urlutils.join(self.branch.get_branch_path(), 
291                                   new_child_path), baton, None, -1, self.pool)
292
293             # copy if they existed at different location
294             elif self.old_inv.id2path(child_ie.file_id) != new_child_path:
295                 old_child_path = self.old_inv.id2path(child_ie.file_id)
296                 mutter('copy dir %r -> %r' % (old_child_path, new_child_path))
297                 self._record_file_id(child_ie, new_child_path)
298                 child_baton = self.editor.add_directory(
299                     urlutils.join(self.branch.get_branch_path(), new_child_path),
300                     baton, 
301                     urlutils.join(self.repository.transport.svn_url, self.base_path, old_child_path), self.base_revnum, self.pool)
302
303             # open if they existed at the same location and 
304             # the directory was touched
305             elif self.new_inventory[child_ie.file_id].revision is None:
306                 mutter('open dir %r' % new_child_path)
307
308                 child_baton = self.editor.open_directory(
309                         urlutils.join(self.branch.get_branch_path(), new_child_path), 
310                         baton, self.base_revnum, self.pool)
311             else:
312                 assert child_ie.file_id not in self.modified_dirs
313                 continue
314
315             # Handle this directory
316             if child_ie.file_id in self.modified_dirs:
317                 self._dir_process(new_child_path, child_ie.file_id, child_baton)
318
319             self.editor.close_directory(child_baton, self.pool)
320
321     def open_branch_batons(self, root, elements, existing_elements, 
322                            base_path, base_rev):
323         """Open a specified directory given a baton for the repository root.
324
325         :param root: Baton for the repository root
326         :param elements: List of directory names to open
327         :param existing_elements: List of directory names that exist
328         :param base_path: Path to base top-level branch on
329         :param base_rev: Revision of path to base top-level branch on
330         """
331         ret = [root]
332
333         mutter('opening branch %r (base %r:%r)' % (elements, base_path, 
334                                                    base_rev))
335
336         # Open paths leading up to branch
337         for i in range(1, len(elements)-1):
338             # Does directory already exist?
339             ret.append(self.editor.open_directory(
340                 "/".join(existing_elements[0:i+1]), ret[-1], -1, self.pool))
341
342         assert (len(existing_elements) == len(elements) or 
343                 len(existing_elements)+1 == len(elements))
344
345         # Branch already exists and stayed at the same location, open:
346         # TODO: What if the branch didn't change but the new revision 
347         # was based on an older revision of the branch?
348         # This needs to also check that base_rev was the latest version of 
349         # branch_path.
350         if (len(existing_elements) == len(elements) and 
351             base_path.strip("/") == "/".join(elements).strip("/")):
352             ret.append(self.editor.open_directory(
353                 "/".join(elements), ret[-1], base_rev, self.pool))
354         else: # Branch has to be created
355             # Already exists, old copy needs to be removed
356             if len(existing_elements) == len(elements):
357                 self.editor.delete_entry("/".join(elements), -1, ret[-1])
358             if base_path is not None:
359                 base_url = urlutils.join(self.repository.transport.svn_url, base_path)
360             else:
361                 base_url = None
362             ret.append(self.editor.add_directory(
363                 "/".join(elements), ret[-1], base_url, base_rev, self.pool))
364
365         return ret
366
367     def commit(self, message):
368         """Finish the commit.
369
370         """
371         def done(revision, date, author):
372             """Callback that is called by the Subversion commit editor 
373             once the commit finishes.
374
375             :param revision: Revision number
376             :param date: Date recorded for this commit
377             """
378             assert revision > 0
379             self.revnum = revision
380             self.date = date
381             self.author = author
382         
383         bp_parts = self.branch.get_branch_path().split("/")
384         lock = self.repository.transport.lock_write(".")
385
386         try:
387             existing_bp_parts =_check_dirs_exist(self.repository.transport, 
388                                               bp_parts, -1)
389             self.revnum = None
390             self.editor = self.repository.transport.get_commit_editor(
391                 message.encode("utf-8"), done, None, False)
392
393             root = self.editor.open_root(self.base_revnum)
394             
395             # TODO: Accept create_prefix argument
396             branch_batons = self.open_branch_batons(root, bp_parts,
397                 existing_bp_parts, self.base_path, self.base_revnum)
398
399             self._dir_process("", self.new_inventory.root.file_id, 
400                 branch_batons[-1])
401
402             # Set all the revprops
403             for prop, value in self._svnprops.items():
404                 if value is not None:
405                     value = value.encode('utf-8')
406                 self.editor.change_dir_prop(branch_batons[-1], prop, value, 
407                                             self.pool)
408
409             branch_batons.reverse()
410             for baton in branch_batons:
411                 self.editor.close_directory(baton, self.pool)
412
413             self.editor.close()
414         finally:
415             lock.unlock()
416
417         assert self.revnum is not None
418
419         # Make sure the logwalker doesn't try to use ra 
420         # during checkouts...
421         self.repository._log.fetch_revisions(self.revnum)
422
423         revid = self.branch.generate_revision_id(self.revnum)
424
425         assert self._new_revision_id is None or self._new_revision_id == revid
426
427         mutter('commit %d finished. author: %r, date: %r, revid: %r' % 
428                (self.revnum, self.author, self.date, revid))
429
430         return revid
431
432     def _record_file_id(self, ie, path):
433         mutter('adding fileid mapping %s -> %s' % (path, ie.file_id))
434         self._svnprops[SVN_PROP_BZR_FILEIDS] += "%s\t%s\n" % (escape_svn_path(path), ie.file_id)
435
436     def record_entry_contents(self, ie, parent_invs, path, tree):
437         """Record the content of ie from tree into the commit if needed.
438
439         Side effect: sets ie.revision when unchanged
440
441         :param ie: An inventory entry present in the commit.
442         :param parent_invs: The inventories of the parent revisions of the
443             commit.
444         :param path: The path the entry is at in the tree.
445         :param tree: The tree which contains this entry and should be used to 
446         obtain content.
447         """
448         assert self.new_inventory.root is not None or ie.parent_id is None
449         self.new_inventory.add(ie)
450
451         # ie.revision is always None if the InventoryEntry is considered
452         # for committing. ie.snapshot will record the correct revision 
453         # which may be the sole parent if it is untouched.
454         if ie.revision is not None:
455             return
456
457         previous_entries = ie.find_previous_heads(parent_invs, 
458             self.repository.weave_store, self.repository.get_transaction())
459
460         # we are creating a new revision for ie in the history store
461         # and inventory.
462         ie.snapshot(self._new_revision_id, path, previous_entries, tree, self)
463
464
465 def replay_delta(builder, old_tree, new_tree):
466     """Replays a delta to a commit builder.
467
468     :param builder: The commit builder.
469     :param old_tree: Original tree on top of which the delta should be applied
470     :param new_tree: New tree that should be committed
471     """
472     delta = new_tree.changes_from(old_tree)
473     def touch_id(id):
474         ie = builder.new_inventory[id]
475         path = builder.new_inventory.id2path(id)
476
477         id = ie.file_id
478         while builder.new_inventory[id].parent_id is not None:
479             if builder.new_inventory[id].revision is None:
480                 break
481             builder.new_inventory[id].revision = None
482             if builder.new_inventory[id].kind == 'directory':
483                 builder.modified_directory(id, [])
484             id = builder.new_inventory[id].parent_id
485
486         if ie.kind == 'link':
487             builder.modified_link(ie.file_id, [], ie.symlink_target)
488         elif ie.kind == 'file':
489             def get_text():
490                 return new_tree.get_file_text(ie.file_id)
491             builder.modified_file_text(ie.file_id, [], get_text)
492
493     for (_, id, _) in delta.added:
494         touch_id(id)
495
496     for (_, id, _, _, _) in delta.modified:
497         touch_id(id)
498
499     for (oldpath, _, id, _, _, _) in delta.renamed:
500         touch_id(id)
501         old_parent_id = old_tree.inventory.path2id(os.path.dirname(oldpath))
502         if old_parent_id in builder.new_inventory:
503             touch_id(old_parent_id)
504
505     for (path, _, _) in delta.removed:
506         old_parent_id = old_tree.inventory.path2id(os.path.dirname(path))
507         if old_parent_id in builder.new_inventory:
508             touch_id(old_parent_id)
509
510     builder.finish_inventory()
511
512
513 def push_as_merged(target, source, revision_id):
514     """Push a revision as merged revision.
515
516     This will create a new revision in the target repository that 
517     merges the specified revision but does not contain any other differences. 
518     This is done so that the revision that is being pushed does not need 
519     to completely match the target revision and so it can not have the 
520     same revision id.
521
522     :param target: Repository to push to
523     :param source: Repository to pull the revision from
524     :param revision_id: Revision id of the revision to push
525     :return: The revision id of the created revision
526     """
527     assert isinstance(source, Branch)
528     rev = source.repository.get_revision(revision_id)
529     inv = source.repository.get_inventory(revision_id)
530
531     # revision on top of which to commit
532     prev_revid = target.last_revision()
533
534     mutter('committing %r on top of %r' % (revision_id, prev_revid))
535
536     old_tree = source.repository.revision_tree(revision_id)
537     if source.repository.has_revision(prev_revid):
538         new_tree = source.repository.revision_tree(prev_revid)
539     else:
540         new_tree = target.repository.revision_tree(prev_revid)
541
542     builder = SvnCommitBuilder(target.repository, target, 
543                                [revision_id, prev_revid],
544                                target.get_config(),
545                                None,
546                                None,
547                                None,
548                                rev.properties, 
549                                None,
550                                new_tree.inventory)
551                          
552     builder.new_inventory = inv
553     replay_delta(builder, new_tree, old_tree)
554
555     try:
556         return builder.commit(rev.message)
557     except SubversionException, (_, num):
558         if num == svn.core.SVN_ERR_FS_TXN_OUT_OF_DATE:
559             raise DivergedBranches(source, target)
560         raise
561
562
563 def push_new(target_repository, target_branch_path, source, stop_revision=None):
564     """Push a revision into Subversion, creating a new branch.
565
566     This will do a new commit in the target branch.
567
568     :param target_branch_path: Path to create new branch at
569     :param source: Branch to pull the revision from
570     :param revision_id: Revision id of the revision to push
571     """
572     assert isinstance(source, Branch)
573     if stop_revision is None:
574         stop_revision = source.last_revision()
575     history = source.revision_history()
576     revhistory = deepcopy(history)
577     start_revid = NULL_REVISION
578     while len(revhistory) > 0:
579         revid = revhistory.pop()
580         # We've found the revision to push if there is a revision 
581         # which LHS parent is present or if this is the first revision.
582         if (len(revhistory) == 0 or 
583             target_repository.has_revision(revhistory[-1])):
584             start_revid = revid
585             break
586
587     # Get commit builder but specify that target_branch_path should
588     # be created and copied from (copy_path, copy_revnum)
589     class ImaginaryBranch:
590         def __init__(self, repository):
591             self.repository = repository
592             self._revision_history = None
593
594         def get_config(self):
595             return None
596
597         def last_revision_info(self):
598             last_revid = self.last_revision()
599             if last_revid is None:
600                 return (0, None)
601             return (history.index(last_revid), last_revid)
602
603         def last_revision(self):
604             parents = source.repository.revision_parents(start_revid)
605             if parents == []:
606                 return None
607             return parents[0]
608
609         def get_branch_path(self, revnum=None):
610             return target_branch_path
611
612         def generate_revision_id(self, revnum):
613             return self.repository.generate_revision_id(
614                 revnum, self.get_branch_path(revnum), 
615                 str(self.repository.get_scheme()))
616
617     push(ImaginaryBranch(target_repository), source, start_revid)
618
619
620 def push(target, source, revision_id):
621     """Push a revision into Subversion.
622
623     This will do a new commit in the target branch.
624
625     :param target: Branch to push to
626     :param source: Branch to pull the revision from
627     :param revision_id: Revision id of the revision to push
628     """
629     assert isinstance(source, Branch)
630     mutter('pushing %r' % (revision_id))
631     rev = source.repository.get_revision(revision_id)
632     inv = source.repository.get_inventory(revision_id)
633
634     # revision on top of which to commit
635     assert (target.last_revision() in rev.parent_ids or 
636             target.last_revision() is None and rev.parent_ids == [])
637
638     old_tree = source.repository.revision_tree(revision_id)
639     new_tree = source.repository.revision_tree(target.last_revision())
640
641     builder = SvnCommitBuilder(target.repository, target, 
642                                rev.parent_ids,
643                                target.get_config(),
644                                rev.timestamp,
645                                rev.timezone,
646                                rev.committer,
647                                rev.properties, 
648                                revision_id,
649                                new_tree.inventory)
650                          
651     builder.new_inventory = inv
652     replay_delta(builder, new_tree, old_tree)
653     try:
654         return builder.commit(rev.message)
655     except SubversionException, (msg, num):
656         if num == svn.core.SVN_ERR_FS_TXN_OUT_OF_DATE:
657             raise DivergedBranches(source, target)
658         raise
659
660
661 class InterToSvnRepository(InterRepository):
662     """Any to Subversion repository actions."""
663
664     _matching_repo_format = SvnRepositoryFormat()
665
666     @staticmethod
667     def _get_repo_format_to_test():
668         return SvnRepositoryFormat()
669
670     def copy_content(self, revision_id=None, basis=None, pb=None):
671         """See InterRepository.copy_content."""
672         assert revision_id is not None, "fetching all revisions not supported"
673         # Go back over the LHS parent until we reach a revid we know
674         todo = []
675         while not self.target.has_revision(revision_id):
676             todo.append(revision_id)
677             revision_id = self.source.revision_parents(revision_id)[0]
678             if revision_id == NULL_REVISION:
679                 raise "Unrelated repositories."
680         mutter("pushing %r into svn" % todo)
681         target_branch = None
682         for revision_id in todo:
683             if pb is not None:
684                 pb.update("pushing revisions", todo.index(revision_id), len(todo))
685             rev = self.source.get_revision(revision_id)
686             inv = self.source.get_inventory(revision_id)
687
688             mutter('pushing %r' % (revision_id))
689
690             old_tree = self.source.revision_tree(revision_id)
691             parent_revid = self.source.revision_parents(revision_id)[0]
692             new_tree = self.source.revision_tree(parent_revid)
693
694             (bp, _, scheme) = self.target.lookup_revision_id(parent_revid)
695             if target_branch is None or target_branch.get_branch_path() != bp:
696                 target_branch = Branch.open(urlutils.join(self.target.base, bp))
697
698             builder = SvnCommitBuilder(self.target, target_branch, 
699                                rev.parent_ids,
700                                target_branch.get_config(),
701                                rev.timestamp,
702                                rev.timezone,
703                                rev.committer,
704                                rev.properties, 
705                                revision_id,
706                                new_tree.inventory)
707                          
708             builder.new_inventory = inv
709             replay_delta(builder, new_tree, old_tree)
710             builder.commit(rev.message)
711  
712
713     def fetch(self, revision_id=None, pb=None):
714         """Fetch revisions. """
715         self.copy_content(revision_id=revision_id, pb=pb)
716
717     @staticmethod
718     def is_compatible(source, target):
719         """Be compatible with SvnRepository."""
720         return isinstance(target, SvnRepository)