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