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