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 path = "/".join(current).strip("/")
50 if transport.check_path(path, base_rev) == svn.core.svn_node_dir:
55 class SvnCommitBuilder(RootCommitBuilder):
56 """Commit Builder implementation wrapped around svn_delta_editor. """
58 def __init__(self, repository, branch, parents, config, timestamp,
59 timezone, committer, revprops, revision_id, old_inv=None):
60 """Instantiate a new SvnCommitBuilder.
62 :param repository: SvnRepository to commit to.
63 :param branch: SvnBranch to commit to.
64 :param parents: List of parent revision ids.
65 :param config: Branch configuration to use.
66 :param timestamp: Optional timestamp recorded for commit.
67 :param timezone: Optional timezone for timestamp.
68 :param committer: Optional committer to set for commit.
69 :param revprops: Revision properties to set.
70 :param revision_id: Revision id for the new revision.
71 :param old_inv: Optional revision on top of which
72 the commit is happening
74 super(SvnCommitBuilder, self).__init__(repository, parents,
75 config, timestamp, timezone, committer, revprops, revision_id)
79 # Keep track of what Subversion properties to set later on
81 self._svnprops[SVN_PROP_BZR_REVISION_INFO] = generate_revision_metadata(
82 timestamp, timezone, committer, revprops)
83 self._svnprops[SVN_PROP_BZR_FILEIDS] = ""
85 # Gather information about revision on top of which the commit is
88 self.base_revid = None
90 self.base_revid = parents[0]
91 self.base_revno = self.branch.revision_id_to_revno(self.base_revid)
92 if self.base_revid is None:
95 self.base_scheme = repository.get_scheme()
97 (self.base_path, self.base_revnum, self.base_scheme) = \
98 repository.lookup_revision_id(self.base_revid)
100 # Determine revisions merged in this one
101 merges = filter(lambda x: x != self.base_revid, parents)
104 self._record_merges(merges)
106 # Set appropriate property if revision id was specified by
108 if revision_id is not None:
109 self._record_revision_id(revision_id)
112 if self.base_revid is None:
113 self.old_inv = Inventory(root_id=None)
115 self.old_inv = self.repository.get_inventory(self.base_revid)
117 self.old_inv = old_inv
118 # Not all repositories appear to set Inventory.revision_id,
119 # so allow None as well.
120 assert self.old_inv.revision_id in (None, self.base_revid)
122 self.modified_files = {}
123 self.modified_dirs = set()
125 def mutter(self, text):
126 if 'commit' in debug.debug_flags:
129 def _record_revision_id(self, revid):
130 """Store the revision id in a file property.
132 :param revid: The revision id.
134 if self.base_revid is not None:
135 old = self.repository.branchprop_list.get_property(
136 self.base_path, self.base_revnum,
137 SVN_PROP_BZR_REVISION_ID+str(self.base_scheme), "")
141 self._svnprops[SVN_PROP_BZR_REVISION_ID+str(self.base_scheme)] = \
142 old + "%d %s\n" % (self.base_revno+1, revid)
144 def _record_merges(self, merges):
145 """Store the extra merges (non-LHS parents) in a file property.
147 :param merges: List of parents.
150 if self.base_revid is not None:
151 old = self.repository.branchprop_list.get_property(
152 self.base_path, self.base_revnum,
153 SVN_PROP_BZR_ANCESTRY+str(self.base_scheme), "")
156 self._svnprops[SVN_PROP_BZR_ANCESTRY+str(self.base_scheme)] = old + "\t".join(merges) + "\n"
158 if self.base_revid is not None:
159 old = self.repository.branchprop_list.get_property(
160 self.base_path, self.base_revnum, SVN_PROP_SVK_MERGE, "")
168 new += "%s\n" % revision_id_to_svk_feature(merge)
169 except InvalidRevisionId:
173 self._svnprops[SVN_PROP_SVK_MERGE] = old + new
175 def _generate_revision_if_needed(self):
176 """See CommitBuilder._generate_revision_if_needed()."""
178 def finish_inventory(self):
179 """See CommitBuilder.finish_inventory()."""
181 def modified_file_text(self, file_id, file_parents,
182 get_content_byte_lines, text_sha1=None,
184 """See CommitBuilder.modified_file_text()."""
185 new_lines = get_content_byte_lines()
186 self.modified_files[file_id] = "".join(new_lines)
187 return osutils.sha_strings(new_lines), sum(map(len, new_lines))
189 def modified_link(self, file_id, file_parents, link_target):
190 """See CommitBuilder.modified_link()."""
191 self.modified_files[file_id] = "link %s" % link_target
193 def modified_directory(self, file_id, file_parents):
194 """See CommitBuilder.modified_directory()."""
195 self.modified_dirs.add(file_id)
197 def _file_process(self, file_id, contents, baton):
198 """Pass the changes to a file to the Subversion commit editor.
200 :param file_id: Id of the file to modify.
201 :param contents: Contents of the file.
202 :param baton: Baton under which the file is known to the editor.
204 assert baton is not None
205 if contents == "" and not file_id in self.old_inv:
206 # Don't send diff if a new file with empty contents is
207 # added, because it created weird exceptions over svn+ssh://
210 (txdelta, txbaton) = self.editor.apply_textdelta(baton, None, self.pool)
211 svn.delta.svn_txdelta_send_string(contents, txdelta, txbaton, self.pool)
213 def _dir_process(self, path, file_id, baton):
214 """Pass the changes to a directory to the commit editor.
216 :param path: Path (from repository root) to the directory.
217 :param file_id: File id of the directory
218 :param baton: Baton of the directory for the editor.
220 assert baton is not None
221 # Loop over entries of file_id in self.old_inv
222 # remove if they no longer exist with the same name
224 if file_id in self.old_inv:
225 for child_name in self.old_inv[file_id].children:
226 child_ie = self.old_inv.get_child(file_id, child_name)
229 # ... path no longer exists
230 not child_ie.file_id in self.new_inventory or
232 child_ie.parent_id != self.new_inventory[child_ie.file_id].parent_id or
234 self.new_inventory[child_ie.file_id].name != child_name):
235 self.mutter('removing %r(%r)' % (child_name, child_ie.file_id))
236 self.editor.delete_entry(
238 self.branch.get_branch_path(), path, child_name),
239 self.base_revnum, baton, self.pool)
241 # Loop over file children of file_id in self.new_inventory
242 for child_name in self.new_inventory[file_id].children:
243 child_ie = self.new_inventory.get_child(file_id, child_name)
244 assert child_ie is not None
246 if not (child_ie.kind in ('file', 'symlink')):
249 new_child_path = self.new_inventory.id2path(child_ie.file_id)
250 # add them if they didn't exist in old_inv
251 if not child_ie.file_id in self.old_inv:
252 self.mutter('adding %s %r' % (child_ie.kind, new_child_path))
253 self._record_file_id(child_ie, new_child_path)
254 child_baton = self.editor.add_file(
255 urlutils.join(self.branch.get_branch_path(),
256 new_child_path), baton, None, -1, self.pool)
259 # copy if they existed at different location
260 elif (self.old_inv.id2path(child_ie.file_id) != new_child_path or
261 self.old_inv[child_ie.file_id].parent_id != child_ie.parent_id):
262 self.mutter('copy %s %r -> %r' % (child_ie.kind,
263 self.old_inv.id2path(child_ie.file_id),
265 self._record_file_id(child_ie, new_child_path)
266 child_baton = self.editor.add_file(
267 urlutils.join(self.branch.get_branch_path(), new_child_path), baton,
268 urlutils.join(self.repository.transport.svn_url, self.base_path, self.old_inv.id2path(child_ie.file_id)),
269 self.base_revnum, self.pool)
271 # open if they existed at the same location
272 elif child_ie.revision is None:
273 self.mutter('open %s %r' % (child_ie.kind, new_child_path))
275 child_baton = self.editor.open_file(
276 urlutils.join(self.branch.get_branch_path(),
278 baton, self.base_revnum, self.pool)
281 # Old copy of the file was retained. No need to send changes
282 assert child_ie.file_id not in self.modified_files
285 if child_ie.file_id in self.old_inv:
286 old_executable = self.old_inv[child_ie.file_id].executable
287 old_special = (self.old_inv[child_ie.file_id].kind == 'symlink')
290 old_executable = False
292 if child_baton is not None:
293 if old_executable != child_ie.executable:
294 if child_ie.executable:
295 value = svn.core.SVN_PROP_EXECUTABLE_VALUE
298 self.editor.change_file_prop(child_baton,
299 svn.core.SVN_PROP_EXECUTABLE, value, self.pool)
301 if old_special != (child_ie.kind == 'symlink'):
302 if child_ie.kind == 'symlink':
303 value = svn.core.SVN_PROP_SPECIAL_VALUE
307 self.editor.change_file_prop(child_baton,
308 svn.core.SVN_PROP_SPECIAL, value, self.pool)
311 if child_ie.file_id in self.modified_files:
312 self._file_process(child_ie.file_id,
313 self.modified_files[child_ie.file_id], child_baton)
315 if child_baton is not None:
316 self.editor.close_file(child_baton, None, self.pool)
318 # Loop over subdirectories of file_id in self.new_inventory
319 for child_name in self.new_inventory[file_id].children:
320 child_ie = self.new_inventory.get_child(file_id, child_name)
321 if child_ie.kind != 'directory':
324 new_child_path = self.new_inventory.id2path(child_ie.file_id)
325 # add them if they didn't exist in old_inv
326 if not child_ie.file_id in self.old_inv:
327 self.mutter('adding dir %r' % child_ie.name)
328 self._record_file_id(child_ie, new_child_path)
329 child_baton = self.editor.add_directory(
330 urlutils.join(self.branch.get_branch_path(),
331 new_child_path), baton, None, -1, self.pool)
333 # copy if they existed at different location
334 elif self.old_inv.id2path(child_ie.file_id) != new_child_path:
335 old_child_path = self.old_inv.id2path(child_ie.file_id)
336 self.mutter('copy dir %r -> %r' % (old_child_path, new_child_path))
337 self._record_file_id(child_ie, new_child_path)
338 child_baton = self.editor.add_directory(
339 urlutils.join(self.branch.get_branch_path(), new_child_path),
341 urlutils.join(self.repository.transport.svn_url, self.base_path, old_child_path), self.base_revnum, self.pool)
343 # open if they existed at the same location and
344 # the directory was touched
345 elif self.new_inventory[child_ie.file_id].revision is None:
346 self.mutter('open dir %r' % new_child_path)
348 child_baton = self.editor.open_directory(
349 urlutils.join(self.branch.get_branch_path(), new_child_path),
350 baton, self.base_revnum, self.pool)
352 assert child_ie.file_id not in self.modified_dirs
355 # Handle this directory
356 if child_ie.file_id in self.modified_dirs:
357 self._dir_process(new_child_path, child_ie.file_id, child_baton)
359 self.editor.close_directory(child_baton, self.pool)
361 def open_branch_batons(self, root, elements, existing_elements,
362 base_path, base_rev, replace_existing):
363 """Open a specified directory given a baton for the repository root.
365 :param root: Baton for the repository root
366 :param elements: List of directory names to open
367 :param existing_elements: List of directory names that exist
368 :param base_path: Path to base top-level branch on
369 :param base_rev: Revision of path to base top-level branch on
370 :param replace_existing: Whether the current branch should be replaced
374 self.mutter('opening branch %r (base %r:%r)' % (elements, base_path,
377 # Open paths leading up to branch
378 for i in range(0, len(elements)-1):
379 # Does directory already exist?
380 ret.append(self.editor.open_directory(
381 "/".join(existing_elements[0:i+1]), ret[-1], -1, self.pool))
383 assert (len(existing_elements) == len(elements) or
384 len(existing_elements)+1 == len(elements))
386 # Branch already exists and stayed at the same location, open:
387 # TODO: What if the branch didn't change but the new revision
388 # was based on an older revision of the branch?
389 # This needs to also check that base_rev was the latest version of
391 if (len(existing_elements) == len(elements) and
392 not replace_existing):
393 ret.append(self.editor.open_directory(
394 "/".join(elements), ret[-1], base_rev, self.pool))
395 else: # Branch has to be created
396 # Already exists, old copy needs to be removed
397 name = "/".join(elements)
399 self.mutter("removing branch dir %r" % name)
400 self.editor.delete_entry(name, -1, ret[-1])
401 if base_path is not None:
402 base_url = urlutils.join(self.repository.transport.svn_url, base_path)
405 self.mutter("adding branch dir %r" % name)
406 ret.append(self.editor.add_directory(
407 name, ret[-1], base_url, base_rev, self.pool))
411 def commit(self, message):
412 """Finish the commit.
415 def done(revision, date, author):
416 """Callback that is called by the Subversion commit editor
417 once the commit finishes.
419 :param revision: Revision number
420 :param date: Date recorded for this commit
423 self.revnum = revision
427 bp_parts = self.branch.get_branch_path().split("/")
428 repository_latest_revnum = self.repository.transport.get_latest_revnum()
429 lock = self.repository.transport.lock_write(".")
432 existing_bp_parts = _check_dirs_exist(self.repository.transport,
435 self.editor = self.repository.transport.get_commit_editor(
436 {svn.core.SVN_PROP_REVISION_LOG: message.encode("utf-8")},
439 root = self.editor.open_root(self.base_revnum)
441 replace_existing = False
442 if len(bp_parts) == len(existing_bp_parts):
443 if self.base_path.strip("/") != "/".join(bp_parts).strip("/"):
444 replace_existing = True
445 elif self.base_revnum < self.repository._log.find_latest_change(self.branch.get_branch_path(), repository_latest_revnum, include_children=True):
446 replace_existing = True
448 # TODO: Accept create_prefix argument
449 branch_batons = self.open_branch_batons(root, bp_parts,
450 existing_bp_parts, self.base_path, self.base_revnum,
453 # Make sure the root id is stored properly
454 if (self.old_inv.root is None or
455 self.new_inventory.root.file_id != self.old_inv.root.file_id):
456 self._record_file_id(self.new_inventory.root, "")
458 self._dir_process("", self.new_inventory.root.file_id,
461 # Set all the revprops
462 for prop, value in self._svnprops.items():
463 if value is not None:
464 value = value.encode('utf-8')
465 self.editor.change_dir_prop(branch_batons[-1], prop, value,
467 self.mutter("setting revision property %r to %r" % (prop, value))
469 for baton in reversed(branch_batons):
470 self.editor.close_directory(baton, self.pool)
476 assert self.revnum is not None
478 # Make sure the logwalker doesn't try to use ra
479 # during checkouts...
480 self.repository._log.fetch_revisions(self.revnum)
482 revid = self.branch.generate_revision_id(self.revnum)
484 assert self._new_revision_id is None or self._new_revision_id == revid
486 self.mutter('commit %d finished. author: %r, date: %r, revid: %r' %
487 (self.revnum, self.author, self.date, revid))
491 def _record_file_id(self, ie, path):
492 """Store the file id of an inventory entry in a file property.
494 :param ie: Inventory entry.
495 :param path: Path of the inventory entry.
497 self._svnprops[SVN_PROP_BZR_FILEIDS] += "%s\t%s\n" % (urllib.quote(path), ie.file_id)
499 def record_entry_contents(self, ie, parent_invs, path, tree,
501 """Record the content of ie from tree into the commit if needed.
503 Side effect: sets ie.revision when unchanged
505 :param ie: An inventory entry present in the commit.
506 :param parent_invs: The inventories of the parent revisions of the
508 :param path: The path the entry is at in the tree.
509 :param tree: The tree which contains this entry and should be used to
512 assert self.new_inventory.root is not None or ie.parent_id is None
513 self.new_inventory.add(ie)
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 rev = source.repository.get_revision(revision_id)
648 mutter('pushing %r (%r)' % (revision_id, rev.parent_ids))
650 # revision on top of which to commit
651 if rev.parent_ids == []:
654 base_revid = rev.parent_ids[0]
656 old_tree = source.repository.revision_tree(revision_id)
657 base_tree = source.repository.revision_tree(base_revid)
659 builder = SvnCommitBuilder(target.repository, target, rev.parent_ids,
660 target.get_config(), rev.timestamp,
661 rev.timezone, rev.committer, rev.properties,
662 revision_id, base_tree.inventory)
664 builder.new_inventory = source.repository.get_inventory(revision_id)
665 replay_delta(builder, base_tree, old_tree)
667 builder.commit(rev.message)
668 except SubversionException, (_, num):
669 if num == svn.core.SVN_ERR_FS_TXN_OUT_OF_DATE:
670 raise DivergedBranches(source, target)
673 crev = target.repository.get_revision(revision_id)
674 ctree = target.repository.revision_tree(revision_id)
675 treedelta = ctree.changes_from(old_tree)
676 assert not treedelta.has_changed(), "treedelta: %r" % treedelta
677 assert crev.committer == rev.committer
678 assert crev.timezone == rev.timezone
679 assert crev.timestamp == rev.timestamp
680 assert crev.message == rev.message
681 assert crev.properties == rev.properties
684 class InterToSvnRepository(InterRepository):
685 """Any to Subversion repository actions."""
687 _matching_repo_format = SvnRepositoryFormat()
690 def _get_repo_format_to_test():
691 """See InterRepository._get_repo_format_to_test()."""
694 def copy_content(self, revision_id=None, pb=None):
695 """See InterRepository.copy_content."""
696 assert revision_id is not None, "fetching all revisions not supported"
697 # Go back over the LHS parent until we reach a revid we know
699 while not self.target.has_revision(revision_id):
700 todo.append(revision_id)
701 revision_id = self.source.revision_parents(revision_id)[0]
702 if revision_id == NULL_REVISION:
703 raise UnrelatedBranches()
707 mutter("pushing %r into svn" % todo)
709 for revision_id in todo:
711 pb.update("pushing revisions", todo.index(revision_id), len(todo))
712 rev = self.source.get_revision(revision_id)
714 mutter('pushing %r' % (revision_id))
716 old_tree = self.source.revision_tree(revision_id)
717 parent_revid = rev.parent_ids[0]
718 base_tree = self.source.revision_tree(parent_revid)
720 (bp, _, _) = self.target.lookup_revision_id(parent_revid)
721 if target_branch is None:
722 target_branch = Branch.open(urlutils.join(self.target.base, bp))
723 if target_branch.get_branch_path() != bp:
724 target_branch.set_branch_path(bp)
726 builder = SvnCommitBuilder(self.target, target_branch,
727 rev.parent_ids, target_branch.get_config(),
728 rev.timestamp, rev.timezone, rev.committer,
729 rev.properties, revision_id, base_tree.inventory)
731 builder.new_inventory = self.source.get_inventory(revision_id)
732 replay_delta(builder, base_tree, old_tree)
733 builder.commit(rev.message)
736 def fetch(self, revision_id=None, pb=None):
737 """Fetch revisions. """
738 self.copy_content(revision_id=revision_id, pb=pb)
741 def is_compatible(source, target):
742 """Be compatible with SvnRepository."""
743 return isinstance(target, SvnRepository)