work around another assert in the subversion bindings.
[jelmer/subvertpy.git] / upgrade.py
1 # Copyright (C) 2006 by Jelmer Vernooij
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 """Upgrading revisions made with older versions of the mapping."""
17
18 from bzrlib.errors import BzrError, InvalidRevisionId
19 from bzrlib.trace import info, mutter
20 import bzrlib.ui as ui
21
22 from errors import RebaseNotPresent
23 from revids import (generate_svn_revision_id, parse_svn_revision_id, 
24                     MAPPING_VERSION,  unescape_svn_path)
25 from scheme import BranchingScheme, guess_scheme_from_branch_path
26
27 class UpgradeChangesContent(BzrError):
28     """Inconsistency was found upgrading the mapping of a revision."""
29     _fmt = """Upgrade will change contents in revision %(revid)s. Use --allow-changes to override."""
30
31     def __init__(self, revid):
32         self.revid = revid
33
34
35 def parse_legacy_revision_id(revid):
36     """Try to parse a legacy Subversion revision id.
37     
38     :param revid: Revision id to parse
39     :return: tuple with (uuid, branch_path, revision number, scheme, mapping version)
40     """
41     if revid.startswith("svn-v1:"):
42         revid = revid[len("svn-v1:"):]
43         at = revid.index("@")
44         fash = revid.rindex("-")
45         uuid = revid[at+1:fash]
46         branch_path = unescape_svn_path(revid[fash+1:])
47         revnum = int(revid[0:at])
48         assert revnum >= 0
49         return (uuid, branch_path, revnum, None, 1)
50     elif revid.startswith("svn-v2:"):
51         revid = revid[len("svn-v2:"):]
52         at = revid.index("@")
53         fash = revid.rindex("-")
54         uuid = revid[at+1:fash]
55         branch_path = unescape_svn_path(revid[fash+1:])
56         revnum = int(revid[0:at])
57         assert revnum >= 0
58         return (uuid, branch_path, revnum, None, 2)
59     elif revid.startswith("svn-v3-"):
60         (uuid, bp, rev, scheme) = parse_svn_revision_id(revid)
61         if scheme == "undefined":
62             scheme = None
63         return (uuid, bp, rev, scheme, 3)
64
65     raise InvalidRevisionId(revid, None)
66
67
68 def create_upgraded_revid(revid):
69     """Create a new revision id for an upgraded version of a revision.
70     
71     Prevents suffix to be appended needlessly.
72
73     :param revid: Original revision id.
74     :return: New revision id
75     """
76     suffix = "-svn%d-upgrade" % MAPPING_VERSION
77     if revid.endswith("-upgrade"):
78         return revid[0:revid.rfind("-svn")] + suffix
79     else:
80         return revid + suffix
81
82
83 def upgrade_workingtree(wt, svn_repository, allow_changes=False, verbose=False):
84     upgrade_branch(wt.branch, svn_repository, allow_changes=allow_changes, verbose=verbose)
85     last_revid = wt.branch.last_revision()
86     wt.set_parent_trees([(last_revid, wt.branch.repository.revision_tree(last_revid))])
87     # TODO: Should also adjust file ids in working tree if necessary
88
89
90 def upgrade_branch(branch, svn_repository, allow_changes=False, verbose=False):
91     """Upgrade a branch to the current mapping version.
92     
93     :param branch: Branch to upgrade.
94     :param svn_repository: Repository to fetch new revisions from
95     :param allow_changes: Allow changes in mappings.
96     :param verbose: Whether to print verbose list of rewrites
97     """
98     revid = branch.last_revision()
99     renames = upgrade_repository(branch.repository, svn_repository, 
100               revid, allow_changes=allow_changes, verbose=verbose)
101     if len(renames) > 0:
102         branch.generate_revision_history(renames[revid])
103
104
105 def check_revision_changed(oldrev, newrev):
106     """Check if two revisions are different. This is exactly the same 
107     as Revision.equals() except that it does not check the revision_id."""
108     if (newrev.inventory_sha1 != oldrev.inventory_sha1 or
109         newrev.timestamp != oldrev.timestamp or
110         newrev.message != oldrev.message or
111         newrev.timezone != oldrev.timezone or
112         newrev.committer != oldrev.committer or
113         newrev.properties != oldrev.properties):
114         raise UpgradeChangesContent(oldrev.revision_id)
115
116
117 def generate_upgrade_map(revs):
118     rename_map = {}
119     pb = ui.ui_factory.nested_progress_bar()
120     # Create a list of revisions that can be renamed during the upgade
121     try:
122         for revid in revs:
123             pb.update('gather revision information', revs.index(revid), len(revs))
124             try:
125                 (uuid, bp, rev, scheme, _) = parse_legacy_revision_id(revid)
126             except InvalidRevisionId:
127                 # Not a bzr-svn revision, nothing to do
128                 continue
129             if scheme is None:
130                 scheme = guess_scheme_from_branch_path(bp)
131             newrevid = generate_svn_revision_id(uuid, rev, bp, scheme)
132             rename_map[revid] = newrevid
133     finally:
134         pb.finished()
135
136     return rename_map
137
138
139 def create_upgrade_plan(repository, svn_repository, revision_id=None,
140                         allow_changes=False):
141     """Generate a rebase plan for upgrading revisions.
142
143     :param repository: Repository to do upgrade in
144     :param svn_repository: Subversion repository to fetch new revisions from.
145     :param revision_id: Revision to upgrade (None for all revisions in 
146         repository.)
147     :param allow_changes: Whether an upgrade is allowed to change the contents
148         of revisions.
149     :return: Tuple with a rebase plan and map of renamed revisions.
150     """
151     try:
152         from bzrlib.plugins.rebase.rebase import generate_transpose_plan
153     except ImportError, e:
154         raise RebaseNotPresent(e)
155
156     graph = repository.get_revision_graph(revision_id)
157     upgrade_map = generate_upgrade_map(graph.keys())
158    
159     # Make sure all the required current version revisions are present
160     for revid in upgrade_map.values():
161         if not repository.has_revision(revid):
162             repository.fetch(svn_repository, revid)
163
164     if not allow_changes:
165         for oldrevid, newrevid in upgrade_map.items():
166             oldrev = repository.get_revision(oldrevid)
167             newrev = repository.get_revision(newrevid)
168             check_revision_changed(oldrev, newrev)
169
170     plan = generate_transpose_plan(graph, upgrade_map, 
171                                    repository.revision_parents,
172                                    create_upgraded_revid)
173     def remove_parents((oldrevid, (newrevid, parents))):
174         return (oldrevid, newrevid)
175     upgrade_map.update(dict(map(remove_parents, plan.items())))
176
177     return (plan, upgrade_map)
178
179  
180 def upgrade_repository(repository, svn_repository, revision_id=None, 
181                        allow_changes=False, verbose=False):
182     """Upgrade the revisions in repository until the specified stop revision.
183
184     :param repository: Repository in which to upgrade.
185     :param svn_repository: Repository to fetch new revisions from.
186     :param revision_id: Revision id up until which to upgrade, or None for 
187                         all revisions.
188     :param allow_changes: Allow changes to mappings.
189     :param verbose: Whether to print list of rewrites
190     :return: Dictionary of mapped revisions
191     """
192     try:
193         from bzrlib.plugins.rebase.rebase import (
194             replay_snapshot, rebase, rebase_todo)
195     except ImportError, e:
196         raise RebaseNotPresent(e)
197
198     # Find revisions that need to be upgraded, create
199     # dictionary with revision ids in key, new parents in value
200     try:
201         repository.lock_write()
202         svn_repository.lock_read()
203         (plan, revid_renames) = create_upgrade_plan(repository, svn_repository, 
204                                                     revision_id=revision_id,
205                                                     allow_changes=allow_changes)
206         if verbose:
207             for revid in rebase_todo(repository, plan):
208                 info("%s -> %s" % (revid, plan[revid][0]))
209         def fix_revid(revid):
210             try:
211                 (uuid, bp, rev, scheme, _) = parse_legacy_revision_id(revid)
212             except InvalidRevisionId:
213                 return revid
214             if scheme is None:
215                 scheme = guess_scheme_from_branch_path(bp)
216             return generate_svn_revision_id(uuid, rev, bp, scheme)
217         def replay(repository, oldrevid, newrevid, new_parents):
218             return replay_snapshot(repository, oldrevid, newrevid, new_parents,
219                                    revid_renames, fix_revid)
220         rebase(repository, plan, replay)
221         return revid_renames
222     finally:
223         repository.unlock()
224         svn_repository.unlock()