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
18 from svn.core import Pool, SubversionException
20 from bzrlib.errors import InvalidRevisionId, DivergedBranches
21 from bzrlib.inventory import Inventory
22 import bzrlib.osutils as osutils
23 from bzrlib.repository import RootCommitBuilder
24 from bzrlib.trace import mutter
26 from repository import (SvnRepository, SVN_PROP_BZR_MERGE, SVN_PROP_BZR_FILEIDS,
27 SVN_PROP_SVK_MERGE, SVN_PROP_BZR_REVPROP_PREFIX,
28 revision_id_to_svk_feature, escape_svn_path)
32 class SvnCommitBuilder(RootCommitBuilder):
33 """Commit Builder implementation wrapped around svn_delta_editor. """
35 def __init__(self, repository, branch, parents, config, revprops,
37 """Instantiate a new SvnCommitBuilder.
39 :param repository: SvnRepository to commit to.
40 :param branch: SvnBranch to commit to.
41 :param parents: List of parent revision ids.
42 :param config: Branch configuration to use.
43 :param revprops: Revision properties to set.
45 super(SvnCommitBuilder, self).__init__(repository, parents,
46 config, None, None, None, revprops, None)
47 assert isinstance(repository, SvnRepository)
52 for prop in self._revprops:
53 self._svnprops[SVN_PROP_BZR_REVPROP_PREFIX+prop] = self._revprops[prop]
55 self.merges = filter(lambda x: x != self.branch.last_revision(),
58 if len(self.merges) > 0:
60 if branch.last_revision():
61 (bp, revnum) = repository.parse_revision_id(branch.last_revision())
62 old = repository.branchprop_list.get_property(bp, revnum, SVN_PROP_BZR_MERGE, "")
65 self._svnprops[SVN_PROP_BZR_MERGE] = old + "\t".join(self.merges) + "\n"
67 if branch.last_revision() is not None:
68 old = repository.branchprop_list.get_property(bp, revnum, SVN_PROP_SVK_MERGE)
76 new += "%s\n" % revision_id_to_svk_feature(p)
77 except InvalidRevisionId:
81 self._svnprops[SVN_PROP_SVK_MERGE] = old + new
83 # At least one of the parents has to be the last revision on the
84 # mainline in # Subversion.
85 assert (self.branch.last_revision() is None or
86 self.branch.last_revision() in parents)
89 if self.branch.last_revision() is None:
90 self.old_inv = Inventory(root_id=None)
92 self.old_inv = self.repository.get_inventory(
93 self.branch.last_revision())
95 self.old_inv = old_inv
96 assert self.old_inv.revision_id == self.branch.last_revision()
98 self.modified_files = {}
99 self.modified_dirs = []
101 def _generate_revision_if_needed(self):
104 def finish_inventory(self):
107 def modified_file_text(self, file_id, file_parents,
108 get_content_byte_lines, text_sha1=None,
110 mutter('modifying file %s' % file_id)
111 new_lines = get_content_byte_lines()
112 self.modified_files[file_id] = "".join(new_lines)
113 return osutils.sha_strings(new_lines), sum(map(len, new_lines))
115 def modified_link(self, file_id, file_parents, link_target):
116 mutter('modifying link %s' % file_id)
117 self.modified_files[file_id] = "link %s" % link_target
119 def modified_directory(self, file_id, file_parents):
120 mutter('modifying directory %s' % file_id)
121 self.modified_dirs.append(file_id)
123 def _file_process(self, file_id, contents, baton):
124 (txdelta, txbaton) = svn.delta.editor_invoke_apply_textdelta(
125 self.editor, baton, None, self.pool)
127 svn.delta.svn_txdelta_send_string(contents, txdelta, txbaton, self.pool)
129 def _dir_process(self, path, file_id, baton):
130 mutter('processing %r' % path)
132 # Set all the revprops
133 for prop, value in self._svnprops.items():
134 mutter('setting %r: %r on branch' % (prop, value))
135 if value is not None:
136 value = value.encode('utf-8')
137 svn.delta.editor_invoke_change_dir_prop(self.editor, baton,
138 prop, value, self.pool)
140 # Loop over entries of file_id in self.old_inv
141 # remove if they no longer exist with the same name
143 if file_id in self.old_inv:
144 for child_name in self.old_inv[file_id].children:
145 child_ie = self.old_inv.get_child(file_id, child_name)
147 # ... path no longer exists
148 if (not child_ie.file_id in self.new_inventory or
150 child_ie.parent_id != self.new_inventory[child_ie.file_id].parent_id or
152 self.new_inventory[child_ie.file_id].name != child_name):
153 mutter('removing %r' % child_ie.file_id)
154 svn.delta.editor_invoke_delete_entry(self.editor,
155 os.path.join(self.branch.branch_path, self.old_inv.id2path(child_ie.file_id)),
156 self.base_revnum, baton, self.pool)
158 # Loop over file members of file_id in self.new_inventory
159 for child_name in self.new_inventory[file_id].children:
160 child_ie = self.new_inventory.get_child(file_id, child_name)
161 assert child_ie is not None
163 if not (child_ie.kind in ('file', 'symlink')):
166 # add them if they didn't exist in old_inv
167 if not child_ie.file_id in self.old_inv:
168 mutter('adding %s %r' % (child_ie.kind, self.new_inventory.id2path(child_ie.file_id)))
170 child_baton = svn.delta.editor_invoke_add_file(self.editor,
171 os.path.join(self.branch.branch_path, self.new_inventory.id2path(child_ie.file_id)),
172 baton, None, -1, self.pool)
175 # copy if they existed at different location
176 elif self.old_inv.id2path(child_ie.file_id) != self.new_inventory.id2path(child_ie.file_id):
177 mutter('copy %s %r -> %r' % (child_ie.kind,
178 self.old_inv.id2path(child_ie.file_id),
179 self.new_inventory.id2path(child_ie.file_id)))
181 child_baton = svn.delta.editor_invoke_add_file(self.editor,
182 os.path.join(self.branch.branch_path, self.new_inventory.id2path(child_ie.file_id)), baton,
183 "%s/%s" % (self.branch.base, self.old_inv.id2path(child_ie.file_id)),
184 self.base_revnum, self.pool)
186 # open if they existed at the same location
187 elif child_ie.revision is None:
188 mutter('open %s %r' % (child_ie.kind,
189 self.new_inventory.id2path(child_ie.file_id)))
191 child_baton = svn.delta.editor_invoke_open_file(self.editor,
192 os.path.join(self.branch.branch_path, self.new_inventory.id2path(child_ie.file_id)),
193 baton, self.base_revnum, self.pool)
199 if child_ie.file_id in self.old_inv:
200 old_executable = self.old_inv[child_ie.file_id].executable
201 old_special = (self.old_inv[child_ie.file_id].kind == 'symlink')
204 old_executable = False
206 if child_baton is not None:
207 if old_executable != child_ie.executable:
208 if child_ie.executable:
209 value = svn.core.SVN_PROP_EXECUTABLE_VALUE
212 svn.delta.editor_invoke_change_file_prop(self.editor, child_baton, svn.core.SVN_PROP_EXECUTABLE, value, self.pool)
214 if old_special != (child_ie.kind == 'symlink'):
215 if child_ie.kind == 'symlink':
216 value = svn.core.SVN_PROP_SPECIAL_VALUE
220 svn.delta.editor_invoke_change_file_prop(self.editor, child_baton, svn.core.SVN_PROP_SPECIAL, value, self.pool)
223 if child_ie.file_id in self.modified_files:
224 self._file_process(child_ie.file_id, self.modified_files[child_ie.file_id],
227 if child_baton is not None:
228 svn.delta.editor_invoke_close_file(self.editor, child_baton, None, self.pool)
230 # Loop over subdirectories of file_id in self.new_inventory
231 for child_name in self.new_inventory[file_id].children:
232 child_ie = self.new_inventory.get_child(file_id, child_name)
233 if child_ie.kind != 'directory':
236 # add them if they didn't exist in old_inv
237 if not child_ie.file_id in self.old_inv:
238 mutter('adding dir %r' % child_ie.name)
239 child_baton = svn.delta.editor_invoke_add_directory(
241 os.path.join(self.branch.branch_path, self.new_inventory.id2path(child_ie.file_id)),
242 baton, None, -1, self.pool)
244 # copy if they existed at different location
245 elif self.old_inv.id2path(child_ie.file_id) != self.new_inventory.id2path(child_ie.file_id):
246 mutter('copy dir %r -> %r' % (self.old_inv.id2path(child_ie.file_id),
247 self.new_inventory.id2path(child_ie.file_id)))
248 child_baton = svn.delta.editor_invoke_add_directory(
250 os.path.join(self.branch.branch_path, self.new_inventory.id2path(child_ie.file_id)),
252 "%s/%s" % (self.branch.base, self.old_inv.id2path(child_ie.file_id)),
253 self.base_revnum, self.pool)
255 # open if they existed at the same location and
256 # the directory was touched
257 elif self.new_inventory[child_ie.file_id].revision is None:
258 mutter('open dir %r' % self.new_inventory.id2path(child_ie.file_id))
260 child_baton = svn.delta.editor_invoke_open_directory(self.editor,
261 os.path.join(self.branch.branch_path, self.new_inventory.id2path(child_ie.file_id)),
262 baton, self.base_revnum, self.pool)
266 # Handle this directory
267 if child_ie.file_id in self.modified_dirs:
268 self._dir_process(self.new_inventory.id2path(child_ie.file_id),
269 child_ie.file_id, child_baton)
271 svn.delta.editor_invoke_close_directory(self.editor, child_baton,
274 def open_branch_batons(self, root, elements):
277 mutter('opening branch %r' % elements)
279 for i in range(1, len(elements)):
280 if i == len(elements):
281 revnum = self.base_revnum
284 ret.append(svn.delta.editor_invoke_open_directory(self.editor,
285 "/".join(elements[0:i+1]), ret[-1], revnum, self.pool))
289 def commit(self, message):
290 def done(revision, date, author):
292 self.revnum = revision
295 mutter('committed %r, author: %r, date: %r' % (revision, author, date))
297 mutter('obtaining commit editor')
299 self.editor, editor_baton = self.repository.transport.get_commit_editor(
300 message.encode("utf-8"), done, None, False)
302 if self.branch.last_revision() is None:
305 self.base_revnum = self.repository.parse_revision_id(
306 self.branch.last_revision())[1]
308 root = svn.delta.editor_invoke_open_root(self.editor, editor_baton,
311 branch_batons = self.open_branch_batons(root,
312 self.branch.branch_path.split("/"))
314 self._dir_process("", self.new_inventory.root.file_id, branch_batons[-1])
316 branch_batons.reverse()
317 for baton in branch_batons:
318 svn.delta.editor_invoke_close_directory(self.editor, baton,
321 svn.delta.editor_invoke_close_edit(self.editor, editor_baton)
323 assert self.revnum is not None
324 revid = self.repository.generate_revision_id(self.revnum,
325 self.branch.branch_path)
327 #FIXME: Use public API:
328 self.branch.revision_history()
329 self.branch._revision_history.append(revid)
331 mutter('commit finished. author: %r, date: %r' %
332 (self.author, self.date))
334 # Make sure the logwalker doesn't try to use ra
335 # during checkouts...
336 self.repository._log.fetch_revisions(self.revnum)
340 def record_entry_contents(self, ie, parent_invs, path, tree):
341 """Record the content of ie from tree into the commit if needed.
343 Side effect: sets ie.revision when unchanged
345 :param ie: An inventory entry present in the commit.
346 :param parent_invs: The inventories of the parent revisions of the
348 :param path: The path the entry is at in the tree.
349 :param tree: The tree which contains this entry and should be used to
352 assert self.new_inventory.root is not None or ie.parent_id is None
353 self.new_inventory.add(ie)
355 # ie.revision is always None if the InventoryEntry is considered
356 # for committing. ie.snapshot will record the correct revision
357 # which may be the sole parent if it is untouched.
358 mutter('recording %s' % ie.file_id)
359 if ie.revision is not None:
362 # Make sure that ie.file_id exists in the map
363 if not ie.file_id in self.old_inv:
364 if not self._svnprops.has_key(SVN_PROP_BZR_FILEIDS):
365 self._svnprops[SVN_PROP_BZR_FILEIDS] = ""
366 mutter('adding fileid mapping %s -> %s' % (path, ie.file_id))
367 self._svnprops[SVN_PROP_BZR_FILEIDS] += "%s\t%s\n" % (escape_svn_path(path), ie.file_id)
369 previous_entries = ie.find_previous_heads(
371 self.repository.weave_store,
372 self.repository.get_transaction())
374 # we are creating a new revision for ie in the history store
376 ie.snapshot(self._new_revision_id, path, previous_entries, tree, self)
379 def push_as_merged(target, source, revision_id):
380 rev = source.repository.get_revision(revision_id)
381 inv = source.repository.get_inventory(revision_id)
383 # revision on top of which to commit
384 prev_revid = target.last_revision()
386 mutter('committing %r on top of %r' % (revision_id, prev_revid))
388 old_tree = source.repository.revision_tree(revision_id)
389 if source.repository.has_revision(prev_revid):
390 new_tree = source.repository.revision_tree(prev_revid)
392 new_tree = target.repository.revision_tree(prev_revid)
394 builder = SvnCommitBuilder(target.repository, target,
395 [revision_id, prev_revid],
400 delta = new_tree.changes_from(old_tree)
401 builder.new_inventory = inv
403 for (_, ie) in inv.entries():
404 if not delta.touches_file_id(ie.file_id):
408 while inv[id].parent_id is not None:
409 if inv[id].revision is None:
411 inv[id].revision = None
412 if inv[id].kind == 'directory':
413 builder.modified_directory(id, [])
414 id = inv[id].parent_id
416 if ie.kind == 'link':
417 builder.modified_link(ie.file_id, [], ie.symlink_target)
418 elif ie.kind == 'file':
420 return old_tree.get_file_text(ie.file_id)
421 builder.modified_file_text(ie.file_id, [], get_text)
424 return builder.commit(rev.message)
425 except SubversionException, (_, num):
426 if num == svn.core.SVN_ERR_FS_TXN_OUT_OF_DATE:
427 raise DivergedBranches(source, target)