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