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