1 # Copyright (C) 2006-2007 Jelmer Vernooij <jelmer@samba.org>
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 2 of the License, or
6 # (at your option) any later version.
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.
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."""
19 from svn.core import Pool, SubversionException, svn_time_to_cstring
21 from bzrlib import debug, osutils, urlutils
22 from bzrlib.branch import Branch
23 from bzrlib.errors import (BzrError, InvalidRevisionId, DivergedBranches,
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
30 from copy import deepcopy
31 from cStringIO import StringIO
32 from errors import ChangesRootLHSHistory, MissingPrefix, RevpropChangeFailed
33 from repository import (SVN_PROP_SVK_MERGE, revision_id_to_svk_feature,
34 generate_revision_metadata, SvnRepositoryFormat,
35 SvnRepository, format_highres_date)
36 from mapping import (SVN_PROP_BZR_ANCESTRY, SVN_PROP_BZR_FILEIDS,
37 SVN_PROP_BZR_REVISION_INFO, SVN_PROP_BZR_REVISION_ID,
38 SVN_REVPROP_BZR_COMMITTER, SVN_REVPROP_BZR_FILEIDS,
39 SVN_REVPROP_BZR_MERGE, SVN_REVPROP_BZR_REVISION_ID,
40 SVN_REVPROP_BZR_REVPROP_PREFIX, SVN_REVPROP_BZR_ROOT,
41 SVN_REVPROP_BZR_TIMESTAMP, SVN_REVPROP_BZR_MAPPING_VERSION,
43 from repository import (revision_id_to_svk_feature, generate_revision_metadata,
44 SvnRepositoryFormat, SvnRepository, SVN_PROP_SVK_MERGE)
48 def _check_dirs_exist(transport, bp_parts, base_rev):
49 """Make sure that the specified directories exist.
51 :param transport: SvnRaTransport to use.
52 :param bp_parts: List of directory names in the format returned by
54 :param base_rev: Base revision to check.
55 :return: List of the directories that exists in base_rev.
57 for i in range(len(bp_parts), 0, -1):
58 current = bp_parts[:i]
59 path = "/".join(current).strip("/")
60 if transport.check_path(path, base_rev) == svn.core.svn_node_dir:
65 def set_svn_revprops(transport, revnum, revprops):
66 """Attempt to change the revision properties on the
69 :param transport: SvnRaTransport connected to target repository
70 :param revnum: Revision number of revision to change metadata of.
71 :param revprops: Dictionary with revision properties to set.
73 for (name, value) in revprops.items():
75 transport.change_rev_prop(revnum, name, value)
76 except SubversionException, (_, svn.core.SVN_ERR_REPOS_DISABLED_FEATURE):
77 raise RevpropChangeFailed(name)
80 class SvnCommitBuilder(RootCommitBuilder):
81 """Commit Builder implementation wrapped around svn_delta_editor. """
83 def __init__(self, repository, branch, parents, config, timestamp,
84 timezone, committer, revprops, revision_id, old_inv=None):
85 """Instantiate a new SvnCommitBuilder.
87 :param repository: SvnRepository to commit to.
88 :param branch: SvnBranch to commit to.
89 :param parents: List of parent revision ids.
90 :param config: Branch configuration to use.
91 :param timestamp: Optional timestamp recorded for commit.
92 :param timezone: Optional timezone for timestamp.
93 :param committer: Optional committer to set for commit.
94 :param revprops: Revision properties to set.
95 :param revision_id: Revision id for the new revision.
96 :param old_inv: Optional revision on top of which
97 the commit is happening
99 super(SvnCommitBuilder, self).__init__(repository, parents,
100 config, timestamp, timezone, committer, revprops, revision_id)
104 # Keep track of what Subversion properties to set later on
106 self._svnprops[SVN_PROP_BZR_REVISION_INFO] = generate_revision_metadata(
107 timestamp, timezone, committer, revprops)
108 self._svnprops[SVN_PROP_BZR_FILEIDS] = ""
110 self._svn_revprops = {SVN_REVPROP_BZR_MAPPING_VERSION: str(MAPPING_VERSION)}
112 if timestamp is not None:
113 self._svn_revprops[SVN_REVPROP_BZR_TIMESTAMP] = format_highres_date(timestamp, timezone)
115 if committer is not None:
116 self._svn_revprops[SVN_REVPROP_BZR_COMMITTER] = committer.encode("utf-8")
118 if revprops is not None:
119 for name, value in revprops.items():
120 self._svn_revprops[SVN_REVPROP_BZR_REVPROP_PREFIX+name] = value
122 # Gather information about revision on top of which the commit is
125 self.base_revid = None
127 self.base_revid = parents[0]
129 self.base_revno = self.branch.revision_id_to_revno(self.base_revid)
130 if self.base_revid is None:
131 self.base_revnum = -1
132 self.base_path = None
133 self.base_scheme = repository.get_scheme()
135 (self.base_path, self.base_revnum, self.base_scheme) = \
136 repository.lookup_revision_id(self.base_revid)
138 # Determine revisions merged in this one
139 merges = filter(lambda x: x != self.base_revid, parents)
142 self._record_merges(merges)
144 # Set appropriate property if revision id was specified by
146 if revision_id is not None:
147 self._record_revision_id(revision_id)
150 if self.base_revid is None:
151 self.old_inv = Inventory(root_id=None)
153 self.old_inv = self.repository.get_inventory(self.base_revid)
155 self.old_inv = old_inv
156 # Not all repositories appear to set Inventory.revision_id,
157 # so allow None as well.
158 assert self.old_inv.revision_id in (None, self.base_revid)
160 self.modified_files = {}
161 self.modified_dirs = set()
163 def mutter(self, text):
164 if 'commit' in debug.debug_flags:
167 def _record_revision_id(self, revid):
168 """Store the revision id in a file property.
170 :param revid: The revision id.
172 if self.base_revid is not None:
173 old = self.repository.branchprop_list.get_property(
174 self.base_path, self.base_revnum,
175 SVN_PROP_BZR_REVISION_ID+str(self.base_scheme), "")
179 self._svnprops[SVN_PROP_BZR_REVISION_ID+str(self.base_scheme)] = \
180 old + "%d %s\n" % (self.base_revno+1, revid)
181 self._svn_revprops[SVN_REVPROP_BZR_REVISION_ID] = revid
183 def _record_merges(self, merges):
184 """Store the extra merges (non-LHS parents) in a file property.
186 :param merges: List of parents.
189 if self.base_revid is not None:
190 old = self.repository.branchprop_list.get_property(
191 self.base_path, self.base_revnum,
192 SVN_PROP_BZR_ANCESTRY+str(self.base_scheme), "")
195 self._svnprops[SVN_PROP_BZR_ANCESTRY+str(self.base_scheme)] = old + "\t".join(merges) + "\n"
197 if self.base_revid is not None:
198 old = self.repository.branchprop_list.get_property(
199 self.base_path, self.base_revnum, SVN_PROP_SVK_MERGE, "")
207 new += "%s\n" % revision_id_to_svk_feature(merge)
208 except InvalidRevisionId:
212 self._svnprops[SVN_PROP_SVK_MERGE] = old + new
214 self._svn_revprops[SVN_REVPROP_BZR_MERGE] = "".join(map(lambda x: x + "\n", merges))
216 def _generate_revision_if_needed(self):
217 """See CommitBuilder._generate_revision_if_needed()."""
219 def finish_inventory(self):
220 """See CommitBuilder.finish_inventory()."""
222 def modified_file_text(self, file_id, file_parents,
223 get_content_byte_lines, text_sha1=None,
225 """See CommitBuilder.modified_file_text()."""
226 new_lines = get_content_byte_lines()
227 self.modified_files[file_id] = "".join(new_lines)
228 return osutils.sha_strings(new_lines), sum(map(len, new_lines))
230 def modified_link(self, file_id, file_parents, link_target):
231 """See CommitBuilder.modified_link()."""
232 self.modified_files[file_id] = "link %s" % link_target
234 def modified_directory(self, file_id, file_parents):
235 """See CommitBuilder.modified_directory()."""
236 self.modified_dirs.add(file_id)
238 def _file_process(self, file_id, contents, baton):
239 """Pass the changes to a file to the Subversion commit editor.
241 :param file_id: Id of the file to modify.
242 :param contents: Contents of the file.
243 :param baton: Baton under which the file is known to the editor.
245 assert baton is not None
246 (txdelta, txbaton) = self.editor.apply_textdelta(baton, None, self.pool)
247 digest = svn.delta.svn_txdelta_send_stream(StringIO(contents), txdelta, txbaton, self.pool)
248 if 'validate' in debug.debug_flags:
249 from fetch import md5_strings
250 assert digest == md5_strings(contents)
252 def _dir_process(self, path, file_id, baton):
253 """Pass the changes to a directory to the commit editor.
255 :param path: Path (from repository root) to the directory.
256 :param file_id: File id of the directory
257 :param baton: Baton of the directory for the editor.
259 assert baton is not None
260 # Loop over entries of file_id in self.old_inv
261 # remove if they no longer exist with the same name
263 if file_id in self.old_inv:
264 for child_name in self.old_inv[file_id].children:
265 child_ie = self.old_inv.get_child(file_id, child_name)
268 # ... path no longer exists
269 not child_ie.file_id in self.new_inventory or
271 child_ie.parent_id != self.new_inventory[child_ie.file_id].parent_id or
273 self.new_inventory[child_ie.file_id].name != child_name):
274 self.mutter('removing %r(%r)' % (child_name, child_ie.file_id))
275 self.editor.delete_entry(
276 urlutils.join(self.branch.get_branch_path(), path, child_name),
277 self.base_revnum, baton, self.pool)
279 # Loop over file children of file_id in self.new_inventory
280 for child_name in self.new_inventory[file_id].children:
281 child_ie = self.new_inventory.get_child(file_id, child_name)
282 assert child_ie is not None
284 if not (child_ie.kind in ('file', 'symlink')):
287 new_child_path = self.new_inventory.id2path(child_ie.file_id)
288 # add them if they didn't exist in old_inv
289 if not child_ie.file_id in self.old_inv:
290 self.mutter('adding %s %r' % (child_ie.kind, new_child_path))
291 child_baton = self.editor.add_file(
292 urlutils.join(self.branch.get_branch_path(),
293 new_child_path), baton, None, -1, self.pool)
296 # copy if they existed at different location
297 elif (self.old_inv.id2path(child_ie.file_id) != new_child_path or
298 self.old_inv[child_ie.file_id].parent_id != child_ie.parent_id):
299 self.mutter('copy %s %r -> %r' % (child_ie.kind,
300 self.old_inv.id2path(child_ie.file_id),
302 child_baton = self.editor.add_file(
303 urlutils.join(self.branch.get_branch_path(), new_child_path), baton,
304 urlutils.join(self.repository.transport.svn_url, self.base_path, self.old_inv.id2path(child_ie.file_id)),
305 self.base_revnum, self.pool)
307 # open if they existed at the same location
308 elif child_ie.revision is None:
309 self.mutter('open %s %r' % (child_ie.kind, new_child_path))
311 child_baton = self.editor.open_file(
312 urlutils.join(self.branch.get_branch_path(),
314 baton, self.base_revnum, self.pool)
317 # Old copy of the file was retained. No need to send changes
318 assert child_ie.file_id not in self.modified_files
321 if child_ie.file_id in self.old_inv:
322 old_executable = self.old_inv[child_ie.file_id].executable
323 old_special = (self.old_inv[child_ie.file_id].kind == 'symlink')
326 old_executable = False
328 if child_baton is not None:
329 if old_executable != child_ie.executable:
330 if child_ie.executable:
331 value = svn.core.SVN_PROP_EXECUTABLE_VALUE
334 self.editor.change_file_prop(child_baton,
335 svn.core.SVN_PROP_EXECUTABLE, value, self.pool)
337 if old_special != (child_ie.kind == 'symlink'):
338 if child_ie.kind == 'symlink':
339 value = svn.core.SVN_PROP_SPECIAL_VALUE
343 self.editor.change_file_prop(child_baton,
344 svn.core.SVN_PROP_SPECIAL, value, self.pool)
347 if child_ie.file_id in self.modified_files:
348 self._file_process(child_ie.file_id,
349 self.modified_files[child_ie.file_id], child_baton)
351 if child_baton is not None:
352 self.editor.close_file(child_baton, None, self.pool)
354 # Loop over subdirectories of file_id in self.new_inventory
355 for child_name in self.new_inventory[file_id].children:
356 child_ie = self.new_inventory.get_child(file_id, child_name)
357 if child_ie.kind != 'directory':
360 new_child_path = self.new_inventory.id2path(child_ie.file_id)
361 # add them if they didn't exist in old_inv
362 if not child_ie.file_id in self.old_inv:
363 self.mutter('adding dir %r' % child_ie.name)
364 child_baton = self.editor.add_directory(
365 urlutils.join(self.branch.get_branch_path(),
366 new_child_path), baton, None, -1, self.pool)
368 # copy if they existed at different location
369 elif self.old_inv.id2path(child_ie.file_id) != new_child_path:
370 old_child_path = self.old_inv.id2path(child_ie.file_id)
371 self.mutter('copy dir %r -> %r' % (old_child_path, new_child_path))
372 child_baton = self.editor.add_directory(
373 urlutils.join(self.branch.get_branch_path(), new_child_path),
375 urlutils.join(self.repository.transport.svn_url, self.base_path, old_child_path), self.base_revnum, self.pool)
377 # open if they existed at the same location and
378 # the directory was touched
379 elif self.new_inventory[child_ie.file_id].revision is None:
380 self.mutter('open dir %r' % new_child_path)
382 child_baton = self.editor.open_directory(
383 urlutils.join(self.branch.get_branch_path(), new_child_path),
384 baton, self.base_revnum, self.pool)
386 assert child_ie.file_id not in self.modified_dirs
389 # Handle this directory
390 if child_ie.file_id in self.modified_dirs:
391 self._dir_process(new_child_path, child_ie.file_id, child_baton)
393 self.editor.close_directory(child_baton, self.pool)
395 def open_branch_batons(self, root, elements, existing_elements,
396 base_path, base_rev, replace_existing):
397 """Open a specified directory given a baton for the repository root.
399 :param root: Baton for the repository root
400 :param elements: List of directory names to open
401 :param existing_elements: List of directory names that exist
402 :param base_path: Path to base top-level branch on
403 :param base_rev: Revision of path to base top-level branch on
404 :param replace_existing: Whether the current branch should be replaced
408 self.mutter('opening branch %r (base %r:%r)' % (elements, base_path,
411 # Open paths leading up to branch
412 for i in range(0, len(elements)-1):
413 # Does directory already exist?
414 ret.append(self.editor.open_directory(
415 "/".join(existing_elements[0:i+1]), ret[-1], -1, self.pool))
417 if (len(existing_elements) != len(elements) and
418 len(existing_elements)+1 != len(elements)):
419 raise MissingPrefix("/".join(elements))
421 # Branch already exists and stayed at the same location, open:
422 # TODO: What if the branch didn't change but the new revision
423 # was based on an older revision of the branch?
424 # This needs to also check that base_rev was the latest version of
426 if (len(existing_elements) == len(elements) and
427 not replace_existing):
428 ret.append(self.editor.open_directory(
429 "/".join(elements), ret[-1], base_rev, self.pool))
430 else: # Branch has to be created
431 # Already exists, old copy needs to be removed
432 name = "/".join(elements)
435 raise ChangesRootLHSHistory()
436 self.mutter("removing branch dir %r" % name)
437 self.editor.delete_entry(name, -1, ret[-1])
438 if base_path is not None:
439 base_url = urlutils.join(self.repository.transport.svn_url, base_path)
442 self.mutter("adding branch dir %r" % name)
443 ret.append(self.editor.add_directory(
444 name, ret[-1], base_url, base_rev, self.pool))
448 def commit(self, message):
449 """Finish the commit.
452 def done(revision_data, pool):
453 """Callback that is called by the Subversion commit editor
454 once the commit finishes.
456 :param revision_metadata: Revision metadata
458 self.revision_metadata = revision_data
460 self._svn_revprops[SVN_REVPROP_BZR_ROOT] = self.branch.get_branch_path()
461 bp_parts = self.branch.get_branch_path().split("/")
462 repository_latest_revnum = self.repository.transport.get_latest_revnum()
463 lock = self.repository.transport.lock_write(".")
464 set_revprops = self.repository.get_config().get_set_revprops()
465 remaining_revprops = self._svn_revprops # Keep track of the revprops that haven't been set yet
468 def _dir_process_file_id(old_inv, new_inv, path, file_id):
469 for child_name in new_inv[file_id].children:
470 child_ie = new_inv.get_child(file_id, child_name)
471 new_child_path = new_inv.id2path(child_ie.file_id)
472 assert child_ie is not None
474 if (not child_ie.file_id in old_inv or
475 old_inv.id2path(child_ie.file_id) != new_child_path or
476 old_inv[child_ie.file_id].parent_id != child_ie.parent_id):
477 yield (child_ie.file_id, new_child_path)
479 if (child_ie.kind == 'directory' and
480 child_ie.file_id in self.modified_dirs):
481 _dir_process_file_id(old_inv, new_inv, new_child_path, child_ie.file_id)
485 if (self.old_inv.root is None or
486 self.new_inventory.root.file_id != self.old_inv.root.file_id):
487 fileids.append((self.new_inventory.root.file_id, ""))
489 fileids += list(_dir_process_file_id(self.old_inv, self.new_inventory, "", self.new_inventory.root.file_id))
492 file_id_text = "".join(["%s\t%s\n" % (urllib.quote(path), file_id) for (file_id, path) in fileids])
493 self._svn_revprops[SVN_REVPROP_BZR_FILEIDS] = file_id_text
494 self._svnprops[SVN_PROP_BZR_FILEIDS] = file_id_text
497 existing_bp_parts = _check_dirs_exist(self.repository.transport,
499 self.revision_metadata = None
500 self._svn_revprops[svn.core.SVN_PROP_REVISION_LOG] = message.encode("utf-8")
502 self.editor = self.repository.transport.get_commit_editor(self._svn_revprops,
504 self._svn_revprops = {}
505 except NotImplementedError:
508 # Try without bzr: revprops
509 self.editor = self.repository.transport.get_commit_editor({
510 svn.core.SVN_PROP_REVISION_LOG: self._svn_revprops[svn.core.SVN_PROP_REVISION_LOG]},
512 del self._svn_revprops[svn.core.SVN_PROP_REVISION_LOG]
514 root = self.editor.open_root(self.base_revnum)
516 replace_existing = False
517 # See whether the base of the commit matches the lhs parent
518 # if not, we need to replace the existing directory
519 if len(bp_parts) == len(existing_bp_parts):
520 if self.base_path.strip("/") != "/".join(bp_parts).strip("/"):
521 replace_existing = True
522 elif self.base_revnum < self.repository._log.find_latest_change(self.branch.get_branch_path(), repository_latest_revnum, include_children=True):
523 replace_existing = True
525 # TODO: Accept create_prefix argument (#118787)
526 branch_batons = self.open_branch_batons(root, bp_parts,
527 existing_bp_parts, self.base_path, self.base_revnum,
530 self._dir_process("", self.new_inventory.root.file_id,
533 # Set all the revprops
534 for prop, value in self._svnprops.items():
535 if value is not None:
536 value = value.encode('utf-8')
537 self.editor.change_dir_prop(branch_batons[-1], prop, value,
540 for baton in reversed(branch_batons):
541 self.editor.close_directory(baton, self.pool)
547 assert self.revision_metadata is not None
549 # Make sure the logwalker doesn't try to use ra
550 # during checkouts...
551 self.repository._log.fetch_revisions(self.revision_metadata.revision)
553 revid = self.branch.generate_revision_id(self.revision_metadata.revision)
555 assert self._new_revision_id is None or self._new_revision_id == revid
557 self.mutter('commit %d finished. author: %r, date: %r, revid: %r' %
558 (self.revision_metadata.revision, self.revision_metadata.author, self.revision_metadata.date, revid))
560 if self.repository.get_config().get_override_svn_revprops():
561 set_svn_revprops(self.repository.transport, self.revision_metadata.revision, {
562 svn.core.SVN_PROP_REVISION_AUTHOR: self._committer,
563 svn.core.SVN_PROP_REVISION_DATE: svn_time_to_cstring(1000000*self._timestamp)
567 set_svn_revprops(self.repository.transport, self.revision_metadata.revision,
569 except RevpropChangeFailed:
570 pass # Ignore for now
574 def record_entry_contents(self, ie, parent_invs, path, tree,
576 """Record the content of ie from tree into the commit if needed.
578 Side effect: sets ie.revision when unchanged
580 :param ie: An inventory entry present in the commit.
581 :param parent_invs: The inventories of the parent revisions of the
583 :param path: The path the entry is at in the tree.
584 :param tree: The tree which contains this entry and should be used to
586 :param content_summary: Summary data from the tree about the paths
587 content - stat, length, exec, sha/link target. This is only
588 accessed when the entry has a revision of None - that is when
589 it is a candidate to commit.
591 self.new_inventory.add(ie)
594 def replay_delta(builder, old_tree, new_tree):
595 """Replays a delta to a commit builder.
597 :param builder: The commit builder.
598 :param old_tree: Original tree on top of which the delta should be applied
599 :param new_tree: New tree that should be committed
601 for path, ie in new_tree.inventory.iter_entries():
602 builder.record_entry_contents(ie.copy(), [old_tree.inventory],
603 path, new_tree, None)
604 builder.finish_inventory()
605 delta = new_tree.changes_from(old_tree)
607 ie = builder.new_inventory[id]
610 while builder.new_inventory[id].parent_id is not None:
611 if builder.new_inventory[id].revision is None:
613 builder.new_inventory[id].revision = None
614 if builder.new_inventory[id].kind == 'directory':
615 builder.modified_directory(id, [])
616 id = builder.new_inventory[id].parent_id
618 assert ie.kind in ('symlink', 'file', 'directory')
619 if ie.kind == 'symlink':
620 builder.modified_link(ie.file_id, [], ie.symlink_target)
621 elif ie.kind == 'file':
623 return new_tree.get_file_text(ie.file_id)
624 builder.modified_file_text(ie.file_id, [], get_text)
626 for (_, id, _) in delta.added:
629 for (_, id, _, _, _) in delta.modified:
632 for (oldpath, _, id, _, _, _) in delta.renamed:
634 old_parent_id = old_tree.inventory.path2id(urlutils.dirname(oldpath))
635 if old_parent_id in builder.new_inventory:
636 touch_id(old_parent_id)
638 for (path, _, _) in delta.removed:
639 old_parent_id = old_tree.inventory.path2id(urlutils.dirname(path))
640 if old_parent_id in builder.new_inventory:
641 touch_id(old_parent_id)
644 def push_new(target_repository, target_branch_path, source,
646 """Push a revision into Subversion, creating a new branch.
648 This will do a new commit in the target branch.
650 :param target_branch_path: Path to create new branch at
651 :param source: Branch to pull the revision from
652 :param revision_id: Revision id of the revision to push
654 assert isinstance(source, Branch)
655 if stop_revision is None:
656 stop_revision = source.last_revision()
657 history = source.revision_history()
658 revhistory = deepcopy(history)
659 start_revid = NULL_REVISION
660 while len(revhistory) > 0:
661 revid = revhistory.pop()
662 # We've found the revision to push if there is a revision
663 # which LHS parent is present or if this is the first revision.
664 if (len(revhistory) == 0 or
665 target_repository.has_revision(revhistory[-1])):
669 # Get commit builder but specify that target_branch_path should
670 # be created and copied from (copy_path, copy_revnum)
671 class ImaginaryBranch:
672 """Simple branch that pretends to be empty but already exist."""
673 def __init__(self, repository):
674 self.repository = repository
675 self._revision_history = None
677 def get_config(self):
678 """See Branch.get_config()."""
681 def revision_id_to_revno(self, revid):
684 return history.index(revid)
686 def last_revision_info(self):
687 """See Branch.last_revision_info()."""
688 last_revid = self.last_revision()
689 if last_revid is None:
691 return (history.index(last_revid), last_revid)
693 def last_revision(self):
694 """See Branch.last_revision()."""
695 parents = source.repository.revision_parents(start_revid)
700 def get_branch_path(self, revnum=None):
701 """See SvnBranch.get_branch_path()."""
702 return target_branch_path
704 def generate_revision_id(self, revnum):
705 """See SvnBranch.generate_revision_id()."""
706 return self.repository.generate_revision_id(
707 revnum, self.get_branch_path(revnum),
708 str(self.repository.get_scheme()))
710 push(ImaginaryBranch(target_repository), source, start_revid)
713 def push(target, source, revision_id):
714 """Push a revision into Subversion.
716 This will do a new commit in the target branch.
718 :param target: Branch to push to
719 :param source: Branch to pull the revision from
720 :param revision_id: Revision id of the revision to push
722 assert isinstance(source, Branch)
723 rev = source.repository.get_revision(revision_id)
724 mutter('pushing %r (%r)' % (revision_id, rev.parent_ids))
726 # revision on top of which to commit
727 if rev.parent_ids == []:
730 base_revid = rev.parent_ids[0]
734 old_tree = source.repository.revision_tree(revision_id)
735 base_tree = source.repository.revision_tree(base_revid)
737 builder = SvnCommitBuilder(target.repository, target, rev.parent_ids,
738 target.get_config(), rev.timestamp,
739 rev.timezone, rev.committer, rev.properties,
740 revision_id, base_tree.inventory)
742 replay_delta(builder, base_tree, old_tree)
746 builder.commit(rev.message)
747 except SubversionException, (_, num):
748 if num == svn.core.SVN_ERR_FS_TXN_OUT_OF_DATE:
749 raise DivergedBranches(source, target)
751 except ChangesRootLHSHistory:
752 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)
754 if 'validate' in debug.debug_flags:
755 crev = target.repository.get_revision(revision_id)
756 ctree = target.repository.revision_tree(revision_id)
757 treedelta = ctree.changes_from(old_tree)
758 assert not treedelta.has_changed(), "treedelta: %r" % treedelta
759 assert crev.committer == rev.committer
760 assert crev.timezone == rev.timezone
761 assert crev.timestamp == rev.timestamp
762 assert crev.message == rev.message
763 assert crev.properties == rev.properties
766 class InterToSvnRepository(InterRepository):
767 """Any to Subversion repository actions."""
769 _matching_repo_format = SvnRepositoryFormat()
772 def _get_repo_format_to_test():
773 """See InterRepository._get_repo_format_to_test()."""
776 def copy_content(self, revision_id=None, pb=None):
777 """See InterRepository.copy_content."""
778 self.source.lock_read()
780 assert revision_id is not None, "fetching all revisions not supported"
781 # Go back over the LHS parent until we reach a revid we know
783 while not self.target.has_revision(revision_id):
784 todo.append(revision_id)
785 revision_id = self.source.revision_parents(revision_id)[0]
786 if revision_id == NULL_REVISION:
787 raise UnrelatedBranches()
791 mutter("pushing %r into svn" % todo)
793 for revision_id in todo:
795 pb.update("pushing revisions", todo.index(revision_id), len(todo))
796 rev = self.source.get_revision(revision_id)
798 mutter('pushing %r' % (revision_id))
800 old_tree = self.source.revision_tree(revision_id)
801 parent_revid = rev.parent_ids[0]
802 base_tree = self.source.revision_tree(parent_revid)
804 (bp, _, _) = self.target.lookup_revision_id(parent_revid)
805 if target_branch is None:
806 target_branch = Branch.open(urlutils.join(self.target.base, bp))
807 if target_branch.get_branch_path() != bp:
808 target_branch.set_branch_path(bp)
810 builder = SvnCommitBuilder(self.target, target_branch,
811 rev.parent_ids, target_branch.get_config(),
812 rev.timestamp, rev.timezone, rev.committer,
813 rev.properties, revision_id, base_tree.inventory)
815 replay_delta(builder, base_tree, old_tree)
816 builder.commit(rev.message)
821 def fetch(self, revision_id=None, pb=None, find_ghosts=False):
822 """Fetch revisions. """
823 self.copy_content(revision_id=revision_id, pb=pb)
826 def is_compatible(source, target):
827 """Be compatible with SvnRepository."""
828 return isinstance(target, SvnRepository)