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