Remove some unused imports.
[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                         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=None)
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), 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