Merge 0.4.
[jelmer/subvertpy.git] / mapping3 / __init__.py
1 # Copyright (C) 2005-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, see <http://www.gnu.org/licenses/>.
15
16 from bzrlib import osutils, ui
17 from bzrlib.errors import InvalidRevisionId
18 from bzrlib.trace import mutter
19
20 from bzrlib.plugins.svn import mapping, properties
21 from bzrlib.plugins.svn.core import SubversionException, NODE_DIR
22 from bzrlib.plugins.svn.errors import ERR_FS_NOT_DIRECTORY, ERR_FS_NOT_FOUND, ERR_RA_DAV_PATH_NOT_FOUND
23 from bzrlib.plugins.svn.layout import RepositoryLayout
24 from bzrlib.plugins.svn.mapping3.scheme import (BranchingScheme, guess_scheme_from_branch_path, 
25                              guess_scheme_from_history, ListBranchingScheme, 
26                              parse_list_scheme_text, NoBranchingScheme,
27                              TrunkBranchingScheme, ListBranchingScheme)
28 import sha
29
30 SVN_PROP_BZR_BRANCHING_SCHEME = 'bzr:branching-scheme'
31
32 # Number of revisions to evaluate when guessing the branching scheme
33 SCHEME_GUESS_SAMPLE_SIZE = 2000
34
35 def expand_branch_pattern(begin, todo, check_path, get_children):
36     path = "/".join(begin)
37     if len(todo) == 0:
38         if check_path(path):
39             return [path]
40         else:
41             return []
42     if not "*" in todo[0]:
43         return expand_branch_pattern(begin+[todo[0]], todo[1:], check_path, get_children)
44     children = get_children(path)
45     if children is None:
46         return []
47     ret = []
48     for c in children:
49         if len(todo) == 1:
50             ret.append("/".join(begin+[c]))
51         else:
52             ret += expand_branch_pattern(begin+[c], todo[1:], check_path, get_children)
53     return ret
54
55
56 class SchemeDerivedLayout(RepositoryLayout):
57     def __init__(self, repository, scheme):
58         self.repository = repository
59         self.scheme = scheme
60
61     def parse(self, path):
62         (bp, rp) = self.scheme.unprefix(path)
63         if self.scheme.is_tag(bp):
64             type = "tag"
65         else:
66             type = "branch"
67         return (type, "", bp, rp)
68
69     def get_branches(self, revnum, project=""):
70         def check_path(path):
71             return self.repository.transport.check_path(path, revnum) == NODE_DIR
72         def find_children(path):
73             try:
74                 assert not path.startswith("/")
75                 dirents = self.repository.transport.get_dir(path, revnum)[0]
76             except SubversionException, (msg, num):
77                 if num in (ERR_FS_NOT_DIRECTORY, ERR_FS_NOT_FOUND, ERR_RA_DAV_PATH_NOT_FOUND):
78                     return None
79                 raise
80             return dirents.keys()
81
82         for pattern in self.scheme.branch_list:
83             for bp in expand_branch_pattern([], pattern.split("/"), check_path,
84                     find_children):
85                 yield "", bp, bp.split("/")[-1]
86
87     def is_branch_parent(self, path):
88         # Na, na, na...
89         return self.scheme.is_branch_parent(path)
90
91     def is_tag_parent(self, path):
92         # Na, na, na...
93         return self.scheme.is_tag_parent(path)
94
95
96 def get_stored_scheme(repository):
97     """Retrieve the stored branching scheme, either in the repository 
98     or in the configuration file.
99     """
100     scheme = repository.get_config().get_branching_scheme()
101     if scheme is not None:
102         return (scheme, repository.get_config().branching_scheme_is_mandatory())
103
104     last_revnum = repository.get_latest_revnum()
105     scheme = get_property_scheme(repository, last_revnum)
106     if scheme is not None:
107         return (scheme, True)
108
109     return (None, False)
110
111
112 def get_property_scheme(repository, revnum=None):
113     if revnum is None:
114         revnum = repository.get_latest_revnum()
115     text = repository.branchprop_list.get_properties("", revnum).get(SVN_PROP_BZR_BRANCHING_SCHEME, None)
116     if text is None:
117         return None
118     return ListBranchingScheme(parse_list_scheme_text(text))
119
120
121 def set_property_scheme(repository, scheme):
122     editor = repository.transport.get_commit_editor(
123             {properties.PROP_REVISION_LOG: "Updating branching scheme for Bazaar."})
124     root = editor.open_root()
125     root.change_prop(SVN_PROP_BZR_BRANCHING_SCHEME, 
126             "".join(map(lambda x: x+"\n", scheme.branch_list)).encode("utf-8"))
127     root.close()
128     editor.close()
129
130
131 def repository_guess_scheme(repository, last_revnum, branch_path=None):
132     pb = ui.ui_factory.nested_progress_bar()
133     try:
134         scheme = guess_scheme_from_history(
135             repository._log.iter_changes(None, last_revnum, max(0, last_revnum-SCHEME_GUESS_SAMPLE_SIZE), pb=pb), last_revnum, branch_path)
136     finally:
137         pb.finished()
138     mutter("Guessed branching scheme: %r" % scheme)
139     return scheme
140
141
142 def config_set_scheme(repository, scheme, mandatory=False):
143     repository.get_config().set_branching_scheme(str(scheme), 
144                                                  mandatory=mandatory)
145
146 def set_branching_scheme(repository, scheme, mandatory=False):
147     repository.get_mapping().scheme = scheme
148     config_set_scheme(repository, scheme, mandatory)
149
150
151 class BzrSvnMappingv3(mapping.BzrSvnMapping):
152     """The third version of the mappings as used in the 0.4.x series.
153
154     """
155     experimental = False
156     upgrade_suffix = "-svn3"
157     revid_prefix = "svn-v3-"
158
159     def __init__(self, scheme):
160         mapping.BzrSvnMapping.__init__(self)
161         self.scheme = scheme
162         assert not isinstance(scheme, str)
163
164     def get_mandated_layout(self, repository):
165         return SchemeDerivedLayout(repository, self.scheme)
166
167     @classmethod
168     def from_repository(cls, repository, _hinted_branch_path=None):
169         (scheme, mandatory) = get_stored_scheme(repository)
170         if mandatory:
171             return cls(scheme) 
172
173         if scheme is not None:
174             if (_hinted_branch_path is None or 
175                 scheme.is_branch(_hinted_branch_path)):
176                 return cls(scheme)
177
178         last_revnum = repository.get_latest_revnum()
179         scheme = repository_guess_scheme(repository, last_revnum, _hinted_branch_path)
180         if last_revnum > 20:
181             config_set_scheme(repository, scheme, mandatory=False)
182
183         return cls(scheme)
184
185     def __repr__(self):
186         return "%s(%r)" % (self.__class__.__name__, self.scheme)
187
188     def generate_file_id(self, uuid, revnum, branch, inv_path):
189         assert isinstance(uuid, str)
190         assert isinstance(revnum, int)
191         assert isinstance(branch, str)
192         assert isinstance(inv_path, unicode)
193         inv_path = inv_path.encode("utf-8")
194         ret = "%d@%s:%s:%s" % (revnum, uuid, mapping.escape_svn_path(branch), 
195                                mapping.escape_svn_path(inv_path))
196         if len(ret) > 150:
197             ret = "%d@%s:%s;%s" % (revnum, uuid, 
198                                 mapping.escape_svn_path(branch),
199                                 sha.new(inv_path).hexdigest())
200         assert isinstance(ret, str)
201         return osutils.safe_file_id(ret)
202
203     @staticmethod
204     def supports_roundtripping():
205         return True
206
207     @classmethod
208     def _parse_revision_id(cls, revid):
209         assert isinstance(revid, str)
210
211         if not revid.startswith(cls.revid_prefix):
212             raise InvalidRevisionId(revid, "")
213
214         try:
215             (version, uuid, branch_path, srevnum) = revid.split(":")
216         except ValueError:
217             raise InvalidRevisionId(revid, "")
218
219         scheme = version[len(cls.revid_prefix):]
220
221         branch_path = mapping.unescape_svn_path(branch_path)
222
223         return (uuid, branch_path, int(srevnum), scheme)
224
225     @classmethod
226     def parse_revision_id(cls, revid):
227         (uuid, branch_path, srevnum, scheme) = cls._parse_revision_id(revid)
228         # Some older versions of bzr-svn 0.4 did not always set a branching
229         # scheme but set "undefined" instead.
230         if scheme == "undefined":
231             scheme = guess_scheme_from_branch_path(branch_path)
232         else:
233             scheme = BranchingScheme.find_scheme(scheme)
234
235         return (uuid, branch_path, srevnum, cls(scheme))
236
237     def is_branch(self, branch_path):
238         return (self.scheme.is_branch(branch_path) or 
239                 self.scheme.is_tag(branch_path))
240
241     def is_tag(self, tag_path):
242         return self.scheme.is_tag(tag_path)
243
244     @classmethod
245     def _generate_revision_id(cls, uuid, revnum, path, scheme):
246         assert isinstance(revnum, int)
247         assert isinstance(path, str)
248         assert revnum >= 0
249         assert revnum > 0 or path == "", \
250                 "Trying to generate revid for (%r,%r)" % (path, revnum)
251         return "%s%s:%s:%s:%d" % (cls.revid_prefix, scheme, uuid, \
252                        mapping.escape_svn_path(path.strip("/")), revnum)
253
254     def generate_revision_id(self, uuid, revnum, path):
255         return self._generate_revision_id(uuid, revnum, path, self.scheme)
256
257     def unprefix(self, branch_path, repos_path):
258         (bp, np) = self.scheme.unprefix(repos_path)
259         assert branch_path == bp
260         return np
261
262     def __eq__(self, other):
263         return type(self) == type(other) and self.scheme == other.scheme
264
265 class BzrSvnMappingv3FileProps(mapping.BzrSvnMappingFileProps, BzrSvnMappingv3):
266     pass
267
268
269 class BzrSvnMappingv3RevProps(mapping.BzrSvnMappingRevProps, BzrSvnMappingv3):
270     pass
271
272
273 class BzrSvnMappingv3Hybrid(BzrSvnMappingv3):
274     def __init__(self, scheme):
275         BzrSvnMappingv3.__init__(self, scheme)
276         self.revprops = BzrSvnMappingv3RevProps(scheme)
277         self.fileprops = BzrSvnMappingv3FileProps(scheme)
278
279     def get_rhs_parents(self, branch_path, svn_revprops, fileprops):
280         if svn_revprops.has_key(mapping.SVN_REVPROP_BZR_MAPPING_VERSION):
281             return self.revprops.get_rhs_parents(branch_path, svn_revprops, fileprops)
282         else:
283             return self.fileprops.get_rhs_parents(branch_path, svn_revprops, fileprops)
284
285     def get_revision_id(self, branch_path, revprops, fileprops):
286         if revprops.has_key(mapping.SVN_REVPROP_BZR_MAPPING_VERSION):
287             return self.revprops.get_revision_id(branch_path, revprops, fileprops)
288         else:
289             return self.fileprops.get_revision_id(branch_path, revprops, fileprops)
290
291     def import_fileid_map(self, svn_revprops, fileprops):
292         if svn_revprops.has_key(mapping.SVN_REVPROP_BZR_MAPPING_VERSION):
293             return self.revprops.import_fileid_map(svn_revprops, fileprops)
294         else:
295             return self.fileprops.import_fileid_map(svn_revprops, fileprops)
296
297     def export_revision(self, branch_root, timestamp, timezone, committer, revprops, revision_id, 
298                         revno, merges, fileprops):
299         (_, fileprops) = self.fileprops.export_revision(branch_root, timestamp, timezone, committer, 
300                                       revprops, revision_id, revno, merges, fileprops)
301         (revprops, _) = self.revprops.export_revision(branch_root, timestamp, timezone, committer, 
302                                       revprops, revision_id, revno, merges, fileprops)
303         return (revprops, fileprops)
304
305     def export_fileid_map(self, fileids, revprops, fileprops):
306         self.fileprops.export_fileid_map(fileids, revprops, fileprops)
307         self.revprops.export_fileid_map(fileids, revprops, fileprops)
308
309     def import_revision(self, svn_revprops, fileprops, rev):
310         self.fileprops.import_revision(svn_revprops, fileprops, rev)
311         self.revprops.import_revision(svn_revprops, fileprops, rev)
312
313