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