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