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