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