##// END OF EJS Templates
convert: make subversion import transport locally...
FUJIWARA Katsunori -
r28459:3ea62e74 default
parent child Browse files
Show More
@@ -1,1358 +1,1358 b''
1 # Subversion 1.4/1.5 Python API backend
1 # Subversion 1.4/1.5 Python API backend
2 #
2 #
3 # Copyright(C) 2007 Daniel Holth et al
3 # Copyright(C) 2007 Daniel Holth et al
4 from __future__ import absolute_import
4 from __future__ import absolute_import
5
5
6 import cPickle as pickle
6 import cPickle as pickle
7 import cStringIO
7 import cStringIO
8 import os
8 import os
9 import re
9 import re
10 import sys
10 import sys
11 import tempfile
11 import tempfile
12 import urllib
12 import urllib
13 import urllib2
13 import urllib2
14 import xml.dom.minidom
14 import xml.dom.minidom
15
15
16 from mercurial import (
16 from mercurial import (
17 encoding,
17 encoding,
18 error,
18 error,
19 scmutil,
19 scmutil,
20 strutil,
20 strutil,
21 util,
21 util,
22 )
22 )
23 from mercurial.i18n import _
23 from mercurial.i18n import _
24
24
25 from . import common
25 from . import common
26
26
27 StringIO = cStringIO.StringIO
27 StringIO = cStringIO.StringIO
28 propertycache = util.propertycache
28 propertycache = util.propertycache
29
29
30 commandline = common.commandline
30 commandline = common.commandline
31 commit = common.commit
31 commit = common.commit
32 converter_sink = common.converter_sink
32 converter_sink = common.converter_sink
33 converter_source = common.converter_source
33 converter_source = common.converter_source
34 decodeargs = common.decodeargs
34 decodeargs = common.decodeargs
35 encodeargs = common.encodeargs
35 encodeargs = common.encodeargs
36 makedatetimestamp = common.makedatetimestamp
36 makedatetimestamp = common.makedatetimestamp
37 mapfile = common.mapfile
37 mapfile = common.mapfile
38 MissingTool = common.MissingTool
38 MissingTool = common.MissingTool
39 NoRepo = common.NoRepo
39 NoRepo = common.NoRepo
40
40
41 # Subversion stuff. Works best with very recent Python SVN bindings
41 # Subversion stuff. Works best with very recent Python SVN bindings
42 # e.g. SVN 1.5 or backports. Thanks to the bzr folks for enhancing
42 # e.g. SVN 1.5 or backports. Thanks to the bzr folks for enhancing
43 # these bindings.
43 # these bindings.
44
44
45 try:
45 try:
46 from svn.core import (
46 from svn.core import (
47 Pool,
47 Pool,
48 SubversionException,
48 SubversionException,
49 )
49 )
50 import svn
50 import svn
51 import svn.client
51 import svn.client
52 import svn.core
52 import svn.core
53 import svn.ra
53 import svn.ra
54 import svn.delta
54 import svn.delta
55 import transport
55 from . import transport
56 import warnings
56 import warnings
57 warnings.filterwarnings('ignore',
57 warnings.filterwarnings('ignore',
58 module='svn.core',
58 module='svn.core',
59 category=DeprecationWarning)
59 category=DeprecationWarning)
60
60
61 except ImportError:
61 except ImportError:
62 svn = None
62 svn = None
63
63
64 class SvnPathNotFound(Exception):
64 class SvnPathNotFound(Exception):
65 pass
65 pass
66
66
67 def revsplit(rev):
67 def revsplit(rev):
68 """Parse a revision string and return (uuid, path, revnum).
68 """Parse a revision string and return (uuid, path, revnum).
69 >>> revsplit('svn:a2147622-4a9f-4db4-a8d3-13562ff547b2'
69 >>> revsplit('svn:a2147622-4a9f-4db4-a8d3-13562ff547b2'
70 ... '/proj%20B/mytrunk/mytrunk@1')
70 ... '/proj%20B/mytrunk/mytrunk@1')
71 ('a2147622-4a9f-4db4-a8d3-13562ff547b2', '/proj%20B/mytrunk/mytrunk', 1)
71 ('a2147622-4a9f-4db4-a8d3-13562ff547b2', '/proj%20B/mytrunk/mytrunk', 1)
72 >>> revsplit('svn:8af66a51-67f5-4354-b62c-98d67cc7be1d@1')
72 >>> revsplit('svn:8af66a51-67f5-4354-b62c-98d67cc7be1d@1')
73 ('', '', 1)
73 ('', '', 1)
74 >>> revsplit('@7')
74 >>> revsplit('@7')
75 ('', '', 7)
75 ('', '', 7)
76 >>> revsplit('7')
76 >>> revsplit('7')
77 ('', '', 0)
77 ('', '', 0)
78 >>> revsplit('bad')
78 >>> revsplit('bad')
79 ('', '', 0)
79 ('', '', 0)
80 """
80 """
81 parts = rev.rsplit('@', 1)
81 parts = rev.rsplit('@', 1)
82 revnum = 0
82 revnum = 0
83 if len(parts) > 1:
83 if len(parts) > 1:
84 revnum = int(parts[1])
84 revnum = int(parts[1])
85 parts = parts[0].split('/', 1)
85 parts = parts[0].split('/', 1)
86 uuid = ''
86 uuid = ''
87 mod = ''
87 mod = ''
88 if len(parts) > 1 and parts[0].startswith('svn:'):
88 if len(parts) > 1 and parts[0].startswith('svn:'):
89 uuid = parts[0][4:]
89 uuid = parts[0][4:]
90 mod = '/' + parts[1]
90 mod = '/' + parts[1]
91 return uuid, mod, revnum
91 return uuid, mod, revnum
92
92
93 def quote(s):
93 def quote(s):
94 # As of svn 1.7, many svn calls expect "canonical" paths. In
94 # As of svn 1.7, many svn calls expect "canonical" paths. In
95 # theory, we should call svn.core.*canonicalize() on all paths
95 # theory, we should call svn.core.*canonicalize() on all paths
96 # before passing them to the API. Instead, we assume the base url
96 # before passing them to the API. Instead, we assume the base url
97 # is canonical and copy the behaviour of svn URL encoding function
97 # is canonical and copy the behaviour of svn URL encoding function
98 # so we can extend it safely with new components. The "safe"
98 # so we can extend it safely with new components. The "safe"
99 # characters were taken from the "svn_uri__char_validity" table in
99 # characters were taken from the "svn_uri__char_validity" table in
100 # libsvn_subr/path.c.
100 # libsvn_subr/path.c.
101 return urllib.quote(s, "!$&'()*+,-./:=@_~")
101 return urllib.quote(s, "!$&'()*+,-./:=@_~")
102
102
103 def geturl(path):
103 def geturl(path):
104 try:
104 try:
105 return svn.client.url_from_path(svn.core.svn_path_canonicalize(path))
105 return svn.client.url_from_path(svn.core.svn_path_canonicalize(path))
106 except SubversionException:
106 except SubversionException:
107 # svn.client.url_from_path() fails with local repositories
107 # svn.client.url_from_path() fails with local repositories
108 pass
108 pass
109 if os.path.isdir(path):
109 if os.path.isdir(path):
110 path = os.path.normpath(os.path.abspath(path))
110 path = os.path.normpath(os.path.abspath(path))
111 if os.name == 'nt':
111 if os.name == 'nt':
112 path = '/' + util.normpath(path)
112 path = '/' + util.normpath(path)
113 # Module URL is later compared with the repository URL returned
113 # Module URL is later compared with the repository URL returned
114 # by svn API, which is UTF-8.
114 # by svn API, which is UTF-8.
115 path = encoding.tolocal(path)
115 path = encoding.tolocal(path)
116 path = 'file://%s' % quote(path)
116 path = 'file://%s' % quote(path)
117 return svn.core.svn_path_canonicalize(path)
117 return svn.core.svn_path_canonicalize(path)
118
118
119 def optrev(number):
119 def optrev(number):
120 optrev = svn.core.svn_opt_revision_t()
120 optrev = svn.core.svn_opt_revision_t()
121 optrev.kind = svn.core.svn_opt_revision_number
121 optrev.kind = svn.core.svn_opt_revision_number
122 optrev.value.number = number
122 optrev.value.number = number
123 return optrev
123 return optrev
124
124
125 class changedpath(object):
125 class changedpath(object):
126 def __init__(self, p):
126 def __init__(self, p):
127 self.copyfrom_path = p.copyfrom_path
127 self.copyfrom_path = p.copyfrom_path
128 self.copyfrom_rev = p.copyfrom_rev
128 self.copyfrom_rev = p.copyfrom_rev
129 self.action = p.action
129 self.action = p.action
130
130
131 def get_log_child(fp, url, paths, start, end, limit=0,
131 def get_log_child(fp, url, paths, start, end, limit=0,
132 discover_changed_paths=True, strict_node_history=False):
132 discover_changed_paths=True, strict_node_history=False):
133 protocol = -1
133 protocol = -1
134 def receiver(orig_paths, revnum, author, date, message, pool):
134 def receiver(orig_paths, revnum, author, date, message, pool):
135 paths = {}
135 paths = {}
136 if orig_paths is not None:
136 if orig_paths is not None:
137 for k, v in orig_paths.iteritems():
137 for k, v in orig_paths.iteritems():
138 paths[k] = changedpath(v)
138 paths[k] = changedpath(v)
139 pickle.dump((paths, revnum, author, date, message),
139 pickle.dump((paths, revnum, author, date, message),
140 fp, protocol)
140 fp, protocol)
141
141
142 try:
142 try:
143 # Use an ra of our own so that our parent can consume
143 # Use an ra of our own so that our parent can consume
144 # our results without confusing the server.
144 # our results without confusing the server.
145 t = transport.SvnRaTransport(url=url)
145 t = transport.SvnRaTransport(url=url)
146 svn.ra.get_log(t.ra, paths, start, end, limit,
146 svn.ra.get_log(t.ra, paths, start, end, limit,
147 discover_changed_paths,
147 discover_changed_paths,
148 strict_node_history,
148 strict_node_history,
149 receiver)
149 receiver)
150 except IOError:
150 except IOError:
151 # Caller may interrupt the iteration
151 # Caller may interrupt the iteration
152 pickle.dump(None, fp, protocol)
152 pickle.dump(None, fp, protocol)
153 except Exception as inst:
153 except Exception as inst:
154 pickle.dump(str(inst), fp, protocol)
154 pickle.dump(str(inst), fp, protocol)
155 else:
155 else:
156 pickle.dump(None, fp, protocol)
156 pickle.dump(None, fp, protocol)
157 fp.close()
157 fp.close()
158 # With large history, cleanup process goes crazy and suddenly
158 # With large history, cleanup process goes crazy and suddenly
159 # consumes *huge* amount of memory. The output file being closed,
159 # consumes *huge* amount of memory. The output file being closed,
160 # there is no need for clean termination.
160 # there is no need for clean termination.
161 os._exit(0)
161 os._exit(0)
162
162
163 def debugsvnlog(ui, **opts):
163 def debugsvnlog(ui, **opts):
164 """Fetch SVN log in a subprocess and channel them back to parent to
164 """Fetch SVN log in a subprocess and channel them back to parent to
165 avoid memory collection issues.
165 avoid memory collection issues.
166 """
166 """
167 if svn is None:
167 if svn is None:
168 raise error.Abort(_('debugsvnlog could not load Subversion python '
168 raise error.Abort(_('debugsvnlog could not load Subversion python '
169 'bindings'))
169 'bindings'))
170
170
171 util.setbinary(sys.stdin)
171 util.setbinary(sys.stdin)
172 util.setbinary(sys.stdout)
172 util.setbinary(sys.stdout)
173 args = decodeargs(sys.stdin.read())
173 args = decodeargs(sys.stdin.read())
174 get_log_child(sys.stdout, *args)
174 get_log_child(sys.stdout, *args)
175
175
176 class logstream(object):
176 class logstream(object):
177 """Interruptible revision log iterator."""
177 """Interruptible revision log iterator."""
178 def __init__(self, stdout):
178 def __init__(self, stdout):
179 self._stdout = stdout
179 self._stdout = stdout
180
180
181 def __iter__(self):
181 def __iter__(self):
182 while True:
182 while True:
183 try:
183 try:
184 entry = pickle.load(self._stdout)
184 entry = pickle.load(self._stdout)
185 except EOFError:
185 except EOFError:
186 raise error.Abort(_('Mercurial failed to run itself, check'
186 raise error.Abort(_('Mercurial failed to run itself, check'
187 ' hg executable is in PATH'))
187 ' hg executable is in PATH'))
188 try:
188 try:
189 orig_paths, revnum, author, date, message = entry
189 orig_paths, revnum, author, date, message = entry
190 except (TypeError, ValueError):
190 except (TypeError, ValueError):
191 if entry is None:
191 if entry is None:
192 break
192 break
193 raise error.Abort(_("log stream exception '%s'") % entry)
193 raise error.Abort(_("log stream exception '%s'") % entry)
194 yield entry
194 yield entry
195
195
196 def close(self):
196 def close(self):
197 if self._stdout:
197 if self._stdout:
198 self._stdout.close()
198 self._stdout.close()
199 self._stdout = None
199 self._stdout = None
200
200
201 class directlogstream(list):
201 class directlogstream(list):
202 """Direct revision log iterator.
202 """Direct revision log iterator.
203 This can be used for debugging and development but it will probably leak
203 This can be used for debugging and development but it will probably leak
204 memory and is not suitable for real conversions."""
204 memory and is not suitable for real conversions."""
205 def __init__(self, url, paths, start, end, limit=0,
205 def __init__(self, url, paths, start, end, limit=0,
206 discover_changed_paths=True, strict_node_history=False):
206 discover_changed_paths=True, strict_node_history=False):
207
207
208 def receiver(orig_paths, revnum, author, date, message, pool):
208 def receiver(orig_paths, revnum, author, date, message, pool):
209 paths = {}
209 paths = {}
210 if orig_paths is not None:
210 if orig_paths is not None:
211 for k, v in orig_paths.iteritems():
211 for k, v in orig_paths.iteritems():
212 paths[k] = changedpath(v)
212 paths[k] = changedpath(v)
213 self.append((paths, revnum, author, date, message))
213 self.append((paths, revnum, author, date, message))
214
214
215 # Use an ra of our own so that our parent can consume
215 # Use an ra of our own so that our parent can consume
216 # our results without confusing the server.
216 # our results without confusing the server.
217 t = transport.SvnRaTransport(url=url)
217 t = transport.SvnRaTransport(url=url)
218 svn.ra.get_log(t.ra, paths, start, end, limit,
218 svn.ra.get_log(t.ra, paths, start, end, limit,
219 discover_changed_paths,
219 discover_changed_paths,
220 strict_node_history,
220 strict_node_history,
221 receiver)
221 receiver)
222
222
223 def close(self):
223 def close(self):
224 pass
224 pass
225
225
226 # Check to see if the given path is a local Subversion repo. Verify this by
226 # Check to see if the given path is a local Subversion repo. Verify this by
227 # looking for several svn-specific files and directories in the given
227 # looking for several svn-specific files and directories in the given
228 # directory.
228 # directory.
229 def filecheck(ui, path, proto):
229 def filecheck(ui, path, proto):
230 for x in ('locks', 'hooks', 'format', 'db'):
230 for x in ('locks', 'hooks', 'format', 'db'):
231 if not os.path.exists(os.path.join(path, x)):
231 if not os.path.exists(os.path.join(path, x)):
232 return False
232 return False
233 return True
233 return True
234
234
235 # Check to see if a given path is the root of an svn repo over http. We verify
235 # Check to see if a given path is the root of an svn repo over http. We verify
236 # this by requesting a version-controlled URL we know can't exist and looking
236 # this by requesting a version-controlled URL we know can't exist and looking
237 # for the svn-specific "not found" XML.
237 # for the svn-specific "not found" XML.
238 def httpcheck(ui, path, proto):
238 def httpcheck(ui, path, proto):
239 try:
239 try:
240 opener = urllib2.build_opener()
240 opener = urllib2.build_opener()
241 rsp = opener.open('%s://%s/!svn/ver/0/.svn' % (proto, path))
241 rsp = opener.open('%s://%s/!svn/ver/0/.svn' % (proto, path))
242 data = rsp.read()
242 data = rsp.read()
243 except urllib2.HTTPError as inst:
243 except urllib2.HTTPError as inst:
244 if inst.code != 404:
244 if inst.code != 404:
245 # Except for 404 we cannot know for sure this is not an svn repo
245 # Except for 404 we cannot know for sure this is not an svn repo
246 ui.warn(_('svn: cannot probe remote repository, assume it could '
246 ui.warn(_('svn: cannot probe remote repository, assume it could '
247 'be a subversion repository. Use --source-type if you '
247 'be a subversion repository. Use --source-type if you '
248 'know better.\n'))
248 'know better.\n'))
249 return True
249 return True
250 data = inst.fp.read()
250 data = inst.fp.read()
251 except Exception:
251 except Exception:
252 # Could be urllib2.URLError if the URL is invalid or anything else.
252 # Could be urllib2.URLError if the URL is invalid or anything else.
253 return False
253 return False
254 return '<m:human-readable errcode="160013">' in data
254 return '<m:human-readable errcode="160013">' in data
255
255
256 protomap = {'http': httpcheck,
256 protomap = {'http': httpcheck,
257 'https': httpcheck,
257 'https': httpcheck,
258 'file': filecheck,
258 'file': filecheck,
259 }
259 }
260 def issvnurl(ui, url):
260 def issvnurl(ui, url):
261 try:
261 try:
262 proto, path = url.split('://', 1)
262 proto, path = url.split('://', 1)
263 if proto == 'file':
263 if proto == 'file':
264 if (os.name == 'nt' and path[:1] == '/' and path[1:2].isalpha()
264 if (os.name == 'nt' and path[:1] == '/' and path[1:2].isalpha()
265 and path[2:6].lower() == '%3a/'):
265 and path[2:6].lower() == '%3a/'):
266 path = path[:2] + ':/' + path[6:]
266 path = path[:2] + ':/' + path[6:]
267 path = urllib.url2pathname(path)
267 path = urllib.url2pathname(path)
268 except ValueError:
268 except ValueError:
269 proto = 'file'
269 proto = 'file'
270 path = os.path.abspath(url)
270 path = os.path.abspath(url)
271 if proto == 'file':
271 if proto == 'file':
272 path = util.pconvert(path)
272 path = util.pconvert(path)
273 check = protomap.get(proto, lambda *args: False)
273 check = protomap.get(proto, lambda *args: False)
274 while '/' in path:
274 while '/' in path:
275 if check(ui, path, proto):
275 if check(ui, path, proto):
276 return True
276 return True
277 path = path.rsplit('/', 1)[0]
277 path = path.rsplit('/', 1)[0]
278 return False
278 return False
279
279
280 # SVN conversion code stolen from bzr-svn and tailor
280 # SVN conversion code stolen from bzr-svn and tailor
281 #
281 #
282 # Subversion looks like a versioned filesystem, branches structures
282 # Subversion looks like a versioned filesystem, branches structures
283 # are defined by conventions and not enforced by the tool. First,
283 # are defined by conventions and not enforced by the tool. First,
284 # we define the potential branches (modules) as "trunk" and "branches"
284 # we define the potential branches (modules) as "trunk" and "branches"
285 # children directories. Revisions are then identified by their
285 # children directories. Revisions are then identified by their
286 # module and revision number (and a repository identifier).
286 # module and revision number (and a repository identifier).
287 #
287 #
288 # The revision graph is really a tree (or a forest). By default, a
288 # The revision graph is really a tree (or a forest). By default, a
289 # revision parent is the previous revision in the same module. If the
289 # revision parent is the previous revision in the same module. If the
290 # module directory is copied/moved from another module then the
290 # module directory is copied/moved from another module then the
291 # revision is the module root and its parent the source revision in
291 # revision is the module root and its parent the source revision in
292 # the parent module. A revision has at most one parent.
292 # the parent module. A revision has at most one parent.
293 #
293 #
294 class svn_source(converter_source):
294 class svn_source(converter_source):
295 def __init__(self, ui, url, revs=None):
295 def __init__(self, ui, url, revs=None):
296 super(svn_source, self).__init__(ui, url, revs=revs)
296 super(svn_source, self).__init__(ui, url, revs=revs)
297
297
298 if not (url.startswith('svn://') or url.startswith('svn+ssh://') or
298 if not (url.startswith('svn://') or url.startswith('svn+ssh://') or
299 (os.path.exists(url) and
299 (os.path.exists(url) and
300 os.path.exists(os.path.join(url, '.svn'))) or
300 os.path.exists(os.path.join(url, '.svn'))) or
301 issvnurl(ui, url)):
301 issvnurl(ui, url)):
302 raise NoRepo(_("%s does not look like a Subversion repository")
302 raise NoRepo(_("%s does not look like a Subversion repository")
303 % url)
303 % url)
304 if svn is None:
304 if svn is None:
305 raise MissingTool(_('could not load Subversion python bindings'))
305 raise MissingTool(_('could not load Subversion python bindings'))
306
306
307 try:
307 try:
308 version = svn.core.SVN_VER_MAJOR, svn.core.SVN_VER_MINOR
308 version = svn.core.SVN_VER_MAJOR, svn.core.SVN_VER_MINOR
309 if version < (1, 4):
309 if version < (1, 4):
310 raise MissingTool(_('Subversion python bindings %d.%d found, '
310 raise MissingTool(_('Subversion python bindings %d.%d found, '
311 '1.4 or later required') % version)
311 '1.4 or later required') % version)
312 except AttributeError:
312 except AttributeError:
313 raise MissingTool(_('Subversion python bindings are too old, 1.4 '
313 raise MissingTool(_('Subversion python bindings are too old, 1.4 '
314 'or later required'))
314 'or later required'))
315
315
316 self.lastrevs = {}
316 self.lastrevs = {}
317
317
318 latest = None
318 latest = None
319 try:
319 try:
320 # Support file://path@rev syntax. Useful e.g. to convert
320 # Support file://path@rev syntax. Useful e.g. to convert
321 # deleted branches.
321 # deleted branches.
322 at = url.rfind('@')
322 at = url.rfind('@')
323 if at >= 0:
323 if at >= 0:
324 latest = int(url[at + 1:])
324 latest = int(url[at + 1:])
325 url = url[:at]
325 url = url[:at]
326 except ValueError:
326 except ValueError:
327 pass
327 pass
328 self.url = geturl(url)
328 self.url = geturl(url)
329 self.encoding = 'UTF-8' # Subversion is always nominal UTF-8
329 self.encoding = 'UTF-8' # Subversion is always nominal UTF-8
330 try:
330 try:
331 self.transport = transport.SvnRaTransport(url=self.url)
331 self.transport = transport.SvnRaTransport(url=self.url)
332 self.ra = self.transport.ra
332 self.ra = self.transport.ra
333 self.ctx = self.transport.client
333 self.ctx = self.transport.client
334 self.baseurl = svn.ra.get_repos_root(self.ra)
334 self.baseurl = svn.ra.get_repos_root(self.ra)
335 # Module is either empty or a repository path starting with
335 # Module is either empty or a repository path starting with
336 # a slash and not ending with a slash.
336 # a slash and not ending with a slash.
337 self.module = urllib.unquote(self.url[len(self.baseurl):])
337 self.module = urllib.unquote(self.url[len(self.baseurl):])
338 self.prevmodule = None
338 self.prevmodule = None
339 self.rootmodule = self.module
339 self.rootmodule = self.module
340 self.commits = {}
340 self.commits = {}
341 self.paths = {}
341 self.paths = {}
342 self.uuid = svn.ra.get_uuid(self.ra)
342 self.uuid = svn.ra.get_uuid(self.ra)
343 except SubversionException:
343 except SubversionException:
344 ui.traceback()
344 ui.traceback()
345 svnversion = '%d.%d.%d' % (svn.core.SVN_VER_MAJOR,
345 svnversion = '%d.%d.%d' % (svn.core.SVN_VER_MAJOR,
346 svn.core.SVN_VER_MINOR,
346 svn.core.SVN_VER_MINOR,
347 svn.core.SVN_VER_MICRO)
347 svn.core.SVN_VER_MICRO)
348 raise NoRepo(_("%s does not look like a Subversion repository "
348 raise NoRepo(_("%s does not look like a Subversion repository "
349 "to libsvn version %s")
349 "to libsvn version %s")
350 % (self.url, svnversion))
350 % (self.url, svnversion))
351
351
352 if revs:
352 if revs:
353 if len(revs) > 1:
353 if len(revs) > 1:
354 raise error.Abort(_('subversion source does not support '
354 raise error.Abort(_('subversion source does not support '
355 'specifying multiple revisions'))
355 'specifying multiple revisions'))
356 try:
356 try:
357 latest = int(revs[0])
357 latest = int(revs[0])
358 except ValueError:
358 except ValueError:
359 raise error.Abort(_('svn: revision %s is not an integer') %
359 raise error.Abort(_('svn: revision %s is not an integer') %
360 revs[0])
360 revs[0])
361
361
362 self.trunkname = self.ui.config('convert', 'svn.trunk',
362 self.trunkname = self.ui.config('convert', 'svn.trunk',
363 'trunk').strip('/')
363 'trunk').strip('/')
364 self.startrev = self.ui.config('convert', 'svn.startrev', default=0)
364 self.startrev = self.ui.config('convert', 'svn.startrev', default=0)
365 try:
365 try:
366 self.startrev = int(self.startrev)
366 self.startrev = int(self.startrev)
367 if self.startrev < 0:
367 if self.startrev < 0:
368 self.startrev = 0
368 self.startrev = 0
369 except ValueError:
369 except ValueError:
370 raise error.Abort(_('svn: start revision %s is not an integer')
370 raise error.Abort(_('svn: start revision %s is not an integer')
371 % self.startrev)
371 % self.startrev)
372
372
373 try:
373 try:
374 self.head = self.latest(self.module, latest)
374 self.head = self.latest(self.module, latest)
375 except SvnPathNotFound:
375 except SvnPathNotFound:
376 self.head = None
376 self.head = None
377 if not self.head:
377 if not self.head:
378 raise error.Abort(_('no revision found in module %s')
378 raise error.Abort(_('no revision found in module %s')
379 % self.module)
379 % self.module)
380 self.last_changed = self.revnum(self.head)
380 self.last_changed = self.revnum(self.head)
381
381
382 self._changescache = (None, None)
382 self._changescache = (None, None)
383
383
384 if os.path.exists(os.path.join(url, '.svn/entries')):
384 if os.path.exists(os.path.join(url, '.svn/entries')):
385 self.wc = url
385 self.wc = url
386 else:
386 else:
387 self.wc = None
387 self.wc = None
388 self.convertfp = None
388 self.convertfp = None
389
389
390 def setrevmap(self, revmap):
390 def setrevmap(self, revmap):
391 lastrevs = {}
391 lastrevs = {}
392 for revid in revmap.iterkeys():
392 for revid in revmap.iterkeys():
393 uuid, module, revnum = revsplit(revid)
393 uuid, module, revnum = revsplit(revid)
394 lastrevnum = lastrevs.setdefault(module, revnum)
394 lastrevnum = lastrevs.setdefault(module, revnum)
395 if revnum > lastrevnum:
395 if revnum > lastrevnum:
396 lastrevs[module] = revnum
396 lastrevs[module] = revnum
397 self.lastrevs = lastrevs
397 self.lastrevs = lastrevs
398
398
399 def exists(self, path, optrev):
399 def exists(self, path, optrev):
400 try:
400 try:
401 svn.client.ls(self.url.rstrip('/') + '/' + quote(path),
401 svn.client.ls(self.url.rstrip('/') + '/' + quote(path),
402 optrev, False, self.ctx)
402 optrev, False, self.ctx)
403 return True
403 return True
404 except SubversionException:
404 except SubversionException:
405 return False
405 return False
406
406
407 def getheads(self):
407 def getheads(self):
408
408
409 def isdir(path, revnum):
409 def isdir(path, revnum):
410 kind = self._checkpath(path, revnum)
410 kind = self._checkpath(path, revnum)
411 return kind == svn.core.svn_node_dir
411 return kind == svn.core.svn_node_dir
412
412
413 def getcfgpath(name, rev):
413 def getcfgpath(name, rev):
414 cfgpath = self.ui.config('convert', 'svn.' + name)
414 cfgpath = self.ui.config('convert', 'svn.' + name)
415 if cfgpath is not None and cfgpath.strip() == '':
415 if cfgpath is not None and cfgpath.strip() == '':
416 return None
416 return None
417 path = (cfgpath or name).strip('/')
417 path = (cfgpath or name).strip('/')
418 if not self.exists(path, rev):
418 if not self.exists(path, rev):
419 if self.module.endswith(path) and name == 'trunk':
419 if self.module.endswith(path) and name == 'trunk':
420 # we are converting from inside this directory
420 # we are converting from inside this directory
421 return None
421 return None
422 if cfgpath:
422 if cfgpath:
423 raise error.Abort(_('expected %s to be at %r, but not found'
423 raise error.Abort(_('expected %s to be at %r, but not found'
424 ) % (name, path))
424 ) % (name, path))
425 return None
425 return None
426 self.ui.note(_('found %s at %r\n') % (name, path))
426 self.ui.note(_('found %s at %r\n') % (name, path))
427 return path
427 return path
428
428
429 rev = optrev(self.last_changed)
429 rev = optrev(self.last_changed)
430 oldmodule = ''
430 oldmodule = ''
431 trunk = getcfgpath('trunk', rev)
431 trunk = getcfgpath('trunk', rev)
432 self.tags = getcfgpath('tags', rev)
432 self.tags = getcfgpath('tags', rev)
433 branches = getcfgpath('branches', rev)
433 branches = getcfgpath('branches', rev)
434
434
435 # If the project has a trunk or branches, we will extract heads
435 # If the project has a trunk or branches, we will extract heads
436 # from them. We keep the project root otherwise.
436 # from them. We keep the project root otherwise.
437 if trunk:
437 if trunk:
438 oldmodule = self.module or ''
438 oldmodule = self.module or ''
439 self.module += '/' + trunk
439 self.module += '/' + trunk
440 self.head = self.latest(self.module, self.last_changed)
440 self.head = self.latest(self.module, self.last_changed)
441 if not self.head:
441 if not self.head:
442 raise error.Abort(_('no revision found in module %s')
442 raise error.Abort(_('no revision found in module %s')
443 % self.module)
443 % self.module)
444
444
445 # First head in the list is the module's head
445 # First head in the list is the module's head
446 self.heads = [self.head]
446 self.heads = [self.head]
447 if self.tags is not None:
447 if self.tags is not None:
448 self.tags = '%s/%s' % (oldmodule , (self.tags or 'tags'))
448 self.tags = '%s/%s' % (oldmodule , (self.tags or 'tags'))
449
449
450 # Check if branches bring a few more heads to the list
450 # Check if branches bring a few more heads to the list
451 if branches:
451 if branches:
452 rpath = self.url.strip('/')
452 rpath = self.url.strip('/')
453 branchnames = svn.client.ls(rpath + '/' + quote(branches),
453 branchnames = svn.client.ls(rpath + '/' + quote(branches),
454 rev, False, self.ctx)
454 rev, False, self.ctx)
455 for branch in sorted(branchnames):
455 for branch in sorted(branchnames):
456 module = '%s/%s/%s' % (oldmodule, branches, branch)
456 module = '%s/%s/%s' % (oldmodule, branches, branch)
457 if not isdir(module, self.last_changed):
457 if not isdir(module, self.last_changed):
458 continue
458 continue
459 brevid = self.latest(module, self.last_changed)
459 brevid = self.latest(module, self.last_changed)
460 if not brevid:
460 if not brevid:
461 self.ui.note(_('ignoring empty branch %s\n') % branch)
461 self.ui.note(_('ignoring empty branch %s\n') % branch)
462 continue
462 continue
463 self.ui.note(_('found branch %s at %d\n') %
463 self.ui.note(_('found branch %s at %d\n') %
464 (branch, self.revnum(brevid)))
464 (branch, self.revnum(brevid)))
465 self.heads.append(brevid)
465 self.heads.append(brevid)
466
466
467 if self.startrev and self.heads:
467 if self.startrev and self.heads:
468 if len(self.heads) > 1:
468 if len(self.heads) > 1:
469 raise error.Abort(_('svn: start revision is not supported '
469 raise error.Abort(_('svn: start revision is not supported '
470 'with more than one branch'))
470 'with more than one branch'))
471 revnum = self.revnum(self.heads[0])
471 revnum = self.revnum(self.heads[0])
472 if revnum < self.startrev:
472 if revnum < self.startrev:
473 raise error.Abort(
473 raise error.Abort(
474 _('svn: no revision found after start revision %d')
474 _('svn: no revision found after start revision %d')
475 % self.startrev)
475 % self.startrev)
476
476
477 return self.heads
477 return self.heads
478
478
479 def _getchanges(self, rev, full):
479 def _getchanges(self, rev, full):
480 (paths, parents) = self.paths[rev]
480 (paths, parents) = self.paths[rev]
481 copies = {}
481 copies = {}
482 if parents:
482 if parents:
483 files, self.removed, copies = self.expandpaths(rev, paths, parents)
483 files, self.removed, copies = self.expandpaths(rev, paths, parents)
484 if full or not parents:
484 if full or not parents:
485 # Perform a full checkout on roots
485 # Perform a full checkout on roots
486 uuid, module, revnum = revsplit(rev)
486 uuid, module, revnum = revsplit(rev)
487 entries = svn.client.ls(self.baseurl + quote(module),
487 entries = svn.client.ls(self.baseurl + quote(module),
488 optrev(revnum), True, self.ctx)
488 optrev(revnum), True, self.ctx)
489 files = [n for n, e in entries.iteritems()
489 files = [n for n, e in entries.iteritems()
490 if e.kind == svn.core.svn_node_file]
490 if e.kind == svn.core.svn_node_file]
491 self.removed = set()
491 self.removed = set()
492
492
493 files.sort()
493 files.sort()
494 files = zip(files, [rev] * len(files))
494 files = zip(files, [rev] * len(files))
495 return (files, copies)
495 return (files, copies)
496
496
497 def getchanges(self, rev, full):
497 def getchanges(self, rev, full):
498 # reuse cache from getchangedfiles
498 # reuse cache from getchangedfiles
499 if self._changescache[0] == rev and not full:
499 if self._changescache[0] == rev and not full:
500 (files, copies) = self._changescache[1]
500 (files, copies) = self._changescache[1]
501 else:
501 else:
502 (files, copies) = self._getchanges(rev, full)
502 (files, copies) = self._getchanges(rev, full)
503 # caller caches the result, so free it here to release memory
503 # caller caches the result, so free it here to release memory
504 del self.paths[rev]
504 del self.paths[rev]
505 return (files, copies, set())
505 return (files, copies, set())
506
506
507 def getchangedfiles(self, rev, i):
507 def getchangedfiles(self, rev, i):
508 # called from filemap - cache computed values for reuse in getchanges
508 # called from filemap - cache computed values for reuse in getchanges
509 (files, copies) = self._getchanges(rev, False)
509 (files, copies) = self._getchanges(rev, False)
510 self._changescache = (rev, (files, copies))
510 self._changescache = (rev, (files, copies))
511 return [f[0] for f in files]
511 return [f[0] for f in files]
512
512
513 def getcommit(self, rev):
513 def getcommit(self, rev):
514 if rev not in self.commits:
514 if rev not in self.commits:
515 uuid, module, revnum = revsplit(rev)
515 uuid, module, revnum = revsplit(rev)
516 self.module = module
516 self.module = module
517 self.reparent(module)
517 self.reparent(module)
518 # We assume that:
518 # We assume that:
519 # - requests for revisions after "stop" come from the
519 # - requests for revisions after "stop" come from the
520 # revision graph backward traversal. Cache all of them
520 # revision graph backward traversal. Cache all of them
521 # down to stop, they will be used eventually.
521 # down to stop, they will be used eventually.
522 # - requests for revisions before "stop" come to get
522 # - requests for revisions before "stop" come to get
523 # isolated branches parents. Just fetch what is needed.
523 # isolated branches parents. Just fetch what is needed.
524 stop = self.lastrevs.get(module, 0)
524 stop = self.lastrevs.get(module, 0)
525 if revnum < stop:
525 if revnum < stop:
526 stop = revnum + 1
526 stop = revnum + 1
527 self._fetch_revisions(revnum, stop)
527 self._fetch_revisions(revnum, stop)
528 if rev not in self.commits:
528 if rev not in self.commits:
529 raise error.Abort(_('svn: revision %s not found') % revnum)
529 raise error.Abort(_('svn: revision %s not found') % revnum)
530 revcommit = self.commits[rev]
530 revcommit = self.commits[rev]
531 # caller caches the result, so free it here to release memory
531 # caller caches the result, so free it here to release memory
532 del self.commits[rev]
532 del self.commits[rev]
533 return revcommit
533 return revcommit
534
534
535 def checkrevformat(self, revstr, mapname='splicemap'):
535 def checkrevformat(self, revstr, mapname='splicemap'):
536 """ fails if revision format does not match the correct format"""
536 """ fails if revision format does not match the correct format"""
537 if not re.match(r'svn:[0-9a-f]{8,8}-[0-9a-f]{4,4}-'
537 if not re.match(r'svn:[0-9a-f]{8,8}-[0-9a-f]{4,4}-'
538 '[0-9a-f]{4,4}-[0-9a-f]{4,4}-[0-9a-f]'
538 '[0-9a-f]{4,4}-[0-9a-f]{4,4}-[0-9a-f]'
539 '{12,12}(.*)\@[0-9]+$',revstr):
539 '{12,12}(.*)\@[0-9]+$',revstr):
540 raise error.Abort(_('%s entry %s is not a valid revision'
540 raise error.Abort(_('%s entry %s is not a valid revision'
541 ' identifier') % (mapname, revstr))
541 ' identifier') % (mapname, revstr))
542
542
543 def numcommits(self):
543 def numcommits(self):
544 return int(self.head.rsplit('@', 1)[1]) - self.startrev
544 return int(self.head.rsplit('@', 1)[1]) - self.startrev
545
545
546 def gettags(self):
546 def gettags(self):
547 tags = {}
547 tags = {}
548 if self.tags is None:
548 if self.tags is None:
549 return tags
549 return tags
550
550
551 # svn tags are just a convention, project branches left in a
551 # svn tags are just a convention, project branches left in a
552 # 'tags' directory. There is no other relationship than
552 # 'tags' directory. There is no other relationship than
553 # ancestry, which is expensive to discover and makes them hard
553 # ancestry, which is expensive to discover and makes them hard
554 # to update incrementally. Worse, past revisions may be
554 # to update incrementally. Worse, past revisions may be
555 # referenced by tags far away in the future, requiring a deep
555 # referenced by tags far away in the future, requiring a deep
556 # history traversal on every calculation. Current code
556 # history traversal on every calculation. Current code
557 # performs a single backward traversal, tracking moves within
557 # performs a single backward traversal, tracking moves within
558 # the tags directory (tag renaming) and recording a new tag
558 # the tags directory (tag renaming) and recording a new tag
559 # everytime a project is copied from outside the tags
559 # everytime a project is copied from outside the tags
560 # directory. It also lists deleted tags, this behaviour may
560 # directory. It also lists deleted tags, this behaviour may
561 # change in the future.
561 # change in the future.
562 pendings = []
562 pendings = []
563 tagspath = self.tags
563 tagspath = self.tags
564 start = svn.ra.get_latest_revnum(self.ra)
564 start = svn.ra.get_latest_revnum(self.ra)
565 stream = self._getlog([self.tags], start, self.startrev)
565 stream = self._getlog([self.tags], start, self.startrev)
566 try:
566 try:
567 for entry in stream:
567 for entry in stream:
568 origpaths, revnum, author, date, message = entry
568 origpaths, revnum, author, date, message = entry
569 if not origpaths:
569 if not origpaths:
570 origpaths = []
570 origpaths = []
571 copies = [(e.copyfrom_path, e.copyfrom_rev, p) for p, e
571 copies = [(e.copyfrom_path, e.copyfrom_rev, p) for p, e
572 in origpaths.iteritems() if e.copyfrom_path]
572 in origpaths.iteritems() if e.copyfrom_path]
573 # Apply moves/copies from more specific to general
573 # Apply moves/copies from more specific to general
574 copies.sort(reverse=True)
574 copies.sort(reverse=True)
575
575
576 srctagspath = tagspath
576 srctagspath = tagspath
577 if copies and copies[-1][2] == tagspath:
577 if copies and copies[-1][2] == tagspath:
578 # Track tags directory moves
578 # Track tags directory moves
579 srctagspath = copies.pop()[0]
579 srctagspath = copies.pop()[0]
580
580
581 for source, sourcerev, dest in copies:
581 for source, sourcerev, dest in copies:
582 if not dest.startswith(tagspath + '/'):
582 if not dest.startswith(tagspath + '/'):
583 continue
583 continue
584 for tag in pendings:
584 for tag in pendings:
585 if tag[0].startswith(dest):
585 if tag[0].startswith(dest):
586 tagpath = source + tag[0][len(dest):]
586 tagpath = source + tag[0][len(dest):]
587 tag[:2] = [tagpath, sourcerev]
587 tag[:2] = [tagpath, sourcerev]
588 break
588 break
589 else:
589 else:
590 pendings.append([source, sourcerev, dest])
590 pendings.append([source, sourcerev, dest])
591
591
592 # Filter out tags with children coming from different
592 # Filter out tags with children coming from different
593 # parts of the repository like:
593 # parts of the repository like:
594 # /tags/tag.1 (from /trunk:10)
594 # /tags/tag.1 (from /trunk:10)
595 # /tags/tag.1/foo (from /branches/foo:12)
595 # /tags/tag.1/foo (from /branches/foo:12)
596 # Here/tags/tag.1 discarded as well as its children.
596 # Here/tags/tag.1 discarded as well as its children.
597 # It happens with tools like cvs2svn. Such tags cannot
597 # It happens with tools like cvs2svn. Such tags cannot
598 # be represented in mercurial.
598 # be represented in mercurial.
599 addeds = dict((p, e.copyfrom_path) for p, e
599 addeds = dict((p, e.copyfrom_path) for p, e
600 in origpaths.iteritems()
600 in origpaths.iteritems()
601 if e.action == 'A' and e.copyfrom_path)
601 if e.action == 'A' and e.copyfrom_path)
602 badroots = set()
602 badroots = set()
603 for destroot in addeds:
603 for destroot in addeds:
604 for source, sourcerev, dest in pendings:
604 for source, sourcerev, dest in pendings:
605 if (not dest.startswith(destroot + '/')
605 if (not dest.startswith(destroot + '/')
606 or source.startswith(addeds[destroot] + '/')):
606 or source.startswith(addeds[destroot] + '/')):
607 continue
607 continue
608 badroots.add(destroot)
608 badroots.add(destroot)
609 break
609 break
610
610
611 for badroot in badroots:
611 for badroot in badroots:
612 pendings = [p for p in pendings if p[2] != badroot
612 pendings = [p for p in pendings if p[2] != badroot
613 and not p[2].startswith(badroot + '/')]
613 and not p[2].startswith(badroot + '/')]
614
614
615 # Tell tag renamings from tag creations
615 # Tell tag renamings from tag creations
616 renamings = []
616 renamings = []
617 for source, sourcerev, dest in pendings:
617 for source, sourcerev, dest in pendings:
618 tagname = dest.split('/')[-1]
618 tagname = dest.split('/')[-1]
619 if source.startswith(srctagspath):
619 if source.startswith(srctagspath):
620 renamings.append([source, sourcerev, tagname])
620 renamings.append([source, sourcerev, tagname])
621 continue
621 continue
622 if tagname in tags:
622 if tagname in tags:
623 # Keep the latest tag value
623 # Keep the latest tag value
624 continue
624 continue
625 # From revision may be fake, get one with changes
625 # From revision may be fake, get one with changes
626 try:
626 try:
627 tagid = self.latest(source, sourcerev)
627 tagid = self.latest(source, sourcerev)
628 if tagid and tagname not in tags:
628 if tagid and tagname not in tags:
629 tags[tagname] = tagid
629 tags[tagname] = tagid
630 except SvnPathNotFound:
630 except SvnPathNotFound:
631 # It happens when we are following directories
631 # It happens when we are following directories
632 # we assumed were copied with their parents
632 # we assumed were copied with their parents
633 # but were really created in the tag
633 # but were really created in the tag
634 # directory.
634 # directory.
635 pass
635 pass
636 pendings = renamings
636 pendings = renamings
637 tagspath = srctagspath
637 tagspath = srctagspath
638 finally:
638 finally:
639 stream.close()
639 stream.close()
640 return tags
640 return tags
641
641
642 def converted(self, rev, destrev):
642 def converted(self, rev, destrev):
643 if not self.wc:
643 if not self.wc:
644 return
644 return
645 if self.convertfp is None:
645 if self.convertfp is None:
646 self.convertfp = open(os.path.join(self.wc, '.svn', 'hg-shamap'),
646 self.convertfp = open(os.path.join(self.wc, '.svn', 'hg-shamap'),
647 'a')
647 'a')
648 self.convertfp.write('%s %d\n' % (destrev, self.revnum(rev)))
648 self.convertfp.write('%s %d\n' % (destrev, self.revnum(rev)))
649 self.convertfp.flush()
649 self.convertfp.flush()
650
650
651 def revid(self, revnum, module=None):
651 def revid(self, revnum, module=None):
652 return 'svn:%s%s@%s' % (self.uuid, module or self.module, revnum)
652 return 'svn:%s%s@%s' % (self.uuid, module or self.module, revnum)
653
653
654 def revnum(self, rev):
654 def revnum(self, rev):
655 return int(rev.split('@')[-1])
655 return int(rev.split('@')[-1])
656
656
657 def latest(self, path, stop=None):
657 def latest(self, path, stop=None):
658 """Find the latest revid affecting path, up to stop revision
658 """Find the latest revid affecting path, up to stop revision
659 number. If stop is None, default to repository latest
659 number. If stop is None, default to repository latest
660 revision. It may return a revision in a different module,
660 revision. It may return a revision in a different module,
661 since a branch may be moved without a change being
661 since a branch may be moved without a change being
662 reported. Return None if computed module does not belong to
662 reported. Return None if computed module does not belong to
663 rootmodule subtree.
663 rootmodule subtree.
664 """
664 """
665 def findchanges(path, start, stop=None):
665 def findchanges(path, start, stop=None):
666 stream = self._getlog([path], start, stop or 1)
666 stream = self._getlog([path], start, stop or 1)
667 try:
667 try:
668 for entry in stream:
668 for entry in stream:
669 paths, revnum, author, date, message = entry
669 paths, revnum, author, date, message = entry
670 if stop is None and paths:
670 if stop is None and paths:
671 # We do not know the latest changed revision,
671 # We do not know the latest changed revision,
672 # keep the first one with changed paths.
672 # keep the first one with changed paths.
673 break
673 break
674 if revnum <= stop:
674 if revnum <= stop:
675 break
675 break
676
676
677 for p in paths:
677 for p in paths:
678 if (not path.startswith(p) or
678 if (not path.startswith(p) or
679 not paths[p].copyfrom_path):
679 not paths[p].copyfrom_path):
680 continue
680 continue
681 newpath = paths[p].copyfrom_path + path[len(p):]
681 newpath = paths[p].copyfrom_path + path[len(p):]
682 self.ui.debug("branch renamed from %s to %s at %d\n" %
682 self.ui.debug("branch renamed from %s to %s at %d\n" %
683 (path, newpath, revnum))
683 (path, newpath, revnum))
684 path = newpath
684 path = newpath
685 break
685 break
686 if not paths:
686 if not paths:
687 revnum = None
687 revnum = None
688 return revnum, path
688 return revnum, path
689 finally:
689 finally:
690 stream.close()
690 stream.close()
691
691
692 if not path.startswith(self.rootmodule):
692 if not path.startswith(self.rootmodule):
693 # Requests on foreign branches may be forbidden at server level
693 # Requests on foreign branches may be forbidden at server level
694 self.ui.debug('ignoring foreign branch %r\n' % path)
694 self.ui.debug('ignoring foreign branch %r\n' % path)
695 return None
695 return None
696
696
697 if stop is None:
697 if stop is None:
698 stop = svn.ra.get_latest_revnum(self.ra)
698 stop = svn.ra.get_latest_revnum(self.ra)
699 try:
699 try:
700 prevmodule = self.reparent('')
700 prevmodule = self.reparent('')
701 dirent = svn.ra.stat(self.ra, path.strip('/'), stop)
701 dirent = svn.ra.stat(self.ra, path.strip('/'), stop)
702 self.reparent(prevmodule)
702 self.reparent(prevmodule)
703 except SubversionException:
703 except SubversionException:
704 dirent = None
704 dirent = None
705 if not dirent:
705 if not dirent:
706 raise SvnPathNotFound(_('%s not found up to revision %d')
706 raise SvnPathNotFound(_('%s not found up to revision %d')
707 % (path, stop))
707 % (path, stop))
708
708
709 # stat() gives us the previous revision on this line of
709 # stat() gives us the previous revision on this line of
710 # development, but it might be in *another module*. Fetch the
710 # development, but it might be in *another module*. Fetch the
711 # log and detect renames down to the latest revision.
711 # log and detect renames down to the latest revision.
712 revnum, realpath = findchanges(path, stop, dirent.created_rev)
712 revnum, realpath = findchanges(path, stop, dirent.created_rev)
713 if revnum is None:
713 if revnum is None:
714 # Tools like svnsync can create empty revision, when
714 # Tools like svnsync can create empty revision, when
715 # synchronizing only a subtree for instance. These empty
715 # synchronizing only a subtree for instance. These empty
716 # revisions created_rev still have their original values
716 # revisions created_rev still have their original values
717 # despite all changes having disappeared and can be
717 # despite all changes having disappeared and can be
718 # returned by ra.stat(), at least when stating the root
718 # returned by ra.stat(), at least when stating the root
719 # module. In that case, do not trust created_rev and scan
719 # module. In that case, do not trust created_rev and scan
720 # the whole history.
720 # the whole history.
721 revnum, realpath = findchanges(path, stop)
721 revnum, realpath = findchanges(path, stop)
722 if revnum is None:
722 if revnum is None:
723 self.ui.debug('ignoring empty branch %r\n' % realpath)
723 self.ui.debug('ignoring empty branch %r\n' % realpath)
724 return None
724 return None
725
725
726 if not realpath.startswith(self.rootmodule):
726 if not realpath.startswith(self.rootmodule):
727 self.ui.debug('ignoring foreign branch %r\n' % realpath)
727 self.ui.debug('ignoring foreign branch %r\n' % realpath)
728 return None
728 return None
729 return self.revid(revnum, realpath)
729 return self.revid(revnum, realpath)
730
730
731 def reparent(self, module):
731 def reparent(self, module):
732 """Reparent the svn transport and return the previous parent."""
732 """Reparent the svn transport and return the previous parent."""
733 if self.prevmodule == module:
733 if self.prevmodule == module:
734 return module
734 return module
735 svnurl = self.baseurl + quote(module)
735 svnurl = self.baseurl + quote(module)
736 prevmodule = self.prevmodule
736 prevmodule = self.prevmodule
737 if prevmodule is None:
737 if prevmodule is None:
738 prevmodule = ''
738 prevmodule = ''
739 self.ui.debug("reparent to %s\n" % svnurl)
739 self.ui.debug("reparent to %s\n" % svnurl)
740 svn.ra.reparent(self.ra, svnurl)
740 svn.ra.reparent(self.ra, svnurl)
741 self.prevmodule = module
741 self.prevmodule = module
742 return prevmodule
742 return prevmodule
743
743
744 def expandpaths(self, rev, paths, parents):
744 def expandpaths(self, rev, paths, parents):
745 changed, removed = set(), set()
745 changed, removed = set(), set()
746 copies = {}
746 copies = {}
747
747
748 new_module, revnum = revsplit(rev)[1:]
748 new_module, revnum = revsplit(rev)[1:]
749 if new_module != self.module:
749 if new_module != self.module:
750 self.module = new_module
750 self.module = new_module
751 self.reparent(self.module)
751 self.reparent(self.module)
752
752
753 for i, (path, ent) in enumerate(paths):
753 for i, (path, ent) in enumerate(paths):
754 self.ui.progress(_('scanning paths'), i, item=path,
754 self.ui.progress(_('scanning paths'), i, item=path,
755 total=len(paths))
755 total=len(paths))
756 entrypath = self.getrelpath(path)
756 entrypath = self.getrelpath(path)
757
757
758 kind = self._checkpath(entrypath, revnum)
758 kind = self._checkpath(entrypath, revnum)
759 if kind == svn.core.svn_node_file:
759 if kind == svn.core.svn_node_file:
760 changed.add(self.recode(entrypath))
760 changed.add(self.recode(entrypath))
761 if not ent.copyfrom_path or not parents:
761 if not ent.copyfrom_path or not parents:
762 continue
762 continue
763 # Copy sources not in parent revisions cannot be
763 # Copy sources not in parent revisions cannot be
764 # represented, ignore their origin for now
764 # represented, ignore their origin for now
765 pmodule, prevnum = revsplit(parents[0])[1:]
765 pmodule, prevnum = revsplit(parents[0])[1:]
766 if ent.copyfrom_rev < prevnum:
766 if ent.copyfrom_rev < prevnum:
767 continue
767 continue
768 copyfrom_path = self.getrelpath(ent.copyfrom_path, pmodule)
768 copyfrom_path = self.getrelpath(ent.copyfrom_path, pmodule)
769 if not copyfrom_path:
769 if not copyfrom_path:
770 continue
770 continue
771 self.ui.debug("copied to %s from %s@%s\n" %
771 self.ui.debug("copied to %s from %s@%s\n" %
772 (entrypath, copyfrom_path, ent.copyfrom_rev))
772 (entrypath, copyfrom_path, ent.copyfrom_rev))
773 copies[self.recode(entrypath)] = self.recode(copyfrom_path)
773 copies[self.recode(entrypath)] = self.recode(copyfrom_path)
774 elif kind == 0: # gone, but had better be a deleted *file*
774 elif kind == 0: # gone, but had better be a deleted *file*
775 self.ui.debug("gone from %s\n" % ent.copyfrom_rev)
775 self.ui.debug("gone from %s\n" % ent.copyfrom_rev)
776 pmodule, prevnum = revsplit(parents[0])[1:]
776 pmodule, prevnum = revsplit(parents[0])[1:]
777 parentpath = pmodule + "/" + entrypath
777 parentpath = pmodule + "/" + entrypath
778 fromkind = self._checkpath(entrypath, prevnum, pmodule)
778 fromkind = self._checkpath(entrypath, prevnum, pmodule)
779
779
780 if fromkind == svn.core.svn_node_file:
780 if fromkind == svn.core.svn_node_file:
781 removed.add(self.recode(entrypath))
781 removed.add(self.recode(entrypath))
782 elif fromkind == svn.core.svn_node_dir:
782 elif fromkind == svn.core.svn_node_dir:
783 oroot = parentpath.strip('/')
783 oroot = parentpath.strip('/')
784 nroot = path.strip('/')
784 nroot = path.strip('/')
785 children = self._iterfiles(oroot, prevnum)
785 children = self._iterfiles(oroot, prevnum)
786 for childpath in children:
786 for childpath in children:
787 childpath = childpath.replace(oroot, nroot)
787 childpath = childpath.replace(oroot, nroot)
788 childpath = self.getrelpath("/" + childpath, pmodule)
788 childpath = self.getrelpath("/" + childpath, pmodule)
789 if childpath:
789 if childpath:
790 removed.add(self.recode(childpath))
790 removed.add(self.recode(childpath))
791 else:
791 else:
792 self.ui.debug('unknown path in revision %d: %s\n' % \
792 self.ui.debug('unknown path in revision %d: %s\n' % \
793 (revnum, path))
793 (revnum, path))
794 elif kind == svn.core.svn_node_dir:
794 elif kind == svn.core.svn_node_dir:
795 if ent.action == 'M':
795 if ent.action == 'M':
796 # If the directory just had a prop change,
796 # If the directory just had a prop change,
797 # then we shouldn't need to look for its children.
797 # then we shouldn't need to look for its children.
798 continue
798 continue
799 if ent.action == 'R' and parents:
799 if ent.action == 'R' and parents:
800 # If a directory is replacing a file, mark the previous
800 # If a directory is replacing a file, mark the previous
801 # file as deleted
801 # file as deleted
802 pmodule, prevnum = revsplit(parents[0])[1:]
802 pmodule, prevnum = revsplit(parents[0])[1:]
803 pkind = self._checkpath(entrypath, prevnum, pmodule)
803 pkind = self._checkpath(entrypath, prevnum, pmodule)
804 if pkind == svn.core.svn_node_file:
804 if pkind == svn.core.svn_node_file:
805 removed.add(self.recode(entrypath))
805 removed.add(self.recode(entrypath))
806 elif pkind == svn.core.svn_node_dir:
806 elif pkind == svn.core.svn_node_dir:
807 # We do not know what files were kept or removed,
807 # We do not know what files were kept or removed,
808 # mark them all as changed.
808 # mark them all as changed.
809 for childpath in self._iterfiles(pmodule, prevnum):
809 for childpath in self._iterfiles(pmodule, prevnum):
810 childpath = self.getrelpath("/" + childpath)
810 childpath = self.getrelpath("/" + childpath)
811 if childpath:
811 if childpath:
812 changed.add(self.recode(childpath))
812 changed.add(self.recode(childpath))
813
813
814 for childpath in self._iterfiles(path, revnum):
814 for childpath in self._iterfiles(path, revnum):
815 childpath = self.getrelpath("/" + childpath)
815 childpath = self.getrelpath("/" + childpath)
816 if childpath:
816 if childpath:
817 changed.add(self.recode(childpath))
817 changed.add(self.recode(childpath))
818
818
819 # Handle directory copies
819 # Handle directory copies
820 if not ent.copyfrom_path or not parents:
820 if not ent.copyfrom_path or not parents:
821 continue
821 continue
822 # Copy sources not in parent revisions cannot be
822 # Copy sources not in parent revisions cannot be
823 # represented, ignore their origin for now
823 # represented, ignore their origin for now
824 pmodule, prevnum = revsplit(parents[0])[1:]
824 pmodule, prevnum = revsplit(parents[0])[1:]
825 if ent.copyfrom_rev < prevnum:
825 if ent.copyfrom_rev < prevnum:
826 continue
826 continue
827 copyfrompath = self.getrelpath(ent.copyfrom_path, pmodule)
827 copyfrompath = self.getrelpath(ent.copyfrom_path, pmodule)
828 if not copyfrompath:
828 if not copyfrompath:
829 continue
829 continue
830 self.ui.debug("mark %s came from %s:%d\n"
830 self.ui.debug("mark %s came from %s:%d\n"
831 % (path, copyfrompath, ent.copyfrom_rev))
831 % (path, copyfrompath, ent.copyfrom_rev))
832 children = self._iterfiles(ent.copyfrom_path, ent.copyfrom_rev)
832 children = self._iterfiles(ent.copyfrom_path, ent.copyfrom_rev)
833 for childpath in children:
833 for childpath in children:
834 childpath = self.getrelpath("/" + childpath, pmodule)
834 childpath = self.getrelpath("/" + childpath, pmodule)
835 if not childpath:
835 if not childpath:
836 continue
836 continue
837 copytopath = path + childpath[len(copyfrompath):]
837 copytopath = path + childpath[len(copyfrompath):]
838 copytopath = self.getrelpath(copytopath)
838 copytopath = self.getrelpath(copytopath)
839 copies[self.recode(copytopath)] = self.recode(childpath)
839 copies[self.recode(copytopath)] = self.recode(childpath)
840
840
841 self.ui.progress(_('scanning paths'), None)
841 self.ui.progress(_('scanning paths'), None)
842 changed.update(removed)
842 changed.update(removed)
843 return (list(changed), removed, copies)
843 return (list(changed), removed, copies)
844
844
845 def _fetch_revisions(self, from_revnum, to_revnum):
845 def _fetch_revisions(self, from_revnum, to_revnum):
846 if from_revnum < to_revnum:
846 if from_revnum < to_revnum:
847 from_revnum, to_revnum = to_revnum, from_revnum
847 from_revnum, to_revnum = to_revnum, from_revnum
848
848
849 self.child_cset = None
849 self.child_cset = None
850
850
851 def parselogentry(orig_paths, revnum, author, date, message):
851 def parselogentry(orig_paths, revnum, author, date, message):
852 """Return the parsed commit object or None, and True if
852 """Return the parsed commit object or None, and True if
853 the revision is a branch root.
853 the revision is a branch root.
854 """
854 """
855 self.ui.debug("parsing revision %d (%d changes)\n" %
855 self.ui.debug("parsing revision %d (%d changes)\n" %
856 (revnum, len(orig_paths)))
856 (revnum, len(orig_paths)))
857
857
858 branched = False
858 branched = False
859 rev = self.revid(revnum)
859 rev = self.revid(revnum)
860 # branch log might return entries for a parent we already have
860 # branch log might return entries for a parent we already have
861
861
862 if rev in self.commits or revnum < to_revnum:
862 if rev in self.commits or revnum < to_revnum:
863 return None, branched
863 return None, branched
864
864
865 parents = []
865 parents = []
866 # check whether this revision is the start of a branch or part
866 # check whether this revision is the start of a branch or part
867 # of a branch renaming
867 # of a branch renaming
868 orig_paths = sorted(orig_paths.iteritems())
868 orig_paths = sorted(orig_paths.iteritems())
869 root_paths = [(p, e) for p, e in orig_paths
869 root_paths = [(p, e) for p, e in orig_paths
870 if self.module.startswith(p)]
870 if self.module.startswith(p)]
871 if root_paths:
871 if root_paths:
872 path, ent = root_paths[-1]
872 path, ent = root_paths[-1]
873 if ent.copyfrom_path:
873 if ent.copyfrom_path:
874 branched = True
874 branched = True
875 newpath = ent.copyfrom_path + self.module[len(path):]
875 newpath = ent.copyfrom_path + self.module[len(path):]
876 # ent.copyfrom_rev may not be the actual last revision
876 # ent.copyfrom_rev may not be the actual last revision
877 previd = self.latest(newpath, ent.copyfrom_rev)
877 previd = self.latest(newpath, ent.copyfrom_rev)
878 if previd is not None:
878 if previd is not None:
879 prevmodule, prevnum = revsplit(previd)[1:]
879 prevmodule, prevnum = revsplit(previd)[1:]
880 if prevnum >= self.startrev:
880 if prevnum >= self.startrev:
881 parents = [previd]
881 parents = [previd]
882 self.ui.note(
882 self.ui.note(
883 _('found parent of branch %s at %d: %s\n') %
883 _('found parent of branch %s at %d: %s\n') %
884 (self.module, prevnum, prevmodule))
884 (self.module, prevnum, prevmodule))
885 else:
885 else:
886 self.ui.debug("no copyfrom path, don't know what to do.\n")
886 self.ui.debug("no copyfrom path, don't know what to do.\n")
887
887
888 paths = []
888 paths = []
889 # filter out unrelated paths
889 # filter out unrelated paths
890 for path, ent in orig_paths:
890 for path, ent in orig_paths:
891 if self.getrelpath(path) is None:
891 if self.getrelpath(path) is None:
892 continue
892 continue
893 paths.append((path, ent))
893 paths.append((path, ent))
894
894
895 # Example SVN datetime. Includes microseconds.
895 # Example SVN datetime. Includes microseconds.
896 # ISO-8601 conformant
896 # ISO-8601 conformant
897 # '2007-01-04T17:35:00.902377Z'
897 # '2007-01-04T17:35:00.902377Z'
898 date = util.parsedate(date[:19] + " UTC", ["%Y-%m-%dT%H:%M:%S"])
898 date = util.parsedate(date[:19] + " UTC", ["%Y-%m-%dT%H:%M:%S"])
899 if self.ui.configbool('convert', 'localtimezone'):
899 if self.ui.configbool('convert', 'localtimezone'):
900 date = makedatetimestamp(date[0])
900 date = makedatetimestamp(date[0])
901
901
902 if message:
902 if message:
903 log = self.recode(message)
903 log = self.recode(message)
904 else:
904 else:
905 log = ''
905 log = ''
906
906
907 if author:
907 if author:
908 author = self.recode(author)
908 author = self.recode(author)
909 else:
909 else:
910 author = ''
910 author = ''
911
911
912 try:
912 try:
913 branch = self.module.split("/")[-1]
913 branch = self.module.split("/")[-1]
914 if branch == self.trunkname:
914 if branch == self.trunkname:
915 branch = None
915 branch = None
916 except IndexError:
916 except IndexError:
917 branch = None
917 branch = None
918
918
919 cset = commit(author=author,
919 cset = commit(author=author,
920 date=util.datestr(date, '%Y-%m-%d %H:%M:%S %1%2'),
920 date=util.datestr(date, '%Y-%m-%d %H:%M:%S %1%2'),
921 desc=log,
921 desc=log,
922 parents=parents,
922 parents=parents,
923 branch=branch,
923 branch=branch,
924 rev=rev)
924 rev=rev)
925
925
926 self.commits[rev] = cset
926 self.commits[rev] = cset
927 # The parents list is *shared* among self.paths and the
927 # The parents list is *shared* among self.paths and the
928 # commit object. Both will be updated below.
928 # commit object. Both will be updated below.
929 self.paths[rev] = (paths, cset.parents)
929 self.paths[rev] = (paths, cset.parents)
930 if self.child_cset and not self.child_cset.parents:
930 if self.child_cset and not self.child_cset.parents:
931 self.child_cset.parents[:] = [rev]
931 self.child_cset.parents[:] = [rev]
932 self.child_cset = cset
932 self.child_cset = cset
933 return cset, branched
933 return cset, branched
934
934
935 self.ui.note(_('fetching revision log for "%s" from %d to %d\n') %
935 self.ui.note(_('fetching revision log for "%s" from %d to %d\n') %
936 (self.module, from_revnum, to_revnum))
936 (self.module, from_revnum, to_revnum))
937
937
938 try:
938 try:
939 firstcset = None
939 firstcset = None
940 lastonbranch = False
940 lastonbranch = False
941 stream = self._getlog([self.module], from_revnum, to_revnum)
941 stream = self._getlog([self.module], from_revnum, to_revnum)
942 try:
942 try:
943 for entry in stream:
943 for entry in stream:
944 paths, revnum, author, date, message = entry
944 paths, revnum, author, date, message = entry
945 if revnum < self.startrev:
945 if revnum < self.startrev:
946 lastonbranch = True
946 lastonbranch = True
947 break
947 break
948 if not paths:
948 if not paths:
949 self.ui.debug('revision %d has no entries\n' % revnum)
949 self.ui.debug('revision %d has no entries\n' % revnum)
950 # If we ever leave the loop on an empty
950 # If we ever leave the loop on an empty
951 # revision, do not try to get a parent branch
951 # revision, do not try to get a parent branch
952 lastonbranch = lastonbranch or revnum == 0
952 lastonbranch = lastonbranch or revnum == 0
953 continue
953 continue
954 cset, lastonbranch = parselogentry(paths, revnum, author,
954 cset, lastonbranch = parselogentry(paths, revnum, author,
955 date, message)
955 date, message)
956 if cset:
956 if cset:
957 firstcset = cset
957 firstcset = cset
958 if lastonbranch:
958 if lastonbranch:
959 break
959 break
960 finally:
960 finally:
961 stream.close()
961 stream.close()
962
962
963 if not lastonbranch and firstcset and not firstcset.parents:
963 if not lastonbranch and firstcset and not firstcset.parents:
964 # The first revision of the sequence (the last fetched one)
964 # The first revision of the sequence (the last fetched one)
965 # has invalid parents if not a branch root. Find the parent
965 # has invalid parents if not a branch root. Find the parent
966 # revision now, if any.
966 # revision now, if any.
967 try:
967 try:
968 firstrevnum = self.revnum(firstcset.rev)
968 firstrevnum = self.revnum(firstcset.rev)
969 if firstrevnum > 1:
969 if firstrevnum > 1:
970 latest = self.latest(self.module, firstrevnum - 1)
970 latest = self.latest(self.module, firstrevnum - 1)
971 if latest:
971 if latest:
972 firstcset.parents.append(latest)
972 firstcset.parents.append(latest)
973 except SvnPathNotFound:
973 except SvnPathNotFound:
974 pass
974 pass
975 except SubversionException as xxx_todo_changeme:
975 except SubversionException as xxx_todo_changeme:
976 (inst, num) = xxx_todo_changeme.args
976 (inst, num) = xxx_todo_changeme.args
977 if num == svn.core.SVN_ERR_FS_NO_SUCH_REVISION:
977 if num == svn.core.SVN_ERR_FS_NO_SUCH_REVISION:
978 raise error.Abort(_('svn: branch has no revision %s')
978 raise error.Abort(_('svn: branch has no revision %s')
979 % to_revnum)
979 % to_revnum)
980 raise
980 raise
981
981
982 def getfile(self, file, rev):
982 def getfile(self, file, rev):
983 # TODO: ra.get_file transmits the whole file instead of diffs.
983 # TODO: ra.get_file transmits the whole file instead of diffs.
984 if file in self.removed:
984 if file in self.removed:
985 return None, None
985 return None, None
986 mode = ''
986 mode = ''
987 try:
987 try:
988 new_module, revnum = revsplit(rev)[1:]
988 new_module, revnum = revsplit(rev)[1:]
989 if self.module != new_module:
989 if self.module != new_module:
990 self.module = new_module
990 self.module = new_module
991 self.reparent(self.module)
991 self.reparent(self.module)
992 io = StringIO()
992 io = StringIO()
993 info = svn.ra.get_file(self.ra, file, revnum, io)
993 info = svn.ra.get_file(self.ra, file, revnum, io)
994 data = io.getvalue()
994 data = io.getvalue()
995 # ra.get_file() seems to keep a reference on the input buffer
995 # ra.get_file() seems to keep a reference on the input buffer
996 # preventing collection. Release it explicitly.
996 # preventing collection. Release it explicitly.
997 io.close()
997 io.close()
998 if isinstance(info, list):
998 if isinstance(info, list):
999 info = info[-1]
999 info = info[-1]
1000 mode = ("svn:executable" in info) and 'x' or ''
1000 mode = ("svn:executable" in info) and 'x' or ''
1001 mode = ("svn:special" in info) and 'l' or mode
1001 mode = ("svn:special" in info) and 'l' or mode
1002 except SubversionException as e:
1002 except SubversionException as e:
1003 notfound = (svn.core.SVN_ERR_FS_NOT_FOUND,
1003 notfound = (svn.core.SVN_ERR_FS_NOT_FOUND,
1004 svn.core.SVN_ERR_RA_DAV_PATH_NOT_FOUND)
1004 svn.core.SVN_ERR_RA_DAV_PATH_NOT_FOUND)
1005 if e.apr_err in notfound: # File not found
1005 if e.apr_err in notfound: # File not found
1006 return None, None
1006 return None, None
1007 raise
1007 raise
1008 if mode == 'l':
1008 if mode == 'l':
1009 link_prefix = "link "
1009 link_prefix = "link "
1010 if data.startswith(link_prefix):
1010 if data.startswith(link_prefix):
1011 data = data[len(link_prefix):]
1011 data = data[len(link_prefix):]
1012 return data, mode
1012 return data, mode
1013
1013
1014 def _iterfiles(self, path, revnum):
1014 def _iterfiles(self, path, revnum):
1015 """Enumerate all files in path at revnum, recursively."""
1015 """Enumerate all files in path at revnum, recursively."""
1016 path = path.strip('/')
1016 path = path.strip('/')
1017 pool = Pool()
1017 pool = Pool()
1018 rpath = '/'.join([self.baseurl, quote(path)]).strip('/')
1018 rpath = '/'.join([self.baseurl, quote(path)]).strip('/')
1019 entries = svn.client.ls(rpath, optrev(revnum), True, self.ctx, pool)
1019 entries = svn.client.ls(rpath, optrev(revnum), True, self.ctx, pool)
1020 if path:
1020 if path:
1021 path += '/'
1021 path += '/'
1022 return ((path + p) for p, e in entries.iteritems()
1022 return ((path + p) for p, e in entries.iteritems()
1023 if e.kind == svn.core.svn_node_file)
1023 if e.kind == svn.core.svn_node_file)
1024
1024
1025 def getrelpath(self, path, module=None):
1025 def getrelpath(self, path, module=None):
1026 if module is None:
1026 if module is None:
1027 module = self.module
1027 module = self.module
1028 # Given the repository url of this wc, say
1028 # Given the repository url of this wc, say
1029 # "http://server/plone/CMFPlone/branches/Plone-2_0-branch"
1029 # "http://server/plone/CMFPlone/branches/Plone-2_0-branch"
1030 # extract the "entry" portion (a relative path) from what
1030 # extract the "entry" portion (a relative path) from what
1031 # svn log --xml says, i.e.
1031 # svn log --xml says, i.e.
1032 # "/CMFPlone/branches/Plone-2_0-branch/tests/PloneTestCase.py"
1032 # "/CMFPlone/branches/Plone-2_0-branch/tests/PloneTestCase.py"
1033 # that is to say "tests/PloneTestCase.py"
1033 # that is to say "tests/PloneTestCase.py"
1034 if path.startswith(module):
1034 if path.startswith(module):
1035 relative = path.rstrip('/')[len(module):]
1035 relative = path.rstrip('/')[len(module):]
1036 if relative.startswith('/'):
1036 if relative.startswith('/'):
1037 return relative[1:]
1037 return relative[1:]
1038 elif relative == '':
1038 elif relative == '':
1039 return relative
1039 return relative
1040
1040
1041 # The path is outside our tracked tree...
1041 # The path is outside our tracked tree...
1042 self.ui.debug('%r is not under %r, ignoring\n' % (path, module))
1042 self.ui.debug('%r is not under %r, ignoring\n' % (path, module))
1043 return None
1043 return None
1044
1044
1045 def _checkpath(self, path, revnum, module=None):
1045 def _checkpath(self, path, revnum, module=None):
1046 if module is not None:
1046 if module is not None:
1047 prevmodule = self.reparent('')
1047 prevmodule = self.reparent('')
1048 path = module + '/' + path
1048 path = module + '/' + path
1049 try:
1049 try:
1050 # ra.check_path does not like leading slashes very much, it leads
1050 # ra.check_path does not like leading slashes very much, it leads
1051 # to PROPFIND subversion errors
1051 # to PROPFIND subversion errors
1052 return svn.ra.check_path(self.ra, path.strip('/'), revnum)
1052 return svn.ra.check_path(self.ra, path.strip('/'), revnum)
1053 finally:
1053 finally:
1054 if module is not None:
1054 if module is not None:
1055 self.reparent(prevmodule)
1055 self.reparent(prevmodule)
1056
1056
1057 def _getlog(self, paths, start, end, limit=0, discover_changed_paths=True,
1057 def _getlog(self, paths, start, end, limit=0, discover_changed_paths=True,
1058 strict_node_history=False):
1058 strict_node_history=False):
1059 # Normalize path names, svn >= 1.5 only wants paths relative to
1059 # Normalize path names, svn >= 1.5 only wants paths relative to
1060 # supplied URL
1060 # supplied URL
1061 relpaths = []
1061 relpaths = []
1062 for p in paths:
1062 for p in paths:
1063 if not p.startswith('/'):
1063 if not p.startswith('/'):
1064 p = self.module + '/' + p
1064 p = self.module + '/' + p
1065 relpaths.append(p.strip('/'))
1065 relpaths.append(p.strip('/'))
1066 args = [self.baseurl, relpaths, start, end, limit,
1066 args = [self.baseurl, relpaths, start, end, limit,
1067 discover_changed_paths, strict_node_history]
1067 discover_changed_paths, strict_node_history]
1068 # developer config: convert.svn.debugsvnlog
1068 # developer config: convert.svn.debugsvnlog
1069 if not self.ui.configbool('convert', 'svn.debugsvnlog', True):
1069 if not self.ui.configbool('convert', 'svn.debugsvnlog', True):
1070 return directlogstream(*args)
1070 return directlogstream(*args)
1071 arg = encodeargs(args)
1071 arg = encodeargs(args)
1072 hgexe = util.hgexecutable()
1072 hgexe = util.hgexecutable()
1073 cmd = '%s debugsvnlog' % util.shellquote(hgexe)
1073 cmd = '%s debugsvnlog' % util.shellquote(hgexe)
1074 stdin, stdout = util.popen2(util.quotecommand(cmd))
1074 stdin, stdout = util.popen2(util.quotecommand(cmd))
1075 stdin.write(arg)
1075 stdin.write(arg)
1076 try:
1076 try:
1077 stdin.close()
1077 stdin.close()
1078 except IOError:
1078 except IOError:
1079 raise error.Abort(_('Mercurial failed to run itself, check'
1079 raise error.Abort(_('Mercurial failed to run itself, check'
1080 ' hg executable is in PATH'))
1080 ' hg executable is in PATH'))
1081 return logstream(stdout)
1081 return logstream(stdout)
1082
1082
1083 pre_revprop_change = '''#!/bin/sh
1083 pre_revprop_change = '''#!/bin/sh
1084
1084
1085 REPOS="$1"
1085 REPOS="$1"
1086 REV="$2"
1086 REV="$2"
1087 USER="$3"
1087 USER="$3"
1088 PROPNAME="$4"
1088 PROPNAME="$4"
1089 ACTION="$5"
1089 ACTION="$5"
1090
1090
1091 if [ "$ACTION" = "M" -a "$PROPNAME" = "svn:log" ]; then exit 0; fi
1091 if [ "$ACTION" = "M" -a "$PROPNAME" = "svn:log" ]; then exit 0; fi
1092 if [ "$ACTION" = "A" -a "$PROPNAME" = "hg:convert-branch" ]; then exit 0; fi
1092 if [ "$ACTION" = "A" -a "$PROPNAME" = "hg:convert-branch" ]; then exit 0; fi
1093 if [ "$ACTION" = "A" -a "$PROPNAME" = "hg:convert-rev" ]; then exit 0; fi
1093 if [ "$ACTION" = "A" -a "$PROPNAME" = "hg:convert-rev" ]; then exit 0; fi
1094
1094
1095 echo "Changing prohibited revision property" >&2
1095 echo "Changing prohibited revision property" >&2
1096 exit 1
1096 exit 1
1097 '''
1097 '''
1098
1098
1099 class svn_sink(converter_sink, commandline):
1099 class svn_sink(converter_sink, commandline):
1100 commit_re = re.compile(r'Committed revision (\d+).', re.M)
1100 commit_re = re.compile(r'Committed revision (\d+).', re.M)
1101 uuid_re = re.compile(r'Repository UUID:\s*(\S+)', re.M)
1101 uuid_re = re.compile(r'Repository UUID:\s*(\S+)', re.M)
1102
1102
1103 def prerun(self):
1103 def prerun(self):
1104 if self.wc:
1104 if self.wc:
1105 os.chdir(self.wc)
1105 os.chdir(self.wc)
1106
1106
1107 def postrun(self):
1107 def postrun(self):
1108 if self.wc:
1108 if self.wc:
1109 os.chdir(self.cwd)
1109 os.chdir(self.cwd)
1110
1110
1111 def join(self, name):
1111 def join(self, name):
1112 return os.path.join(self.wc, '.svn', name)
1112 return os.path.join(self.wc, '.svn', name)
1113
1113
1114 def revmapfile(self):
1114 def revmapfile(self):
1115 return self.join('hg-shamap')
1115 return self.join('hg-shamap')
1116
1116
1117 def authorfile(self):
1117 def authorfile(self):
1118 return self.join('hg-authormap')
1118 return self.join('hg-authormap')
1119
1119
1120 def __init__(self, ui, path):
1120 def __init__(self, ui, path):
1121
1121
1122 converter_sink.__init__(self, ui, path)
1122 converter_sink.__init__(self, ui, path)
1123 commandline.__init__(self, ui, 'svn')
1123 commandline.__init__(self, ui, 'svn')
1124 self.delete = []
1124 self.delete = []
1125 self.setexec = []
1125 self.setexec = []
1126 self.delexec = []
1126 self.delexec = []
1127 self.copies = []
1127 self.copies = []
1128 self.wc = None
1128 self.wc = None
1129 self.cwd = os.getcwd()
1129 self.cwd = os.getcwd()
1130
1130
1131 created = False
1131 created = False
1132 if os.path.isfile(os.path.join(path, '.svn', 'entries')):
1132 if os.path.isfile(os.path.join(path, '.svn', 'entries')):
1133 self.wc = os.path.realpath(path)
1133 self.wc = os.path.realpath(path)
1134 self.run0('update')
1134 self.run0('update')
1135 else:
1135 else:
1136 if not re.search(r'^(file|http|https|svn|svn\+ssh)\://', path):
1136 if not re.search(r'^(file|http|https|svn|svn\+ssh)\://', path):
1137 path = os.path.realpath(path)
1137 path = os.path.realpath(path)
1138 if os.path.isdir(os.path.dirname(path)):
1138 if os.path.isdir(os.path.dirname(path)):
1139 if not os.path.exists(os.path.join(path, 'db', 'fs-type')):
1139 if not os.path.exists(os.path.join(path, 'db', 'fs-type')):
1140 ui.status(_('initializing svn repository %r\n') %
1140 ui.status(_('initializing svn repository %r\n') %
1141 os.path.basename(path))
1141 os.path.basename(path))
1142 commandline(ui, 'svnadmin').run0('create', path)
1142 commandline(ui, 'svnadmin').run0('create', path)
1143 created = path
1143 created = path
1144 path = util.normpath(path)
1144 path = util.normpath(path)
1145 if not path.startswith('/'):
1145 if not path.startswith('/'):
1146 path = '/' + path
1146 path = '/' + path
1147 path = 'file://' + path
1147 path = 'file://' + path
1148
1148
1149 wcpath = os.path.join(os.getcwd(), os.path.basename(path) + '-wc')
1149 wcpath = os.path.join(os.getcwd(), os.path.basename(path) + '-wc')
1150 ui.status(_('initializing svn working copy %r\n')
1150 ui.status(_('initializing svn working copy %r\n')
1151 % os.path.basename(wcpath))
1151 % os.path.basename(wcpath))
1152 self.run0('checkout', path, wcpath)
1152 self.run0('checkout', path, wcpath)
1153
1153
1154 self.wc = wcpath
1154 self.wc = wcpath
1155 self.opener = scmutil.opener(self.wc)
1155 self.opener = scmutil.opener(self.wc)
1156 self.wopener = scmutil.opener(self.wc)
1156 self.wopener = scmutil.opener(self.wc)
1157 self.childmap = mapfile(ui, self.join('hg-childmap'))
1157 self.childmap = mapfile(ui, self.join('hg-childmap'))
1158 if util.checkexec(self.wc):
1158 if util.checkexec(self.wc):
1159 self.is_exec = util.isexec
1159 self.is_exec = util.isexec
1160 else:
1160 else:
1161 self.is_exec = None
1161 self.is_exec = None
1162
1162
1163 if created:
1163 if created:
1164 hook = os.path.join(created, 'hooks', 'pre-revprop-change')
1164 hook = os.path.join(created, 'hooks', 'pre-revprop-change')
1165 fp = open(hook, 'w')
1165 fp = open(hook, 'w')
1166 fp.write(pre_revprop_change)
1166 fp.write(pre_revprop_change)
1167 fp.close()
1167 fp.close()
1168 util.setflags(hook, False, True)
1168 util.setflags(hook, False, True)
1169
1169
1170 output = self.run0('info')
1170 output = self.run0('info')
1171 self.uuid = self.uuid_re.search(output).group(1).strip()
1171 self.uuid = self.uuid_re.search(output).group(1).strip()
1172
1172
1173 def wjoin(self, *names):
1173 def wjoin(self, *names):
1174 return os.path.join(self.wc, *names)
1174 return os.path.join(self.wc, *names)
1175
1175
1176 @propertycache
1176 @propertycache
1177 def manifest(self):
1177 def manifest(self):
1178 # As of svn 1.7, the "add" command fails when receiving
1178 # As of svn 1.7, the "add" command fails when receiving
1179 # already tracked entries, so we have to track and filter them
1179 # already tracked entries, so we have to track and filter them
1180 # ourselves.
1180 # ourselves.
1181 m = set()
1181 m = set()
1182 output = self.run0('ls', recursive=True, xml=True)
1182 output = self.run0('ls', recursive=True, xml=True)
1183 doc = xml.dom.minidom.parseString(output)
1183 doc = xml.dom.minidom.parseString(output)
1184 for e in doc.getElementsByTagName('entry'):
1184 for e in doc.getElementsByTagName('entry'):
1185 for n in e.childNodes:
1185 for n in e.childNodes:
1186 if n.nodeType != n.ELEMENT_NODE or n.tagName != 'name':
1186 if n.nodeType != n.ELEMENT_NODE or n.tagName != 'name':
1187 continue
1187 continue
1188 name = ''.join(c.data for c in n.childNodes
1188 name = ''.join(c.data for c in n.childNodes
1189 if c.nodeType == c.TEXT_NODE)
1189 if c.nodeType == c.TEXT_NODE)
1190 # Entries are compared with names coming from
1190 # Entries are compared with names coming from
1191 # mercurial, so bytes with undefined encoding. Our
1191 # mercurial, so bytes with undefined encoding. Our
1192 # best bet is to assume they are in local
1192 # best bet is to assume they are in local
1193 # encoding. They will be passed to command line calls
1193 # encoding. They will be passed to command line calls
1194 # later anyway, so they better be.
1194 # later anyway, so they better be.
1195 m.add(encoding.tolocal(name.encode('utf-8')))
1195 m.add(encoding.tolocal(name.encode('utf-8')))
1196 break
1196 break
1197 return m
1197 return m
1198
1198
1199 def putfile(self, filename, flags, data):
1199 def putfile(self, filename, flags, data):
1200 if 'l' in flags:
1200 if 'l' in flags:
1201 self.wopener.symlink(data, filename)
1201 self.wopener.symlink(data, filename)
1202 else:
1202 else:
1203 try:
1203 try:
1204 if os.path.islink(self.wjoin(filename)):
1204 if os.path.islink(self.wjoin(filename)):
1205 os.unlink(filename)
1205 os.unlink(filename)
1206 except OSError:
1206 except OSError:
1207 pass
1207 pass
1208 self.wopener.write(filename, data)
1208 self.wopener.write(filename, data)
1209
1209
1210 if self.is_exec:
1210 if self.is_exec:
1211 if self.is_exec(self.wjoin(filename)):
1211 if self.is_exec(self.wjoin(filename)):
1212 if 'x' not in flags:
1212 if 'x' not in flags:
1213 self.delexec.append(filename)
1213 self.delexec.append(filename)
1214 else:
1214 else:
1215 if 'x' in flags:
1215 if 'x' in flags:
1216 self.setexec.append(filename)
1216 self.setexec.append(filename)
1217 util.setflags(self.wjoin(filename), False, 'x' in flags)
1217 util.setflags(self.wjoin(filename), False, 'x' in flags)
1218
1218
1219 def _copyfile(self, source, dest):
1219 def _copyfile(self, source, dest):
1220 # SVN's copy command pukes if the destination file exists, but
1220 # SVN's copy command pukes if the destination file exists, but
1221 # our copyfile method expects to record a copy that has
1221 # our copyfile method expects to record a copy that has
1222 # already occurred. Cross the semantic gap.
1222 # already occurred. Cross the semantic gap.
1223 wdest = self.wjoin(dest)
1223 wdest = self.wjoin(dest)
1224 exists = os.path.lexists(wdest)
1224 exists = os.path.lexists(wdest)
1225 if exists:
1225 if exists:
1226 fd, tempname = tempfile.mkstemp(
1226 fd, tempname = tempfile.mkstemp(
1227 prefix='hg-copy-', dir=os.path.dirname(wdest))
1227 prefix='hg-copy-', dir=os.path.dirname(wdest))
1228 os.close(fd)
1228 os.close(fd)
1229 os.unlink(tempname)
1229 os.unlink(tempname)
1230 os.rename(wdest, tempname)
1230 os.rename(wdest, tempname)
1231 try:
1231 try:
1232 self.run0('copy', source, dest)
1232 self.run0('copy', source, dest)
1233 finally:
1233 finally:
1234 self.manifest.add(dest)
1234 self.manifest.add(dest)
1235 if exists:
1235 if exists:
1236 try:
1236 try:
1237 os.unlink(wdest)
1237 os.unlink(wdest)
1238 except OSError:
1238 except OSError:
1239 pass
1239 pass
1240 os.rename(tempname, wdest)
1240 os.rename(tempname, wdest)
1241
1241
1242 def dirs_of(self, files):
1242 def dirs_of(self, files):
1243 dirs = set()
1243 dirs = set()
1244 for f in files:
1244 for f in files:
1245 if os.path.isdir(self.wjoin(f)):
1245 if os.path.isdir(self.wjoin(f)):
1246 dirs.add(f)
1246 dirs.add(f)
1247 for i in strutil.rfindall(f, '/'):
1247 for i in strutil.rfindall(f, '/'):
1248 dirs.add(f[:i])
1248 dirs.add(f[:i])
1249 return dirs
1249 return dirs
1250
1250
1251 def add_dirs(self, files):
1251 def add_dirs(self, files):
1252 add_dirs = [d for d in sorted(self.dirs_of(files))
1252 add_dirs = [d for d in sorted(self.dirs_of(files))
1253 if d not in self.manifest]
1253 if d not in self.manifest]
1254 if add_dirs:
1254 if add_dirs:
1255 self.manifest.update(add_dirs)
1255 self.manifest.update(add_dirs)
1256 self.xargs(add_dirs, 'add', non_recursive=True, quiet=True)
1256 self.xargs(add_dirs, 'add', non_recursive=True, quiet=True)
1257 return add_dirs
1257 return add_dirs
1258
1258
1259 def add_files(self, files):
1259 def add_files(self, files):
1260 files = [f for f in files if f not in self.manifest]
1260 files = [f for f in files if f not in self.manifest]
1261 if files:
1261 if files:
1262 self.manifest.update(files)
1262 self.manifest.update(files)
1263 self.xargs(files, 'add', quiet=True)
1263 self.xargs(files, 'add', quiet=True)
1264 return files
1264 return files
1265
1265
1266 def addchild(self, parent, child):
1266 def addchild(self, parent, child):
1267 self.childmap[parent] = child
1267 self.childmap[parent] = child
1268
1268
1269 def revid(self, rev):
1269 def revid(self, rev):
1270 return u"svn:%s@%s" % (self.uuid, rev)
1270 return u"svn:%s@%s" % (self.uuid, rev)
1271
1271
1272 def putcommit(self, files, copies, parents, commit, source, revmap, full,
1272 def putcommit(self, files, copies, parents, commit, source, revmap, full,
1273 cleanp2):
1273 cleanp2):
1274 for parent in parents:
1274 for parent in parents:
1275 try:
1275 try:
1276 return self.revid(self.childmap[parent])
1276 return self.revid(self.childmap[parent])
1277 except KeyError:
1277 except KeyError:
1278 pass
1278 pass
1279
1279
1280 # Apply changes to working copy
1280 # Apply changes to working copy
1281 for f, v in files:
1281 for f, v in files:
1282 data, mode = source.getfile(f, v)
1282 data, mode = source.getfile(f, v)
1283 if data is None:
1283 if data is None:
1284 self.delete.append(f)
1284 self.delete.append(f)
1285 else:
1285 else:
1286 self.putfile(f, mode, data)
1286 self.putfile(f, mode, data)
1287 if f in copies:
1287 if f in copies:
1288 self.copies.append([copies[f], f])
1288 self.copies.append([copies[f], f])
1289 if full:
1289 if full:
1290 self.delete.extend(sorted(self.manifest.difference(files)))
1290 self.delete.extend(sorted(self.manifest.difference(files)))
1291 files = [f[0] for f in files]
1291 files = [f[0] for f in files]
1292
1292
1293 entries = set(self.delete)
1293 entries = set(self.delete)
1294 files = frozenset(files)
1294 files = frozenset(files)
1295 entries.update(self.add_dirs(files.difference(entries)))
1295 entries.update(self.add_dirs(files.difference(entries)))
1296 if self.copies:
1296 if self.copies:
1297 for s, d in self.copies:
1297 for s, d in self.copies:
1298 self._copyfile(s, d)
1298 self._copyfile(s, d)
1299 self.copies = []
1299 self.copies = []
1300 if self.delete:
1300 if self.delete:
1301 self.xargs(self.delete, 'delete')
1301 self.xargs(self.delete, 'delete')
1302 for f in self.delete:
1302 for f in self.delete:
1303 self.manifest.remove(f)
1303 self.manifest.remove(f)
1304 self.delete = []
1304 self.delete = []
1305 entries.update(self.add_files(files.difference(entries)))
1305 entries.update(self.add_files(files.difference(entries)))
1306 if self.delexec:
1306 if self.delexec:
1307 self.xargs(self.delexec, 'propdel', 'svn:executable')
1307 self.xargs(self.delexec, 'propdel', 'svn:executable')
1308 self.delexec = []
1308 self.delexec = []
1309 if self.setexec:
1309 if self.setexec:
1310 self.xargs(self.setexec, 'propset', 'svn:executable', '*')
1310 self.xargs(self.setexec, 'propset', 'svn:executable', '*')
1311 self.setexec = []
1311 self.setexec = []
1312
1312
1313 fd, messagefile = tempfile.mkstemp(prefix='hg-convert-')
1313 fd, messagefile = tempfile.mkstemp(prefix='hg-convert-')
1314 fp = os.fdopen(fd, 'w')
1314 fp = os.fdopen(fd, 'w')
1315 fp.write(commit.desc)
1315 fp.write(commit.desc)
1316 fp.close()
1316 fp.close()
1317 try:
1317 try:
1318 output = self.run0('commit',
1318 output = self.run0('commit',
1319 username=util.shortuser(commit.author),
1319 username=util.shortuser(commit.author),
1320 file=messagefile,
1320 file=messagefile,
1321 encoding='utf-8')
1321 encoding='utf-8')
1322 try:
1322 try:
1323 rev = self.commit_re.search(output).group(1)
1323 rev = self.commit_re.search(output).group(1)
1324 except AttributeError:
1324 except AttributeError:
1325 if parents and not files:
1325 if parents and not files:
1326 return parents[0]
1326 return parents[0]
1327 self.ui.warn(_('unexpected svn output:\n'))
1327 self.ui.warn(_('unexpected svn output:\n'))
1328 self.ui.warn(output)
1328 self.ui.warn(output)
1329 raise error.Abort(_('unable to cope with svn output'))
1329 raise error.Abort(_('unable to cope with svn output'))
1330 if commit.rev:
1330 if commit.rev:
1331 self.run('propset', 'hg:convert-rev', commit.rev,
1331 self.run('propset', 'hg:convert-rev', commit.rev,
1332 revprop=True, revision=rev)
1332 revprop=True, revision=rev)
1333 if commit.branch and commit.branch != 'default':
1333 if commit.branch and commit.branch != 'default':
1334 self.run('propset', 'hg:convert-branch', commit.branch,
1334 self.run('propset', 'hg:convert-branch', commit.branch,
1335 revprop=True, revision=rev)
1335 revprop=True, revision=rev)
1336 for parent in parents:
1336 for parent in parents:
1337 self.addchild(parent, rev)
1337 self.addchild(parent, rev)
1338 return self.revid(rev)
1338 return self.revid(rev)
1339 finally:
1339 finally:
1340 os.unlink(messagefile)
1340 os.unlink(messagefile)
1341
1341
1342 def puttags(self, tags):
1342 def puttags(self, tags):
1343 self.ui.warn(_('writing Subversion tags is not yet implemented\n'))
1343 self.ui.warn(_('writing Subversion tags is not yet implemented\n'))
1344 return None, None
1344 return None, None
1345
1345
1346 def hascommitfrommap(self, rev):
1346 def hascommitfrommap(self, rev):
1347 # We trust that revisions referenced in a map still is present
1347 # We trust that revisions referenced in a map still is present
1348 # TODO: implement something better if necessary and feasible
1348 # TODO: implement something better if necessary and feasible
1349 return True
1349 return True
1350
1350
1351 def hascommitforsplicemap(self, rev):
1351 def hascommitforsplicemap(self, rev):
1352 # This is not correct as one can convert to an existing subversion
1352 # This is not correct as one can convert to an existing subversion
1353 # repository and childmap would not list all revisions. Too bad.
1353 # repository and childmap would not list all revisions. Too bad.
1354 if rev in self.childmap:
1354 if rev in self.childmap:
1355 return True
1355 return True
1356 raise error.Abort(_('splice map revision %s not found in subversion '
1356 raise error.Abort(_('splice map revision %s not found in subversion '
1357 'child map (revision lookups are not implemented)')
1357 'child map (revision lookups are not implemented)')
1358 % rev)
1358 % rev)
General Comments 0
You need to be logged in to leave comments. Login now