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