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