Move property definitions to a separate file.
[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 import svn.delta
19 from svn.core import Pool, SubversionException, svn_time_to_cstring
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 properties
32
33 from cStringIO import StringIO
34 from bzrlib.plugins.svn.errors import ChangesRootLHSHistory, MissingPrefix, RevpropChangeFailed
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.repository import SvnRepositoryFormat, SvnRepository
40
41 import urllib
42
43
44 def _revision_id_to_svk_feature(revid):
45     """Create a SVK feature identifier from a revision id.
46
47     :param revid: Revision id to convert.
48     :return: Matching SVK feature identifier.
49     """
50     assert isinstance(revid, str)
51     (uuid, branch, revnum, _) = parse_revision_id(revid)
52     # TODO: What about renamed revisions? Should use 
53     # repository.lookup_revision_id here.
54     return generate_svk_feature(uuid, branch, revnum)
55
56
57 def _check_dirs_exist(transport, bp_parts, base_rev):
58     """Make sure that the specified directories exist.
59
60     :param transport: SvnRaTransport to use.
61     :param bp_parts: List of directory names in the format returned by 
62         os.path.split()
63     :param base_rev: Base revision to check.
64     :return: List of the directories that exists in base_rev.
65     """
66     for i in range(len(bp_parts), 0, -1):
67         current = bp_parts[:i]
68         path = "/".join(current).strip("/")
69         if transport.check_path(path, base_rev) == svn.core.svn_node_dir:
70             return current
71     return []
72
73
74 def set_svn_revprops(transport, revnum, revprops):
75     """Attempt to change the revision properties on the
76     specified revision.
77
78     :param transport: SvnRaTransport connected to target repository
79     :param revnum: Revision number of revision to change metadata of.
80     :param revprops: Dictionary with revision properties to set.
81     """
82     for (name, value) in revprops.items():
83         try:
84             transport.change_rev_prop(revnum, name, value)
85         except SubversionException, (_, svn.core.SVN_ERR_REPOS_DISABLED_FEATURE):
86             raise RevpropChangeFailed(name)
87
88
89 class SvnCommitBuilder(RootCommitBuilder):
90     """Commit Builder implementation wrapped around svn_delta_editor. """
91
92     def __init__(self, repository, branch, parents, config, timestamp, 
93                  timezone, committer, revprops, revision_id, old_inv=None):
94         """Instantiate a new SvnCommitBuilder.
95
96         :param repository: SvnRepository to commit to.
97         :param branch: SvnBranch to commit to.
98         :param parents: List of parent revision ids.
99         :param config: Branch configuration to use.
100         :param timestamp: Optional timestamp recorded for commit.
101         :param timezone: Optional timezone for timestamp.
102         :param committer: Optional committer to set for commit.
103         :param revprops: Revision properties to set.
104         :param revision_id: Revision id for the new revision.
105         :param old_inv: Optional revision on top of which 
106             the commit is happening
107         """
108         super(SvnCommitBuilder, self).__init__(repository, parents, 
109             config, timestamp, timezone, committer, revprops, revision_id)
110         self.branch = branch
111         self.pool = Pool()
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):
166         if 'commit' in debug.debug_flags:
167             mutter(text)
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, baton):
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 baton: Baton under which the file is known to the editor.
197         """
198         assert baton is not None
199         (txdelta, txbaton) = self.editor.apply_textdelta(baton, None, self.pool)
200         digest = svn.delta.svn_txdelta_send_stream(StringIO(contents), txdelta, txbaton, self.pool)
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, baton):
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 baton: Baton of the directory for the editor.
211         """
212         assert baton 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                     self.editor.delete_entry(
229                         urlutils.join(self.branch.get_branch_path(), path, child_name), 
230                         self.base_revnum, baton, self.pool)
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_baton = self.editor.add_file(
247                     full_new_child_path, baton, None, -1, self.pool)
248
249
250             # copy if they existed at different location
251             elif (self.old_inv.id2path(child_ie.file_id) != new_child_path or
252                     self.old_inv[child_ie.file_id].parent_id != child_ie.parent_id):
253                 self.mutter('copy %s %r -> %r' % (child_ie.kind, 
254                                   self.old_inv.id2path(child_ie.file_id), 
255                                   new_child_path))
256                 child_baton = self.editor.add_file(
257                         full_new_child_path, baton,
258                     urlutils.join(self.repository.transport.svn_url, self.base_path, self.old_inv.id2path(child_ie.file_id)),
259                     self.base_revnum, self.pool)
260
261             # open if they existed at the same location
262             elif child_ie.revision is None:
263                 self.mutter('open %s %r' % (child_ie.kind, new_child_path))
264
265                 child_baton = self.editor.open_file(
266                         full_new_child_path, baton, self.base_revnum, self.pool)
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_baton = 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_baton is not None:
281                 if old_executable != child_ie.executable:
282                     if child_ie.executable:
283                         value = properties.PROP_EXECUTABLE_VALUE
284                     else:
285                         value = None
286                     self.editor.change_file_prop(child_baton, 
287                             properties.PROP_EXECUTABLE, value, self.pool)
288
289                 if old_special != (child_ie.kind == 'symlink'):
290                     if child_ie.kind == 'symlink':
291                         value = properties.PROP_SPECIAL_VALUE
292                     else:
293                         value = None
294
295                     self.editor.change_file_prop(child_baton, 
296                             properties.PROP_SPECIAL, value, self.pool)
297
298             # handle the file
299             if child_ie.file_id in self.modified_files:
300                 self._file_process(child_ie.file_id, 
301                     self.modified_files[child_ie.file_id], child_baton)
302
303             if child_baton is not None:
304                 self.editor.close_file(child_baton, None, self.pool)
305
306         # Loop over subdirectories of file_id in self.new_inventory
307         for child_name in self.new_inventory[file_id].children:
308             child_ie = self.new_inventory.get_child(file_id, child_name)
309             if child_ie.kind != 'directory':
310                 continue
311
312             new_child_path = self.new_inventory.id2path(child_ie.file_id)
313             # add them if they didn't exist in old_inv 
314             if not child_ie.file_id in self.old_inv:
315                 self.mutter('adding dir %r' % child_ie.name)
316                 child_baton = self.editor.add_directory(
317                     urlutils.join(self.branch.get_branch_path(), 
318                                   new_child_path), baton, None, -1, self.pool)
319
320             # copy if they existed at different location
321             elif self.old_inv.id2path(child_ie.file_id) != new_child_path:
322                 old_child_path = self.old_inv.id2path(child_ie.file_id)
323                 self.mutter('copy dir %r -> %r' % (old_child_path, new_child_path))
324                 child_baton = self.editor.add_directory(
325                     urlutils.join(self.branch.get_branch_path(), new_child_path),
326                     baton, 
327                     urlutils.join(self.repository.transport.svn_url, self.base_path, old_child_path), self.base_revnum, self.pool)
328
329             # open if they existed at the same location and 
330             # the directory was touched
331             elif self.new_inventory[child_ie.file_id].revision is None:
332                 self.mutter('open dir %r' % new_child_path)
333
334                 child_baton = self.editor.open_directory(
335                         urlutils.join(self.branch.get_branch_path(), new_child_path), 
336                         baton, self.base_revnum, self.pool)
337             else:
338                 assert child_ie.file_id not in self.modified_dirs
339                 continue
340
341             # Handle this directory
342             if child_ie.file_id in self.modified_dirs:
343                 self._dir_process(new_child_path, child_ie.file_id, child_baton)
344
345             self.editor.close_directory(child_baton, self.pool)
346
347     def open_branch_batons(self, root, elements, existing_elements, 
348                            base_path, base_rev, replace_existing):
349         """Open a specified directory given a baton for the repository root.
350
351         :param root: Baton for the repository root
352         :param elements: List of directory names to open
353         :param existing_elements: List of directory names that exist
354         :param base_path: Path to base top-level branch on
355         :param base_rev: Revision of path to base top-level branch on
356         :param replace_existing: Whether the current branch should be replaced
357         """
358         ret = [root]
359
360         self.mutter('opening branch %r (base %r:%r)' % (elements, base_path, 
361                                                    base_rev))
362
363         # Open paths leading up to branch
364         for i in range(0, len(elements)-1):
365             # Does directory already exist?
366             ret.append(self.editor.open_directory(
367                 "/".join(existing_elements[0:i+1]), ret[-1], -1, self.pool))
368
369         if (len(existing_elements) != len(elements) and
370             len(existing_elements)+1 != len(elements)):
371             raise MissingPrefix("/".join(elements))
372
373         # Branch already exists and stayed at the same location, open:
374         # TODO: What if the branch didn't change but the new revision 
375         # was based on an older revision of the branch?
376         # This needs to also check that base_rev was the latest version of 
377         # branch_path.
378         if (len(existing_elements) == len(elements) and 
379             not replace_existing):
380             ret.append(self.editor.open_directory(
381                 "/".join(elements), ret[-1], base_rev, self.pool))
382         else: # Branch has to be created
383             # Already exists, old copy needs to be removed
384             name = "/".join(elements)
385             if replace_existing:
386                 if name == "":
387                     raise ChangesRootLHSHistory()
388                 self.mutter("removing branch dir %r" % name)
389                 self.editor.delete_entry(name, -1, ret[-1])
390             if base_path is not None:
391                 base_url = urlutils.join(self.repository.transport.svn_url, base_path)
392             else:
393                 base_url = None
394             self.mutter("adding branch dir %r" % name)
395             ret.append(self.editor.add_directory(
396                 name, ret[-1], base_url, base_rev, self.pool))
397
398         return ret
399
400     def commit(self, message):
401         """Finish the commit.
402
403         """
404         def done(revision_data, pool):
405             """Callback that is called by the Subversion commit editor 
406             once the commit finishes.
407
408             :param revision_data: Revision metadata
409             """
410             self.revision_metadata = revision_data
411         
412         bp_parts = self.branch.get_branch_path().split("/")
413         repository_latest_revnum = self.repository.get_latest_revnum()
414         lock = self.repository.transport.lock_write(".")
415         set_revprops = self._config.get_set_revprops()
416         remaining_revprops = self._svn_revprops # Keep track of the revprops that haven't been set yet
417
418         # Store file ids
419         def _dir_process_file_id(old_inv, new_inv, path, file_id):
420             ret = []
421             for child_name in new_inv[file_id].children:
422                 child_ie = new_inv.get_child(file_id, child_name)
423                 new_child_path = new_inv.id2path(child_ie.file_id)
424                 assert child_ie is not None
425
426                 if (not child_ie.file_id in old_inv or 
427                     old_inv.id2path(child_ie.file_id) != new_child_path or
428                     old_inv[child_ie.file_id].parent_id != child_ie.parent_id):
429                     ret.append((child_ie.file_id, new_child_path))
430
431                 if (child_ie.kind == 'directory' and 
432                     child_ie.file_id in self.modified_dirs):
433                     ret += _dir_process_file_id(old_inv, new_inv, new_child_path, child_ie.file_id)
434             return ret
435
436         fileids = {}
437
438         if (self.old_inv.root is None or 
439             self.new_inventory.root.file_id != self.old_inv.root.file_id):
440             fileids[""] = self.new_inventory.root.file_id
441
442         for id, path in _dir_process_file_id(self.old_inv, self.new_inventory, "", self.new_inventory.root.file_id):
443             fileids[path] = id
444
445         self.base_mapping.export_fileid_map(fileids, self._svn_revprops, self._svnprops)
446         if self._config.get_log_strip_trailing_newline():
447             self.base_mapping.export_message(message, self._svn_revprops, self._svnprops)
448             message = message.rstrip("\n")
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             try:
459                 self.editor = self.repository.transport.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 = 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_batons = self.open_branch_batons(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_batons[-1])
492
493             # Set all the revprops
494             for prop, value in self._svnprops.items():
495                 if not properties.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                 self.editor.change_dir_prop(branch_batons[-1], prop, value, 
500                                             self.pool)
501                 self.mutter("Setting root file property %r -> %r" % (prop, value))
502
503             for baton in reversed(branch_batons):
504                 self.editor.close_directory(baton, self.pool)
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         revid = self.branch.generate_revision_id(self.revision_metadata.revision)
515
516         assert self._new_revision_id is None or self._new_revision_id == revid
517
518         self.mutter('commit %d finished. author: %r, date: %r, revid: %r' % 
519                (self.revision_metadata.revision, self.revision_metadata.author, 
520                    self.revision_metadata.date, revid))
521
522         override_svn_revprops = self._config.get_override_svn_revprops()
523         if override_svn_revprops is not None:
524             new_revprops = {}
525             if properties.PROP_REVISION_AUTHOR in override_svn_revprops:
526                 new_revprops[properties.PROP_REVISION_AUTHOR] = self._committer.encode("utf-8")
527             if properties.PROP_REVISION_DATE in override_svn_revprops:
528                 new_revprops[properties.PROP_REVISION_DATE] = svn_time_to_cstring(1000000*self._timestamp)
529             set_svn_revprops(self.repository.transport, self.revision_metadata.revision, new_revprops)
530
531         try:
532             set_svn_revprops(self.repository.transport, self.revision_metadata.revision, 
533                          self._svn_revprops) 
534         except RevpropChangeFailed:
535             pass # Ignore for now
536
537         return revid
538
539     def record_entry_contents(self, ie, parent_invs, path, tree,
540                               content_summary):
541         """Record the content of ie from tree into the commit if needed.
542
543         Side effect: sets ie.revision when unchanged
544
545         :param ie: An inventory entry present in the commit.
546         :param parent_invs: The inventories of the parent revisions of the
547             commit.
548         :param path: The path the entry is at in the tree.
549         :param tree: The tree which contains this entry and should be used to 
550             obtain content.
551         :param content_summary: Summary data from the tree about the paths
552                 content - stat, length, exec, sha/link target. This is only
553                 accessed when the entry has a revision of None - that is when 
554                 it is a candidate to commit.
555         """
556         self.new_inventory.add(ie)
557
558
559 def replay_delta(builder, old_tree, new_tree):
560     """Replays a delta to a commit builder.
561
562     :param builder: The commit builder.
563     :param old_tree: Original tree on top of which the delta should be applied
564     :param new_tree: New tree that should be committed
565     """
566     for path, ie in new_tree.inventory.iter_entries():
567         builder.record_entry_contents(ie.copy(), [old_tree.inventory], 
568                                       path, new_tree, None)
569     builder.finish_inventory()
570     delta = new_tree.changes_from(old_tree)
571     def touch_id(id):
572         ie = builder.new_inventory[id]
573
574         id = ie.file_id
575         while builder.new_inventory[id].parent_id is not None:
576             if builder.new_inventory[id].revision is None:
577                 break
578             builder.new_inventory[id].revision = None
579             if builder.new_inventory[id].kind == 'directory':
580                 builder.modified_directory(id, [])
581             id = builder.new_inventory[id].parent_id
582
583         assert ie.kind in ('symlink', 'file', 'directory')
584         if ie.kind == 'symlink':
585             builder.modified_link(ie.file_id, [], ie.symlink_target)
586         elif ie.kind == 'file':
587             def get_text():
588                 return new_tree.get_file_text(ie.file_id)
589             builder.modified_file_text(ie.file_id, [], get_text)
590
591     for (_, id, _) in delta.added:
592         touch_id(id)
593
594     for (_, id, _, _, _) in delta.modified:
595         touch_id(id)
596
597     for (oldpath, _, id, _, _, _) in delta.renamed:
598         touch_id(id)
599         old_parent_id = old_tree.inventory.path2id(urlutils.dirname(oldpath))
600         if old_parent_id in builder.new_inventory:
601             touch_id(old_parent_id)
602
603     for (path, _, _) in delta.removed:
604         old_parent_id = old_tree.inventory.path2id(urlutils.dirname(path))
605         if old_parent_id in builder.new_inventory:
606             touch_id(old_parent_id)
607
608
609 def push_new(target_repository, target_branch_path, source, 
610              stop_revision=None):
611     """Push a revision into Subversion, creating a new branch.
612
613     This will do a new commit in the target branch.
614
615     :param target_branch_path: Path to create new branch at
616     :param source: Branch to pull the revision from
617     :param revision_id: Revision id of the revision to push
618     """
619     assert isinstance(source, Branch)
620     if stop_revision is None:
621         stop_revision = source.last_revision()
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)
676
677
678 def push_revision_tree(target, config, source_repo, base_revid, revision_id, 
679                        rev):
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                          
688     replay_delta(builder, base_tree, old_tree)
689     try:
690         builder.commit(rev.message)
691     except SubversionException, (_, num):
692         if num == svn.core.SVN_ERR_FS_TXN_OUT_OF_DATE:
693             raise DivergedBranches(source, target)
694         raise
695     except ChangesRootLHSHistory:
696         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)
697     
698     if source_repo.has_signature_for_revision_id(revision_id):
699         pass # FIXME: Copy revision signature for rev
700
701
702 def push(target, source, revision_id):
703     """Push a revision into Subversion.
704
705     This will do a new commit in the target branch.
706
707     :param target: Branch to push to
708     :param source: Branch to pull the revision from
709     :param revision_id: Revision id of the revision to push
710     """
711     assert isinstance(source, Branch)
712     rev = source.repository.get_revision(revision_id)
713     mutter('pushing %r (%r)' % (revision_id, rev.parent_ids))
714
715     # revision on top of which to commit
716     if rev.parent_ids == []:
717         base_revid = None
718     else:
719         base_revid = rev.parent_ids[0]
720
721     source.lock_read()
722     try:
723         push_revision_tree(target, target.get_config(), source.repository, base_revid, revision_id, rev)
724     finally:
725         source.unlock()
726
727     if 'validate' in debug.debug_flags:
728         crev = target.repository.get_revision(revision_id)
729         ctree = target.repository.revision_tree(revision_id)
730         treedelta = ctree.changes_from(old_tree)
731         assert not treedelta.has_changed(), "treedelta: %r" % treedelta
732         assert crev.committer == rev.committer
733         assert crev.timezone == rev.timezone
734         assert crev.timestamp == rev.timestamp
735         assert crev.message == rev.message
736         assert crev.properties == rev.properties
737
738
739 class InterToSvnRepository(InterRepository):
740     """Any to Subversion repository actions."""
741
742     _matching_repo_format = SvnRepositoryFormat()
743
744     @staticmethod
745     def _get_repo_format_to_test():
746         """See InterRepository._get_repo_format_to_test()."""
747         return None
748
749     def copy_content(self, revision_id=None, pb=None):
750         """See InterRepository.copy_content."""
751         self.source.lock_read()
752         try:
753             assert revision_id is not None, "fetching all revisions not supported"
754             # Go back over the LHS parent until we reach a revid we know
755             todo = []
756             while not self.target.has_revision(revision_id):
757                 todo.append(revision_id)
758                 try:
759                     revision_id = self.source.get_parent_map(revision_id)[revision_id][0]
760                 except KeyError:
761                     # We hit a ghost
762                     break
763                 if revision_id == NULL_REVISION:
764                     raise UnrelatedBranches()
765             if todo == []:
766                 # Nothing to do
767                 return
768             mutter("pushing %r into svn" % todo)
769             target_branch = None
770             for revision_id in todo:
771                 if pb is not None:
772                     pb.update("pushing revisions", todo.index(revision_id), len(todo))
773                 rev = self.source.get_revision(revision_id)
774
775                 mutter('pushing %r' % (revision_id))
776
777                 parent_revid = rev.parent_ids[0]
778
779                 (bp, _, _) = self.target.lookup_revision_id(parent_revid)
780                 if target_branch is None:
781                     target_branch = Branch.open(urlutils.join(self.target.base, bp))
782                 if target_branch.get_branch_path() != bp:
783                     target_branch.set_branch_path(bp)
784
785                 push_revision_tree(target_branch, target_branch.get_config(), self.source, parent_revid, revision_id, rev)
786         finally:
787             self.source.unlock()
788  
789
790     def fetch(self, revision_id=None, pb=None, find_ghosts=False):
791         """Fetch revisions. """
792         self.copy_content(revision_id=revision_id, pb=pb)
793
794     @staticmethod
795     def is_compatible(source, target):
796         """Be compatible with SvnRepository."""
797         return isinstance(target, SvnRepository)