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