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, ROOT_ID
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)
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, "%\t\n"), 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)