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