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