Be somewhat more verbose.
[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         path = "/".join(current).strip("/")
50         if transport.check_path(path, base_rev) == svn.core.svn_node_dir:
51             return current
52     return []
53
54
55 class SvnCommitBuilder(RootCommitBuilder):
56     """Commit Builder implementation wrapped around svn_delta_editor. """
57
58     def __init__(self, repository, branch, parents, config, timestamp, 
59                  timezone, committer, revprops, revision_id, old_inv=None):
60         """Instantiate a new SvnCommitBuilder.
61
62         :param repository: SvnRepository to commit to.
63         :param branch: SvnBranch to commit to.
64         :param parents: List of parent revision ids.
65         :param config: Branch configuration to use.
66         :param timestamp: Optional timestamp recorded for commit.
67         :param timezone: Optional timezone for timestamp.
68         :param committer: Optional committer to set for commit.
69         :param revprops: Revision properties to set.
70         :param revision_id: Revision id for the new revision.
71         :param old_inv: Optional revision on top of which 
72             the commit is happening
73         """
74         super(SvnCommitBuilder, self).__init__(repository, parents, 
75             config, timestamp, timezone, committer, revprops, revision_id)
76         self.branch = branch
77         self.pool = Pool()
78
79         # Keep track of what Subversion properties to set later on
80         self._svnprops = {}
81         self._svnprops[SVN_PROP_BZR_REVISION_INFO] = generate_revision_metadata(
82             timestamp, timezone, committer, revprops)
83         self._svnprops[SVN_PROP_BZR_FILEIDS] = ""
84
85         # Gather information about revision on top of which the commit is 
86         # happening
87         if parents == []:
88             self.base_revid = None
89         else:
90             self.base_revid = parents[0]
91         self.base_revno = self.branch.revision_id_to_revno(self.base_revid)
92         if self.base_revid is None:
93             self.base_revnum = -1
94             self.base_path = None
95             self.base_scheme = repository.get_scheme()
96         else:
97             (self.base_path, self.base_revnum, self.base_scheme) = \
98                 repository.lookup_revision_id(self.base_revid)
99
100         # Determine revisions merged in this one
101         merges = filter(lambda x: x != self.base_revid, parents)
102
103         if len(merges) > 0:
104             self._record_merges(merges)
105
106         # Set appropriate property if revision id was specified by 
107         # caller
108         if revision_id is not None:
109             self._record_revision_id(revision_id)
110
111         if old_inv is None:
112             if self.base_revid is None:
113                 self.old_inv = Inventory(root_id=None)
114             else:
115                 self.old_inv = self.repository.get_inventory(self.base_revid)
116         else:
117             self.old_inv = old_inv
118             # Not all repositories appear to set Inventory.revision_id, 
119             # so allow None as well.
120             assert self.old_inv.revision_id in (None, self.base_revid)
121
122         self.modified_files = {}
123         self.modified_dirs = set()
124
125     def mutter(self, text):
126         if 'commit' in debug.debug_flags:
127             mutter(text)
128
129     def _record_revision_id(self, revid):
130         """Store the revision id in a file property.
131
132         :param revid: The revision id.
133         """
134         if self.base_revid is not None:
135             old = self.repository.branchprop_list.get_property(
136                     self.base_path, self.base_revnum, 
137                         SVN_PROP_BZR_REVISION_ID+str(self.base_scheme), "")
138         else:
139             old = ""
140
141         self._svnprops[SVN_PROP_BZR_REVISION_ID+str(self.base_scheme)] = \
142                 old + "%d %s\n" % (self.base_revno+1, revid)
143
144     def _record_merges(self, merges):
145         """Store the extra merges (non-LHS parents) in a file property.
146
147         :param merges: List of parents.
148         """
149         # Bazaar Parents
150         if self.base_revid is not None:
151             old = self.repository.branchprop_list.get_property(
152                   self.base_path, self.base_revnum, 
153                   SVN_PROP_BZR_ANCESTRY+str(self.base_scheme), "")
154         else:
155             old = ""
156         self._svnprops[SVN_PROP_BZR_ANCESTRY+str(self.base_scheme)] = old + "\t".join(merges) + "\n"
157
158         if self.base_revid is not None:
159             old = self.repository.branchprop_list.get_property(
160                 self.base_path, self.base_revnum, SVN_PROP_SVK_MERGE, "")
161         else:
162             old = ""
163
164         new = ""
165         # SVK compatibility
166         for merge in merges:
167             try:
168                 new += "%s\n" % revision_id_to_svk_feature(merge)
169             except InvalidRevisionId:
170                 pass
171
172         if new != "":
173             self._svnprops[SVN_PROP_SVK_MERGE] = old + new
174         
175     def _generate_revision_if_needed(self):
176         """See CommitBuilder._generate_revision_if_needed()."""
177
178     def finish_inventory(self):
179         """See CommitBuilder.finish_inventory()."""
180
181     def modified_file_text(self, file_id, file_parents,
182                            get_content_byte_lines, text_sha1=None,
183                            text_size=None):
184         """See CommitBuilder.modified_file_text()."""
185         new_lines = get_content_byte_lines()
186         self.modified_files[file_id] = "".join(new_lines)
187         return osutils.sha_strings(new_lines), sum(map(len, new_lines))
188
189     def modified_link(self, file_id, file_parents, link_target):
190         """See CommitBuilder.modified_link()."""
191         self.modified_files[file_id] = "link %s" % link_target
192
193     def modified_directory(self, file_id, file_parents):
194         """See CommitBuilder.modified_directory()."""
195         self.modified_dirs.add(file_id)
196
197     def _file_process(self, file_id, contents, baton):
198         """Pass the changes to a file to the Subversion commit editor.
199
200         :param file_id: Id of the file to modify.
201         :param contents: Contents of the file.
202         :param baton: Baton under which the file is known to the editor.
203         """
204         assert baton is not None
205         if contents == "" and not file_id in self.old_inv:
206             # Don't send diff if a new file with empty contents is 
207             # added, because it created weird exceptions over svn+ssh:// 
208             # or https://
209             return
210         (txdelta, txbaton) = self.editor.apply_textdelta(baton, None, self.pool)
211         svn.delta.svn_txdelta_send_string(contents, txdelta, txbaton, self.pool)
212
213     def _dir_process(self, path, file_id, baton):
214         """Pass the changes to a directory to the commit editor.
215
216         :param path: Path (from repository root) to the directory.
217         :param file_id: File id of the directory
218         :param baton: Baton of the directory for the editor.
219         """
220         assert baton is not None
221         # Loop over entries of file_id in self.old_inv
222         # remove if they no longer exist with the same name
223         # or parents
224         if file_id in self.old_inv:
225             for child_name in self.old_inv[file_id].children:
226                 child_ie = self.old_inv.get_child(file_id, child_name)
227                 # remove if...
228                 if (
229                     # ... path no longer exists
230                     not child_ie.file_id in self.new_inventory or 
231                     # ... parent changed
232                     child_ie.parent_id != self.new_inventory[child_ie.file_id].parent_id or
233                     # ... name changed
234                     self.new_inventory[child_ie.file_id].name != child_name):
235                     self.mutter('removing %r(%r)' % (child_name, child_ie.file_id))
236                     self.editor.delete_entry(
237                             urlutils.join(
238                                 self.branch.get_branch_path(), path, child_name), 
239                             self.base_revnum, baton, self.pool)
240
241         # Loop over file children of file_id in self.new_inventory
242         for child_name in self.new_inventory[file_id].children:
243             child_ie = self.new_inventory.get_child(file_id, child_name)
244             assert child_ie is not None
245
246             if not (child_ie.kind in ('file', 'symlink')):
247                 continue
248
249             new_child_path = self.new_inventory.id2path(child_ie.file_id)
250             # add them if they didn't exist in old_inv 
251             if not child_ie.file_id in self.old_inv:
252                 self.mutter('adding %s %r' % (child_ie.kind, new_child_path))
253                 self._record_file_id(child_ie, new_child_path)
254                 child_baton = self.editor.add_file(
255                     urlutils.join(self.branch.get_branch_path(), 
256                                   new_child_path), baton, None, -1, self.pool)
257
258
259             # copy if they existed at different location
260             elif (self.old_inv.id2path(child_ie.file_id) != new_child_path or
261                     self.old_inv[child_ie.file_id].parent_id != child_ie.parent_id):
262                 self.mutter('copy %s %r -> %r' % (child_ie.kind, 
263                                   self.old_inv.id2path(child_ie.file_id), 
264                                   new_child_path))
265                 self._record_file_id(child_ie, new_child_path)
266                 child_baton = self.editor.add_file(
267                     urlutils.join(self.branch.get_branch_path(), new_child_path), baton, 
268                     urlutils.join(self.repository.transport.svn_url, self.base_path, self.old_inv.id2path(child_ie.file_id)),
269                     self.base_revnum, self.pool)
270
271             # open if they existed at the same location
272             elif child_ie.revision is None:
273                 self.mutter('open %s %r' % (child_ie.kind, new_child_path))
274
275                 child_baton = self.editor.open_file(
276                     urlutils.join(self.branch.get_branch_path(), 
277                         new_child_path), 
278                     baton, self.base_revnum, self.pool)
279
280             else:
281                 # Old copy of the file was retained. No need to send changes
282                 assert child_ie.file_id not in self.modified_files
283                 child_baton = None
284
285             if child_ie.file_id in self.old_inv:
286                 old_executable = self.old_inv[child_ie.file_id].executable
287                 old_special = (self.old_inv[child_ie.file_id].kind == 'symlink')
288             else:
289                 old_special = False
290                 old_executable = False
291
292             if child_baton is not None:
293                 if old_executable != child_ie.executable:
294                     if child_ie.executable:
295                         value = svn.core.SVN_PROP_EXECUTABLE_VALUE
296                     else:
297                         value = None
298                     self.editor.change_file_prop(child_baton, 
299                             svn.core.SVN_PROP_EXECUTABLE, value, self.pool)
300
301                 if old_special != (child_ie.kind == 'symlink'):
302                     if child_ie.kind == 'symlink':
303                         value = svn.core.SVN_PROP_SPECIAL_VALUE
304                     else:
305                         value = None
306
307                     self.editor.change_file_prop(child_baton, 
308                             svn.core.SVN_PROP_SPECIAL, value, self.pool)
309
310             # handle the file
311             if child_ie.file_id in self.modified_files:
312                 self._file_process(child_ie.file_id, 
313                     self.modified_files[child_ie.file_id], child_baton)
314
315             if child_baton is not None:
316                 self.editor.close_file(child_baton, None, self.pool)
317
318         # Loop over subdirectories of file_id in self.new_inventory
319         for child_name in self.new_inventory[file_id].children:
320             child_ie = self.new_inventory.get_child(file_id, child_name)
321             if child_ie.kind != 'directory':
322                 continue
323
324             new_child_path = self.new_inventory.id2path(child_ie.file_id)
325             # add them if they didn't exist in old_inv 
326             if not child_ie.file_id in self.old_inv:
327                 self.mutter('adding dir %r' % child_ie.name)
328                 self._record_file_id(child_ie, new_child_path)
329                 child_baton = self.editor.add_directory(
330                     urlutils.join(self.branch.get_branch_path(), 
331                                   new_child_path), baton, None, -1, self.pool)
332
333             # copy if they existed at different location
334             elif self.old_inv.id2path(child_ie.file_id) != new_child_path:
335                 old_child_path = self.old_inv.id2path(child_ie.file_id)
336                 self.mutter('copy dir %r -> %r' % (old_child_path, new_child_path))
337                 self._record_file_id(child_ie, new_child_path)
338                 child_baton = self.editor.add_directory(
339                     urlutils.join(self.branch.get_branch_path(), new_child_path),
340                     baton, 
341                     urlutils.join(self.repository.transport.svn_url, self.base_path, old_child_path), self.base_revnum, self.pool)
342
343             # open if they existed at the same location and 
344             # the directory was touched
345             elif self.new_inventory[child_ie.file_id].revision is None:
346                 self.mutter('open dir %r' % new_child_path)
347
348                 child_baton = self.editor.open_directory(
349                         urlutils.join(self.branch.get_branch_path(), new_child_path), 
350                         baton, self.base_revnum, self.pool)
351             else:
352                 assert child_ie.file_id not in self.modified_dirs
353                 continue
354
355             # Handle this directory
356             if child_ie.file_id in self.modified_dirs:
357                 self._dir_process(new_child_path, child_ie.file_id, child_baton)
358
359             self.editor.close_directory(child_baton, self.pool)
360
361     def open_branch_batons(self, root, elements, existing_elements, 
362                            base_path, base_rev, replace_existing):
363         """Open a specified directory given a baton for the repository root.
364
365         :param root: Baton for the repository root
366         :param elements: List of directory names to open
367         :param existing_elements: List of directory names that exist
368         :param base_path: Path to base top-level branch on
369         :param base_rev: Revision of path to base top-level branch on
370         :param replace_existing: Whether the current branch should be replaced
371         """
372         ret = [root]
373
374         self.mutter('opening branch %r (base %r:%r)' % (elements, base_path, 
375                                                    base_rev))
376
377         # Open paths leading up to branch
378         for i in range(0, len(elements)-1):
379             # Does directory already exist?
380             ret.append(self.editor.open_directory(
381                 "/".join(existing_elements[0:i+1]), ret[-1], -1, self.pool))
382
383         assert (len(existing_elements) == len(elements) or 
384                 len(existing_elements)+1 == len(elements))
385
386         # Branch already exists and stayed at the same location, open:
387         # TODO: What if the branch didn't change but the new revision 
388         # was based on an older revision of the branch?
389         # This needs to also check that base_rev was the latest version of 
390         # branch_path.
391         if (len(existing_elements) == len(elements) and 
392             not replace_existing):
393             ret.append(self.editor.open_directory(
394                 "/".join(elements), ret[-1], base_rev, self.pool))
395         else: # Branch has to be created
396             # Already exists, old copy needs to be removed
397             name = "/".join(elements)
398             if replace_existing:
399                 self.mutter("removing branch dir %r" % name)
400                 self.editor.delete_entry(name, -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             self.mutter("adding branch dir %r" % name)
406             ret.append(self.editor.add_directory(
407                 name, ret[-1], base_url, base_rev, self.pool))
408
409         return ret
410
411     def commit(self, message):
412         """Finish the commit.
413
414         """
415         def done(revision, date, author):
416             """Callback that is called by the Subversion commit editor 
417             once the commit finishes.
418
419             :param revision: Revision number
420             :param date: Date recorded for this commit
421             """
422             assert revision > 0
423             self.revnum = revision
424             self.date = date
425             self.author = author
426         
427         bp_parts = self.branch.get_branch_path().split("/")
428         repository_latest_revnum = self.repository.transport.get_latest_revnum()
429         lock = self.repository.transport.lock_write(".")
430
431         try:
432             existing_bp_parts = _check_dirs_exist(self.repository.transport, 
433                                               bp_parts, -1)
434             self.revnum = None
435             self.editor = self.repository.transport.get_commit_editor(
436                   {svn.core.SVN_PROP_REVISION_LOG: message.encode("utf-8")}, 
437                   done, None, False)
438
439             root = self.editor.open_root(self.base_revnum)
440
441             replace_existing = False
442             if len(bp_parts) == len(existing_bp_parts):
443                 if self.base_path.strip("/") != "/".join(bp_parts).strip("/"):
444                     replace_existing = True
445                 elif self.base_revnum < self.repository._log.find_latest_change(self.branch.get_branch_path(), repository_latest_revnum, include_children=True):
446                     replace_existing = True
447
448             # TODO: Accept create_prefix argument
449             branch_batons = self.open_branch_batons(root, bp_parts,
450                 existing_bp_parts, self.base_path, self.base_revnum,
451                 replace_existing)
452
453             # Make sure the root id is stored properly
454             if (self.old_inv.root is None or 
455                 self.new_inventory.root.file_id != self.old_inv.root.file_id):
456                 self._record_file_id(self.new_inventory.root, "")
457
458             self._dir_process("", self.new_inventory.root.file_id, 
459                 branch_batons[-1])
460
461             # Set all the revprops
462             for prop, value in self._svnprops.items():
463                 if value is not None:
464                     value = value.encode('utf-8')
465                 self.editor.change_dir_prop(branch_batons[-1], prop, value, 
466                                             self.pool)
467                 self.mutter("setting revision property %r to %r" % (prop, value))
468
469             for baton in reversed(branch_batons):
470                 self.editor.close_directory(baton, self.pool)
471
472             self.editor.close()
473         finally:
474             lock.unlock()
475
476         assert self.revnum is not None
477
478         # Make sure the logwalker doesn't try to use ra 
479         # during checkouts...
480         self.repository._log.fetch_revisions(self.revnum)
481
482         revid = self.branch.generate_revision_id(self.revnum)
483
484         assert self._new_revision_id is None or self._new_revision_id == revid
485
486         self.mutter('commit %d finished. author: %r, date: %r, revid: %r' % 
487                (self.revnum, self.author, self.date, revid))
488
489         return revid
490
491     def _record_file_id(self, ie, path):
492         """Store the file id of an inventory entry in a file property.
493
494         :param ie: Inventory entry.
495         :param path: Path of the inventory entry.
496         """
497         self._svnprops[SVN_PROP_BZR_FILEIDS] += "%s\t%s\n" % (urllib.quote(path), ie.file_id)
498
499     def record_entry_contents(self, ie, parent_invs, path, tree,
500                               content_summary):
501         """Record the content of ie from tree into the commit if needed.
502
503         Side effect: sets ie.revision when unchanged
504
505         :param ie: An inventory entry present in the commit.
506         :param parent_invs: The inventories of the parent revisions of the
507             commit.
508         :param path: The path the entry is at in the tree.
509         :param tree: The tree which contains this entry and should be used to 
510         obtain content.
511         """
512         assert self.new_inventory.root is not None or ie.parent_id is None
513         self.new_inventory.add(ie)
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     rev = source.repository.get_revision(revision_id)
648     mutter('pushing %r (%r)' % (revision_id, rev.parent_ids))
649
650     # revision on top of which to commit
651     if rev.parent_ids == []:
652         base_revid = None
653     else:
654         base_revid = rev.parent_ids[0]
655
656     old_tree = source.repository.revision_tree(revision_id)
657     base_tree = source.repository.revision_tree(base_revid)
658
659     builder = SvnCommitBuilder(target.repository, target, rev.parent_ids,
660                                target.get_config(), rev.timestamp,
661                                rev.timezone, rev.committer, rev.properties, 
662                                revision_id, base_tree.inventory)
663                          
664     builder.new_inventory = source.repository.get_inventory(revision_id)
665     replay_delta(builder, base_tree, old_tree)
666     try:
667         builder.commit(rev.message)
668     except SubversionException, (_, num):
669         if num == svn.core.SVN_ERR_FS_TXN_OUT_OF_DATE:
670             raise DivergedBranches(source, target)
671         raise
672     if validate:
673         crev = target.repository.get_revision(revision_id)
674         ctree = target.repository.revision_tree(revision_id)
675         treedelta = ctree.changes_from(old_tree)
676         assert not treedelta.has_changed(), "treedelta: %r" % treedelta
677         assert crev.committer == rev.committer
678         assert crev.timezone == rev.timezone
679         assert crev.timestamp == rev.timestamp
680         assert crev.message == rev.message
681         assert crev.properties == rev.properties
682
683
684 class InterToSvnRepository(InterRepository):
685     """Any to Subversion repository actions."""
686
687     _matching_repo_format = SvnRepositoryFormat()
688
689     @staticmethod
690     def _get_repo_format_to_test():
691         """See InterRepository._get_repo_format_to_test()."""
692         return None
693
694     def copy_content(self, revision_id=None, pb=None):
695         """See InterRepository.copy_content."""
696         assert revision_id is not None, "fetching all revisions not supported"
697         # Go back over the LHS parent until we reach a revid we know
698         todo = []
699         while not self.target.has_revision(revision_id):
700             todo.append(revision_id)
701             revision_id = self.source.revision_parents(revision_id)[0]
702             if revision_id == NULL_REVISION:
703                 raise UnrelatedBranches()
704         if todo == []:
705             # Nothing to do
706             return
707         mutter("pushing %r into svn" % todo)
708         target_branch = None
709         for revision_id in todo:
710             if pb is not None:
711                 pb.update("pushing revisions", todo.index(revision_id), len(todo))
712             rev = self.source.get_revision(revision_id)
713
714             mutter('pushing %r' % (revision_id))
715
716             old_tree = self.source.revision_tree(revision_id)
717             parent_revid = rev.parent_ids[0]
718             base_tree = self.source.revision_tree(parent_revid)
719
720             (bp, _, _) = self.target.lookup_revision_id(parent_revid)
721             if target_branch is None:
722                 target_branch = Branch.open(urlutils.join(self.target.base, bp))
723             if target_branch.get_branch_path() != bp:
724                 target_branch.set_branch_path(bp)
725
726             builder = SvnCommitBuilder(self.target, target_branch, 
727                                rev.parent_ids, target_branch.get_config(),
728                                rev.timestamp, rev.timezone, rev.committer,
729                                rev.properties, revision_id, base_tree.inventory)
730                          
731             builder.new_inventory = self.source.get_inventory(revision_id)
732             replay_delta(builder, base_tree, old_tree)
733             builder.commit(rev.message)
734  
735
736     def fetch(self, revision_id=None, pb=None):
737         """Fetch revisions. """
738         self.copy_content(revision_id=revision_id, pb=pb)
739
740     @staticmethod
741     def is_compatible(source, target):
742         """Be compatible with SvnRepository."""
743         return isinstance(target, SvnRepository)