Merge 0.4.
[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 """Committing and pushing to Subversion repositories."""
17
18 import svn.delta
19 from svn.core import Pool, SubversionException, svn_time_to_cstring
20
21 from bzrlib import debug, osutils, urlutils
22 from bzrlib.branch import Branch
23 from bzrlib.errors import (BzrError, InvalidRevisionId, DivergedBranches, 
24                            UnrelatedBranches)
25 from bzrlib.inventory import Inventory
26 from bzrlib.repository import RootCommitBuilder, InterRepository
27 from bzrlib.revision import NULL_REVISION
28 from bzrlib.trace import mutter
29
30 from copy import deepcopy
31 from cStringIO import StringIO
32 from errors import ChangesRootLHSHistory, MissingPrefix, RevpropChangeFailed
33 from repository import (SVN_PROP_SVK_MERGE, revision_id_to_svk_feature,
34                         generate_revision_metadata, SvnRepositoryFormat, 
35                         SvnRepository, format_highres_date)
36 from mapping import (SVN_PROP_BZR_ANCESTRY, SVN_PROP_BZR_FILEIDS,
37                      SVN_PROP_BZR_REVISION_INFO, SVN_PROP_BZR_REVISION_ID, 
38                      SVN_REVPROP_BZR_COMMITTER, SVN_REVPROP_BZR_FILEIDS,
39                      SVN_REVPROP_BZR_MERGE, SVN_REVPROP_BZR_REVISION_ID,
40                      SVN_REVPROP_BZR_REVPROP_PREFIX, SVN_REVPROP_BZR_ROOT,
41                      SVN_REVPROP_BZR_TIMESTAMP, SVN_REVPROP_BZR_MAPPING_VERSION,
42                      MAPPING_VERSION)
43 from repository import (revision_id_to_svk_feature, generate_revision_metadata, 
44                         SvnRepositoryFormat, SvnRepository, SVN_PROP_SVK_MERGE)
45 import urllib
46
47
48 def _check_dirs_exist(transport, bp_parts, base_rev):
49     """Make sure that the specified directories exist.
50
51     :param transport: SvnRaTransport to use.
52     :param bp_parts: List of directory names in the format returned by 
53         os.path.split()
54     :param base_rev: Base revision to check.
55     :return: List of the directories that exists in base_rev.
56     """
57     for i in range(len(bp_parts), 0, -1):
58         current = bp_parts[:i]
59         path = "/".join(current).strip("/")
60         if transport.check_path(path, base_rev) == svn.core.svn_node_dir:
61             return current
62     return []
63
64
65 def set_svn_revprops(transport, revnum, revprops):
66     """Attempt to change the revision properties on the
67     specified revision.
68
69     :param transport: SvnRaTransport connected to target repository
70     :param revnum: Revision number of revision to change metadata of.
71     :param revprops: Dictionary with revision properties to set.
72     """
73     for (name, value) in revprops.items():
74         try:
75             transport.change_rev_prop(revnum, name, value)
76         except SubversionException, (_, svn.core.SVN_ERR_REPOS_DISABLED_FEATURE):
77             raise RevpropChangeFailed(name)
78
79
80 class SvnCommitBuilder(RootCommitBuilder):
81     """Commit Builder implementation wrapped around svn_delta_editor. """
82
83     def __init__(self, repository, branch, parents, config, timestamp, 
84                  timezone, committer, revprops, revision_id, old_inv=None):
85         """Instantiate a new SvnCommitBuilder.
86
87         :param repository: SvnRepository to commit to.
88         :param branch: SvnBranch to commit to.
89         :param parents: List of parent revision ids.
90         :param config: Branch configuration to use.
91         :param timestamp: Optional timestamp recorded for commit.
92         :param timezone: Optional timezone for timestamp.
93         :param committer: Optional committer to set for commit.
94         :param revprops: Revision properties to set.
95         :param revision_id: Revision id for the new revision.
96         :param old_inv: Optional revision on top of which 
97             the commit is happening
98         """
99         super(SvnCommitBuilder, self).__init__(repository, parents, 
100             config, timestamp, timezone, committer, revprops, revision_id)
101         self.branch = branch
102         self.pool = Pool()
103
104         # Keep track of what Subversion properties to set later on
105         self._svnprops = {}
106         self._svnprops[SVN_PROP_BZR_REVISION_INFO] = generate_revision_metadata(
107             timestamp, timezone, committer, revprops)
108         self._svnprops[SVN_PROP_BZR_FILEIDS] = ""
109
110         self._svn_revprops = {SVN_REVPROP_BZR_MAPPING_VERSION: str(MAPPING_VERSION)}
111
112         if timestamp is not None:
113             self._svn_revprops[SVN_REVPROP_BZR_TIMESTAMP] = format_highres_date(timestamp, timezone)
114
115         if committer is not None:
116             self._svn_revprops[SVN_REVPROP_BZR_COMMITTER] = committer.encode("utf-8")
117
118         if revprops is not None:
119             for name, value in revprops.items():
120                 self._svn_revprops[SVN_REVPROP_BZR_REVPROP_PREFIX+name] = value
121
122         # Gather information about revision on top of which the commit is 
123         # happening
124         if parents == []:
125             self.base_revid = None
126         else:
127             self.base_revid = parents[0]
128
129         self.base_revno = self.branch.revision_id_to_revno(self.base_revid)
130         if self.base_revid is None:
131             self.base_revnum = -1
132             self.base_path = None
133             self.base_scheme = repository.get_scheme()
134         else:
135             (self.base_path, self.base_revnum, self.base_scheme) = \
136                 repository.lookup_revision_id(self.base_revid)
137
138         # Determine revisions merged in this one
139         merges = filter(lambda x: x != self.base_revid, parents)
140
141         if len(merges) > 0:
142             self._record_merges(merges)
143
144         # Set appropriate property if revision id was specified by 
145         # caller
146         if revision_id is not None:
147             self._record_revision_id(revision_id)
148
149         if old_inv is None:
150             if self.base_revid is None:
151                 self.old_inv = Inventory(root_id=None)
152             else:
153                 self.old_inv = self.repository.get_inventory(self.base_revid)
154         else:
155             self.old_inv = old_inv
156             # Not all repositories appear to set Inventory.revision_id, 
157             # so allow None as well.
158             assert self.old_inv.revision_id in (None, self.base_revid)
159
160         self.modified_files = {}
161         self.modified_dirs = set()
162
163     def mutter(self, text):
164         if 'commit' in debug.debug_flags:
165             mutter(text)
166
167     def _record_revision_id(self, revid):
168         """Store the revision id in a file property.
169
170         :param revid: The revision id.
171         """
172         if self.base_revid is not None:
173             old = self.repository.branchprop_list.get_property(
174                     self.base_path, self.base_revnum, 
175                         SVN_PROP_BZR_REVISION_ID+str(self.base_scheme), "")
176         else:
177             old = ""
178
179         self._svnprops[SVN_PROP_BZR_REVISION_ID+str(self.base_scheme)] = \
180                 old + "%d %s\n" % (self.base_revno+1, revid)
181         self._svn_revprops[SVN_REVPROP_BZR_REVISION_ID] = revid
182
183     def _record_merges(self, merges):
184         """Store the extra merges (non-LHS parents) in a file property.
185
186         :param merges: List of parents.
187         """
188         # Bazaar Parents
189         if self.base_revid is not None:
190             old = self.repository.branchprop_list.get_property(
191                   self.base_path, self.base_revnum, 
192                   SVN_PROP_BZR_ANCESTRY+str(self.base_scheme), "")
193         else:
194             old = ""
195         self._svnprops[SVN_PROP_BZR_ANCESTRY+str(self.base_scheme)] = old + "\t".join(merges) + "\n"
196
197         if self.base_revid is not None:
198             old = self.repository.branchprop_list.get_property(
199                 self.base_path, self.base_revnum, SVN_PROP_SVK_MERGE, "")
200         else:
201             old = ""
202
203         new = ""
204         # SVK compatibility
205         for merge in merges:
206             try:
207                 new += "%s\n" % revision_id_to_svk_feature(merge)
208             except InvalidRevisionId:
209                 pass
210
211         if new != "":
212             self._svnprops[SVN_PROP_SVK_MERGE] = old + new
213
214         self._svn_revprops[SVN_REVPROP_BZR_MERGE] = "".join(map(lambda x: x + "\n", merges))
215         
216     def _generate_revision_if_needed(self):
217         """See CommitBuilder._generate_revision_if_needed()."""
218
219     def finish_inventory(self):
220         """See CommitBuilder.finish_inventory()."""
221
222     def modified_file_text(self, file_id, file_parents,
223                            get_content_byte_lines, text_sha1=None,
224                            text_size=None):
225         """See CommitBuilder.modified_file_text()."""
226         new_lines = get_content_byte_lines()
227         self.modified_files[file_id] = "".join(new_lines)
228         return osutils.sha_strings(new_lines), sum(map(len, new_lines))
229
230     def modified_link(self, file_id, file_parents, link_target):
231         """See CommitBuilder.modified_link()."""
232         self.modified_files[file_id] = "link %s" % link_target
233
234     def modified_directory(self, file_id, file_parents):
235         """See CommitBuilder.modified_directory()."""
236         self.modified_dirs.add(file_id)
237
238     def _file_process(self, file_id, contents, baton):
239         """Pass the changes to a file to the Subversion commit editor.
240
241         :param file_id: Id of the file to modify.
242         :param contents: Contents of the file.
243         :param baton: Baton under which the file is known to the editor.
244         """
245         assert baton is not None
246         (txdelta, txbaton) = self.editor.apply_textdelta(baton, None, self.pool)
247         digest = svn.delta.svn_txdelta_send_stream(StringIO(contents), txdelta, txbaton, self.pool)
248         if 'validate' in debug.debug_flags:
249             from fetch import md5_strings
250             assert digest == md5_strings(contents)
251
252     def _dir_process(self, path, file_id, baton):
253         """Pass the changes to a directory to the commit editor.
254
255         :param path: Path (from repository root) to the directory.
256         :param file_id: File id of the directory
257         :param baton: Baton of the directory for the editor.
258         """
259         assert baton is not None
260         # Loop over entries of file_id in self.old_inv
261         # remove if they no longer exist with the same name
262         # or parents
263         if file_id in self.old_inv:
264             for child_name in self.old_inv[file_id].children:
265                 child_ie = self.old_inv.get_child(file_id, child_name)
266                 # remove if...
267                 if (
268                     # ... path no longer exists
269                     not child_ie.file_id in self.new_inventory or 
270                     # ... parent changed
271                     child_ie.parent_id != self.new_inventory[child_ie.file_id].parent_id or
272                     # ... name changed
273                     self.new_inventory[child_ie.file_id].name != child_name):
274                     self.mutter('removing %r(%r)' % (child_name, child_ie.file_id))
275                     self.editor.delete_entry(
276                         urlutils.join(self.branch.get_branch_path(), path, child_name), 
277                         self.base_revnum, baton, self.pool)
278
279         # Loop over file children of file_id in self.new_inventory
280         for child_name in self.new_inventory[file_id].children:
281             child_ie = self.new_inventory.get_child(file_id, child_name)
282             assert child_ie is not None
283
284             if not (child_ie.kind in ('file', 'symlink')):
285                 continue
286
287             new_child_path = self.new_inventory.id2path(child_ie.file_id)
288             # add them if they didn't exist in old_inv 
289             if not child_ie.file_id in self.old_inv:
290                 self.mutter('adding %s %r' % (child_ie.kind, new_child_path))
291                 child_baton = self.editor.add_file(
292                     urlutils.join(self.branch.get_branch_path(), 
293                                   new_child_path), baton, None, -1, self.pool)
294
295
296             # copy if they existed at different location
297             elif (self.old_inv.id2path(child_ie.file_id) != new_child_path or
298                     self.old_inv[child_ie.file_id].parent_id != child_ie.parent_id):
299                 self.mutter('copy %s %r -> %r' % (child_ie.kind, 
300                                   self.old_inv.id2path(child_ie.file_id), 
301                                   new_child_path))
302                 child_baton = self.editor.add_file(
303                     urlutils.join(self.branch.get_branch_path(), new_child_path), baton, 
304                     urlutils.join(self.repository.transport.svn_url, self.base_path, self.old_inv.id2path(child_ie.file_id)),
305                     self.base_revnum, self.pool)
306
307             # open if they existed at the same location
308             elif child_ie.revision is None:
309                 self.mutter('open %s %r' % (child_ie.kind, new_child_path))
310
311                 child_baton = self.editor.open_file(
312                     urlutils.join(self.branch.get_branch_path(), 
313                         new_child_path), 
314                     baton, self.base_revnum, self.pool)
315
316             else:
317                 # Old copy of the file was retained. No need to send changes
318                 assert child_ie.file_id not in self.modified_files
319                 child_baton = None
320
321             if child_ie.file_id in self.old_inv:
322                 old_executable = self.old_inv[child_ie.file_id].executable
323                 old_special = (self.old_inv[child_ie.file_id].kind == 'symlink')
324             else:
325                 old_special = False
326                 old_executable = False
327
328             if child_baton is not None:
329                 if old_executable != child_ie.executable:
330                     if child_ie.executable:
331                         value = svn.core.SVN_PROP_EXECUTABLE_VALUE
332                     else:
333                         value = None
334                     self.editor.change_file_prop(child_baton, 
335                             svn.core.SVN_PROP_EXECUTABLE, value, self.pool)
336
337                 if old_special != (child_ie.kind == 'symlink'):
338                     if child_ie.kind == 'symlink':
339                         value = svn.core.SVN_PROP_SPECIAL_VALUE
340                     else:
341                         value = None
342
343                     self.editor.change_file_prop(child_baton, 
344                             svn.core.SVN_PROP_SPECIAL, value, self.pool)
345
346             # handle the file
347             if child_ie.file_id in self.modified_files:
348                 self._file_process(child_ie.file_id, 
349                     self.modified_files[child_ie.file_id], child_baton)
350
351             if child_baton is not None:
352                 self.editor.close_file(child_baton, None, self.pool)
353
354         # Loop over subdirectories of file_id in self.new_inventory
355         for child_name in self.new_inventory[file_id].children:
356             child_ie = self.new_inventory.get_child(file_id, child_name)
357             if child_ie.kind != 'directory':
358                 continue
359
360             new_child_path = self.new_inventory.id2path(child_ie.file_id)
361             # add them if they didn't exist in old_inv 
362             if not child_ie.file_id in self.old_inv:
363                 self.mutter('adding dir %r' % child_ie.name)
364                 child_baton = self.editor.add_directory(
365                     urlutils.join(self.branch.get_branch_path(), 
366                                   new_child_path), baton, None, -1, self.pool)
367
368             # copy if they existed at different location
369             elif self.old_inv.id2path(child_ie.file_id) != new_child_path:
370                 old_child_path = self.old_inv.id2path(child_ie.file_id)
371                 self.mutter('copy dir %r -> %r' % (old_child_path, new_child_path))
372                 child_baton = self.editor.add_directory(
373                     urlutils.join(self.branch.get_branch_path(), new_child_path),
374                     baton, 
375                     urlutils.join(self.repository.transport.svn_url, self.base_path, old_child_path), self.base_revnum, self.pool)
376
377             # open if they existed at the same location and 
378             # the directory was touched
379             elif self.new_inventory[child_ie.file_id].revision is None:
380                 self.mutter('open dir %r' % new_child_path)
381
382                 child_baton = self.editor.open_directory(
383                         urlutils.join(self.branch.get_branch_path(), new_child_path), 
384                         baton, self.base_revnum, self.pool)
385             else:
386                 assert child_ie.file_id not in self.modified_dirs
387                 continue
388
389             # Handle this directory
390             if child_ie.file_id in self.modified_dirs:
391                 self._dir_process(new_child_path, child_ie.file_id, child_baton)
392
393             self.editor.close_directory(child_baton, self.pool)
394
395     def open_branch_batons(self, root, elements, existing_elements, 
396                            base_path, base_rev, replace_existing):
397         """Open a specified directory given a baton for the repository root.
398
399         :param root: Baton for the repository root
400         :param elements: List of directory names to open
401         :param existing_elements: List of directory names that exist
402         :param base_path: Path to base top-level branch on
403         :param base_rev: Revision of path to base top-level branch on
404         :param replace_existing: Whether the current branch should be replaced
405         """
406         ret = [root]
407
408         self.mutter('opening branch %r (base %r:%r)' % (elements, base_path, 
409                                                    base_rev))
410
411         # Open paths leading up to branch
412         for i in range(0, len(elements)-1):
413             # Does directory already exist?
414             ret.append(self.editor.open_directory(
415                 "/".join(existing_elements[0:i+1]), ret[-1], -1, self.pool))
416
417         if (len(existing_elements) != len(elements) and
418             len(existing_elements)+1 != len(elements)):
419             raise MissingPrefix("/".join(elements))
420
421         # Branch already exists and stayed at the same location, open:
422         # TODO: What if the branch didn't change but the new revision 
423         # was based on an older revision of the branch?
424         # This needs to also check that base_rev was the latest version of 
425         # branch_path.
426         if (len(existing_elements) == len(elements) and 
427             not replace_existing):
428             ret.append(self.editor.open_directory(
429                 "/".join(elements), ret[-1], base_rev, self.pool))
430         else: # Branch has to be created
431             # Already exists, old copy needs to be removed
432             name = "/".join(elements)
433             if replace_existing:
434                 if name == "":
435                     raise ChangesRootLHSHistory()
436                 self.mutter("removing branch dir %r" % name)
437                 self.editor.delete_entry(name, -1, ret[-1])
438             if base_path is not None:
439                 base_url = urlutils.join(self.repository.transport.svn_url, base_path)
440             else:
441                 base_url = None
442             self.mutter("adding branch dir %r" % name)
443             ret.append(self.editor.add_directory(
444                 name, ret[-1], base_url, base_rev, self.pool))
445
446         return ret
447
448     def commit(self, message):
449         """Finish the commit.
450
451         """
452         def done(revision_data, pool):
453             """Callback that is called by the Subversion commit editor 
454             once the commit finishes.
455
456             :param revision_metadata: Revision metadata
457             """
458             self.revision_metadata = revision_data
459         
460         self._svn_revprops[SVN_REVPROP_BZR_ROOT] = self.branch.get_branch_path()
461         bp_parts = self.branch.get_branch_path().split("/")
462         repository_latest_revnum = self.repository.transport.get_latest_revnum()
463         lock = self.repository.transport.lock_write(".")
464         set_revprops = self.repository.get_config().get_set_revprops()
465         remaining_revprops = self._svn_revprops # Keep track of the revprops that haven't been set yet
466
467         # Store file ids
468         def _dir_process_file_id(old_inv, new_inv, path, file_id):
469             for child_name in new_inv[file_id].children:
470                 child_ie = new_inv.get_child(file_id, child_name)
471                 new_child_path = new_inv.id2path(child_ie.file_id)
472                 assert child_ie is not None
473
474                 if (not child_ie.file_id in old_inv or 
475                     old_inv.id2path(child_ie.file_id) != new_child_path or
476                     old_inv[child_ie.file_id].parent_id != child_ie.parent_id):
477                     yield (child_ie.file_id, new_child_path)
478
479                 if (child_ie.kind == 'directory' and 
480                     child_ie.file_id in self.modified_dirs):
481                     _dir_process_file_id(old_inv, new_inv, new_child_path, child_ie.file_id)
482
483         fileids = []
484
485         if (self.old_inv.root is None or 
486             self.new_inventory.root.file_id != self.old_inv.root.file_id):
487             fileids.append((self.new_inventory.root.file_id, ""))
488
489         fileids += list(_dir_process_file_id(self.old_inv, self.new_inventory, "", self.new_inventory.root.file_id))
490
491         if fileids != []:
492             file_id_text = "".join(["%s\t%s\n" % (urllib.quote(path), file_id) for (file_id, path) in fileids])
493             self._svn_revprops[SVN_REVPROP_BZR_FILEIDS] = file_id_text
494             self._svnprops[SVN_PROP_BZR_FILEIDS] = file_id_text
495
496         try:
497             existing_bp_parts = _check_dirs_exist(self.repository.transport, 
498                                               bp_parts, -1)
499             self.revision_metadata = None
500             self._svn_revprops[svn.core.SVN_PROP_REVISION_LOG] = message.encode("utf-8")
501             try:
502                 self.editor = self.repository.transport.get_commit_editor(self._svn_revprops, 
503                                                                           done, None, False)
504                 self._svn_revprops = {}
505             except NotImplementedError:
506                 if set_revprops:
507                     raise
508                 # Try without bzr: revprops
509                 self.editor = self.repository.transport.get_commit_editor({
510                     svn.core.SVN_PROP_REVISION_LOG: self._svn_revprops[svn.core.SVN_PROP_REVISION_LOG]},
511                     done, None, False)
512                 del self._svn_revprops[svn.core.SVN_PROP_REVISION_LOG]
513
514             root = self.editor.open_root(self.base_revnum)
515
516             replace_existing = False
517             # See whether the base of the commit matches the lhs parent
518             # if not, we need to replace the existing directory
519             if len(bp_parts) == len(existing_bp_parts):
520                 if self.base_path.strip("/") != "/".join(bp_parts).strip("/"):
521                     replace_existing = True
522                 elif self.base_revnum < self.repository._log.find_latest_change(self.branch.get_branch_path(), repository_latest_revnum, include_children=True):
523                     replace_existing = True
524
525             # TODO: Accept create_prefix argument (#118787)
526             branch_batons = self.open_branch_batons(root, bp_parts,
527                 existing_bp_parts, self.base_path, self.base_revnum, 
528                 replace_existing)
529
530             self._dir_process("", self.new_inventory.root.file_id, 
531                 branch_batons[-1])
532
533             # Set all the revprops
534             for prop, value in self._svnprops.items():
535                 if value is not None:
536                     value = value.encode('utf-8')
537                 self.editor.change_dir_prop(branch_batons[-1], prop, value, 
538                                             self.pool)
539
540             for baton in reversed(branch_batons):
541                 self.editor.close_directory(baton, self.pool)
542
543             self.editor.close()
544         finally:
545             lock.unlock()
546
547         assert self.revision_metadata is not None
548
549         # Make sure the logwalker doesn't try to use ra 
550         # during checkouts...
551         self.repository._log.fetch_revisions(self.revision_metadata.revision)
552
553         revid = self.branch.generate_revision_id(self.revision_metadata.revision)
554
555         assert self._new_revision_id is None or self._new_revision_id == revid
556
557         self.mutter('commit %d finished. author: %r, date: %r, revid: %r' % 
558                (self.revision_metadata.revision, self.revision_metadata.author, self.revision_metadata.date, revid))
559
560         if self.repository.get_config().get_override_svn_revprops():
561             set_svn_revprops(self.repository.transport, self.revision_metadata.revision, {
562                 svn.core.SVN_PROP_REVISION_AUTHOR: self._committer,
563                 svn.core.SVN_PROP_REVISION_DATE: svn_time_to_cstring(1000000*self._timestamp)
564                 })
565
566         try:
567             set_svn_revprops(self.repository.transport, self.revision_metadata.revision, 
568                          self._svn_revprops) 
569         except RevpropChangeFailed:
570             pass # Ignore for now
571
572         return revid
573
574     def record_entry_contents(self, ie, parent_invs, path, tree,
575                               content_summary):
576         """Record the content of ie from tree into the commit if needed.
577
578         Side effect: sets ie.revision when unchanged
579
580         :param ie: An inventory entry present in the commit.
581         :param parent_invs: The inventories of the parent revisions of the
582             commit.
583         :param path: The path the entry is at in the tree.
584         :param tree: The tree which contains this entry and should be used to 
585             obtain content.
586         :param content_summary: Summary data from the tree about the paths
587                 content - stat, length, exec, sha/link target. This is only
588                 accessed when the entry has a revision of None - that is when 
589                 it is a candidate to commit.
590         """
591         self.new_inventory.add(ie)
592
593
594 def replay_delta(builder, old_tree, new_tree):
595     """Replays a delta to a commit builder.
596
597     :param builder: The commit builder.
598     :param old_tree: Original tree on top of which the delta should be applied
599     :param new_tree: New tree that should be committed
600     """
601     for path, ie in new_tree.inventory.iter_entries():
602         builder.record_entry_contents(ie.copy(), [old_tree.inventory], 
603                                       path, new_tree, None)
604     builder.finish_inventory()
605     delta = new_tree.changes_from(old_tree)
606     def touch_id(id):
607         ie = builder.new_inventory[id]
608
609         id = ie.file_id
610         while builder.new_inventory[id].parent_id is not None:
611             if builder.new_inventory[id].revision is None:
612                 break
613             builder.new_inventory[id].revision = None
614             if builder.new_inventory[id].kind == 'directory':
615                 builder.modified_directory(id, [])
616             id = builder.new_inventory[id].parent_id
617
618         assert ie.kind in ('symlink', 'file', 'directory')
619         if ie.kind == 'symlink':
620             builder.modified_link(ie.file_id, [], ie.symlink_target)
621         elif ie.kind == 'file':
622             def get_text():
623                 return new_tree.get_file_text(ie.file_id)
624             builder.modified_file_text(ie.file_id, [], get_text)
625
626     for (_, id, _) in delta.added:
627         touch_id(id)
628
629     for (_, id, _, _, _) in delta.modified:
630         touch_id(id)
631
632     for (oldpath, _, id, _, _, _) in delta.renamed:
633         touch_id(id)
634         old_parent_id = old_tree.inventory.path2id(urlutils.dirname(oldpath))
635         if old_parent_id in builder.new_inventory:
636             touch_id(old_parent_id)
637
638     for (path, _, _) in delta.removed:
639         old_parent_id = old_tree.inventory.path2id(urlutils.dirname(path))
640         if old_parent_id in builder.new_inventory:
641             touch_id(old_parent_id)
642
643
644 def push_new(target_repository, target_branch_path, source, 
645              stop_revision=None):
646     """Push a revision into Subversion, creating a new branch.
647
648     This will do a new commit in the target branch.
649
650     :param target_branch_path: Path to create new branch at
651     :param source: Branch to pull the revision from
652     :param revision_id: Revision id of the revision to push
653     """
654     assert isinstance(source, Branch)
655     if stop_revision is None:
656         stop_revision = source.last_revision()
657     history = source.revision_history()
658     revhistory = deepcopy(history)
659     start_revid = NULL_REVISION
660     while len(revhistory) > 0:
661         revid = revhistory.pop()
662         # We've found the revision to push if there is a revision 
663         # which LHS parent is present or if this is the first revision.
664         if (len(revhistory) == 0 or 
665             target_repository.has_revision(revhistory[-1])):
666             start_revid = revid
667             break
668
669     # Get commit builder but specify that target_branch_path should
670     # be created and copied from (copy_path, copy_revnum)
671     class ImaginaryBranch:
672         """Simple branch that pretends to be empty but already exist."""
673         def __init__(self, repository):
674             self.repository = repository
675             self._revision_history = None
676
677         def get_config(self):
678             """See Branch.get_config()."""
679             return None
680
681         def revision_id_to_revno(self, revid):
682             if revid is None:
683                 return 0
684             return history.index(revid)
685
686         def last_revision_info(self):
687             """See Branch.last_revision_info()."""
688             last_revid = self.last_revision()
689             if last_revid is None:
690                 return (0, None)
691             return (history.index(last_revid), last_revid)
692
693         def last_revision(self):
694             """See Branch.last_revision()."""
695             parents = source.repository.revision_parents(start_revid)
696             if parents == []:
697                 return None
698             return parents[0]
699
700         def get_branch_path(self, revnum=None):
701             """See SvnBranch.get_branch_path()."""
702             return target_branch_path
703
704         def generate_revision_id(self, revnum):
705             """See SvnBranch.generate_revision_id()."""
706             return self.repository.generate_revision_id(
707                 revnum, self.get_branch_path(revnum), 
708                 str(self.repository.get_scheme()))
709
710     push(ImaginaryBranch(target_repository), source, start_revid)
711
712
713 def push(target, source, revision_id):
714     """Push a revision into Subversion.
715
716     This will do a new commit in the target branch.
717
718     :param target: Branch to push to
719     :param source: Branch to pull the revision from
720     :param revision_id: Revision id of the revision to push
721     """
722     assert isinstance(source, Branch)
723     rev = source.repository.get_revision(revision_id)
724     mutter('pushing %r (%r)' % (revision_id, rev.parent_ids))
725
726     # revision on top of which to commit
727     if rev.parent_ids == []:
728         base_revid = None
729     else:
730         base_revid = rev.parent_ids[0]
731
732     source.lock_read()
733     try:
734         old_tree = source.repository.revision_tree(revision_id)
735         base_tree = source.repository.revision_tree(base_revid)
736
737         builder = SvnCommitBuilder(target.repository, target, rev.parent_ids,
738                                    target.get_config(), rev.timestamp,
739                                    rev.timezone, rev.committer, rev.properties, 
740                                    revision_id, base_tree.inventory)
741                              
742         replay_delta(builder, base_tree, old_tree)
743     finally:
744         source.unlock()
745     try:
746         builder.commit(rev.message)
747     except SubversionException, (_, num):
748         if num == svn.core.SVN_ERR_FS_TXN_OUT_OF_DATE:
749             raise DivergedBranches(source, target)
750         raise
751     except ChangesRootLHSHistory:
752         raise BzrError("Unable to push revision %r because it would change the ordering of existing revisions on the Subversion repository root. Use rebase and try again or push to a non-root path" % revision_id)
753
754     if 'validate' in debug.debug_flags:
755         crev = target.repository.get_revision(revision_id)
756         ctree = target.repository.revision_tree(revision_id)
757         treedelta = ctree.changes_from(old_tree)
758         assert not treedelta.has_changed(), "treedelta: %r" % treedelta
759         assert crev.committer == rev.committer
760         assert crev.timezone == rev.timezone
761         assert crev.timestamp == rev.timestamp
762         assert crev.message == rev.message
763         assert crev.properties == rev.properties
764
765
766 class InterToSvnRepository(InterRepository):
767     """Any to Subversion repository actions."""
768
769     _matching_repo_format = SvnRepositoryFormat()
770
771     @staticmethod
772     def _get_repo_format_to_test():
773         """See InterRepository._get_repo_format_to_test()."""
774         return None
775
776     def copy_content(self, revision_id=None, pb=None):
777         """See InterRepository.copy_content."""
778         self.source.lock_read()
779         try:
780             assert revision_id is not None, "fetching all revisions not supported"
781             # Go back over the LHS parent until we reach a revid we know
782             todo = []
783             while not self.target.has_revision(revision_id):
784                 todo.append(revision_id)
785                 revision_id = self.source.revision_parents(revision_id)[0]
786                 if revision_id == NULL_REVISION:
787                     raise UnrelatedBranches()
788             if todo == []:
789                 # Nothing to do
790                 return
791             mutter("pushing %r into svn" % todo)
792             target_branch = None
793             for revision_id in todo:
794                 if pb is not None:
795                     pb.update("pushing revisions", todo.index(revision_id), len(todo))
796                 rev = self.source.get_revision(revision_id)
797
798                 mutter('pushing %r' % (revision_id))
799
800                 old_tree = self.source.revision_tree(revision_id)
801                 parent_revid = rev.parent_ids[0]
802                 base_tree = self.source.revision_tree(parent_revid)
803
804                 (bp, _, _) = self.target.lookup_revision_id(parent_revid)
805                 if target_branch is None:
806                     target_branch = Branch.open(urlutils.join(self.target.base, bp))
807                 if target_branch.get_branch_path() != bp:
808                     target_branch.set_branch_path(bp)
809
810                 builder = SvnCommitBuilder(self.target, target_branch, 
811                                    rev.parent_ids, target_branch.get_config(),
812                                    rev.timestamp, rev.timezone, rev.committer,
813                                    rev.properties, revision_id, base_tree.inventory)
814                              
815                 replay_delta(builder, base_tree, old_tree)
816                 builder.commit(rev.message)
817         finally:
818             self.source.unlock()
819  
820
821     def fetch(self, revision_id=None, pb=None, find_ghosts=False):
822         """Fetch revisions. """
823         self.copy_content(revision_id=revision_id, pb=pb)
824
825     @staticmethod
826     def is_compatible(source, target):
827         """Be compatible with SvnRepository."""
828         return isinstance(target, SvnRepository)