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
21 from bzrlib import debug, osutils, urlutils
22 from bzrlib.branch import Branch
23 from bzrlib.errors import InvalidRevisionId, DivergedBranches, UnrelatedBranches
24 from bzrlib.inventory import Inventory
25 from bzrlib.repository import RootCommitBuilder, InterRepository
26 from bzrlib.revision import NULL_REVISION
27 from bzrlib.trace import mutter
29 from copy import deepcopy
30 from repository import (SVN_PROP_BZR_ANCESTRY, SVN_PROP_BZR_FILEIDS,
31 SVN_PROP_SVK_MERGE, SVN_PROP_BZR_REVISION_INFO,
32 SVN_PROP_BZR_REVISION_ID, revision_id_to_svk_feature,
33 generate_revision_metadata, SvnRepositoryFormat,
38 def _check_dirs_exist(transport, bp_parts, base_rev):
39 """Make sure that the specified directories exist.
41 :param transport: SvnRaTransport to use.
42 :param bp_parts: List of directory names in the format returned by
44 :param base_rev: Base revision to check.
45 :return: List of the directories that exists in base_rev.
47 for i in range(len(bp_parts), 0, -1):
48 current = bp_parts[:i]
49 if transport.check_path("/".join(current).strip("/"), base_rev) == svn.core.svn_node_dir:
54 class SvnCommitBuilder(RootCommitBuilder):
55 """Commit Builder implementation wrapped around svn_delta_editor. """
57 def __init__(self, repository, branch, parents, config, timestamp,
58 timezone, committer, revprops, revision_id, old_inv=None):
59 """Instantiate a new SvnCommitBuilder.
61 :param repository: SvnRepository to commit to.
62 :param branch: SvnBranch to commit to.
63 :param parents: List of parent revision ids.
64 :param config: Branch configuration to use.
65 :param timestamp: Optional timestamp recorded for commit.
66 :param timezone: Optional timezone for timestamp.
67 :param committer: Optional committer to set for commit.
68 :param revprops: Revision properties to set.
69 :param revision_id: Revision id for the new revision.
70 :param old_inv: Optional revision on top of which
71 the commit is happening
73 super(SvnCommitBuilder, self).__init__(repository, parents,
74 config, timestamp, timezone, committer, revprops, revision_id)
78 # Keep track of what Subversion properties to set later on
80 self._svnprops[SVN_PROP_BZR_REVISION_INFO] = generate_revision_metadata(
81 timestamp, timezone, committer, revprops)
82 self._svnprops[SVN_PROP_BZR_FILEIDS] = ""
84 # Gather information about revision on top of which the commit is
87 self.base_revid = None
89 self.base_revid = parents[0]
90 self.base_revno = self.branch.revision_id_to_revno(self.base_revid)
91 if self.base_revid is None:
94 self.base_scheme = repository.get_scheme()
96 (self.base_path, self.base_revnum, self.base_scheme) = \
97 repository.lookup_revision_id(self.base_revid)
99 # Determine revisions merged in this one
100 merges = filter(lambda x: x != self.base_revid, parents)
103 self._record_merges(merges)
105 # Set appropriate property if revision id was specified by
107 if revision_id is not None:
108 self._record_revision_id(revision_id)
110 # At least one of the parents has to be the last revision on the
111 # mainline in Subversion.
112 assert (self.base_revid is None or self.base_revid in parents)
115 if self.base_revid is None:
116 self.old_inv = Inventory(root_id=None)
118 self.old_inv = self.repository.get_inventory(self.base_revid)
120 self.old_inv = old_inv
121 # Not all repositories appear to set Inventory.revision_id,
122 # so allow None as well.
123 assert self.old_inv.revision_id in (None, self.base_revid)
125 self.modified_files = {}
126 self.modified_dirs = set()
128 def mutter(self, text):
129 if 'commit' in debug.debug_flags:
132 def _record_revision_id(self, revid):
133 """Store the revision id in a file property.
135 :param revid: The revision id.
137 if self.base_revid is not None:
138 old = self.repository.branchprop_list.get_property(
139 self.base_path, self.base_revnum,
140 SVN_PROP_BZR_REVISION_ID+str(self.base_scheme), "")
144 self._svnprops[SVN_PROP_BZR_REVISION_ID+str(self.base_scheme)] = \
145 old + "%d %s\n" % (self.base_revno+1, revid)
147 def _record_merges(self, merges):
148 """Store the extra merges (non-LHS parents) in a file property.
150 :param merges: List of parents.
153 if self.base_revid is not None:
154 old = self.repository.branchprop_list.get_property(
155 self.base_path, self.base_revnum,
156 SVN_PROP_BZR_ANCESTRY+str(self.base_scheme), "")
159 self._svnprops[SVN_PROP_BZR_ANCESTRY+str(self.base_scheme)] = old + "\t".join(merges) + "\n"
161 if self.base_revid is not None:
162 old = self.repository.branchprop_list.get_property(
163 self.base_path, self.base_revnum, SVN_PROP_SVK_MERGE, "")
171 new += "%s\n" % revision_id_to_svk_feature(merge)
172 except InvalidRevisionId:
176 self._svnprops[SVN_PROP_SVK_MERGE] = old + new
178 def _generate_revision_if_needed(self):
179 """See CommitBuilder._generate_revision_if_needed()."""
181 def finish_inventory(self):
182 """See CommitBuilder.finish_inventory()."""
184 def modified_file_text(self, file_id, file_parents,
185 get_content_byte_lines, text_sha1=None,
187 """See CommitBuilder.modified_file_text()."""
188 new_lines = get_content_byte_lines()
189 self.modified_files[file_id] = "".join(new_lines)
190 return osutils.sha_strings(new_lines), sum(map(len, new_lines))
192 def modified_link(self, file_id, file_parents, link_target):
193 """See CommitBuilder.modified_link()."""
194 self.modified_files[file_id] = "link %s" % link_target
196 def modified_directory(self, file_id, file_parents):
197 """See CommitBuilder.modified_directory()."""
198 self.modified_dirs.add(file_id)
200 def _file_process(self, file_id, contents, baton):
201 """Pass the changes to a file to the Subversion commit editor.
203 :param file_id: Id of the file to modify.
204 :param contents: Contents of the file.
205 :param baton: Baton under which the file is known to the editor.
207 assert baton is not None
208 if contents == "" and not file_id in self.old_inv:
209 # Don't send diff if a new file with empty contents is
210 # added, because it created weird exceptions over svn+ssh://
213 (txdelta, txbaton) = self.editor.apply_textdelta(baton, None, self.pool)
214 svn.delta.svn_txdelta_send_string(contents, txdelta, txbaton, self.pool)
216 def _dir_process(self, path, file_id, baton):
217 """Pass the changes to a directory to the commit editor.
219 :param path: Path (from repository root) to the directory.
220 :param file_id: File id of the directory
221 :param baton: Baton of the directory for the editor.
223 assert baton is not None
224 # Loop over entries of file_id in self.old_inv
225 # remove if they no longer exist with the same name
227 if file_id in self.old_inv:
228 for child_name in self.old_inv[file_id].children:
229 child_ie = self.old_inv.get_child(file_id, child_name)
232 # ... path no longer exists
233 not child_ie.file_id in self.new_inventory or
235 child_ie.parent_id != self.new_inventory[child_ie.file_id].parent_id or
237 self.new_inventory[child_ie.file_id].name != child_name):
238 self.mutter('removing %r(%r)' % (child_name, child_ie.file_id))
239 self.editor.delete_entry(
241 self.branch.get_branch_path(), path, child_name),
242 self.base_revnum, baton, self.pool)
244 # Loop over file children of file_id in self.new_inventory
245 for child_name in self.new_inventory[file_id].children:
246 child_ie = self.new_inventory.get_child(file_id, child_name)
247 assert child_ie is not None
249 if not (child_ie.kind in ('file', 'symlink')):
252 new_child_path = self.new_inventory.id2path(child_ie.file_id)
253 # add them if they didn't exist in old_inv
254 if not child_ie.file_id in self.old_inv:
255 self.mutter('adding %s %r' % (child_ie.kind, new_child_path))
256 self._record_file_id(child_ie, new_child_path)
257 child_baton = self.editor.add_file(
258 urlutils.join(self.branch.get_branch_path(),
259 new_child_path), baton, None, -1, self.pool)
262 # copy if they existed at different location
263 elif (self.old_inv.id2path(child_ie.file_id) != new_child_path or
264 self.old_inv[child_ie.file_id].parent_id != child_ie.parent_id):
265 self.mutter('copy %s %r -> %r' % (child_ie.kind,
266 self.old_inv.id2path(child_ie.file_id),
268 self._record_file_id(child_ie, new_child_path)
269 child_baton = self.editor.add_file(
270 urlutils.join(self.branch.get_branch_path(), new_child_path), baton,
271 urlutils.join(self.repository.transport.svn_url, self.base_path, self.old_inv.id2path(child_ie.file_id)),
272 self.base_revnum, self.pool)
274 # open if they existed at the same location
275 elif child_ie.revision is None:
276 self.mutter('open %s %r' % (child_ie.kind, new_child_path))
278 child_baton = self.editor.open_file(
279 urlutils.join(self.branch.get_branch_path(),
281 baton, self.base_revnum, self.pool)
284 # Old copy of the file was retained. No need to send changes
285 assert child_ie.file_id not in self.modified_files
288 if child_ie.file_id in self.old_inv:
289 old_executable = self.old_inv[child_ie.file_id].executable
290 old_special = (self.old_inv[child_ie.file_id].kind == 'symlink')
293 old_executable = False
295 if child_baton is not None:
296 if old_executable != child_ie.executable:
297 if child_ie.executable:
298 value = svn.core.SVN_PROP_EXECUTABLE_VALUE
301 self.editor.change_file_prop(child_baton,
302 svn.core.SVN_PROP_EXECUTABLE, value, self.pool)
304 if old_special != (child_ie.kind == 'symlink'):
305 if child_ie.kind == 'symlink':
306 value = svn.core.SVN_PROP_SPECIAL_VALUE
310 self.editor.change_file_prop(child_baton,
311 svn.core.SVN_PROP_SPECIAL, value, self.pool)
314 if child_ie.file_id in self.modified_files:
315 self._file_process(child_ie.file_id,
316 self.modified_files[child_ie.file_id], child_baton)
318 if child_baton is not None:
319 self.editor.close_file(child_baton, None, self.pool)
321 # Loop over subdirectories of file_id in self.new_inventory
322 for child_name in self.new_inventory[file_id].children:
323 child_ie = self.new_inventory.get_child(file_id, child_name)
324 if child_ie.kind != 'directory':
327 new_child_path = self.new_inventory.id2path(child_ie.file_id)
328 # add them if they didn't exist in old_inv
329 if not child_ie.file_id in self.old_inv:
330 self.mutter('adding dir %r' % child_ie.name)
331 self._record_file_id(child_ie, new_child_path)
332 child_baton = self.editor.add_directory(
333 urlutils.join(self.branch.get_branch_path(),
334 new_child_path), baton, None, -1, self.pool)
336 # copy if they existed at different location
337 elif self.old_inv.id2path(child_ie.file_id) != new_child_path:
338 old_child_path = self.old_inv.id2path(child_ie.file_id)
339 self.mutter('copy dir %r -> %r' % (old_child_path, new_child_path))
340 self._record_file_id(child_ie, new_child_path)
341 child_baton = self.editor.add_directory(
342 urlutils.join(self.branch.get_branch_path(), new_child_path),
344 urlutils.join(self.repository.transport.svn_url, self.base_path, old_child_path), self.base_revnum, self.pool)
346 # open if they existed at the same location and
347 # the directory was touched
348 elif self.new_inventory[child_ie.file_id].revision is None:
349 self.mutter('open dir %r' % new_child_path)
351 child_baton = self.editor.open_directory(
352 urlutils.join(self.branch.get_branch_path(), new_child_path),
353 baton, self.base_revnum, self.pool)
355 assert child_ie.file_id not in self.modified_dirs
358 # Handle this directory
359 if child_ie.file_id in self.modified_dirs:
360 self._dir_process(new_child_path, child_ie.file_id, child_baton)
362 self.editor.close_directory(child_baton, self.pool)
364 def open_branch_batons(self, root, elements, existing_elements,
365 base_path, base_rev):
366 """Open a specified directory given a baton for the repository root.
368 :param root: Baton for the repository root
369 :param elements: List of directory names to open
370 :param existing_elements: List of directory names that exist
371 :param base_path: Path to base top-level branch on
372 :param base_rev: Revision of path to base top-level branch on
376 self.mutter('opening branch %r (base %r:%r)' % (elements, base_path,
379 # Open paths leading up to branch
380 for i in range(0, len(elements)-1):
381 # Does directory already exist?
382 ret.append(self.editor.open_directory(
383 "/".join(existing_elements[0:i+1]), ret[-1], -1, self.pool))
385 assert (len(existing_elements) == len(elements) or
386 len(existing_elements)+1 == len(elements))
388 # Branch already exists and stayed at the same location, open:
389 # TODO: What if the branch didn't change but the new revision
390 # was based on an older revision of the branch?
391 # This needs to also check that base_rev was the latest version of
393 if (len(existing_elements) == len(elements) and
394 base_path.strip("/") == "/".join(elements).strip("/")):
395 ret.append(self.editor.open_directory(
396 "/".join(elements), ret[-1], base_rev, self.pool))
397 else: # Branch has to be created
398 # Already exists, old copy needs to be removed
399 if len(existing_elements) == len(elements):
400 self.editor.delete_entry("/".join(elements), -1, ret[-1])
401 if base_path is not None:
402 base_url = urlutils.join(self.repository.transport.svn_url, base_path)
405 ret.append(self.editor.add_directory(
406 "/".join(elements), ret[-1], base_url, base_rev, self.pool))
410 def commit(self, message):
411 """Finish the commit.
414 def done(revision, date, author):
415 """Callback that is called by the Subversion commit editor
416 once the commit finishes.
418 :param revision: Revision number
419 :param date: Date recorded for this commit
422 self.revnum = revision
426 bp_parts = self.branch.get_branch_path().split("/")
427 lock = self.repository.transport.lock_write(".")
430 existing_bp_parts = _check_dirs_exist(self.repository.transport,
433 self.editor = self.repository.transport.get_commit_editor(
434 message.encode("utf-8"), done, None, False)
436 root = self.editor.open_root(self.base_revnum)
438 # TODO: Accept create_prefix argument
439 branch_batons = self.open_branch_batons(root, bp_parts,
440 existing_bp_parts, self.base_path, self.base_revnum)
442 # Make sure the root id is stored properly
443 if (self.old_inv.root is None or
444 self.new_inventory.root.file_id != self.old_inv.root.file_id):
445 self._record_file_id(self.new_inventory.root, "")
447 self._dir_process("", self.new_inventory.root.file_id,
450 # Set all the revprops
451 for prop, value in self._svnprops.items():
452 if value is not None:
453 value = value.encode('utf-8')
454 self.editor.change_dir_prop(branch_batons[-1], prop, value,
457 for baton in reversed(branch_batons):
458 self.editor.close_directory(baton, self.pool)
464 assert self.revnum is not None
466 # Make sure the logwalker doesn't try to use ra
467 # during checkouts...
468 self.repository._log.fetch_revisions(self.revnum)
470 revid = self.branch.generate_revision_id(self.revnum)
472 assert self._new_revision_id is None or self._new_revision_id == revid
474 self.mutter('commit %d finished. author: %r, date: %r, revid: %r' %
475 (self.revnum, self.author, self.date, revid))
479 def _record_file_id(self, ie, path):
480 """Store the file id of an inventory entry in a file property.
482 :param ie: Inventory entry.
483 :param path: Path of the inventory entry.
485 self._svnprops[SVN_PROP_BZR_FILEIDS] += "%s\t%s\n" % (urllib.quote(path), ie.file_id)
487 def record_entry_contents(self, ie, parent_invs, path, tree):
488 """Record the content of ie from tree into the commit if needed.
490 Side effect: sets ie.revision when unchanged
492 :param ie: An inventory entry present in the commit.
493 :param parent_invs: The inventories of the parent revisions of the
495 :param path: The path the entry is at in the tree.
496 :param tree: The tree which contains this entry and should be used to
499 assert self.new_inventory.root is not None or ie.parent_id is None
500 self.new_inventory.add(ie)
502 # ie.revision is always None if the InventoryEntry is considered
503 # for committing. ie.snapshot will record the correct revision
504 # which may be the sole parent if it is untouched.
505 if ie.revision is not None:
508 previous_entries = ie.find_previous_heads(parent_invs,
509 self.repository.weave_store, None)
511 # we are creating a new revision for ie in the history store
513 ie.snapshot(self._new_revision_id, path, previous_entries, tree, self)
516 def replay_delta(builder, old_tree, new_tree):
517 """Replays a delta to a commit builder.
519 :param builder: The commit builder.
520 :param old_tree: Original tree on top of which the delta should be applied
521 :param new_tree: New tree that should be committed
523 delta = new_tree.changes_from(old_tree)
525 ie = builder.new_inventory[id]
528 while builder.new_inventory[id].parent_id is not None:
529 if builder.new_inventory[id].revision is None:
531 builder.new_inventory[id].revision = None
532 if builder.new_inventory[id].kind == 'directory':
533 builder.modified_directory(id, [])
534 id = builder.new_inventory[id].parent_id
536 if ie.kind == 'link':
537 builder.modified_link(ie.file_id, [], ie.symlink_target)
538 elif ie.kind == 'file':
540 return new_tree.get_file_text(ie.file_id)
541 builder.modified_file_text(ie.file_id, [], get_text)
543 for (_, id, _) in delta.added:
546 for (_, id, _, _, _) in delta.modified:
549 for (oldpath, _, id, _, _, _) in delta.renamed:
551 old_parent_id = old_tree.inventory.path2id(urlutils.dirname(oldpath))
552 if old_parent_id in builder.new_inventory:
553 touch_id(old_parent_id)
555 for (path, _, _) in delta.removed:
556 old_parent_id = old_tree.inventory.path2id(urlutils.dirname(path))
557 if old_parent_id in builder.new_inventory:
558 touch_id(old_parent_id)
560 builder.finish_inventory()
563 def push_new(target_repository, target_branch_path, source,
564 stop_revision=None, validate=False):
565 """Push a revision into Subversion, creating a new branch.
567 This will do a new commit in the target branch.
569 :param target_branch_path: Path to create new branch at
570 :param source: Branch to pull the revision from
571 :param revision_id: Revision id of the revision to push
572 :param validate: Whether to check the committed revision matches
575 assert isinstance(source, Branch)
576 if stop_revision is None:
577 stop_revision = source.last_revision()
578 history = source.revision_history()
579 revhistory = deepcopy(history)
580 start_revid = NULL_REVISION
581 while len(revhistory) > 0:
582 revid = revhistory.pop()
583 # We've found the revision to push if there is a revision
584 # which LHS parent is present or if this is the first revision.
585 if (len(revhistory) == 0 or
586 target_repository.has_revision(revhistory[-1])):
590 # Get commit builder but specify that target_branch_path should
591 # be created and copied from (copy_path, copy_revnum)
592 class ImaginaryBranch:
593 """Simple branch that pretends to be empty but already exist."""
594 def __init__(self, repository):
595 self.repository = repository
596 self._revision_history = None
598 def get_config(self):
599 """See Branch.get_config()."""
602 def revision_id_to_revno(self, revid):
605 return history.index(revid)
607 def last_revision_info(self):
608 """See Branch.last_revision_info()."""
609 last_revid = self.last_revision()
610 if last_revid is None:
612 return (history.index(last_revid), last_revid)
614 def last_revision(self):
615 """See Branch.last_revision()."""
616 parents = source.repository.revision_parents(start_revid)
621 def get_branch_path(self, revnum=None):
622 """See SvnBranch.get_branch_path()."""
623 return target_branch_path
625 def generate_revision_id(self, revnum):
626 """See SvnBranch.generate_revision_id()."""
627 return self.repository.generate_revision_id(
628 revnum, self.get_branch_path(revnum),
629 str(self.repository.get_scheme()))
631 push(ImaginaryBranch(target_repository), source, start_revid,
635 def push(target, source, revision_id, validate=False):
636 """Push a revision into Subversion.
638 This will do a new commit in the target branch.
640 :param target: Branch to push to
641 :param source: Branch to pull the revision from
642 :param revision_id: Revision id of the revision to push
643 :param validate: Whether to check the committed revision matches
646 assert isinstance(source, Branch)
647 mutter('pushing %r' % (revision_id))
648 rev = source.repository.get_revision(revision_id)
650 if rev.parent_ids == []:
653 base_revid = rev.parent_ids[0]
655 # revision on top of which to commit
656 assert (base_revid in rev.parent_ids or
657 base_revid is None and rev.parent_ids == [])
659 old_tree = source.repository.revision_tree(revision_id)
660 base_tree = source.repository.revision_tree(base_revid)
662 builder = SvnCommitBuilder(target.repository, target,
672 builder.new_inventory = source.repository.get_inventory(revision_id)
673 replay_delta(builder, base_tree, old_tree)
675 builder.commit(rev.message)
676 except SubversionException, (_, num):
677 if num == svn.core.SVN_ERR_FS_TXN_OUT_OF_DATE:
678 raise DivergedBranches(source, target)
681 crev = target.repository.get_revision(revision_id)
682 ctree = target.repository.revision_tree(revision_id)
683 treedelta = ctree.changes_from(old_tree)
684 assert not treedelta.has_changed(), "treedelta: %r" % treedelta
685 assert crev.committer == rev.committer
686 assert crev.timezone == rev.timezone
687 assert crev.timestamp == rev.timestamp
688 assert crev.message == rev.message
689 assert crev.properties == rev.properties
692 class InterToSvnRepository(InterRepository):
693 """Any to Subversion repository actions."""
695 _matching_repo_format = SvnRepositoryFormat()
698 def _get_repo_format_to_test():
699 """See InterRepository._get_repo_format_to_test()."""
702 def copy_content(self, revision_id=None, pb=None):
703 """See InterRepository.copy_content."""
704 assert revision_id is not None, "fetching all revisions not supported"
705 # Go back over the LHS parent until we reach a revid we know
707 while not self.target.has_revision(revision_id):
708 todo.append(revision_id)
709 revision_id = self.source.revision_parents(revision_id)[0]
710 if revision_id == NULL_REVISION:
711 raise UnrelatedBranches()
715 mutter("pushing %r into svn" % todo)
717 for revision_id in todo:
719 pb.update("pushing revisions", todo.index(revision_id), len(todo))
720 rev = self.source.get_revision(revision_id)
722 mutter('pushing %r' % (revision_id))
724 old_tree = self.source.revision_tree(revision_id)
725 parent_revid = rev.parent_ids[0]
726 base_tree = self.source.revision_tree(parent_revid)
728 (bp, _, _) = self.target.lookup_revision_id(parent_revid)
729 if target_branch is None:
730 target_branch = Branch.open(urlutils.join(self.target.base, bp))
731 if target_branch.get_branch_path() != bp:
732 target_branch.set_branch_path(bp)
734 builder = SvnCommitBuilder(self.target, target_branch,
736 target_branch.get_config(),
744 builder.new_inventory = self.source.get_inventory(revision_id)
745 replay_delta(builder, base_tree, old_tree)
746 builder.commit(rev.message)
749 def fetch(self, revision_id=None, pb=None):
750 """Fetch revisions. """
751 self.copy_content(revision_id=revision_id, pb=pb)
754 def is_compatible(source, target):
755 """Be compatible with SvnRepository."""
756 return isinstance(target, SvnRepository)