General cleanups: remove unused imports, fix line lengths, unused parameters.
[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, ROOT_ID
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                         revision_id_to_svk_feature, escape_svn_path)
29
30 import os
31
32 class SvnCommitBuilder(RootCommitBuilder):
33     """Commit Builder implementation wrapped around svn_delta_editor. """
34
35     def __init__(self, repository, branch, parents, config, revprops, 
36                  old_inv=None):
37         """Instantiate a new SvnCommitBuilder.
38
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.
44         """
45         super(SvnCommitBuilder, self).__init__(repository, parents, 
46             config, None, None, None, revprops, None)
47         assert isinstance(repository, SvnRepository)
48         self.branch = branch
49         self.pool = Pool()
50
51         self._svnprops = {}
52         for prop in self._revprops:
53             self._svnprops[SVN_PROP_BZR_REVPROP_PREFIX+prop] = self._revprops[prop]
54
55         self.merges = filter(lambda x: x != self.branch.last_revision(),
56                              parents)
57
58         if len(self.merges) > 0:
59             # Bazaar Parents
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, "")
63             else:
64                 old = ""
65             self._svnprops[SVN_PROP_BZR_MERGE] = old + "\t".join(self.merges) + "\n"
66
67             if branch.last_revision() is not None:
68                 old = repository.branchprop_list.get_property(bp, revnum, SVN_PROP_SVK_MERGE)
69             else:
70                 old = ""
71
72             new = ""
73             # SVK compatibility
74             for p in self.merges:
75                 try:
76                     new += "%s\n" % revision_id_to_svk_feature(p)
77                 except InvalidRevisionId:
78                     pass
79
80             if new != "":
81                 self._svnprops[SVN_PROP_SVK_MERGE] = old + new
82
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)
87
88         if old_inv is None:
89             if self.branch.last_revision() is None:
90                 self.old_inv = Inventory(ROOT_ID)
91             else:
92                 self.old_inv = self.repository.get_inventory(
93                                    self.branch.last_revision())
94         else:
95             self.old_inv = old_inv
96             assert self.old_inv.revision_id == self.branch.last_revision()
97
98         self.modified_files = {}
99         self.modified_dirs = []
100         
101     def _generate_revision_if_needed(self):
102         pass
103
104     def finish_inventory(self):
105         pass
106
107     def modified_file_text(self, file_id, file_parents,
108                            get_content_byte_lines, text_sha1=None,
109                            text_size=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))
114
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
118
119     def modified_directory(self, file_id, file_parents):
120         mutter('modifying directory %s' % file_id)
121         self.modified_dirs.append(file_id)
122
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)
126
127         svn.delta.svn_txdelta_send_string(contents, txdelta, txbaton, self.pool)
128
129     def _dir_process(self, path, file_id, baton):
130         mutter('processing %r' % path)
131         if 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)
139
140         # Loop over entries of file_id in self.old_inv
141         # remove if they no longer exist with the same name
142         # or parents
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)
146                 # remove if...
147                 #  ... path no longer exists
148                 if (not child_ie.file_id in self.new_inventory or 
149                     # ... parent changed
150                     child_ie.parent_id != self.new_inventory[child_ie.file_id].parent_id or
151                     # ... name changed
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)
157
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
162
163             if not (child_ie.kind in ('file', 'symlink')):
164                 continue
165
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)))
169
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)
173
174
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)))
180
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)
185
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)))
190
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)
194
195
196             else:
197                 child_baton = None
198
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')
202             else:
203                 old_special = False
204                 old_executable = False
205
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
210                     else:
211                         value = None
212                     svn.delta.editor_invoke_change_file_prop(self.editor, child_baton, svn.core.SVN_PROP_EXECUTABLE, value, self.pool)
213
214                 if old_special != (child_ie.kind == 'symlink'):
215                     if child_ie.kind == 'symlink':
216                         value = svn.core.SVN_PROP_SPECIAL_VALUE
217                     else:
218                         value = None
219
220                     svn.delta.editor_invoke_change_file_prop(self.editor, child_baton, svn.core.SVN_PROP_SPECIAL, value, self.pool)
221
222             # handle the file
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], 
225                                    child_baton)
226
227             if child_baton is not None:
228                 svn.delta.editor_invoke_close_file(self.editor, child_baton, None, self.pool)
229
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':
234                 continue
235
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(
240                            self.editor, 
241                            os.path.join(self.branch.branch_path, self.new_inventory.id2path(child_ie.file_id)),
242                            baton, None, -1, self.pool)
243
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(
249                            self.editor, 
250                            os.path.join(self.branch.branch_path, self.new_inventory.id2path(child_ie.file_id)),
251                            baton, 
252                            "%s/%s" % (self.branch.base, self.old_inv.id2path(child_ie.file_id)),
253                            self.base_revnum, self.pool)
254
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))
259
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)
263             else:
264                 continue
265
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)
270
271             svn.delta.editor_invoke_close_directory(self.editor, child_baton, 
272                                              self.pool)
273
274     def open_branch_batons(self, root, elements):
275         ret = [root]
276
277         mutter('opening branch %r' % elements)
278
279         for i in range(1, len(elements)):
280             if i == len(elements):
281                 revnum = self.base_revnum
282             else:
283                 revnum = -1
284             ret.append(svn.delta.editor_invoke_open_directory(self.editor, 
285                 "/".join(elements[0:i+1]), ret[-1], revnum, self.pool))
286
287         return ret
288
289     def commit(self, message):
290         def done(revision, date, author):
291             assert revision > 0
292             self.revnum = revision
293             self.date = date
294             self.author = author
295             mutter('committed %r, author: %r, date: %r' % (revision, author, date))
296         
297         mutter('obtaining commit editor')
298         self.revnum = None
299         self.editor, editor_baton = self.repository.transport.get_commit_editor(
300             message.encode("utf-8"), done, None, False)
301
302         if self.branch.last_revision() is None:
303             self.base_revnum = 0
304         else:
305             self.base_revnum = self.repository.parse_revision_id(
306                           self.branch.last_revision())[1]
307
308         root = svn.delta.editor_invoke_open_root(self.editor, editor_baton, 
309                                                  self.base_revnum)
310         
311         branch_batons = self.open_branch_batons(root,
312                                 self.branch.branch_path.split("/"))
313
314         self._dir_process("", self.new_inventory.root.file_id, branch_batons[-1])
315
316         branch_batons.reverse()
317         for baton in branch_batons:
318             svn.delta.editor_invoke_close_directory(self.editor, baton, 
319                                              self.pool)
320
321         svn.delta.editor_invoke_close_edit(self.editor, editor_baton)
322
323         assert self.revnum is not None
324         revid = self.repository.generate_revision_id(self.revnum, 
325                                                     self.branch.branch_path)
326
327         #FIXME: Use public API:
328         self.branch.revision_history()
329         self.branch._revision_history.append(revid)
330
331         mutter('commit finished. author: %r, date: %r' % 
332                (self.author, self.date))
333
334         # Make sure the logwalker doesn't try to use ra 
335         # during checkouts...
336         self.repository._log.fetch_revisions(self.revnum)
337
338         return revid
339
340     def record_entry_contents(self, ie, parent_invs, path, tree):
341         """Record the content of ie from tree into the commit if needed.
342
343         Side effect: sets ie.revision when unchanged
344
345         :param ie: An inventory entry present in the commit.
346         :param parent_invs: The inventories of the parent revisions of the
347             commit.
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 
350         obtain content.
351         """
352         assert self.new_inventory.root is not None or ie.parent_id is None
353         self.new_inventory.add(ie)
354
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:
360             return
361
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)
368
369         previous_entries = ie.find_previous_heads(
370             parent_invs,
371             self.repository.weave_store,
372             self.repository.get_transaction())
373
374         # we are creating a new revision for ie in the history store
375         # and inventory.
376         ie.snapshot(self._new_revision_id, path, previous_entries, tree, self)
377
378
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)
382
383     # revision on top of which to commit
384     prev_revid = target.last_revision()
385
386     mutter('committing %r on top of %r' % (revision_id, prev_revid))
387
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)
391     else:
392         new_tree = target.repository.revision_tree(prev_revid)
393
394     builder = SvnCommitBuilder(target.repository, target, 
395                                [revision_id, prev_revid],
396                                target.get_config(),
397                                rev.properties, 
398                                new_tree.inventory)
399                          
400     delta = new_tree.changes_from(old_tree)
401     builder.new_inventory = inv
402
403     for (_, ie) in inv.entries():
404         if not delta.touches_file_id(ie.file_id):
405             continue
406
407         id = ie.file_id
408         while inv[id].parent_id is not None:
409             if inv[id].revision is None:
410                 break
411             inv[id].revision = None
412             if inv[id].kind == 'directory':
413                 builder.modified_directory(id, [])
414             id = inv[id].parent_id
415
416         if ie.kind == 'link':
417             builder.modified_link(ie.file_id, [], ie.symlink_target)
418         elif ie.kind == 'file':
419             def get_text():
420                 return old_tree.get_file_text(ie.file_id)
421             builder.modified_file_text(ie.file_id, [], get_text)
422
423     try:
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)
428         raise
429