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