d54d6af84cc4b533841db0dc96c361f053d41aba
[jelmer/subvertpy.git] / commit.py
1 # Copyright (C) 2006-2007 Jelmer Vernooij <jelmer@samba.org>
2
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.
7
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.
12
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
17 import svn.delta
18 from svn.core import Pool, SubversionException
19
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
25
26 from repository import (SvnRepository, SVN_PROP_BZR_MERGE, SVN_PROP_BZR_FILEIDS,
27                         SVN_PROP_SVK_MERGE, SVN_PROP_BZR_REVPROP_PREFIX, 
28                         SVN_PROP_BZR_REVISION_ID, revision_id_to_svk_feature)
29 from revids import escape_svn_path
30
31 import os
32
33 class SvnCommitBuilder(RootCommitBuilder):
34     """Commit Builder implementation wrapped around svn_delta_editor. """
35
36     def __init__(self, repository, branch, parents, config, revprops, 
37                  revision_id, old_inv=None):
38         """Instantiate a new SvnCommitBuilder.
39
40         :param repository: SvnRepository to commit to.
41         :param branch: SvnBranch to commit to.
42         :param parents: List of parent revision ids.
43         :param config: Branch configuration to use.
44         :param revprops: Revision properties to set.
45         :param revision_id: Revision id for the new revision.
46         """
47         super(SvnCommitBuilder, self).__init__(repository, parents, 
48             config, None, None, None, revprops, None)
49         assert isinstance(repository, SvnRepository)
50         self.branch = branch
51         self.pool = Pool()
52
53         self._svnprops = {}
54         for prop in self._revprops:
55             self._svnprops[SVN_PROP_BZR_REVPROP_PREFIX+prop] = self._revprops[prop]
56
57         self.merges = filter(lambda x: x != self.branch.last_revision(),
58                              parents)
59
60         if len(self.merges) > 0:
61             # Bazaar Parents
62             if branch.last_revision():
63                 (bp, revnum) = repository.lookup_revision_id(branch.last_revision())
64                 old = repository.branchprop_list.get_property(bp, revnum, SVN_PROP_BZR_MERGE, "")
65             else:
66                 old = ""
67             self._svnprops[SVN_PROP_BZR_MERGE] = old + "\t".join(self.merges) + "\n"
68
69             if branch.last_revision() is not None:
70                 old = repository.branchprop_list.get_property(bp, revnum, SVN_PROP_SVK_MERGE)
71             else:
72                 old = ""
73
74             new = ""
75             # SVK compatibility
76             for p in self.merges:
77                 try:
78                     new += "%s\n" % revision_id_to_svk_feature(p)
79                 except InvalidRevisionId:
80                     pass
81
82             if new != "":
83                 self._svnprops[SVN_PROP_SVK_MERGE] = old + new
84
85         if revision_id is not None:
86             self._svnprops[SVN_PROP_BZR_REVISION_ID] = revision_id
87
88         # At least one of the parents has to be the last revision on the 
89         # mainline in # Subversion.
90         assert (self.branch.last_revision() is None or 
91                 self.branch.last_revision() in parents)
92
93         if old_inv is None:
94             if self.branch.last_revision() is None:
95                 self.old_inv = Inventory(root_id=None)
96             else:
97                 self.old_inv = self.repository.get_inventory(
98                                    self.branch.last_revision())
99         else:
100             self.old_inv = old_inv
101             assert self.old_inv.revision_id == self.branch.last_revision()
102
103         self.modified_files = {}
104         self.modified_dirs = []
105         
106     def _generate_revision_if_needed(self):
107         pass
108
109     def finish_inventory(self):
110         pass
111
112     def modified_file_text(self, file_id, file_parents,
113                            get_content_byte_lines, text_sha1=None,
114                            text_size=None):
115         mutter('modifying file %s' % file_id)
116         new_lines = get_content_byte_lines()
117         self.modified_files[file_id] = "".join(new_lines)
118         return osutils.sha_strings(new_lines), sum(map(len, new_lines))
119
120     def modified_link(self, file_id, file_parents, link_target):
121         mutter('modifying link %s' % file_id)
122         self.modified_files[file_id] = "link %s" % link_target
123
124     def modified_directory(self, file_id, file_parents):
125         mutter('modifying directory %s' % file_id)
126         self.modified_dirs.append(file_id)
127
128     def _file_process(self, file_id, contents, baton):
129         (txdelta, txbaton) = svn.delta.editor_invoke_apply_textdelta(
130                                 self.editor, baton, None, self.pool)
131
132         svn.delta.svn_txdelta_send_string(contents, txdelta, txbaton, self.pool)
133
134     def _dir_process(self, path, file_id, baton):
135         mutter('processing %r' % path)
136         if path == "":
137             # Set all the revprops
138             for prop, value in self._svnprops.items():
139                 mutter('setting %r: %r on branch' % (prop, value))
140                 if value is not None:
141                     value = value.encode('utf-8')
142                 svn.delta.editor_invoke_change_dir_prop(self.editor, baton,
143                             prop, value, self.pool)
144
145         # Loop over entries of file_id in self.old_inv
146         # remove if they no longer exist with the same name
147         # or parents
148         if file_id in self.old_inv:
149             for child_name in self.old_inv[file_id].children:
150                 child_ie = self.old_inv.get_child(file_id, child_name)
151                 # remove if...
152                 #  ... path no longer exists
153                 if (not child_ie.file_id in self.new_inventory or 
154                     # ... parent changed
155                     child_ie.parent_id != self.new_inventory[child_ie.file_id].parent_id or
156                     # ... name changed
157                     self.new_inventory[child_ie.file_id].name != child_name):
158                     mutter('removing %r' % child_ie.file_id)
159                     svn.delta.editor_invoke_delete_entry(self.editor, 
160                             os.path.join(self.branch.branch_path, self.old_inv.id2path(child_ie.file_id)), 
161                             self.base_revnum, baton, self.pool)
162
163         # Loop over file members of file_id in self.new_inventory
164         for child_name in self.new_inventory[file_id].children:
165             child_ie = self.new_inventory.get_child(file_id, child_name)
166             assert child_ie is not None
167
168             if not (child_ie.kind in ('file', 'symlink')):
169                 continue
170
171             # add them if they didn't exist in old_inv 
172             if not child_ie.file_id in self.old_inv:
173                 mutter('adding %s %r' % (child_ie.kind, self.new_inventory.id2path(child_ie.file_id)))
174
175                 child_baton = svn.delta.editor_invoke_add_file(self.editor, 
176                            os.path.join(self.branch.branch_path, self.new_inventory.id2path(child_ie.file_id)),
177                            baton, None, -1, self.pool)
178
179
180             # copy if they existed at different location
181             elif self.old_inv.id2path(child_ie.file_id) != self.new_inventory.id2path(child_ie.file_id):
182                 mutter('copy %s %r -> %r' % (child_ie.kind, 
183                                   self.old_inv.id2path(child_ie.file_id), 
184                                   self.new_inventory.id2path(child_ie.file_id)))
185
186                 child_baton = svn.delta.editor_invoke_add_file(self.editor, 
187                            os.path.join(self.branch.branch_path, self.new_inventory.id2path(child_ie.file_id)), baton, 
188                            "%s/%s" % (self.branch.base, self.old_inv.id2path(child_ie.file_id)),
189                            self.base_revnum, self.pool)
190
191             # open if they existed at the same location
192             elif child_ie.revision is None:
193                 mutter('open %s %r' % (child_ie.kind, 
194                                  self.new_inventory.id2path(child_ie.file_id)))
195
196                 child_baton = svn.delta.editor_invoke_open_file(self.editor,
197                         os.path.join(self.branch.branch_path, self.new_inventory.id2path(child_ie.file_id)), 
198                         baton, self.base_revnum, self.pool)
199
200
201             else:
202                 child_baton = None
203
204             if child_ie.file_id in self.old_inv:
205                 old_executable = self.old_inv[child_ie.file_id].executable
206                 old_special = (self.old_inv[child_ie.file_id].kind == 'symlink')
207             else:
208                 old_special = False
209                 old_executable = False
210
211             if child_baton is not None:
212                 if old_executable != child_ie.executable:
213                     if child_ie.executable:
214                         value = svn.core.SVN_PROP_EXECUTABLE_VALUE
215                     else:
216                         value = None
217                     svn.delta.editor_invoke_change_file_prop(self.editor, child_baton, svn.core.SVN_PROP_EXECUTABLE, value, self.pool)
218
219                 if old_special != (child_ie.kind == 'symlink'):
220                     if child_ie.kind == 'symlink':
221                         value = svn.core.SVN_PROP_SPECIAL_VALUE
222                     else:
223                         value = None
224
225                     svn.delta.editor_invoke_change_file_prop(self.editor, child_baton, svn.core.SVN_PROP_SPECIAL, value, self.pool)
226
227             # handle the file
228             if child_ie.file_id in self.modified_files:
229                 self._file_process(child_ie.file_id, self.modified_files[child_ie.file_id], 
230                                    child_baton)
231
232             if child_baton is not None:
233                 svn.delta.editor_invoke_close_file(self.editor, child_baton, None, self.pool)
234
235         # Loop over subdirectories of file_id in self.new_inventory
236         for child_name in self.new_inventory[file_id].children:
237             child_ie = self.new_inventory.get_child(file_id, child_name)
238             if child_ie.kind != 'directory':
239                 continue
240
241             # add them if they didn't exist in old_inv 
242             if not child_ie.file_id in self.old_inv:
243                 mutter('adding dir %r' % child_ie.name)
244                 child_baton = svn.delta.editor_invoke_add_directory(
245                            self.editor, 
246                            os.path.join(self.branch.branch_path, self.new_inventory.id2path(child_ie.file_id)),
247                            baton, None, -1, self.pool)
248
249             # copy if they existed at different location
250             elif self.old_inv.id2path(child_ie.file_id) != self.new_inventory.id2path(child_ie.file_id):
251                 mutter('copy dir %r -> %r' % (self.old_inv.id2path(child_ie.file_id), 
252                                          self.new_inventory.id2path(child_ie.file_id)))
253                 child_baton = svn.delta.editor_invoke_add_directory(
254                            self.editor, 
255                            os.path.join(self.branch.branch_path, self.new_inventory.id2path(child_ie.file_id)),
256                            baton, 
257                            "%s/%s" % (self.branch.base, self.old_inv.id2path(child_ie.file_id)),
258                            self.base_revnum, self.pool)
259
260             # open if they existed at the same location and 
261             # the directory was touched
262             elif self.new_inventory[child_ie.file_id].revision is None:
263                 mutter('open dir %r' % self.new_inventory.id2path(child_ie.file_id))
264
265                 child_baton = svn.delta.editor_invoke_open_directory(self.editor, 
266                         os.path.join(self.branch.branch_path, self.new_inventory.id2path(child_ie.file_id)), 
267                         baton, self.base_revnum, self.pool)
268             else:
269                 continue
270
271             # Handle this directory
272             if child_ie.file_id in self.modified_dirs:
273                 self._dir_process(self.new_inventory.id2path(child_ie.file_id), 
274                         child_ie.file_id, child_baton)
275
276             svn.delta.editor_invoke_close_directory(self.editor, child_baton, 
277                                              self.pool)
278
279     def open_branch_batons(self, root, elements):
280         ret = [root]
281
282         mutter('opening branch %r' % elements)
283
284         for i in range(1, len(elements)):
285             if i == len(elements):
286                 revnum = self.base_revnum
287             else:
288                 revnum = -1
289             ret.append(svn.delta.editor_invoke_open_directory(self.editor, 
290                 "/".join(elements[0:i+1]), ret[-1], revnum, self.pool))
291
292         return ret
293
294     def commit(self, message):
295         def done(revision, date, author):
296             assert revision > 0
297             self.revnum = revision
298             self.date = date
299             self.author = author
300             mutter('committed %r, author: %r, date: %r' % (revision, author, date))
301         
302         mutter('obtaining commit editor')
303         self.revnum = None
304         self.editor, editor_baton = self.repository.transport.get_commit_editor(
305             message.encode("utf-8"), done, None, False)
306
307         if self.branch.last_revision() is None:
308             self.base_revnum = 0
309         else:
310             self.base_revnum = self.branch.lookup_revision_id(
311                           self.branch.last_revision())
312
313         root = svn.delta.editor_invoke_open_root(self.editor, editor_baton, 
314                                                  self.base_revnum)
315         
316         branch_batons = self.open_branch_batons(root,
317                                 self.branch.branch_path.split("/"))
318
319         self._dir_process("", self.new_inventory.root.file_id, branch_batons[-1])
320
321         branch_batons.reverse()
322         for baton in branch_batons:
323             svn.delta.editor_invoke_close_directory(self.editor, baton, 
324                                              self.pool)
325
326         svn.delta.editor_invoke_close_edit(self.editor, editor_baton)
327
328         assert self.revnum is not None
329         revid = self.repository.generate_revision_id(self.revnum, 
330                                                     self.branch.branch_path)
331
332         #FIXME: Use public API:
333         self.branch.revision_history()
334         self.branch._revision_history.append(revid)
335
336         mutter('commit finished. author: %r, date: %r' % 
337                (self.author, self.date))
338
339         # Make sure the logwalker doesn't try to use ra 
340         # during checkouts...
341         self.repository._log.fetch_revisions(self.revnum)
342
343         return revid
344
345     def record_entry_contents(self, ie, parent_invs, path, tree):
346         """Record the content of ie from tree into the commit if needed.
347
348         Side effect: sets ie.revision when unchanged
349
350         :param ie: An inventory entry present in the commit.
351         :param parent_invs: The inventories of the parent revisions of the
352             commit.
353         :param path: The path the entry is at in the tree.
354         :param tree: The tree which contains this entry and should be used to 
355         obtain content.
356         """
357         assert self.new_inventory.root is not None or ie.parent_id is None
358         self.new_inventory.add(ie)
359
360         # ie.revision is always None if the InventoryEntry is considered
361         # for committing. ie.snapshot will record the correct revision 
362         # which may be the sole parent if it is untouched.
363         mutter('recording %s' % ie.file_id)
364         if ie.revision is not None:
365             return
366
367         # Make sure that ie.file_id exists in the map
368         if not ie.file_id in self.old_inv:
369             if not self._svnprops.has_key(SVN_PROP_BZR_FILEIDS):
370                 self._svnprops[SVN_PROP_BZR_FILEIDS] = ""
371             mutter('adding fileid mapping %s -> %s' % (path, ie.file_id))
372             self._svnprops[SVN_PROP_BZR_FILEIDS] += "%s\t%s\n" % (escape_svn_path(path), ie.file_id)
373
374         previous_entries = ie.find_previous_heads(
375             parent_invs,
376             self.repository.weave_store,
377             self.repository.get_transaction())
378
379         # we are creating a new revision for ie in the history store
380         # and inventory.
381         ie.snapshot(self._new_revision_id, path, previous_entries, tree, self)
382
383
384 def push_as_merged(target, source, revision_id):
385     rev = source.repository.get_revision(revision_id)
386     inv = source.repository.get_inventory(revision_id)
387
388     # revision on top of which to commit
389     prev_revid = target.last_revision()
390
391     mutter('committing %r on top of %r' % (revision_id, prev_revid))
392
393     old_tree = source.repository.revision_tree(revision_id)
394     if source.repository.has_revision(prev_revid):
395         new_tree = source.repository.revision_tree(prev_revid)
396     else:
397         new_tree = target.repository.revision_tree(prev_revid)
398
399     builder = SvnCommitBuilder(target.repository, target, 
400                                [revision_id, prev_revid],
401                                target.get_config(),
402                                rev.properties, 
403                                None,
404                                new_tree.inventory)
405                          
406     delta = new_tree.changes_from(old_tree)
407     builder.new_inventory = inv
408
409     for (_, ie) in inv.entries():
410         if not delta.touches_file_id(ie.file_id):
411             continue
412
413         id = ie.file_id
414         while inv[id].parent_id is not None:
415             if inv[id].revision is None:
416                 break
417             inv[id].revision = None
418             if inv[id].kind == 'directory':
419                 builder.modified_directory(id, [])
420             id = inv[id].parent_id
421
422         if ie.kind == 'link':
423             builder.modified_link(ie.file_id, [], ie.symlink_target)
424         elif ie.kind == 'file':
425             def get_text():
426                 return old_tree.get_file_text(ie.file_id)
427             builder.modified_file_text(ie.file_id, [], get_text)
428
429     try:
430         return builder.commit(rev.message)
431     except SubversionException, (_, num):
432         if num == svn.core.SVN_ERR_FS_TXN_OUT_OF_DATE:
433             raise DivergedBranches(source, target)
434         raise
435