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