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 osutils, urlutils
22 from bzrlib.branch import Branch
23 from bzrlib.errors import InvalidRevisionId, DivergedBranches
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
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,
35 from revids import escape_svn_path
37 from copy import deepcopy
39 def _check_dirs_exist(transport, bp_parts, base_rev):
40 for i in range(len(bp_parts), 0, -1):
41 current = bp_parts[:i]
42 if transport.check_path("/".join(current).strip("/"), base_rev) == svn.core.svn_node_dir:
47 class SvnCommitBuilder(RootCommitBuilder):
48 """Commit Builder implementation wrapped around svn_delta_editor. """
50 def __init__(self, repository, branch, parents, config, timestamp,
51 timezone, committer, revprops, revision_id, old_inv=None):
52 """Instantiate a new SvnCommitBuilder.
54 :param repository: SvnRepository to commit to.
55 :param branch: SvnBranch to commit to.
56 :param parents: List of parent revision ids.
57 :param config: Branch configuration to use.
58 :param timestamp: Optional timestamp recorded for commit.
59 :param timezone: Optional timezone for timestamp.
60 :param committer: Optional committer to set for commit.
61 :param revprops: Revision properties to set.
62 :param revision_id: Revision id for the new revision.
63 :param old_inv: Optional revision on top of which
64 the commit is happening
66 super(SvnCommitBuilder, self).__init__(repository, parents,
67 config, timestamp, timezone, committer, revprops, revision_id)
71 # Keep track of what Subversion properties to set later on
73 self._svnprops[SVN_PROP_BZR_REVISION_INFO] = generate_revision_metadata(
74 timestamp, timezone, committer, revprops)
76 # Gather information about revision on top of which the commit is
78 (self.base_revno, self.base_revid) = self.branch.last_revision_info()
79 if self.base_revid is None:
82 self.base_scheme = repository.get_scheme()
84 (self.base_path, self.base_revnum, self.base_scheme) = \
85 repository.lookup_revision_id(self.base_revid)
87 # Determine revisions merged in this one
88 merges = filter(lambda x: x != self.base_revid, parents)
91 self._record_merges(merges)
93 # Set appropriate property if revision id was specified by
95 if revision_id is not None:
96 self._record_revision_id(revision_id)
98 # At least one of the parents has to be the last revision on the
99 # mainline in Subversion.
100 assert (self.base_revid is None or self.base_revid in parents)
103 if self.base_revid is None:
104 self.old_inv = Inventory(root_id=None)
106 self.old_inv = self.repository.get_inventory(self.base_revid)
108 self.old_inv = old_inv
109 assert self.old_inv.revision_id == self.base_revid
111 self.modified_files = {}
112 self.modified_dirs = set()
114 def _record_revision_id(self, revid):
115 if self.base_revid is not None:
116 old = self.repository.branchprop_list.get_property(
117 self.base_path, self.base_revnum,
118 SVN_PROP_BZR_REVISION_ID+str(self.base_scheme), "")
122 self._svnprops[SVN_PROP_BZR_REVISION_ID+str(self.base_scheme)] = \
123 old + "%d %s\n" % (self.base_revno+1, revid)
125 def _record_merges(self, merges):
127 if self.base_revid is not None:
128 old = self.repository.branchprop_list.get_property(
129 self.base_path, self.base_revnum,
130 SVN_PROP_BZR_ANCESTRY+str(self.base_scheme), "")
133 self._svnprops[SVN_PROP_BZR_ANCESTRY+str(self.base_scheme)] = old + "\t".join(merges) + "\n"
135 if self.base_revid is not None:
136 old = self.repository.branchprop_list.get_property(
137 self.base_path, self.base_revnum, SVN_PROP_SVK_MERGE)
145 new += "%s\n" % revision_id_to_svk_feature(p)
146 except InvalidRevisionId:
150 self._svnprops[SVN_PROP_SVK_MERGE] = old + new
152 def _generate_revision_if_needed(self):
155 def finish_inventory(self):
158 def modified_file_text(self, file_id, file_parents,
159 get_content_byte_lines, text_sha1=None,
161 mutter('modifying file %s' % file_id)
162 new_lines = get_content_byte_lines()
163 self.modified_files[file_id] = "".join(new_lines)
164 return osutils.sha_strings(new_lines), sum(map(len, new_lines))
166 def modified_link(self, file_id, file_parents, link_target):
167 mutter('modifying link %s' % file_id)
168 self.modified_files[file_id] = "link %s" % link_target
170 def modified_directory(self, file_id, file_parents):
171 mutter('modifying directory %s' % file_id)
172 self.modified_dirs.add(file_id)
174 def _file_process(self, file_id, contents, baton):
175 assert baton is not None
176 (txdelta, txbaton) = self.editor.apply_textdelta(baton, None, self.pool)
177 svn.delta.svn_txdelta_send_string(contents, txdelta, txbaton, self.pool)
179 def _dir_process(self, path, file_id, baton):
180 assert baton is not None
181 mutter('processing %r' % path)
182 # Loop over entries of file_id in self.old_inv
183 # remove if they no longer exist with the same name
185 if file_id in self.old_inv:
186 for child_name in self.old_inv[file_id].children:
187 child_ie = self.old_inv.get_child(file_id, child_name)
190 # ... path no longer exists
191 not child_ie.file_id in self.new_inventory or
193 child_ie.parent_id != self.new_inventory[child_ie.file_id].parent_id or
195 self.new_inventory[child_ie.file_id].name != child_name):
196 mutter('removing %r' % child_ie.file_id)
197 self.editor.delete_entry(
199 self.branch.get_branch_path(),
200 self.old_inv.id2path(child_ie.file_id)),
201 self.base_revnum, baton, self.pool)
203 # Loop over file children of file_id in self.new_inventory
204 for child_name in self.new_inventory[file_id].children:
205 child_ie = self.new_inventory.get_child(file_id, child_name)
206 assert child_ie is not None
208 if not (child_ie.kind in ('file', 'symlink')):
211 # add them if they didn't exist in old_inv
212 if not child_ie.file_id in self.old_inv:
213 mutter('adding %s %r' % (child_ie.kind, self.new_inventory.id2path(child_ie.file_id)))
215 child_baton = self.editor.add_file(
217 self.branch.get_branch_path(),
218 self.new_inventory.id2path(child_ie.file_id)),
219 baton, None, -1, self.pool)
222 # copy if they existed at different location
223 elif self.old_inv.id2path(child_ie.file_id) != self.new_inventory.id2path(child_ie.file_id):
224 mutter('copy %s %r -> %r' % (child_ie.kind,
225 self.old_inv.id2path(child_ie.file_id),
226 self.new_inventory.id2path(child_ie.file_id)))
228 child_baton = self.editor.add_file(
229 urlutils.join(self.branch.get_branch_path(), self.new_inventory.id2path(child_ie.file_id)), baton,
230 urlutils.join(self.repository.transport.svn_url, self.base_path, self.old_inv.id2path(child_ie.file_id)),
231 self.base_revnum, self.pool)
233 # open if they existed at the same location
234 elif child_ie.revision is None:
235 mutter('open %s %r' % (child_ie.kind,
236 self.new_inventory.id2path(child_ie.file_id)))
238 child_baton = self.editor.open_file(
239 urlutils.join(self.branch.get_branch_path(), self.new_inventory.id2path(child_ie.file_id)),
240 baton, self.base_revnum, self.pool)
243 # Old copy of the file was retained. No need to send changes
244 assert child_ie.file_id not in self.modified_files
247 if child_ie.file_id in self.old_inv:
248 old_executable = self.old_inv[child_ie.file_id].executable
249 old_special = (self.old_inv[child_ie.file_id].kind == 'symlink')
252 old_executable = False
254 if child_baton is not None:
255 if old_executable != child_ie.executable:
256 if child_ie.executable:
257 value = svn.core.SVN_PROP_EXECUTABLE_VALUE
260 self.editor.change_file_prop(child_baton,
261 svn.core.SVN_PROP_EXECUTABLE, value, self.pool)
263 if old_special != (child_ie.kind == 'symlink'):
264 if child_ie.kind == 'symlink':
265 value = svn.core.SVN_PROP_SPECIAL_VALUE
269 self.editor.change_file_prop(child_baton,
270 svn.core.SVN_PROP_SPECIAL, value, self.pool)
273 if child_ie.file_id in self.modified_files:
274 self._file_process(child_ie.file_id,
275 self.modified_files[child_ie.file_id], child_baton)
277 if child_baton is not None:
278 self.editor.close_file(child_baton, None, self.pool)
280 # Loop over subdirectories of file_id in self.new_inventory
281 for child_name in self.new_inventory[file_id].children:
282 child_ie = self.new_inventory.get_child(file_id, child_name)
283 if child_ie.kind != 'directory':
286 # add them if they didn't exist in old_inv
287 if not child_ie.file_id in self.old_inv:
288 mutter('adding dir %r' % child_ie.name)
289 child_baton = self.editor.add_directory(
290 urlutils.join(self.branch.get_branch_path(), self.new_inventory.id2path(child_ie.file_id)),
291 baton, None, -1, self.pool)
293 # copy if they existed at different location
294 elif self.old_inv.id2path(child_ie.file_id) != self.new_inventory.id2path(child_ie.file_id):
295 mutter('copy dir %r -> %r' % (self.old_inv.id2path(child_ie.file_id),
296 self.new_inventory.id2path(child_ie.file_id)))
297 child_baton = self.editor.add_directory(
298 urlutils.join(self.branch.get_branch_path(), self.new_inventory.id2path(child_ie.file_id)),
300 urlutils.join(self.repository.transport.svn_url, self.base_path, self.old_inv.id2path(child_ie.file_id)),
301 self.base_revnum, self.pool)
303 # open if they existed at the same location and
304 # the directory was touched
305 elif self.new_inventory[child_ie.file_id].revision is None:
306 mutter('open dir %r' % self.new_inventory.id2path(child_ie.file_id))
308 child_baton = self.editor.open_directory(
309 urlutils.join(self.branch.get_branch_path(), self.new_inventory.id2path(child_ie.file_id)),
310 baton, self.base_revnum, self.pool)
312 assert child_ie.file_id not in self.modified_dirs
315 # Handle this directory
316 if child_ie.file_id in self.modified_dirs:
317 self._dir_process(self.new_inventory.id2path(child_ie.file_id),
318 child_ie.file_id, child_baton)
320 self.editor.close_directory(child_baton, self.pool)
322 def open_branch_batons(self, root, elements, existing_elements,
323 base_path, base_rev):
324 """Open a specified directory given a baton for the repository root.
326 :param root: Baton for the repository root
327 :param elements: List of directory names to open
328 :param existing_elements: List of directory names that exist
329 :param base_path: Path to base top-level branch on
330 :param base_rev: Revision of path to base top-level branch on
334 mutter('opening branch %r (base %r:%r)' % (elements, base_path,
337 # Open paths leading up to branch
338 for i in range(1, len(elements)-1):
339 # Does directory already exist?
340 ret.append(self.editor.open_directory(
341 "/".join(existing_elements[0:i+1]), ret[-1], -1, self.pool))
343 assert (len(existing_elements) == len(elements) or
344 len(existing_elements)+1 == len(elements))
346 # Branch already exists and stayed at the same location, open:
347 # TODO: What if the branch didn't change but the new revision
348 # was based on an older revision of the branch?
349 # This needs to also check that base_rev was the latest version of
351 if (len(existing_elements) == len(elements) and
352 base_path.strip("/") == "/".join(elements).strip("/")):
353 ret.append(self.editor.open_directory(
354 "/".join(elements), ret[-1], base_rev, self.pool))
355 else: # Branch has to be created
356 # Already exists, old copy needs to be removed
357 if len(existing_elements) == len(elements):
358 self.editor.delete_entry("/".join(elements), -1, ret[-1])
359 if base_path is not None:
360 base_url = urlutils.join(self.repository.transport.svn_url, base_path)
363 ret.append(self.editor.add_directory(
364 "/".join(elements), ret[-1], base_url, base_rev, self.pool))
368 def commit(self, message):
369 """Finish the commit.
372 def done(revision, date, author):
373 """Callback that is called by the Subversion commit editor
374 once the commit finishes.
376 :param revision: Revision number
377 :param date: Date recorded for this commit
380 self.revnum = revision
384 bp_parts = self.branch.get_branch_path().split("/")
385 lock = self.repository.transport.lock_write(".")
388 existing_bp_parts =_check_dirs_exist(self.repository.transport,
390 mutter('obtaining commit editor')
392 self.editor = self.repository.transport.get_commit_editor(
393 message.encode("utf-8"), done, None, False)
395 root = self.editor.open_root(self.base_revnum)
397 # TODO: Accept create_prefix argument
398 branch_batons = self.open_branch_batons(root, bp_parts,
399 existing_bp_parts, self.base_path, self.base_revnum)
401 self._dir_process("", self.new_inventory.root.file_id,
404 # Set all the revprops
405 for prop, value in self._svnprops.items():
406 mutter('prop: %r -> %r' % (prop, value))
407 if value is not None:
408 value = value.encode('utf-8')
409 self.editor.change_dir_prop(branch_batons[-1], prop, value,
412 branch_batons.reverse()
413 for baton in branch_batons:
414 self.editor.close_directory(baton, self.pool)
420 assert self.revnum is not None
422 # Make sure the logwalker doesn't try to use ra
423 # during checkouts...
424 self.repository._log.fetch_revisions(self.revnum)
426 revid = self.branch.generate_revision_id(self.revnum)
428 # TODO: for some reason, branch properties don't seem to be accessible
430 #assert self._new_revision_id is None or self._new_revision_id == revid
431 if self._new_revision_id is not None:
432 revid = self._new_revision_id
434 mutter('commit %d finished. author: %r, date: %r, revid: %r' %
435 (self.revnum, self.author, self.date, revid))
439 def _record_file_id(self, ie, path):
440 # Make sure that ie.file_id exists in the map
441 if not ie.file_id in self.old_inv:
442 if not self._svnprops.has_key(SVN_PROP_BZR_FILEIDS):
443 self._svnprops[SVN_PROP_BZR_FILEIDS] = ""
444 mutter('adding fileid mapping %s -> %s' % (path, ie.file_id))
445 self._svnprops[SVN_PROP_BZR_FILEIDS] += "%s\t%s\n" % (escape_svn_path(path), ie.file_id)
447 def record_entry_contents(self, ie, parent_invs, path, tree):
448 """Record the content of ie from tree into the commit if needed.
450 Side effect: sets ie.revision when unchanged
452 :param ie: An inventory entry present in the commit.
453 :param parent_invs: The inventories of the parent revisions of the
455 :param path: The path the entry is at in the tree.
456 :param tree: The tree which contains this entry and should be used to
459 assert self.new_inventory.root is not None or ie.parent_id is None
460 self.new_inventory.add(ie)
462 # ie.revision is always None if the InventoryEntry is considered
463 # for committing. ie.snapshot will record the correct revision
464 # which may be the sole parent if it is untouched.
465 if ie.revision is not None:
468 self._record_file_id(ie, path)
470 previous_entries = ie.find_previous_heads(parent_invs,
471 self.repository.weave_store, self.repository.get_transaction())
473 # we are creating a new revision for ie in the history store
475 ie.snapshot(self._new_revision_id, path, previous_entries, tree, self)
478 def replay_delta(builder, delta, old_tree):
479 """Replays a delta to a commit builder.
481 :param builder: The commit builder.
482 :param delta: Treedelta to apply
483 :param old_tree: Original tree on top of which the delta should be applied
486 ie = builder.new_inventory[id]
487 path = builder.new_inventory.id2path(id)
488 builder._record_file_id(ie, path)
491 while builder.new_inventory[id].parent_id is not None:
492 if builder.new_inventory[id].revision is None:
494 builder.new_inventory[id].revision = None
495 if builder.new_inventory[id].kind == 'directory':
496 builder.modified_directory(id, [])
497 id = builder.new_inventory[id].parent_id
499 if ie.kind == 'link':
500 builder.modified_link(ie.file_id, [], ie.symlink_target)
501 elif ie.kind == 'file':
503 return old_tree.get_file_text(ie.file_id)
504 builder.modified_file_text(ie.file_id, [], get_text)
506 for (_, id, _) in delta.added + delta.removed:
509 for (_, id, _, _, _) in delta.modified:
512 for (oldpath, _, id, _, _, _) in delta.renamed:
514 touch_id(old_tree.inventory.path2id(os.path.dirname(oldpath)))
516 builder.finish_inventory()
519 def push_as_merged(target, source, revision_id):
520 """Push a revision as merged revision.
522 This will create a new revision in the target repository that
523 merges the specified revision but does not contain any other differences.
524 This is done so that the revision that is being pushed does not need
525 to completely match the target revision and so it can not have the
528 :param target: Repository to push to
529 :param source: Repository to pull the revision from
530 :param revision_id: Revision id of the revision to push
531 :return: The revision id of the created revision
533 assert isinstance(source, Branch)
534 rev = source.repository.get_revision(revision_id)
535 inv = source.repository.get_inventory(revision_id)
537 # revision on top of which to commit
538 prev_revid = target.last_revision()
540 mutter('committing %r on top of %r' % (revision_id, prev_revid))
542 old_tree = source.repository.revision_tree(revision_id)
543 if source.repository.has_revision(prev_revid):
544 new_tree = source.repository.revision_tree(prev_revid)
546 new_tree = target.repository.revision_tree(prev_revid)
548 builder = SvnCommitBuilder(target.repository, target,
549 [revision_id, prev_revid],
558 delta = old_tree.changes_from(new_tree)
559 builder.new_inventory = inv
560 replay_delta(builder, delta, old_tree)
563 return builder.commit(rev.message)
564 except SubversionException, (_, num):
565 if num == svn.core.SVN_ERR_FS_TXN_OUT_OF_DATE:
566 raise DivergedBranches(source, target)
570 def push_new(target_repository, target_branch_path, source, stop_revision=None):
571 """Push a revision into Subversion, creating a new branch.
573 This will do a new commit in the target branch.
575 :param target_branch_path: Path to create new branch at
576 :param source: Branch to pull the revision from
577 :param revision_id: Revision id of the revision to push
579 assert isinstance(source, Branch)
580 if stop_revision is None:
581 stop_revision = source.last_revision()
582 history = source.revision_history()
583 revhistory = deepcopy(history)
584 start_revid = NULL_REVISION
585 while len(revhistory) > 0:
586 revid = revhistory.pop()
587 # We've found the revision to push if there is a revision
588 # which LHS parent is present or if this is the first revision.
589 if (len(revhistory) == 0 or
590 target_repository.has_revision(revhistory[-1])):
594 # Get commit builder but specify that target_branch_path should
595 # be created and copied from (copy_path, copy_revnum)
596 class ImaginaryBranch:
597 def __init__(self, repository):
598 self.repository = repository
599 self._revision_history = None
601 def get_config(self):
604 def last_revision_info(self):
605 last_revid = self.last_revision()
606 if last_revid is None:
608 return (history.index(last_revid), last_revid)
610 def last_revision(self):
611 parents = source.repository.revision_parents(start_revid)
616 def get_branch_path(self, revnum=None):
617 return target_branch_path
619 def generate_revision_id(self, revnum):
620 return self.repository.generate_revision_id(
621 revnum, self.get_branch_path(revnum),
622 str(self.repository.get_scheme()))
624 push(ImaginaryBranch(target_repository), source, start_revid)
627 def push(target, source, revision_id):
628 """Push a revision into Subversion.
630 This will do a new commit in the target branch.
632 :param target: Branch to push to
633 :param source: Branch to pull the revision from
634 :param revision_id: Revision id of the revision to push
636 assert isinstance(source, Branch)
637 mutter('pushing %r' % (revision_id))
638 rev = source.repository.get_revision(revision_id)
639 inv = source.repository.get_inventory(revision_id)
641 # revision on top of which to commit
642 assert (target.last_revision() in rev.parent_ids or
643 target.last_revision() is None and rev.parent_ids == [])
645 old_tree = source.repository.revision_tree(revision_id)
646 new_tree = source.repository.revision_tree(target.last_revision())
648 builder = SvnCommitBuilder(target.repository, target,
658 delta = old_tree.changes_from(new_tree)
659 builder.new_inventory = inv
660 replay_delta(builder, delta, old_tree)
662 return builder.commit(rev.message)
663 except SubversionException, (msg, num):
664 if num == svn.core.SVN_ERR_FS_TXN_OUT_OF_DATE:
665 raise DivergedBranches(source, target)
668 class InterToSvnRepository(InterRepository):
669 """Any to Subversion repository actions."""
671 _matching_repo_format = SvnRepositoryFormat()
674 def _get_repo_format_to_test():
675 return SvnRepositoryFormat()
677 def copy_content(self, revision_id=None, basis=None, pb=None):
678 """See InterRepository.copy_content."""
679 assert revision_id is not None, "fetching all revisions not supported"
680 # Go back over the LHS parent until we reach a revid we know
682 while not self.target.has_revision(revision_id):
683 todo.append(revision_id)
684 revision_id = self.source.revision_parents(revision_id)[0]
685 if revision_id == NULL_REVISION:
686 raise "Unrelated repositories."
687 mutter("pushing %r into svn" % todo)
688 for revision_id in todo:
690 pb.update("pushing revisions", todo.index(revision_id), len(todo))
691 rev = self.source.get_revision(revision_id)
692 inv = self.source.get_inventory(revision_id)
694 mutter('pushing %r' % (revision_id))
696 old_tree = self.source.revision_tree(revision_id)
697 parent_revid = self.source.revision_parents(revision_id)[0]
698 new_tree = self.source.revision_tree(parent_revid)
700 (bp, _, scheme) = self.target.lookup_revision_id(parent_revid)
701 target_branch = Branch.open(urlutils.join(self.target.base, bp))
703 builder = SvnCommitBuilder(self.target, target_branch,
705 target_branch.get_config(),
713 delta = old_tree.changes_from(new_tree)
714 builder.new_inventory = inv
715 replay_delta(builder, delta, old_tree)
716 builder.commit(rev.message)
719 def fetch(self, revision_id=None, pb=None):
720 """Fetch revisions. """
721 self.copy_content(revision_id=revision_id, pb=pb)
724 def is_compatible(source, target):
725 """Be compatible with SvnRepository."""
726 return isinstance(target, SvnRepository)