Removing remaining imports of old svn bindings.
[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 = constants.PROP_EXECUTABLE_VALUE
284                     else:
285                         value = None
286                     child_editor.change_prop(constants.PROP_EXECUTABLE, value)
287
288                 if old_special != (child_ie.kind == 'symlink'):
289                     if child_ie.kind == 'symlink':
290                         value = constants.PROP_SPECIAL_VALUE
291                     else:
292                         value = None
293
294                     child_editor.change_prop(constants.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             class CommitResult:
405                 def __init__(self, revision, author, date):
406                     self.revision = revision
407                     self.author = author
408                     self.date = date
409             self.revision_metadata = CommitResult(revision, author, date)
410
411         bp_parts = self.branch.get_branch_path().split("/")
412         repository_latest_revnum = self.repository.get_latest_revnum()
413         lock = self.repository.transport.lock_write(".")
414         set_revprops = self._config.get_set_revprops()
415         remaining_revprops = self._svn_revprops # Keep track of the revprops that haven't been set yet
416
417         # Store file ids
418         def _dir_process_file_id(old_inv, new_inv, path, file_id):
419             ret = []
420             for child_name in new_inv[file_id].children:
421                 child_ie = new_inv.get_child(file_id, child_name)
422                 new_child_path = new_inv.id2path(child_ie.file_id)
423                 assert child_ie is not None
424
425                 if (not child_ie.file_id in old_inv or 
426                     old_inv.id2path(child_ie.file_id) != new_child_path or
427                     old_inv[child_ie.file_id].parent_id != child_ie.parent_id):
428                     ret.append((child_ie.file_id, new_child_path))
429
430                 if (child_ie.kind == 'directory' and 
431                     child_ie.file_id in self.modified_dirs):
432                     ret += _dir_process_file_id(old_inv, new_inv, new_child_path, child_ie.file_id)
433             return ret
434
435         fileids = {}
436
437         if (self.old_inv.root is None or 
438             self.new_inventory.root.file_id != self.old_inv.root.file_id):
439             fileids[""] = self.new_inventory.root.file_id
440
441         for id, path in _dir_process_file_id(self.old_inv, self.new_inventory, "", self.new_inventory.root.file_id):
442             fileids[path] = id
443
444         self.base_mapping.export_fileid_map(fileids, self._svn_revprops, self._svnprops)
445         if self._config.get_log_strip_trailing_newline():
446             self.base_mapping.export_message(message, self._svn_revprops, self._svnprops)
447             message = message.rstrip("\n")
448         self._svn_revprops[constants.PROP_REVISION_LOG] = message.encode("utf-8")
449
450         try:
451             existing_bp_parts = _check_dirs_exist(self.repository.transport, 
452                                               bp_parts, -1)
453             for prop in self._svn_revprops:
454                 if not util.is_valid_property_name(prop):
455                     warning("Setting property %r with invalid characters in name" % prop)
456             conn = self.repository.transport.get_connection()
457             try:
458                 try:
459                     self.editor = conn.get_commit_editor(
460                             self._svn_revprops, done, None, False)
461                     self._svn_revprops = {}
462                 except NotImplementedError:
463                     if set_revprops:
464                         raise
465                     # Try without bzr: revprops
466                     self.editor = conn.get_commit_editor({
467                         constants.PROP_REVISION_LOG: self._svn_revprops[constants.PROP_REVISION_LOG]},
468                         done, None, False)
469                     del self._svn_revprops[constants.PROP_REVISION_LOG]
470
471                 root = self.editor.open_root(self.base_revnum)
472
473                 replace_existing = False
474                 # See whether the base of the commit matches the lhs parent
475                 # if not, we need to replace the existing directory
476                 if len(bp_parts) == len(existing_bp_parts):
477                     if self.base_path.strip("/") != "/".join(bp_parts).strip("/"):
478                         replace_existing = True
479                     elif self.base_revnum < self.repository._log.find_latest_change(self.branch.get_branch_path(), repository_latest_revnum):
480                         replace_existing = True
481
482                 if replace_existing and self.branch._get_append_revisions_only():
483                     raise AppendRevisionsOnlyViolation(self.branch.base)
484
485                 # TODO: Accept create_prefix argument (#118787)
486                 branch_editors = self.open_branch_editors(root, bp_parts,
487                     existing_bp_parts, self.base_path, self.base_revnum, 
488                     replace_existing)
489
490                 self._dir_process("", self.new_inventory.root.file_id, 
491                     branch_editors[-1])
492
493                 # Set all the revprops
494                 for prop, value in self._svnprops.items():
495                     if not util.is_valid_property_name(prop):
496                         warning("Setting property %r with invalid characters in name" % prop)
497                     if value is not None:
498                         value = value.encode('utf-8')
499                     branch_editors[-1].change_prop(prop, value)
500                     self.mutter("Setting root file property %r -> %r" % (prop, value))
501
502                 for dir_editor in reversed(branch_editors):
503                     dir_editor.close()
504
505                 self.editor.close()
506             finally:
507                 self.repository.transport.add_connection(conn)
508         finally:
509             lock.unlock()
510
511         assert self.revision_metadata is not None
512
513         self.repository._clear_cached_state()
514
515         revid = self.branch.generate_revision_id(self.revision_metadata.revision)
516
517         assert self._new_revision_id is None or self._new_revision_id == revid
518
519         self.mutter('commit %d finished. author: %r, date: %r, revid: %r' % 
520                (self.revision_metadata.revision, self.revision_metadata.author, 
521                    self.revision_metadata.date, revid))
522
523         override_svn_revprops = self._config.get_override_svn_revprops()
524         if override_svn_revprops is not None:
525             new_revprops = {}
526             if constants.PROP_REVISION_AUTHOR in override_svn_revprops:
527                 new_revprops[constants.PROP_REVISION_AUTHOR] = self._committer.encode("utf-8")
528             if constants.PROP_REVISION_DATE in override_svn_revprops:
529                 new_revprops[constants.PROP_REVISION_DATE] = time_to_cstring(1000000*self._timestamp)
530             set_svn_revprops(self.repository.transport, self.revision_metadata.revision, new_revprops)
531
532         try:
533             set_svn_revprops(self.repository.transport, self.revision_metadata.revision, 
534                          self._svn_revprops) 
535         except RevpropChangeFailed:
536             pass # Ignore for now
537
538         return revid
539
540     def record_entry_contents(self, ie, parent_invs, path, tree,
541                               content_summary):
542         """Record the content of ie from tree into the commit if needed.
543
544         Side effect: sets ie.revision when unchanged
545
546         :param ie: An inventory entry present in the commit.
547         :param parent_invs: The inventories of the parent revisions of the
548             commit.
549         :param path: The path the entry is at in the tree.
550         :param tree: The tree which contains this entry and should be used to 
551             obtain content.
552         :param content_summary: Summary data from the tree about the paths
553                 content - stat, length, exec, sha/link target. This is only
554                 accessed when the entry has a revision of None - that is when 
555                 it is a candidate to commit.
556         """
557         self.new_inventory.add(ie)
558         return self._get_delta(ie, parent_invs[0], path), True
559
560
561 def replay_delta(builder, old_tree, new_tree):
562     """Replays a delta to a commit builder.
563
564     :param builder: The commit builder.
565     :param old_tree: Original tree on top of which the delta should be applied
566     :param new_tree: New tree that should be committed
567     """
568     for path, ie in new_tree.inventory.iter_entries():
569         builder.record_entry_contents(ie.copy(), [old_tree.inventory], 
570                                       path, new_tree, None)
571     builder.finish_inventory()
572     delta = new_tree.changes_from(old_tree)
573     def touch_id(id):
574         ie = builder.new_inventory[id]
575
576         id = ie.file_id
577         while builder.new_inventory[id].parent_id is not None:
578             if builder.new_inventory[id].revision is None:
579                 break
580             builder.new_inventory[id].revision = None
581             if builder.new_inventory[id].kind == 'directory':
582                 builder.modified_directory(id, [])
583             id = builder.new_inventory[id].parent_id
584
585         assert ie.kind in ('symlink', 'file', 'directory')
586         if ie.kind == 'symlink':
587             builder.modified_link(ie.file_id, [], ie.symlink_target)
588         elif ie.kind == 'file':
589             def get_text():
590                 return new_tree.get_file_text(ie.file_id)
591             builder.modified_file_text(ie.file_id, [], get_text)
592
593     for (_, id, _) in delta.added:
594         touch_id(id)
595
596     for (_, id, _, _, _) in delta.modified:
597         touch_id(id)
598
599     for (oldpath, _, id, _, _, _) in delta.renamed:
600         touch_id(id)
601         old_parent_id = old_tree.inventory.path2id(urlutils.dirname(oldpath))
602         if old_parent_id in builder.new_inventory:
603             touch_id(old_parent_id)
604
605     for (path, _, _) in delta.removed:
606         old_parent_id = old_tree.inventory.path2id(urlutils.dirname(path))
607         if old_parent_id in builder.new_inventory:
608             touch_id(old_parent_id)
609
610
611 def push_new(target_repository, target_branch_path, source, 
612              stop_revision=None):
613     """Push a revision into Subversion, creating a new branch.
614
615     This will do a new commit in the target branch.
616
617     :param target_branch_path: Path to create new branch at
618     :param source: Branch to pull the revision from
619     :param revision_id: Revision id of the revision to push
620     """
621     assert isinstance(source, Branch)
622     if stop_revision is None:
623         stop_revision = source.last_revision()
624     history = source.revision_history()
625     revhistory = list(history)
626     start_revid = NULL_REVISION
627     while len(revhistory) > 0:
628         revid = revhistory.pop()
629         # We've found the revision to push if there is a revision 
630         # which LHS parent is present or if this is the first revision.
631         if (len(revhistory) == 0 or 
632             target_repository.has_revision(revhistory[-1])):
633             start_revid = revid
634             break
635
636     # Get commit builder but specify that target_branch_path should
637     # be created and copied from (copy_path, copy_revnum)
638     class ImaginaryBranch(object):
639         """Simple branch that pretends to be empty but already exist."""
640         def __init__(self, repository):
641             self.repository = repository
642             self._revision_history = None
643
644         def get_config(self):
645             """See Branch.get_config()."""
646             return self.repository.get_config()
647
648         def revision_id_to_revno(self, revid):
649             if revid is None:
650                 return 0
651             return history.index(revid)
652
653         def last_revision_info(self):
654             """See Branch.last_revision_info()."""
655             last_revid = self.last_revision()
656             if last_revid is None:
657                 return (0, NULL_REVISION)
658             return (history.index(last_revid), last_revid)
659
660         def last_revision(self):
661             """See Branch.last_revision()."""
662             parents = source.repository.get_parent_map(start_revid)[start_revid]
663             if parents == []:
664                 return NULL_REVISION
665             return parents[0]
666
667         def get_branch_path(self, revnum=None):
668             """See SvnBranch.get_branch_path()."""
669             return target_branch_path
670
671         def generate_revision_id(self, revnum):
672             """See SvnBranch.generate_revision_id()."""
673             return self.repository.generate_revision_id(
674                 revnum, self.get_branch_path(revnum), 
675                 self.repository.get_mapping())
676
677     push(ImaginaryBranch(target_repository), source, start_revid)
678
679
680 def push_revision_tree(target, config, source_repo, base_revid, revision_id, 
681                        rev):
682     old_tree = source_repo.revision_tree(revision_id)
683     base_tree = source_repo.revision_tree(base_revid)
684
685     builder = SvnCommitBuilder(target.repository, target, rev.parent_ids,
686                                config, rev.timestamp,
687                                rev.timezone, rev.committer, rev.properties, 
688                                revision_id, base_tree.inventory)
689                          
690     replay_delta(builder, base_tree, old_tree)
691     try:
692         builder.commit(rev.message)
693     except SubversionException, (_, num):
694         if num == constants.ERR_FS_TXN_OUT_OF_DATE:
695             raise DivergedBranches(source, target)
696         raise
697     except ChangesRootLHSHistory:
698         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)
699     
700     if source_repo.has_signature_for_revision_id(revision_id):
701         pass # FIXME: Copy revision signature for rev
702
703
704 def push(target, source, revision_id):
705     """Push a revision into Subversion.
706
707     This will do a new commit in the target branch.
708
709     :param target: Branch to push to
710     :param source: Branch to pull the revision from
711     :param revision_id: Revision id of the revision to push
712     """
713     assert isinstance(source, Branch)
714     rev = source.repository.get_revision(revision_id)
715     mutter('pushing %r (%r)' % (revision_id, rev.parent_ids))
716
717     # revision on top of which to commit
718     if rev.parent_ids == []:
719         base_revid = None
720     else:
721         base_revid = rev.parent_ids[0]
722
723     source.lock_read()
724     try:
725         push_revision_tree(target, target.get_config(), source.repository, base_revid, revision_id, rev)
726     finally:
727         source.unlock()
728
729     if 'validate' in debug.debug_flags:
730         crev = target.repository.get_revision(revision_id)
731         ctree = target.repository.revision_tree(revision_id)
732         treedelta = ctree.changes_from(old_tree)
733         assert not treedelta.has_changed(), "treedelta: %r" % treedelta
734         assert crev.committer == rev.committer
735         assert crev.timezone == rev.timezone
736         assert crev.timestamp == rev.timestamp
737         assert crev.message == rev.message
738         assert crev.properties == rev.properties
739
740
741 class InterToSvnRepository(InterRepository):
742     """Any to Subversion repository actions."""
743
744     _matching_repo_format = SvnRepositoryFormat()
745
746     @staticmethod
747     def _get_repo_format_to_test():
748         """See InterRepository._get_repo_format_to_test()."""
749         return None
750
751     def copy_content(self, revision_id=None, pb=None):
752         """See InterRepository.copy_content."""
753         self.source.lock_read()
754         try:
755             assert revision_id is not None, "fetching all revisions not supported"
756             # Go back over the LHS parent until we reach a revid we know
757             todo = []
758             while not self.target.has_revision(revision_id):
759                 todo.append(revision_id)
760                 try:
761                     revision_id = self.source.get_parent_map(revision_id)[revision_id][0]
762                 except KeyError:
763                     # We hit a ghost
764                     break
765                 if revision_id == NULL_REVISION:
766                     raise UnrelatedBranches()
767             if todo == []:
768                 # Nothing to do
769                 return
770             mutter("pushing %r into svn" % todo)
771             target_branch = None
772             for revision_id in todo:
773                 if pb is not None:
774                     pb.update("pushing revisions", todo.index(revision_id), len(todo))
775                 rev = self.source.get_revision(revision_id)
776
777                 mutter('pushing %r' % (revision_id))
778
779                 parent_revid = rev.parent_ids[0]
780
781                 (bp, _, _) = self.target.lookup_revision_id(parent_revid)
782                 if target_branch is None:
783                     target_branch = Branch.open(urlutils.join(self.target.base, bp))
784                 if target_branch.get_branch_path() != bp:
785                     target_branch.set_branch_path(bp)
786
787                 push_revision_tree(target_branch, target_branch.get_config(), self.source, parent_revid, revision_id, rev)
788         finally:
789             self.source.unlock()
790  
791
792     def fetch(self, revision_id=None, pb=None, find_ghosts=False):
793         """Fetch revisions. """
794         self.copy_content(revision_id=revision_id, pb=pb)
795
796     @staticmethod
797     def is_compatible(source, target):
798         """Be compatible with SvnRepository."""
799         return isinstance(target, SvnRepository)