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