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)
75 self._svnprops[SVN_PROP_BZR_FILEIDS] = ""
77 # Gather information about revision on top of which the commit is
79 (self.base_revno, self.base_revid) = self.branch.last_revision_info()
80 if self.base_revid is None:
83 self.base_scheme = repository.get_scheme()
85 (self.base_path, self.base_revnum, self.base_scheme) = \
86 repository.lookup_revision_id(self.base_revid)
88 # Determine revisions merged in this one
89 merges = filter(lambda x: x != self.base_revid, parents)
92 self._record_merges(merges)
94 # Set appropriate property if revision id was specified by
96 if revision_id is not None:
97 self._record_revision_id(revision_id)
99 # At least one of the parents has to be the last revision on the
100 # mainline in Subversion.
101 assert (self.base_revid is None or self.base_revid in parents)
104 if self.base_revid is None:
105 self.old_inv = Inventory(root_id=None)
107 self.old_inv = self.repository.get_inventory(self.base_revid)
109 self.old_inv = old_inv
110 # Not all repositories appear to set Inventory.revision_id,
111 # so allow None as well.
112 assert self.old_inv.revision_id in (None, self.base_revid)
114 self.modified_files = {}
115 self.modified_dirs = set()
117 def _record_revision_id(self, revid):
118 if self.base_revid is not None:
119 old = self.repository.branchprop_list.get_property(
120 self.base_path, self.base_revnum,
121 SVN_PROP_BZR_REVISION_ID+str(self.base_scheme), "")
125 self._svnprops[SVN_PROP_BZR_REVISION_ID+str(self.base_scheme)] = \
126 old + "%d %s\n" % (self.base_revno+1, revid)
128 def _record_merges(self, merges):
130 if self.base_revid is not None:
131 old = self.repository.branchprop_list.get_property(
132 self.base_path, self.base_revnum,
133 SVN_PROP_BZR_ANCESTRY+str(self.base_scheme), "")
136 self._svnprops[SVN_PROP_BZR_ANCESTRY+str(self.base_scheme)] = old + "\t".join(merges) + "\n"
138 if self.base_revid is not None:
139 old = self.repository.branchprop_list.get_property(
140 self.base_path, self.base_revnum, SVN_PROP_SVK_MERGE)
148 new += "%s\n" % revision_id_to_svk_feature(p)
149 except InvalidRevisionId:
153 self._svnprops[SVN_PROP_SVK_MERGE] = old + new
155 def _generate_revision_if_needed(self):
158 def finish_inventory(self):
161 def modified_file_text(self, file_id, file_parents,
162 get_content_byte_lines, text_sha1=None,
164 new_lines = get_content_byte_lines()
165 self.modified_files[file_id] = "".join(new_lines)
166 return osutils.sha_strings(new_lines), sum(map(len, new_lines))
168 def modified_link(self, file_id, file_parents, link_target):
169 self.modified_files[file_id] = "link %s" % link_target
171 def modified_directory(self, file_id, file_parents):
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 # Loop over entries of file_id in self.old_inv
182 # remove if they no longer exist with the same name
184 if file_id in self.old_inv:
185 for child_name in self.old_inv[file_id].children:
186 child_ie = self.old_inv.get_child(file_id, child_name)
189 # ... path no longer exists
190 not child_ie.file_id in self.new_inventory or
192 child_ie.parent_id != self.new_inventory[child_ie.file_id].parent_id or
194 self.new_inventory[child_ie.file_id].name != child_name):
195 mutter('removing %r' % child_ie.file_id)
196 self.editor.delete_entry(
198 self.branch.get_branch_path(),
199 self.old_inv.id2path(child_ie.file_id)),
200 self.base_revnum, baton, self.pool)
202 # Loop over file children of file_id in self.new_inventory
203 for child_name in self.new_inventory[file_id].children:
204 child_ie = self.new_inventory.get_child(file_id, child_name)
205 assert child_ie is not None
207 if not (child_ie.kind in ('file', 'symlink')):
210 new_child_path = self.new_inventory.id2path(child_ie.file_id)
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, new_child_path))
214 self._record_file_id(child_ie, new_child_path)
215 child_baton = self.editor.add_file(
216 urlutils.join(self.branch.get_branch_path(),
217 new_child_path), baton, None, -1, self.pool)
220 # copy if they existed at different location
221 elif self.old_inv.id2path(child_ie.file_id) != new_child_path:
222 mutter('copy %s %r -> %r' % (child_ie.kind,
223 self.old_inv.id2path(child_ie.file_id),
225 self._record_file_id(child_ie, new_child_path)
226 child_baton = self.editor.add_file(
227 urlutils.join(self.branch.get_branch_path(), new_child_path), baton,
228 urlutils.join(self.repository.transport.svn_url, self.base_path, self.old_inv.id2path(child_ie.file_id)),
229 self.base_revnum, self.pool)
231 # open if they existed at the same location
232 elif child_ie.revision is None:
233 mutter('open %s %r' % (child_ie.kind, new_child_path))
235 child_baton = self.editor.open_file(
236 urlutils.join(self.branch.get_branch_path(),
238 baton, self.base_revnum, self.pool)
241 # Old copy of the file was retained. No need to send changes
242 assert child_ie.file_id not in self.modified_files
245 if child_ie.file_id in self.old_inv:
246 old_executable = self.old_inv[child_ie.file_id].executable
247 old_special = (self.old_inv[child_ie.file_id].kind == 'symlink')
250 old_executable = False
252 if child_baton is not None:
253 if old_executable != child_ie.executable:
254 if child_ie.executable:
255 value = svn.core.SVN_PROP_EXECUTABLE_VALUE
258 self.editor.change_file_prop(child_baton,
259 svn.core.SVN_PROP_EXECUTABLE, value, self.pool)
261 if old_special != (child_ie.kind == 'symlink'):
262 if child_ie.kind == 'symlink':
263 value = svn.core.SVN_PROP_SPECIAL_VALUE
267 self.editor.change_file_prop(child_baton,
268 svn.core.SVN_PROP_SPECIAL, value, self.pool)
271 if child_ie.file_id in self.modified_files:
272 self._file_process(child_ie.file_id,
273 self.modified_files[child_ie.file_id], child_baton)
275 if child_baton is not None:
276 self.editor.close_file(child_baton, None, self.pool)
278 # Loop over subdirectories of file_id in self.new_inventory
279 for child_name in self.new_inventory[file_id].children:
280 child_ie = self.new_inventory.get_child(file_id, child_name)
281 if child_ie.kind != 'directory':
284 new_child_path = self.new_inventory.id2path(child_ie.file_id)
285 # add them if they didn't exist in old_inv
286 if not child_ie.file_id in self.old_inv:
287 mutter('adding dir %r' % child_ie.name)
288 self._record_file_id(child_ie, new_child_path)
289 child_baton = self.editor.add_directory(
290 urlutils.join(self.branch.get_branch_path(),
291 new_child_path), baton, None, -1, self.pool)
293 # copy if they existed at different location
294 elif self.old_inv.id2path(child_ie.file_id) != new_child_path:
295 old_child_path = self.old_inv.id2path(child_ie.file_id)
296 mutter('copy dir %r -> %r' % (old_child_path, new_child_path))
297 self._record_file_id(child_ie, new_child_path)
298 child_baton = self.editor.add_directory(
299 urlutils.join(self.branch.get_branch_path(), new_child_path),
301 urlutils.join(self.repository.transport.svn_url, self.base_path, old_child_path), 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' % new_child_path)
308 child_baton = self.editor.open_directory(
309 urlutils.join(self.branch.get_branch_path(), new_child_path),
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(new_child_path, child_ie.file_id, child_baton)
319 self.editor.close_directory(child_baton, self.pool)
321 def open_branch_batons(self, root, elements, existing_elements,
322 base_path, base_rev):
323 """Open a specified directory given a baton for the repository root.
325 :param root: Baton for the repository root
326 :param elements: List of directory names to open
327 :param existing_elements: List of directory names that exist
328 :param base_path: Path to base top-level branch on
329 :param base_rev: Revision of path to base top-level branch on
333 mutter('opening branch %r (base %r:%r)' % (elements, base_path,
336 # Open paths leading up to branch
337 for i in range(1, len(elements)-1):
338 # Does directory already exist?
339 ret.append(self.editor.open_directory(
340 "/".join(existing_elements[0:i+1]), ret[-1], -1, self.pool))
342 assert (len(existing_elements) == len(elements) or
343 len(existing_elements)+1 == len(elements))
345 # Branch already exists and stayed at the same location, open:
346 # TODO: What if the branch didn't change but the new revision
347 # was based on an older revision of the branch?
348 # This needs to also check that base_rev was the latest version of
350 if (len(existing_elements) == len(elements) and
351 base_path.strip("/") == "/".join(elements).strip("/")):
352 ret.append(self.editor.open_directory(
353 "/".join(elements), ret[-1], base_rev, self.pool))
354 else: # Branch has to be created
355 # Already exists, old copy needs to be removed
356 if len(existing_elements) == len(elements):
357 self.editor.delete_entry("/".join(elements), -1, ret[-1])
358 if base_path is not None:
359 base_url = urlutils.join(self.repository.transport.svn_url, base_path)
362 ret.append(self.editor.add_directory(
363 "/".join(elements), ret[-1], base_url, base_rev, self.pool))
367 def commit(self, message):
368 """Finish the commit.
371 def done(revision, date, author):
372 """Callback that is called by the Subversion commit editor
373 once the commit finishes.
375 :param revision: Revision number
376 :param date: Date recorded for this commit
379 self.revnum = revision
383 bp_parts = self.branch.get_branch_path().split("/")
384 lock = self.repository.transport.lock_write(".")
387 existing_bp_parts =_check_dirs_exist(self.repository.transport,
390 self.editor = self.repository.transport.get_commit_editor(
391 message.encode("utf-8"), done, None, False)
393 root = self.editor.open_root(self.base_revnum)
395 # TODO: Accept create_prefix argument
396 branch_batons = self.open_branch_batons(root, bp_parts,
397 existing_bp_parts, self.base_path, self.base_revnum)
399 self._dir_process("", self.new_inventory.root.file_id,
402 # Set all the revprops
403 for prop, value in self._svnprops.items():
404 if value is not None:
405 value = value.encode('utf-8')
406 self.editor.change_dir_prop(branch_batons[-1], prop, value,
409 branch_batons.reverse()
410 for baton in branch_batons:
411 self.editor.close_directory(baton, self.pool)
417 assert self.revnum is not None
419 # Make sure the logwalker doesn't try to use ra
420 # during checkouts...
421 self.repository._log.fetch_revisions(self.revnum)
423 revid = self.branch.generate_revision_id(self.revnum)
425 assert self._new_revision_id is None or self._new_revision_id == revid
427 mutter('commit %d finished. author: %r, date: %r, revid: %r' %
428 (self.revnum, self.author, self.date, revid))
432 def _record_file_id(self, ie, path):
433 mutter('adding fileid mapping %s -> %s' % (path, ie.file_id))
434 self._svnprops[SVN_PROP_BZR_FILEIDS] += "%s\t%s\n" % (escape_svn_path(path), ie.file_id)
436 def record_entry_contents(self, ie, parent_invs, path, tree):
437 """Record the content of ie from tree into the commit if needed.
439 Side effect: sets ie.revision when unchanged
441 :param ie: An inventory entry present in the commit.
442 :param parent_invs: The inventories of the parent revisions of the
444 :param path: The path the entry is at in the tree.
445 :param tree: The tree which contains this entry and should be used to
448 assert self.new_inventory.root is not None or ie.parent_id is None
449 self.new_inventory.add(ie)
451 # ie.revision is always None if the InventoryEntry is considered
452 # for committing. ie.snapshot will record the correct revision
453 # which may be the sole parent if it is untouched.
454 if ie.revision is not None:
457 previous_entries = ie.find_previous_heads(parent_invs,
458 self.repository.weave_store, self.repository.get_transaction())
460 # we are creating a new revision for ie in the history store
462 ie.snapshot(self._new_revision_id, path, previous_entries, tree, self)
465 def replay_delta(builder, old_tree, new_tree):
466 """Replays a delta to a commit builder.
468 :param builder: The commit builder.
469 :param old_tree: Original tree on top of which the delta should be applied
470 :param new_tree: New tree that should be committed
472 delta = new_tree.changes_from(old_tree)
474 ie = builder.new_inventory[id]
475 path = builder.new_inventory.id2path(id)
478 while builder.new_inventory[id].parent_id is not None:
479 if builder.new_inventory[id].revision is None:
481 builder.new_inventory[id].revision = None
482 if builder.new_inventory[id].kind == 'directory':
483 builder.modified_directory(id, [])
484 id = builder.new_inventory[id].parent_id
486 if ie.kind == 'link':
487 builder.modified_link(ie.file_id, [], ie.symlink_target)
488 elif ie.kind == 'file':
490 return new_tree.get_file_text(ie.file_id)
491 builder.modified_file_text(ie.file_id, [], get_text)
493 for (_, id, _) in delta.added:
496 for (_, id, _, _, _) in delta.modified:
499 for (oldpath, _, id, _, _, _) in delta.renamed:
501 old_parent_id = old_tree.inventory.path2id(os.path.dirname(oldpath))
502 if old_parent_id in builder.new_inventory:
503 touch_id(old_parent_id)
505 for (path, _, _) in delta.removed:
506 old_parent_id = old_tree.inventory.path2id(os.path.dirname(path))
507 if old_parent_id in builder.new_inventory:
508 touch_id(old_parent_id)
510 builder.finish_inventory()
513 def push_as_merged(target, source, revision_id):
514 """Push a revision as merged revision.
516 This will create a new revision in the target repository that
517 merges the specified revision but does not contain any other differences.
518 This is done so that the revision that is being pushed does not need
519 to completely match the target revision and so it can not have the
522 :param target: Repository to push to
523 :param source: Repository to pull the revision from
524 :param revision_id: Revision id of the revision to push
525 :return: The revision id of the created revision
527 assert isinstance(source, Branch)
528 rev = source.repository.get_revision(revision_id)
529 inv = source.repository.get_inventory(revision_id)
531 # revision on top of which to commit
532 prev_revid = target.last_revision()
534 mutter('committing %r on top of %r' % (revision_id, prev_revid))
536 old_tree = source.repository.revision_tree(revision_id)
537 if source.repository.has_revision(prev_revid):
538 new_tree = source.repository.revision_tree(prev_revid)
540 new_tree = target.repository.revision_tree(prev_revid)
542 builder = SvnCommitBuilder(target.repository, target,
543 [revision_id, prev_revid],
552 builder.new_inventory = inv
553 replay_delta(builder, new_tree, old_tree)
556 return builder.commit(rev.message)
557 except SubversionException, (_, num):
558 if num == svn.core.SVN_ERR_FS_TXN_OUT_OF_DATE:
559 raise DivergedBranches(source, target)
563 def push_new(target_repository, target_branch_path, source, stop_revision=None):
564 """Push a revision into Subversion, creating a new branch.
566 This will do a new commit in the target branch.
568 :param target_branch_path: Path to create new branch at
569 :param source: Branch to pull the revision from
570 :param revision_id: Revision id of the revision to push
572 assert isinstance(source, Branch)
573 if stop_revision is None:
574 stop_revision = source.last_revision()
575 history = source.revision_history()
576 revhistory = deepcopy(history)
577 start_revid = NULL_REVISION
578 while len(revhistory) > 0:
579 revid = revhistory.pop()
580 # We've found the revision to push if there is a revision
581 # which LHS parent is present or if this is the first revision.
582 if (len(revhistory) == 0 or
583 target_repository.has_revision(revhistory[-1])):
587 # Get commit builder but specify that target_branch_path should
588 # be created and copied from (copy_path, copy_revnum)
589 class ImaginaryBranch:
590 def __init__(self, repository):
591 self.repository = repository
592 self._revision_history = None
594 def get_config(self):
597 def last_revision_info(self):
598 last_revid = self.last_revision()
599 if last_revid is None:
601 return (history.index(last_revid), last_revid)
603 def last_revision(self):
604 parents = source.repository.revision_parents(start_revid)
609 def get_branch_path(self, revnum=None):
610 return target_branch_path
612 def generate_revision_id(self, revnum):
613 return self.repository.generate_revision_id(
614 revnum, self.get_branch_path(revnum),
615 str(self.repository.get_scheme()))
617 push(ImaginaryBranch(target_repository), source, start_revid)
620 def push(target, source, revision_id):
621 """Push a revision into Subversion.
623 This will do a new commit in the target branch.
625 :param target: Branch to push to
626 :param source: Branch to pull the revision from
627 :param revision_id: Revision id of the revision to push
629 assert isinstance(source, Branch)
630 mutter('pushing %r' % (revision_id))
631 rev = source.repository.get_revision(revision_id)
632 inv = source.repository.get_inventory(revision_id)
634 # revision on top of which to commit
635 assert (target.last_revision() in rev.parent_ids or
636 target.last_revision() is None and rev.parent_ids == [])
638 old_tree = source.repository.revision_tree(revision_id)
639 new_tree = source.repository.revision_tree(target.last_revision())
641 builder = SvnCommitBuilder(target.repository, target,
651 builder.new_inventory = inv
652 replay_delta(builder, new_tree, old_tree)
654 return builder.commit(rev.message)
655 except SubversionException, (msg, num):
656 if num == svn.core.SVN_ERR_FS_TXN_OUT_OF_DATE:
657 raise DivergedBranches(source, target)
661 class InterToSvnRepository(InterRepository):
662 """Any to Subversion repository actions."""
664 _matching_repo_format = SvnRepositoryFormat()
667 def _get_repo_format_to_test():
668 return SvnRepositoryFormat()
670 def copy_content(self, revision_id=None, basis=None, pb=None):
671 """See InterRepository.copy_content."""
672 assert revision_id is not None, "fetching all revisions not supported"
673 # Go back over the LHS parent until we reach a revid we know
675 while not self.target.has_revision(revision_id):
676 todo.append(revision_id)
677 revision_id = self.source.revision_parents(revision_id)[0]
678 if revision_id == NULL_REVISION:
679 raise "Unrelated repositories."
680 mutter("pushing %r into svn" % todo)
682 for revision_id in todo:
684 pb.update("pushing revisions", todo.index(revision_id), len(todo))
685 rev = self.source.get_revision(revision_id)
686 inv = self.source.get_inventory(revision_id)
688 mutter('pushing %r' % (revision_id))
690 old_tree = self.source.revision_tree(revision_id)
691 parent_revid = self.source.revision_parents(revision_id)[0]
692 new_tree = self.source.revision_tree(parent_revid)
694 (bp, _, scheme) = self.target.lookup_revision_id(parent_revid)
695 if target_branch is None or target_branch.get_branch_path() != bp:
696 target_branch = Branch.open(urlutils.join(self.target.base, bp))
698 builder = SvnCommitBuilder(self.target, target_branch,
700 target_branch.get_config(),
708 builder.new_inventory = inv
709 replay_delta(builder, new_tree, old_tree)
710 builder.commit(rev.message)
713 def fetch(self, revision_id=None, pb=None):
714 """Fetch revisions. """
715 self.copy_content(revision_id=revision_id, pb=pb)
718 def is_compatible(source, target):
719 """Be compatible with SvnRepository."""
720 return isinstance(target, SvnRepository)