1 # Copyright (C) 2006-2008 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 3 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."""
18 from bzrlib import debug, urlutils, ui
19 from bzrlib.branch import Branch
20 from bzrlib.errors import (BzrError, InvalidRevisionId, DivergedBranches,
21 UnrelatedBranches, AppendRevisionsOnlyViolation,
23 from bzrlib.inventory import Inventory
24 from bzrlib.repository import RootCommitBuilder, InterRepository, Repository
25 from bzrlib.revision import NULL_REVISION, ensure_null
26 from bzrlib.trace import mutter, warning
28 from cStringIO import StringIO
30 from bzrlib.plugins.svn import core, mapping, properties
31 from bzrlib.plugins.svn.core import SubversionException
32 from bzrlib.plugins.svn.delta import send_stream
33 from bzrlib.plugins.svn.errors import ChangesRootLHSHistory, MissingPrefix, RevpropChangeFailed, ERR_FS_TXN_OUT_OF_DATE
34 from bzrlib.plugins.svn.svk import (
35 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
42 def _revision_id_to_svk_feature(revid):
43 """Create a SVK feature identifier from a revision id.
45 :param revid: Revision id to convert.
46 :return: Matching SVK feature identifier.
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)
55 def _check_dirs_exist(transport, bp_parts, base_rev):
56 """Make sure that the specified directories exist.
58 :param transport: SvnRaTransport to use.
59 :param bp_parts: List of directory names in the format returned by
61 :param base_rev: Base revision to check.
62 :return: List of the directories that exists in base_rev.
64 for i in range(len(bp_parts), 0, -1):
65 current = bp_parts[:i]
66 path = "/".join(current).strip("/")
67 assert isinstance(path, str)
68 if transport.check_path(path, base_rev) == core.NODE_DIR:
73 def update_svk_features(oldvalue, merges):
74 old_svk_features = parse_svk_features(oldvalue)
75 svk_features = set(old_svk_features)
80 svk_features.add(_revision_id_to_svk_feature(merge))
81 except InvalidRevisionId:
84 if old_svk_features != svk_features:
85 return serialize_svk_features(svk_features)
89 def update_mergeinfo(repository, graph, oldvalue, baserevid, merges):
90 pb = ui.ui_factory.nested_progress_bar()
92 mergeinfo = properties.parse_mergeinfo_property(oldvalue)
93 for i, merge in enumerate(merges):
94 pb.update("updating mergeinfo property", i, len(merges))
95 for (revid, parents) in graph.iter_ancestry([merge]):
96 if graph.is_ancestor(revid, baserevid):
99 (path, revnum, mapping) = repository.lookup_revision_id(revid)
100 except NoSuchRevision:
103 properties.mergeinfo_add_revision(mergeinfo, "/" + path, revnum)
106 newvalue = properties.generate_mergeinfo_property(mergeinfo)
107 if newvalue != oldvalue:
112 def set_svn_revprops(transport, revnum, revprops):
113 """Attempt to change the revision properties on the
116 :param transport: SvnRaTransport connected to target repository
117 :param revnum: Revision number of revision to change metadata of.
118 :param revprops: Dictionary with revision properties to set.
120 for (name, value) in revprops.items():
122 transport.change_rev_prop(revnum, name, value)
123 except SubversionException, (_, ERR_REPOS_DISABLED_FEATURE):
124 raise RevpropChangeFailed(name)
127 class SvnCommitBuilder(RootCommitBuilder):
128 """Commit Builder implementation wrapped around svn_delta_editor. """
130 def __init__(self, repository, branch, parents, config, timestamp,
131 timezone, committer, revprops, revision_id, old_inv=None,
132 push_metadata=True, graph=None, opt_signature=None):
133 """Instantiate a new SvnCommitBuilder.
135 :param repository: SvnRepository to commit to.
136 :param branch: SvnBranch to commit to.
137 :param parents: List of parent revision ids.
138 :param config: Branch configuration to use.
139 :param timestamp: Optional timestamp recorded for commit.
140 :param timezone: Optional timezone for timestamp.
141 :param committer: Optional committer to set for commit.
142 :param revprops: Revision properties to set.
143 :param revision_id: Revision id for the new revision.
144 :param old_inv: Optional revision on top of which
145 the commit is happening
146 :param push_metadata: Whether or not to push all bazaar metadata
147 (in svn file properties, etc).
149 super(SvnCommitBuilder, self).__init__(repository, parents,
150 config, timestamp, timezone, committer, revprops, revision_id)
152 self.push_metadata = push_metadata
154 # Gather information about revision on top of which the commit is
157 self.base_revid = NULL_REVISION
159 self.base_revid = parents[0]
162 graph = self.repository.get_graph()
163 self.base_revno = graph.find_distance_to_null(self.base_revid, [])
164 if self.base_revid == NULL_REVISION:
165 self.base_revnum = -1
166 self.base_path = None
167 self.base_mapping = repository.get_mapping()
169 (self.base_path, self.base_revnum, self.base_mapping) = \
170 repository.lookup_revision_id(self.base_revid)
173 if self.base_revid == NULL_REVISION:
174 self.old_inv = Inventory(root_id=None)
176 self.old_inv = self.repository.get_inventory(self.base_revid)
178 self.old_inv = old_inv
179 # Not all repositories appear to set Inventory.revision_id,
180 # so allow None as well.
181 assert self.old_inv.revision_id in (None, self.base_revid)
183 # Determine revisions merged in this one
184 merges = filter(lambda x: x != self.base_revid, parents)
186 self.visit_dirs = set()
187 self.modified_files = {}
188 if self.base_revid == NULL_REVISION:
189 self._base_branch_props = {}
191 self._base_branch_props = lazy_dict({}, self.repository.branchprop_list.get_properties, self.base_path, self.base_revnum)
192 self.supports_custom_revprops = self.repository.transport.has_capability("commit-revprops")
193 if self.supports_custom_revprops is None and self.base_mapping.supports_custom_revprops() and self.repository.seen_bzr_revprops():
194 raise BzrError("Please upgrade your Subversion client libraries to 1.5 or higher to be able to commit with Subversion mapping %s" % self.base_mapping.name)
196 if self.supports_custom_revprops == True:
197 self._svn_revprops = {}
198 if opt_signature is not None:
199 self._svn_revprops[mapping.SVN_REVPROP_BZR_SIGNATURE] = opt_signature
201 self._svn_revprops = None
202 self._svnprops = dict(self._base_branch_props.items())
203 self.base_mapping.export_revision(
204 self.branch.get_branch_path(), timestamp, timezone, committer, revprops,
205 revision_id, self.base_revno+1, merges, self._svn_revprops, self._svnprops)
208 new_svk_merges = update_svk_features(self._base_branch_props.get(SVN_PROP_SVK_MERGE, ""), merges)
209 if new_svk_merges is not None:
210 self._svnprops[SVN_PROP_SVK_MERGE] = new_svk_merges
212 new_mergeinfo = update_mergeinfo(self.repository, graph, self._base_branch_props.get(properties.PROP_MERGEINFO, ""), self.base_revid, merges)
213 if new_mergeinfo is not None:
214 self._svnprops[properties.PROP_MERGEINFO] = new_mergeinfo
217 def mutter(text, *args):
218 if 'commit' in debug.debug_flags:
221 def _generate_revision_if_needed(self):
222 """See CommitBuilder._generate_revision_if_needed()."""
224 def finish_inventory(self):
225 """See CommitBuilder.finish_inventory()."""
227 def _file_process(self, file_id, contents, file_editor):
228 """Pass the changes to a file to the Subversion commit editor.
230 :param file_id: Id of the file to modify.
231 :param contents: Contents of the file.
232 :param file_editor: Subversion FileEditor object.
234 assert file_editor is not None
235 txdelta = file_editor.apply_textdelta()
236 digest = send_stream(StringIO(contents), txdelta)
237 if 'validate' in debug.debug_flags:
238 from fetch import md5_strings
239 assert digest == md5_strings(contents)
241 def _dir_process(self, path, file_id, dir_editor):
242 """Pass the changes to a directory to the commit editor.
244 :param path: Path (from repository root) to the directory.
245 :param file_id: File id of the directory
246 :param dir_editor: Subversion DirEditor object.
248 assert dir_editor is not None
249 # Loop over entries of file_id in self.old_inv
250 # remove if they no longer exist with the same name
252 if file_id in self.old_inv:
253 for child_name in self.old_inv[file_id].children:
254 child_ie = self.old_inv.get_child(file_id, child_name)
257 # ... path no longer exists
258 not child_ie.file_id in self.new_inventory or
260 child_ie.parent_id != self.new_inventory[child_ie.file_id].parent_id or
262 self.new_inventory[child_ie.file_id].name != child_name):
263 self.mutter('removing %r(%r)', (child_name, child_ie.file_id))
264 dir_editor.delete_entry(
265 urlutils.join(self.branch.get_branch_path(), path, child_name),
268 # Loop over file children of file_id in self.new_inventory
269 for child_name in self.new_inventory[file_id].children:
270 child_ie = self.new_inventory.get_child(file_id, child_name)
271 assert child_ie is not None
273 if not (child_ie.kind in ('file', 'symlink')):
276 new_child_path = self.new_inventory.id2path(child_ie.file_id).encode("utf-8")
277 full_new_child_path = urlutils.join(self.branch.get_branch_path(),
279 # add them if they didn't exist in old_inv
280 if not child_ie.file_id in self.old_inv:
281 self.mutter('adding %s %r', child_ie.kind, new_child_path)
282 child_editor = dir_editor.add_file(full_new_child_path)
284 # copy if they existed at different location
285 elif (self.old_inv.id2path(child_ie.file_id) != new_child_path or
286 self.old_inv[child_ie.file_id].parent_id != child_ie.parent_id):
287 self.mutter('copy %s %r -> %r', child_ie.kind,
288 self.old_inv.id2path(child_ie.file_id),
290 child_editor = dir_editor.add_file(
292 urlutils.join(self.repository.transport.svn_url, self.base_path, self.old_inv.id2path(child_ie.file_id)),
295 # open if they existed at the same location
296 elif child_ie.file_id in self.modified_files:
297 self.mutter('open %s %r', child_ie.kind, new_child_path)
299 child_editor = dir_editor.open_file(
300 full_new_child_path, self.base_revnum)
303 # Old copy of the file was retained. No need to send changes
306 if child_ie.file_id in self.old_inv:
307 old_executable = self.old_inv[child_ie.file_id].executable
308 old_special = (self.old_inv[child_ie.file_id].kind == 'symlink')
311 old_executable = False
313 if child_editor is not None:
314 if old_executable != child_ie.executable:
315 if child_ie.executable:
316 value = properties.PROP_EXECUTABLE_VALUE
319 child_editor.change_prop(
320 properties.PROP_EXECUTABLE, value)
322 if old_special != (child_ie.kind == 'symlink'):
323 if child_ie.kind == 'symlink':
324 value = properties.PROP_SPECIAL_VALUE
328 child_editor.change_prop(
329 properties.PROP_SPECIAL, value)
332 if child_ie.file_id in self.modified_files:
333 self._file_process(child_ie.file_id,
334 self.modified_files[child_ie.file_id], child_editor)
336 if child_editor is not None:
339 # Loop over subdirectories of file_id in self.new_inventory
340 for child_name in self.new_inventory[file_id].children:
341 child_ie = self.new_inventory.get_child(file_id, child_name)
342 if child_ie.kind != 'directory':
345 new_child_path = self.new_inventory.id2path(child_ie.file_id)
346 # add them if they didn't exist in old_inv
347 if not child_ie.file_id in self.old_inv:
348 self.mutter('adding dir %r', child_ie.name)
349 child_editor = dir_editor.add_directory(
350 urlutils.join(self.branch.get_branch_path(),
353 # copy if they existed at different location
354 elif self.old_inv.id2path(child_ie.file_id) != new_child_path:
355 old_child_path = self.old_inv.id2path(child_ie.file_id)
356 self.mutter('copy dir %r -> %r', old_child_path, new_child_path)
357 child_editor = dir_editor.add_directory(
358 urlutils.join(self.branch.get_branch_path(), new_child_path),
359 urlutils.join(self.repository.transport.svn_url, self.base_path, old_child_path), self.base_revnum)
361 # open if they existed at the same location and
362 # the directory was touched
363 elif child_ie.file_id in self.visit_dirs:
364 self.mutter('open dir %r', new_child_path)
366 child_editor = dir_editor.open_directory(
367 urlutils.join(self.branch.get_branch_path(), new_child_path),
372 # Handle this directory
373 self._dir_process(new_child_path, child_ie.file_id, child_editor)
377 def open_branch_editors(self, root, elements, existing_elements,
378 base_path, base_rev, replace_existing):
379 """Open a specified directory given an editor for the repository root.
381 :param root: Editor for the repository root
382 :param elements: List of directory names to open
383 :param existing_elements: List of directory names that exist
384 :param base_path: Path to base top-level branch on
385 :param base_rev: Revision of path to base top-level branch on
386 :param replace_existing: Whether the current branch should be replaced
390 self.mutter('opening branch %r (base %r:%r)', elements, base_path,
393 # Open paths leading up to branch
394 for i in range(0, len(elements)-1):
395 # Does directory already exist?
396 ret.append(ret[-1].open_directory(
397 "/".join(existing_elements[0:i+1]), -1))
399 if (len(existing_elements) != len(elements) and
400 len(existing_elements)+1 != len(elements)):
401 raise MissingPrefix("/".join(elements))
403 # Branch already exists and stayed at the same location, open:
404 # TODO: What if the branch didn't change but the new revision
405 # was based on an older revision of the branch?
406 # This needs to also check that base_rev was the latest version of
408 if (len(existing_elements) == len(elements) and
409 not replace_existing):
410 ret.append(ret[-1].open_directory(
411 "/".join(elements), base_rev))
412 else: # Branch has to be created
413 # Already exists, old copy needs to be removed
414 name = "/".join(elements)
417 raise ChangesRootLHSHistory()
418 self.mutter("removing branch dir %r", name)
419 ret[-1].delete_entry(name, -1)
420 if base_path is not None:
421 base_url = urlutils.join(self.repository.transport.svn_url, base_path)
424 self.mutter("adding branch dir %r", name)
425 ret.append(ret[-1].add_directory(
426 name, base_url, base_rev))
430 def _determine_texts_identity(self):
432 def _dir_process_file_id(old_inv, new_inv, path, file_id):
434 for child_name in new_inv[file_id].children:
435 child_ie = new_inv.get_child(file_id, child_name)
436 new_child_path = new_inv.id2path(child_ie.file_id)
437 assert child_ie is not None
439 if (not child_ie.file_id in old_inv or
440 old_inv.id2path(child_ie.file_id) != new_child_path or
441 old_inv[child_ie.file_id].revision != child_ie.revision or
442 old_inv[child_ie.file_id].parent_id != child_ie.parent_id):
443 ret.append((child_ie.file_id, new_child_path, child_ie.revision))
445 if (child_ie.kind == 'directory' and
446 child_ie.file_id in self.visit_dirs):
447 ret += _dir_process_file_id(old_inv, new_inv, new_child_path, child_ie.file_id)
456 if (self.old_inv.root is None or
457 self.new_inventory.root.file_id != self.old_inv.root.file_id):
458 changes.append((self.new_inventory.root.file_id, "", self.new_inventory.root.revision))
460 changes += _dir_process_file_id(self.old_inv, self.new_inventory, "", self.new_inventory.root.file_id)
462 for id, path, revid in changes:
464 if revid is not None and revid != self.base_revid and revid != self._new_revision_id:
465 text_parents[path] = revid
466 return (fileids, text_parents)
468 def commit(self, message):
469 """Finish the commit.
473 """Callback that is called by the Subversion commit editor
474 once the commit finishes.
476 :param revision_data: Revision metadata
478 self.revision_metadata = args
480 bp = self.branch.get_branch_path()
481 assert isinstance(bp, str), "%r" % bp
482 bp_parts = bp.split("/")
483 repository_latest_revnum = self.repository.get_latest_revnum()
484 lock = self.repository.transport.lock_write(".")
486 if self.push_metadata:
487 (fileids, text_parents) = self._determine_texts_identity()
489 self.base_mapping.export_text_parents(text_parents, self._svn_revprops, self._svnprops)
490 self.base_mapping.export_fileid_map(fileids, self._svn_revprops, self._svnprops)
491 if self._config.get_log_strip_trailing_newline():
492 self.base_mapping.export_message(message, self._svn_revprops, self._svnprops)
493 message = message.rstrip("\n")
494 if not self.supports_custom_revprops:
495 self._svn_revprops = {}
496 self._svn_revprops[properties.PROP_REVISION_LOG] = message.encode("utf-8")
499 existing_bp_parts = _check_dirs_exist(self.repository.transport,
501 self.revision_metadata = None
502 for prop in self._svn_revprops:
503 assert prop.split(":")[0] in ("bzr", "svk", "svn")
504 if not properties.is_valid_property_name(prop):
505 warning("Setting property %r with invalid characters in name", prop)
506 conn = self.repository.transport.get_connection()
507 assert self.supports_custom_revprops or self._svn_revprops.keys() == [properties.PROP_REVISION_LOG], \
508 "revprops: %r" % self._svn_revprops.keys()
509 self.editor = conn.get_commit_editor(
510 self._svn_revprops, done, None, False)
512 root = self.editor.open_root(self.base_revnum)
514 replace_existing = False
515 # See whether the base of the commit matches the lhs parent
516 # if not, we need to replace the existing directory
517 if len(bp_parts) == len(existing_bp_parts):
518 if self.base_path is None or self.base_path.strip("/") != "/".join(bp_parts).strip("/"):
519 replace_existing = True
520 elif self.base_revnum < self.repository._log.find_latest_change(self.branch.get_branch_path(), repository_latest_revnum):
521 replace_existing = True
523 if replace_existing and self.branch._get_append_revisions_only():
524 raise AppendRevisionsOnlyViolation(self.branch.base)
526 # TODO: Accept create_prefix argument
527 branch_editors = self.open_branch_editors(root, bp_parts,
528 existing_bp_parts, self.base_path, self.base_revnum,
531 self._dir_process("", self.new_inventory.root.file_id,
534 # Set all the revprops
535 if self.push_metadata:
536 for prop, value in self._svnprops.items():
537 if value == self._base_branch_props.get(prop):
539 if not properties.is_valid_property_name(prop):
540 warning("Setting property %r with invalid characters in name", prop)
541 assert isinstance(value, str)
542 branch_editors[-1].change_prop(prop, value)
543 self.mutter("Setting root file property %r -> %r", prop, value)
545 for dir_editor in reversed(branch_editors):
549 self.repository.transport.add_connection(conn)
553 self.repository.transport.add_connection(conn)
557 assert self.revision_metadata is not None
559 self.repository._clear_cached_state()
561 (result_revision, result_date, result_author) = self.revision_metadata
563 revid = self.branch.generate_revision_id(result_revision)
565 assert not self.push_metadata or self._new_revision_id is None or self._new_revision_id == revid
567 self.mutter('commit %d finished. author: %r, date: %r, revid: %r',
568 result_revision, result_author,
571 override_svn_revprops = self._config.get_override_svn_revprops()
572 if override_svn_revprops is not None:
574 if properties.PROP_REVISION_AUTHOR in override_svn_revprops:
575 new_revprops[properties.PROP_REVISION_AUTHOR] = self._committer.encode("utf-8")
576 if properties.PROP_REVISION_DATE in override_svn_revprops:
577 new_revprops[properties.PROP_REVISION_DATE] = properties.time_to_cstring(1000000*self._timestamp)
578 set_svn_revprops(self.repository.transport, result_revision, new_revprops)
582 def record_entry_contents(self, ie, parent_invs, path, tree,
584 """Record the content of ie from tree into the commit if needed.
586 Side effect: sets ie.revision when unchanged
588 :param ie: An inventory entry present in the commit.
589 :param parent_invs: The inventories of the parent revisions of the
591 :param path: The path the entry is at in the tree.
592 :param tree: The tree which contains this entry and should be used to
594 :param content_summary: Summary data from the tree about the paths
595 content - stat, length, exec, sha/link target. This is only
596 accessed when the entry has a revision of None - that is when
597 it is a candidate to commit.
599 self.new_inventory.add(ie)
600 assert (ie.file_id not in self.old_inv or
601 self.old_inv[ie.file_id].revision is not None)
602 version_recorded = (ie.revision is None)
603 # If nothing changed since the lhs parent, return:
604 if (ie.file_id in self.old_inv and ie == self.old_inv[ie.file_id] and
605 (ie.kind != 'directory' or ie.children == self.old_inv[ie.file_id].children)):
606 return self._get_delta(ie, self.old_inv, self.new_inventory.id2path(ie.file_id)), version_recorded
607 if ie.kind == 'file':
608 self.modified_files[ie.file_id] = tree.get_file_text(ie.file_id)
609 elif ie.kind == 'symlink':
610 self.modified_files[ie.file_id] = "link %s" % ie.symlink_target
611 elif ie.kind == 'directory':
612 self.visit_dirs.add(ie.file_id)
614 while fid is not None and fid not in self.visit_dirs:
615 self.visit_dirs.add(fid)
616 fid = self.new_inventory[fid].parent_id
617 return self._get_delta(ie, self.old_inv, self.new_inventory.id2path(ie.file_id)), version_recorded
620 def replay_delta(builder, old_tree, new_tree):
621 """Replays a delta to a commit builder.
623 :param builder: The commit builder.
624 :param old_tree: Original tree on top of which the delta should be applied
625 :param new_tree: New tree that should be committed
627 for path, ie in new_tree.inventory.iter_entries():
628 builder.record_entry_contents(ie.copy(), [old_tree.inventory],
629 path, new_tree, None)
630 builder.finish_inventory()
633 def push_new(target_repository, target_branch_path, source, stop_revision,
635 """Push a revision into Subversion, creating a new branch.
637 This will do a new commit in the target branch.
639 :param target_repository: Repository to push to
640 :param target_branch_path: Path to create new branch at
641 :param source: Source repository
643 assert isinstance(source, Repository)
644 revhistory = list(source.iter_reverse_revision_history(stop_revision))
645 history = list(revhistory)
647 start_revid_parent = NULL_REVISION
648 start_revid = stop_revision
649 for revid in revhistory:
650 # We've found the revision to push if there is a revision
651 # which LHS parent is present or if this is the first revision.
652 if target_repository.has_revision(revid):
653 start_revid_parent = revid
656 assert start_revid is not None
657 # Get commit builder but specify that target_branch_path should
658 # be created and copied from (copy_path, copy_revnum)
659 class ImaginaryBranch(object):
660 """Simple branch that pretends to be empty but already exist."""
661 def __init__(self, repository):
662 self.repository = repository
663 self._revision_history = None
665 def _get_append_revisions_only(self):
668 def get_config(self):
669 """See Branch.get_config()."""
670 return self.repository.get_config()
672 def revision_id_to_revno(self, revid):
673 if revid is NULL_REVISION:
675 return history.index(revid)
677 def last_revision_info(self):
678 """See Branch.last_revision_info()."""
679 last_revid = self.last_revision()
680 return (self.revision_id_to_revno(last_revid), last_revid)
682 def last_revision(self):
683 """See Branch.last_revision()."""
684 return start_revid_parent
686 def get_branch_path(self, revnum=None):
687 """See SvnBranch.get_branch_path()."""
688 return target_branch_path
690 def generate_revision_id(self, revnum):
691 """See SvnBranch.generate_revision_id()."""
692 return self.repository.generate_revision_id(
693 revnum, self.get_branch_path(revnum),
694 self.repository.get_mapping())
696 push(target_repository.get_graph(), ImaginaryBranch(target_repository), source, start_revid, push_metadata=push_metadata)
699 def dpush(target, source, stop_revision=None):
700 """Push derivatives of the revisions missing from target from source into
703 :param target: Branch to push into
704 :param source: Branch to retrieve revisions from
705 :param stop_revision: If not None, stop at this revision.
706 :return: Map of old revids to new revids.
710 if stop_revision is None:
711 stop_revision = ensure_null(source.last_revision())
712 if target.last_revision() in (stop_revision, source.last_revision()):
714 graph = target.repository.get_graph()
715 if not source.repository.get_graph().is_ancestor(target.last_revision(),
717 if graph.is_ancestor(stop_revision, target.last_revision()):
719 raise DivergedBranches(source, target)
720 todo = target.mainline_missing_revisions(source, stop_revision)
722 pb = ui.ui_factory.nested_progress_bar()
725 pb.update("pushing revisions", todo.index(revid),
727 revid_map[revid] = push(graph, target, source.repository,
728 revid, push_metadata=False)
729 source.repository.fetch(target.repository,
730 revision_id=revid_map[revid])
731 target._clear_cached_state()
739 def push_revision_tree(graph, target, config, source_repo, base_revid,
740 revision_id, rev, push_metadata=True):
741 old_tree = source_repo.revision_tree(revision_id)
742 base_tree = source_repo.revision_tree(base_revid)
745 base_revids = rev.parent_ids
747 base_revids = [base_revid]
750 opt_signature = source_repo.get_signature_text(rev.revision_id)
751 except NoSuchRevision:
753 builder = SvnCommitBuilder(target.repository, target,
755 config, rev.timestamp,
756 rev.timezone, rev.committer, rev.properties,
757 revision_id, base_tree.inventory,
758 push_metadata=push_metadata,
760 opt_signature=opt_signature)
762 replay_delta(builder, base_tree, old_tree)
764 revid = builder.commit(rev.message)
765 except SubversionException, (_, num):
766 if num == ERR_FS_TXN_OUT_OF_DATE:
767 raise DivergedBranches(source, target)
769 except ChangesRootLHSHistory:
770 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)
776 def push(graph, target, source_repo, revision_id, push_metadata=True):
777 """Push a revision into Subversion.
779 This will do a new commit in the target branch.
781 :param target: Branch to push to
782 :param source_repo: Branch to pull the revision from
783 :param revision_id: Revision id of the revision to push
784 :return: revision id of revision that was pushed
786 assert isinstance(source_repo, Repository)
787 rev = source_repo.get_revision(revision_id)
788 mutter('pushing %r (%r)', revision_id, rev.parent_ids)
790 # revision on top of which to commit
792 if rev.parent_ids == []:
793 base_revid = NULL_REVISION
795 base_revid = rev.parent_ids[0]
797 base_revid = target.last_revision()
799 source_repo.lock_read()
801 revid = push_revision_tree(graph, target, target.get_config(),
802 source_repo, base_revid, revision_id,
803 rev, push_metadata=push_metadata)
807 assert revid == revision_id or not push_metadata
809 if 'validate' in debug.debug_flags and push_metadata:
810 crev = target.repository.get_revision(revision_id)
811 ctree = target.repository.revision_tree(revision_id)
812 assert crev.committer == rev.committer
813 assert crev.timezone == rev.timezone
814 assert crev.timestamp == rev.timestamp
815 assert crev.message == rev.message
816 assert crev.properties == rev.properties
821 class InterToSvnRepository(InterRepository):
822 """Any to Subversion repository actions."""
824 _matching_repo_format = SvnRepositoryFormat()
827 def _get_repo_format_to_test():
828 """See InterRepository._get_repo_format_to_test()."""
831 def copy_content(self, revision_id=None, pb=None):
832 """See InterRepository.copy_content."""
833 self.source.lock_read()
835 assert revision_id is not None, "fetching all revisions not supported"
836 # Go back over the LHS parent until we reach a revid we know
838 while not self.target.has_revision(revision_id):
839 todo.append(revision_id)
841 revision_id = self.source.get_parent_map([revision_id])[revision_id][0]
845 if revision_id == NULL_REVISION:
846 raise UnrelatedBranches()
850 mutter("pushing %r into svn", todo)
852 layout = self.target.get_layout()
853 graph = self.target.get_graph()
854 for revision_id in todo:
856 pb.update("pushing revisions", todo.index(revision_id), len(todo))
857 rev = self.source.get_revision(revision_id)
859 mutter('pushing %r', revision_id)
861 parent_revid = rev.parent_ids[0]
863 (bp, _, _) = self.target.lookup_revision_id(parent_revid)
864 if target_branch is None:
865 target_branch = Branch.open(urlutils.join(self.target.base, bp))
866 if target_branch.get_branch_path() != bp:
867 target_branch.set_branch_path(bp)
869 if layout.push_merged_revisions(target_branch.project) and len(rev.parent_ids) > 1:
870 push_ancestors(self.target, self.source, layout, "", rev.parent_ids, graph)
872 target_config = target_branch.get_config()
873 push_revision_tree(graph, target_branch, target_config,
874 self.source, parent_revid, revision_id, rev)
879 def fetch(self, revision_id=None, pb=None, find_ghosts=False):
880 """Fetch revisions. """
881 self.copy_content(revision_id=revision_id, pb=pb)
884 def is_compatible(source, target):
885 """Be compatible with SvnRepository."""
886 return isinstance(target, SvnRepository)
889 def push_ancestors(target_repo, source_repo, layout, project, parent_revids, graph):
890 for parent_revid in parent_revids[1:]:
891 if target_repo.has_revision(parent_revid):
893 # Push merged revisions
894 unique_ancestors = graph.find_unique_ancestors(parent_revid, [parent_revids[0]])
895 for x in graph.iter_topo_order(unique_ancestors):
896 if target_repo.has_revision(x):
898 rev = source_repo.get_revision(x)
899 nick = (rev.properties.get('branch-nick') or "merged").encode("utf-8").replace("/","_")
900 rhs_branch_path = layout.get_branch_path(nick, project)
901 push_new(target_repo, rhs_branch_path, source_repo, x)