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