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