##// END OF EJS Templates
py3: use io.BytesIO directly...
Gregory Szorc -
r49728:5aafc3c5 default
parent child Browse files
Show More
@@ -1,2401 +1,2402 b''
1 # phabricator.py - simple Phabricator integration
1 # phabricator.py - simple Phabricator integration
2 #
2 #
3 # Copyright 2017 Facebook, Inc.
3 # Copyright 2017 Facebook, Inc.
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7 """simple Phabricator integration (EXPERIMENTAL)
7 """simple Phabricator integration (EXPERIMENTAL)
8
8
9 This extension provides a ``phabsend`` command which sends a stack of
9 This extension provides a ``phabsend`` command which sends a stack of
10 changesets to Phabricator, and a ``phabread`` command which prints a stack of
10 changesets to Phabricator, and a ``phabread`` command which prints a stack of
11 revisions in a format suitable for :hg:`import`, and a ``phabupdate`` command
11 revisions in a format suitable for :hg:`import`, and a ``phabupdate`` command
12 to update statuses in batch.
12 to update statuses in batch.
13
13
14 A "phabstatus" view for :hg:`show` is also provided; it displays status
14 A "phabstatus" view for :hg:`show` is also provided; it displays status
15 information of Phabricator differentials associated with unfinished
15 information of Phabricator differentials associated with unfinished
16 changesets.
16 changesets.
17
17
18 By default, Phabricator requires ``Test Plan`` which might prevent some
18 By default, Phabricator requires ``Test Plan`` which might prevent some
19 changeset from being sent. The requirement could be disabled by changing
19 changeset from being sent. The requirement could be disabled by changing
20 ``differential.require-test-plan-field`` config server side.
20 ``differential.require-test-plan-field`` config server side.
21
21
22 Config::
22 Config::
23
23
24 [phabricator]
24 [phabricator]
25 # Phabricator URL
25 # Phabricator URL
26 url = https://phab.example.com/
26 url = https://phab.example.com/
27
27
28 # Repo callsign. If a repo has a URL https://$HOST/diffusion/FOO, then its
28 # Repo callsign. If a repo has a URL https://$HOST/diffusion/FOO, then its
29 # callsign is "FOO".
29 # callsign is "FOO".
30 callsign = FOO
30 callsign = FOO
31
31
32 # curl command to use. If not set (default), use builtin HTTP library to
32 # curl command to use. If not set (default), use builtin HTTP library to
33 # communicate. If set, use the specified curl command. This could be useful
33 # communicate. If set, use the specified curl command. This could be useful
34 # if you need to specify advanced options that is not easily supported by
34 # if you need to specify advanced options that is not easily supported by
35 # the internal library.
35 # the internal library.
36 curlcmd = curl --connect-timeout 2 --retry 3 --silent
36 curlcmd = curl --connect-timeout 2 --retry 3 --silent
37
37
38 # retry failed command N time (default 0). Useful when using the extension
38 # retry failed command N time (default 0). Useful when using the extension
39 # over flakly connection.
39 # over flakly connection.
40 #
40 #
41 # We wait `retry.interval` between each retry, in seconds.
41 # We wait `retry.interval` between each retry, in seconds.
42 # (default 1 second).
42 # (default 1 second).
43 retry = 3
43 retry = 3
44 retry.interval = 10
44 retry.interval = 10
45
45
46 # the retry option can combine well with the http.timeout one.
46 # the retry option can combine well with the http.timeout one.
47 #
47 #
48 # For example to give up on http request after 20 seconds:
48 # For example to give up on http request after 20 seconds:
49 [http]
49 [http]
50 timeout=20
50 timeout=20
51
51
52 [auth]
52 [auth]
53 example.schemes = https
53 example.schemes = https
54 example.prefix = phab.example.com
54 example.prefix = phab.example.com
55
55
56 # API token. Get it from https://$HOST/conduit/login/
56 # API token. Get it from https://$HOST/conduit/login/
57 example.phabtoken = cli-xxxxxxxxxxxxxxxxxxxxxxxxxxxx
57 example.phabtoken = cli-xxxxxxxxxxxxxxxxxxxxxxxxxxxx
58 """
58 """
59
59
60 from __future__ import absolute_import
60 from __future__ import absolute_import
61
61
62 import base64
62 import base64
63 import contextlib
63 import contextlib
64 import hashlib
64 import hashlib
65 import io
65 import itertools
66 import itertools
66 import json
67 import json
67 import mimetypes
68 import mimetypes
68 import operator
69 import operator
69 import re
70 import re
70 import time
71 import time
71
72
72 from mercurial.node import bin, short
73 from mercurial.node import bin, short
73 from mercurial.i18n import _
74 from mercurial.i18n import _
74 from mercurial.pycompat import getattr
75 from mercurial.pycompat import getattr
75 from mercurial.thirdparty import attr
76 from mercurial.thirdparty import attr
76 from mercurial import (
77 from mercurial import (
77 cmdutil,
78 cmdutil,
78 context,
79 context,
79 copies,
80 copies,
80 encoding,
81 encoding,
81 error,
82 error,
82 exthelper,
83 exthelper,
83 graphmod,
84 graphmod,
84 httpconnection as httpconnectionmod,
85 httpconnection as httpconnectionmod,
85 localrepo,
86 localrepo,
86 logcmdutil,
87 logcmdutil,
87 match,
88 match,
88 mdiff,
89 mdiff,
89 obsutil,
90 obsutil,
90 parser,
91 parser,
91 patch,
92 patch,
92 phases,
93 phases,
93 pycompat,
94 pycompat,
94 rewriteutil,
95 rewriteutil,
95 scmutil,
96 scmutil,
96 smartset,
97 smartset,
97 tags,
98 tags,
98 templatefilters,
99 templatefilters,
99 templateutil,
100 templateutil,
100 url as urlmod,
101 url as urlmod,
101 util,
102 util,
102 )
103 )
103 from mercurial.utils import (
104 from mercurial.utils import (
104 procutil,
105 procutil,
105 stringutil,
106 stringutil,
106 urlutil,
107 urlutil,
107 )
108 )
108 from . import show
109 from . import show
109
110
110
111
111 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
112 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
112 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
113 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
113 # be specifying the version(s) of Mercurial they are tested with, or
114 # be specifying the version(s) of Mercurial they are tested with, or
114 # leave the attribute unspecified.
115 # leave the attribute unspecified.
115 testedwith = b'ships-with-hg-core'
116 testedwith = b'ships-with-hg-core'
116
117
117 eh = exthelper.exthelper()
118 eh = exthelper.exthelper()
118
119
119 cmdtable = eh.cmdtable
120 cmdtable = eh.cmdtable
120 command = eh.command
121 command = eh.command
121 configtable = eh.configtable
122 configtable = eh.configtable
122 templatekeyword = eh.templatekeyword
123 templatekeyword = eh.templatekeyword
123 uisetup = eh.finaluisetup
124 uisetup = eh.finaluisetup
124
125
125 # developer config: phabricator.batchsize
126 # developer config: phabricator.batchsize
126 eh.configitem(
127 eh.configitem(
127 b'phabricator',
128 b'phabricator',
128 b'batchsize',
129 b'batchsize',
129 default=12,
130 default=12,
130 )
131 )
131 eh.configitem(
132 eh.configitem(
132 b'phabricator',
133 b'phabricator',
133 b'callsign',
134 b'callsign',
134 default=None,
135 default=None,
135 )
136 )
136 eh.configitem(
137 eh.configitem(
137 b'phabricator',
138 b'phabricator',
138 b'curlcmd',
139 b'curlcmd',
139 default=None,
140 default=None,
140 )
141 )
141 # developer config: phabricator.debug
142 # developer config: phabricator.debug
142 eh.configitem(
143 eh.configitem(
143 b'phabricator',
144 b'phabricator',
144 b'debug',
145 b'debug',
145 default=False,
146 default=False,
146 )
147 )
147 # developer config: phabricator.repophid
148 # developer config: phabricator.repophid
148 eh.configitem(
149 eh.configitem(
149 b'phabricator',
150 b'phabricator',
150 b'repophid',
151 b'repophid',
151 default=None,
152 default=None,
152 )
153 )
153 eh.configitem(
154 eh.configitem(
154 b'phabricator',
155 b'phabricator',
155 b'retry',
156 b'retry',
156 default=0,
157 default=0,
157 )
158 )
158 eh.configitem(
159 eh.configitem(
159 b'phabricator',
160 b'phabricator',
160 b'retry.interval',
161 b'retry.interval',
161 default=1,
162 default=1,
162 )
163 )
163 eh.configitem(
164 eh.configitem(
164 b'phabricator',
165 b'phabricator',
165 b'url',
166 b'url',
166 default=None,
167 default=None,
167 )
168 )
168 eh.configitem(
169 eh.configitem(
169 b'phabsend',
170 b'phabsend',
170 b'confirm',
171 b'confirm',
171 default=False,
172 default=False,
172 )
173 )
173 eh.configitem(
174 eh.configitem(
174 b'phabimport',
175 b'phabimport',
175 b'secret',
176 b'secret',
176 default=False,
177 default=False,
177 )
178 )
178 eh.configitem(
179 eh.configitem(
179 b'phabimport',
180 b'phabimport',
180 b'obsolete',
181 b'obsolete',
181 default=False,
182 default=False,
182 )
183 )
183
184
184 colortable = {
185 colortable = {
185 b'phabricator.action.created': b'green',
186 b'phabricator.action.created': b'green',
186 b'phabricator.action.skipped': b'magenta',
187 b'phabricator.action.skipped': b'magenta',
187 b'phabricator.action.updated': b'magenta',
188 b'phabricator.action.updated': b'magenta',
188 b'phabricator.drev': b'bold',
189 b'phabricator.drev': b'bold',
189 b'phabricator.status.abandoned': b'magenta dim',
190 b'phabricator.status.abandoned': b'magenta dim',
190 b'phabricator.status.accepted': b'green bold',
191 b'phabricator.status.accepted': b'green bold',
191 b'phabricator.status.closed': b'green',
192 b'phabricator.status.closed': b'green',
192 b'phabricator.status.needsreview': b'yellow',
193 b'phabricator.status.needsreview': b'yellow',
193 b'phabricator.status.needsrevision': b'red',
194 b'phabricator.status.needsrevision': b'red',
194 b'phabricator.status.changesplanned': b'red',
195 b'phabricator.status.changesplanned': b'red',
195 }
196 }
196
197
197 _VCR_FLAGS = [
198 _VCR_FLAGS = [
198 (
199 (
199 b'',
200 b'',
200 b'test-vcr',
201 b'test-vcr',
201 b'',
202 b'',
202 _(
203 _(
203 b'Path to a vcr file. If nonexistent, will record a new vcr transcript'
204 b'Path to a vcr file. If nonexistent, will record a new vcr transcript'
204 b', otherwise will mock all http requests using the specified vcr file.'
205 b', otherwise will mock all http requests using the specified vcr file.'
205 b' (ADVANCED)'
206 b' (ADVANCED)'
206 ),
207 ),
207 ),
208 ),
208 ]
209 ]
209
210
210
211
211 @eh.wrapfunction(localrepo, "loadhgrc")
212 @eh.wrapfunction(localrepo, "loadhgrc")
212 def _loadhgrc(orig, ui, wdirvfs, hgvfs, requirements, *args, **opts):
213 def _loadhgrc(orig, ui, wdirvfs, hgvfs, requirements, *args, **opts):
213 """Load ``.arcconfig`` content into a ui instance on repository open."""
214 """Load ``.arcconfig`` content into a ui instance on repository open."""
214 result = False
215 result = False
215 arcconfig = {}
216 arcconfig = {}
216
217
217 try:
218 try:
218 # json.loads only accepts bytes from 3.6+
219 # json.loads only accepts bytes from 3.6+
219 rawparams = encoding.unifromlocal(wdirvfs.read(b".arcconfig"))
220 rawparams = encoding.unifromlocal(wdirvfs.read(b".arcconfig"))
220 # json.loads only returns unicode strings
221 # json.loads only returns unicode strings
221 arcconfig = pycompat.rapply(
222 arcconfig = pycompat.rapply(
222 lambda x: encoding.unitolocal(x)
223 lambda x: encoding.unitolocal(x)
223 if isinstance(x, pycompat.unicode)
224 if isinstance(x, pycompat.unicode)
224 else x,
225 else x,
225 pycompat.json_loads(rawparams),
226 pycompat.json_loads(rawparams),
226 )
227 )
227
228
228 result = True
229 result = True
229 except ValueError:
230 except ValueError:
230 ui.warn(_(b"invalid JSON in %s\n") % wdirvfs.join(b".arcconfig"))
231 ui.warn(_(b"invalid JSON in %s\n") % wdirvfs.join(b".arcconfig"))
231 except IOError:
232 except IOError:
232 pass
233 pass
233
234
234 cfg = util.sortdict()
235 cfg = util.sortdict()
235
236
236 if b"repository.callsign" in arcconfig:
237 if b"repository.callsign" in arcconfig:
237 cfg[(b"phabricator", b"callsign")] = arcconfig[b"repository.callsign"]
238 cfg[(b"phabricator", b"callsign")] = arcconfig[b"repository.callsign"]
238
239
239 if b"phabricator.uri" in arcconfig:
240 if b"phabricator.uri" in arcconfig:
240 cfg[(b"phabricator", b"url")] = arcconfig[b"phabricator.uri"]
241 cfg[(b"phabricator", b"url")] = arcconfig[b"phabricator.uri"]
241
242
242 if cfg:
243 if cfg:
243 ui.applyconfig(cfg, source=wdirvfs.join(b".arcconfig"))
244 ui.applyconfig(cfg, source=wdirvfs.join(b".arcconfig"))
244
245
245 return (
246 return (
246 orig(ui, wdirvfs, hgvfs, requirements, *args, **opts) or result
247 orig(ui, wdirvfs, hgvfs, requirements, *args, **opts) or result
247 ) # Load .hg/hgrc
248 ) # Load .hg/hgrc
248
249
249
250
250 def vcrcommand(name, flags, spec, helpcategory=None, optionalrepo=False):
251 def vcrcommand(name, flags, spec, helpcategory=None, optionalrepo=False):
251 fullflags = flags + _VCR_FLAGS
252 fullflags = flags + _VCR_FLAGS
252
253
253 def hgmatcher(r1, r2):
254 def hgmatcher(r1, r2):
254 if r1.uri != r2.uri or r1.method != r2.method:
255 if r1.uri != r2.uri or r1.method != r2.method:
255 return False
256 return False
256 r1params = util.urlreq.parseqs(r1.body)
257 r1params = util.urlreq.parseqs(r1.body)
257 r2params = util.urlreq.parseqs(r2.body)
258 r2params = util.urlreq.parseqs(r2.body)
258 for key in r1params:
259 for key in r1params:
259 if key not in r2params:
260 if key not in r2params:
260 return False
261 return False
261 value = r1params[key][0]
262 value = r1params[key][0]
262 # we want to compare json payloads without worrying about ordering
263 # we want to compare json payloads without worrying about ordering
263 if value.startswith(b'{') and value.endswith(b'}'):
264 if value.startswith(b'{') and value.endswith(b'}'):
264 r1json = pycompat.json_loads(value)
265 r1json = pycompat.json_loads(value)
265 r2json = pycompat.json_loads(r2params[key][0])
266 r2json = pycompat.json_loads(r2params[key][0])
266 if r1json != r2json:
267 if r1json != r2json:
267 return False
268 return False
268 elif r2params[key][0] != value:
269 elif r2params[key][0] != value:
269 return False
270 return False
270 return True
271 return True
271
272
272 def sanitiserequest(request):
273 def sanitiserequest(request):
273 request.body = re.sub(
274 request.body = re.sub(
274 br'cli-[a-z0-9]+', br'cli-hahayouwish', request.body
275 br'cli-[a-z0-9]+', br'cli-hahayouwish', request.body
275 )
276 )
276 return request
277 return request
277
278
278 def sanitiseresponse(response):
279 def sanitiseresponse(response):
279 if 'set-cookie' in response['headers']:
280 if 'set-cookie' in response['headers']:
280 del response['headers']['set-cookie']
281 del response['headers']['set-cookie']
281 return response
282 return response
282
283
283 def decorate(fn):
284 def decorate(fn):
284 def inner(*args, **kwargs):
285 def inner(*args, **kwargs):
285 vcr = kwargs.pop('test_vcr')
286 vcr = kwargs.pop('test_vcr')
286 if vcr:
287 if vcr:
287 cassette = pycompat.fsdecode(vcr)
288 cassette = pycompat.fsdecode(vcr)
288 import hgdemandimport
289 import hgdemandimport
289
290
290 with hgdemandimport.deactivated():
291 with hgdemandimport.deactivated():
291 import vcr as vcrmod
292 import vcr as vcrmod
292 import vcr.stubs as stubs
293 import vcr.stubs as stubs
293
294
294 vcr = vcrmod.VCR(
295 vcr = vcrmod.VCR(
295 serializer='json',
296 serializer='json',
296 before_record_request=sanitiserequest,
297 before_record_request=sanitiserequest,
297 before_record_response=sanitiseresponse,
298 before_record_response=sanitiseresponse,
298 custom_patches=[
299 custom_patches=[
299 (
300 (
300 urlmod,
301 urlmod,
301 'httpconnection',
302 'httpconnection',
302 stubs.VCRHTTPConnection,
303 stubs.VCRHTTPConnection,
303 ),
304 ),
304 (
305 (
305 urlmod,
306 urlmod,
306 'httpsconnection',
307 'httpsconnection',
307 stubs.VCRHTTPSConnection,
308 stubs.VCRHTTPSConnection,
308 ),
309 ),
309 ],
310 ],
310 )
311 )
311 vcr.register_matcher('hgmatcher', hgmatcher)
312 vcr.register_matcher('hgmatcher', hgmatcher)
312 with vcr.use_cassette(cassette, match_on=['hgmatcher']):
313 with vcr.use_cassette(cassette, match_on=['hgmatcher']):
313 return fn(*args, **kwargs)
314 return fn(*args, **kwargs)
314 return fn(*args, **kwargs)
315 return fn(*args, **kwargs)
315
316
316 cmd = util.checksignature(inner, depth=2)
317 cmd = util.checksignature(inner, depth=2)
317 cmd.__name__ = fn.__name__
318 cmd.__name__ = fn.__name__
318 cmd.__doc__ = fn.__doc__
319 cmd.__doc__ = fn.__doc__
319
320
320 return command(
321 return command(
321 name,
322 name,
322 fullflags,
323 fullflags,
323 spec,
324 spec,
324 helpcategory=helpcategory,
325 helpcategory=helpcategory,
325 optionalrepo=optionalrepo,
326 optionalrepo=optionalrepo,
326 )(cmd)
327 )(cmd)
327
328
328 return decorate
329 return decorate
329
330
330
331
331 def _debug(ui, *msg, **opts):
332 def _debug(ui, *msg, **opts):
332 """write debug output for Phabricator if ``phabricator.debug`` is set
333 """write debug output for Phabricator if ``phabricator.debug`` is set
333
334
334 Specifically, this avoids dumping Conduit and HTTP auth chatter that is
335 Specifically, this avoids dumping Conduit and HTTP auth chatter that is
335 printed with the --debug argument.
336 printed with the --debug argument.
336 """
337 """
337 if ui.configbool(b"phabricator", b"debug"):
338 if ui.configbool(b"phabricator", b"debug"):
338 flag = ui.debugflag
339 flag = ui.debugflag
339 try:
340 try:
340 ui.debugflag = True
341 ui.debugflag = True
341 ui.write(*msg, **opts)
342 ui.write(*msg, **opts)
342 finally:
343 finally:
343 ui.debugflag = flag
344 ui.debugflag = flag
344
345
345
346
346 def urlencodenested(params):
347 def urlencodenested(params):
347 """like urlencode, but works with nested parameters.
348 """like urlencode, but works with nested parameters.
348
349
349 For example, if params is {'a': ['b', 'c'], 'd': {'e': 'f'}}, it will be
350 For example, if params is {'a': ['b', 'c'], 'd': {'e': 'f'}}, it will be
350 flattened to {'a[0]': 'b', 'a[1]': 'c', 'd[e]': 'f'} and then passed to
351 flattened to {'a[0]': 'b', 'a[1]': 'c', 'd[e]': 'f'} and then passed to
351 urlencode. Note: the encoding is consistent with PHP's http_build_query.
352 urlencode. Note: the encoding is consistent with PHP's http_build_query.
352 """
353 """
353 flatparams = util.sortdict()
354 flatparams = util.sortdict()
354
355
355 def process(prefix, obj):
356 def process(prefix, obj):
356 if isinstance(obj, bool):
357 if isinstance(obj, bool):
357 obj = {True: b'true', False: b'false'}[obj] # Python -> PHP form
358 obj = {True: b'true', False: b'false'}[obj] # Python -> PHP form
358 lister = lambda l: [(b'%d' % k, v) for k, v in enumerate(l)]
359 lister = lambda l: [(b'%d' % k, v) for k, v in enumerate(l)]
359 items = {list: lister, dict: lambda x: x.items()}.get(type(obj))
360 items = {list: lister, dict: lambda x: x.items()}.get(type(obj))
360 if items is None:
361 if items is None:
361 flatparams[prefix] = obj
362 flatparams[prefix] = obj
362 else:
363 else:
363 for k, v in items(obj):
364 for k, v in items(obj):
364 if prefix:
365 if prefix:
365 process(b'%s[%s]' % (prefix, k), v)
366 process(b'%s[%s]' % (prefix, k), v)
366 else:
367 else:
367 process(k, v)
368 process(k, v)
368
369
369 process(b'', params)
370 process(b'', params)
370 return urlutil.urlreq.urlencode(flatparams)
371 return urlutil.urlreq.urlencode(flatparams)
371
372
372
373
373 def readurltoken(ui):
374 def readurltoken(ui):
374 """return conduit url, token and make sure they exist
375 """return conduit url, token and make sure they exist
375
376
376 Currently read from [auth] config section. In the future, it might
377 Currently read from [auth] config section. In the future, it might
377 make sense to read from .arcconfig and .arcrc as well.
378 make sense to read from .arcconfig and .arcrc as well.
378 """
379 """
379 url = ui.config(b'phabricator', b'url')
380 url = ui.config(b'phabricator', b'url')
380 if not url:
381 if not url:
381 raise error.Abort(
382 raise error.Abort(
382 _(b'config %s.%s is required') % (b'phabricator', b'url')
383 _(b'config %s.%s is required') % (b'phabricator', b'url')
383 )
384 )
384
385
385 res = httpconnectionmod.readauthforuri(ui, url, urlutil.url(url).user)
386 res = httpconnectionmod.readauthforuri(ui, url, urlutil.url(url).user)
386 token = None
387 token = None
387
388
388 if res:
389 if res:
389 group, auth = res
390 group, auth = res
390
391
391 ui.debug(b"using auth.%s.* for authentication\n" % group)
392 ui.debug(b"using auth.%s.* for authentication\n" % group)
392
393
393 token = auth.get(b'phabtoken')
394 token = auth.get(b'phabtoken')
394
395
395 if not token:
396 if not token:
396 raise error.Abort(
397 raise error.Abort(
397 _(b'Can\'t find conduit token associated to %s') % (url,)
398 _(b'Can\'t find conduit token associated to %s') % (url,)
398 )
399 )
399
400
400 return url, token
401 return url, token
401
402
402
403
403 def callconduit(ui, name, params):
404 def callconduit(ui, name, params):
404 """call Conduit API, params is a dict. return json.loads result, or None"""
405 """call Conduit API, params is a dict. return json.loads result, or None"""
405 host, token = readurltoken(ui)
406 host, token = readurltoken(ui)
406 url, authinfo = urlutil.url(b'/'.join([host, b'api', name])).authinfo()
407 url, authinfo = urlutil.url(b'/'.join([host, b'api', name])).authinfo()
407 ui.debug(b'Conduit Call: %s %s\n' % (url, pycompat.byterepr(params)))
408 ui.debug(b'Conduit Call: %s %s\n' % (url, pycompat.byterepr(params)))
408 params = params.copy()
409 params = params.copy()
409 params[b'__conduit__'] = {
410 params[b'__conduit__'] = {
410 b'token': token,
411 b'token': token,
411 }
412 }
412 rawdata = {
413 rawdata = {
413 b'params': templatefilters.json(params),
414 b'params': templatefilters.json(params),
414 b'output': b'json',
415 b'output': b'json',
415 b'__conduit__': 1,
416 b'__conduit__': 1,
416 }
417 }
417 data = urlencodenested(rawdata)
418 data = urlencodenested(rawdata)
418 curlcmd = ui.config(b'phabricator', b'curlcmd')
419 curlcmd = ui.config(b'phabricator', b'curlcmd')
419 if curlcmd:
420 if curlcmd:
420 sin, sout = procutil.popen2(
421 sin, sout = procutil.popen2(
421 b'%s -d @- %s' % (curlcmd, procutil.shellquote(url))
422 b'%s -d @- %s' % (curlcmd, procutil.shellquote(url))
422 )
423 )
423 sin.write(data)
424 sin.write(data)
424 sin.close()
425 sin.close()
425 body = sout.read()
426 body = sout.read()
426 else:
427 else:
427 urlopener = urlmod.opener(ui, authinfo)
428 urlopener = urlmod.opener(ui, authinfo)
428 request = util.urlreq.request(pycompat.strurl(url), data=data)
429 request = util.urlreq.request(pycompat.strurl(url), data=data)
429 max_try = ui.configint(b'phabricator', b'retry') + 1
430 max_try = ui.configint(b'phabricator', b'retry') + 1
430 timeout = ui.configwith(float, b'http', b'timeout')
431 timeout = ui.configwith(float, b'http', b'timeout')
431 for try_count in range(max_try):
432 for try_count in range(max_try):
432 try:
433 try:
433 with contextlib.closing(
434 with contextlib.closing(
434 urlopener.open(request, timeout=timeout)
435 urlopener.open(request, timeout=timeout)
435 ) as rsp:
436 ) as rsp:
436 body = rsp.read()
437 body = rsp.read()
437 break
438 break
438 except util.urlerr.urlerror as err:
439 except util.urlerr.urlerror as err:
439 if try_count == max_try - 1:
440 if try_count == max_try - 1:
440 raise
441 raise
441 ui.debug(
442 ui.debug(
442 b'Conduit Request failed (try %d/%d): %r\n'
443 b'Conduit Request failed (try %d/%d): %r\n'
443 % (try_count + 1, max_try, err)
444 % (try_count + 1, max_try, err)
444 )
445 )
445 # failing request might come from overloaded server
446 # failing request might come from overloaded server
446 retry_interval = ui.configint(b'phabricator', b'retry.interval')
447 retry_interval = ui.configint(b'phabricator', b'retry.interval')
447 time.sleep(retry_interval)
448 time.sleep(retry_interval)
448 ui.debug(b'Conduit Response: %s\n' % body)
449 ui.debug(b'Conduit Response: %s\n' % body)
449 parsed = pycompat.rapply(
450 parsed = pycompat.rapply(
450 lambda x: encoding.unitolocal(x)
451 lambda x: encoding.unitolocal(x)
451 if isinstance(x, pycompat.unicode)
452 if isinstance(x, pycompat.unicode)
452 else x,
453 else x,
453 # json.loads only accepts bytes from py3.6+
454 # json.loads only accepts bytes from py3.6+
454 pycompat.json_loads(encoding.unifromlocal(body)),
455 pycompat.json_loads(encoding.unifromlocal(body)),
455 )
456 )
456 if parsed.get(b'error_code'):
457 if parsed.get(b'error_code'):
457 msg = _(b'Conduit Error (%s): %s') % (
458 msg = _(b'Conduit Error (%s): %s') % (
458 parsed[b'error_code'],
459 parsed[b'error_code'],
459 parsed[b'error_info'],
460 parsed[b'error_info'],
460 )
461 )
461 raise error.Abort(msg)
462 raise error.Abort(msg)
462 return parsed[b'result']
463 return parsed[b'result']
463
464
464
465
465 @vcrcommand(b'debugcallconduit', [], _(b'METHOD'), optionalrepo=True)
466 @vcrcommand(b'debugcallconduit', [], _(b'METHOD'), optionalrepo=True)
466 def debugcallconduit(ui, repo, name):
467 def debugcallconduit(ui, repo, name):
467 """call Conduit API
468 """call Conduit API
468
469
469 Call parameters are read from stdin as a JSON blob. Result will be written
470 Call parameters are read from stdin as a JSON blob. Result will be written
470 to stdout as a JSON blob.
471 to stdout as a JSON blob.
471 """
472 """
472 # json.loads only accepts bytes from 3.6+
473 # json.loads only accepts bytes from 3.6+
473 rawparams = encoding.unifromlocal(ui.fin.read())
474 rawparams = encoding.unifromlocal(ui.fin.read())
474 # json.loads only returns unicode strings
475 # json.loads only returns unicode strings
475 params = pycompat.rapply(
476 params = pycompat.rapply(
476 lambda x: encoding.unitolocal(x)
477 lambda x: encoding.unitolocal(x)
477 if isinstance(x, pycompat.unicode)
478 if isinstance(x, pycompat.unicode)
478 else x,
479 else x,
479 pycompat.json_loads(rawparams),
480 pycompat.json_loads(rawparams),
480 )
481 )
481 # json.dumps only accepts unicode strings
482 # json.dumps only accepts unicode strings
482 result = pycompat.rapply(
483 result = pycompat.rapply(
483 lambda x: encoding.unifromlocal(x) if isinstance(x, bytes) else x,
484 lambda x: encoding.unifromlocal(x) if isinstance(x, bytes) else x,
484 callconduit(ui, name, params),
485 callconduit(ui, name, params),
485 )
486 )
486 s = json.dumps(result, sort_keys=True, indent=2, separators=(u',', u': '))
487 s = json.dumps(result, sort_keys=True, indent=2, separators=(u',', u': '))
487 ui.write(b'%s\n' % encoding.unitolocal(s))
488 ui.write(b'%s\n' % encoding.unitolocal(s))
488
489
489
490
490 def getrepophid(repo):
491 def getrepophid(repo):
491 """given callsign, return repository PHID or None"""
492 """given callsign, return repository PHID or None"""
492 # developer config: phabricator.repophid
493 # developer config: phabricator.repophid
493 repophid = repo.ui.config(b'phabricator', b'repophid')
494 repophid = repo.ui.config(b'phabricator', b'repophid')
494 if repophid:
495 if repophid:
495 return repophid
496 return repophid
496 callsign = repo.ui.config(b'phabricator', b'callsign')
497 callsign = repo.ui.config(b'phabricator', b'callsign')
497 if not callsign:
498 if not callsign:
498 return None
499 return None
499 query = callconduit(
500 query = callconduit(
500 repo.ui,
501 repo.ui,
501 b'diffusion.repository.search',
502 b'diffusion.repository.search',
502 {b'constraints': {b'callsigns': [callsign]}},
503 {b'constraints': {b'callsigns': [callsign]}},
503 )
504 )
504 if len(query[b'data']) == 0:
505 if len(query[b'data']) == 0:
505 return None
506 return None
506 repophid = query[b'data'][0][b'phid']
507 repophid = query[b'data'][0][b'phid']
507 repo.ui.setconfig(b'phabricator', b'repophid', repophid)
508 repo.ui.setconfig(b'phabricator', b'repophid', repophid)
508 return repophid
509 return repophid
509
510
510
511
511 _differentialrevisiontagre = re.compile(br'\AD([1-9][0-9]*)\Z')
512 _differentialrevisiontagre = re.compile(br'\AD([1-9][0-9]*)\Z')
512 _differentialrevisiondescre = re.compile(
513 _differentialrevisiondescre = re.compile(
513 br'^Differential Revision:\s*(?P<url>(?:.*)D(?P<id>[1-9][0-9]*))$', re.M
514 br'^Differential Revision:\s*(?P<url>(?:.*)D(?P<id>[1-9][0-9]*))$', re.M
514 )
515 )
515
516
516
517
517 def getoldnodedrevmap(repo, nodelist):
518 def getoldnodedrevmap(repo, nodelist):
518 """find previous nodes that has been sent to Phabricator
519 """find previous nodes that has been sent to Phabricator
519
520
520 return {node: (oldnode, Differential diff, Differential Revision ID)}
521 return {node: (oldnode, Differential diff, Differential Revision ID)}
521 for node in nodelist with known previous sent versions, or associated
522 for node in nodelist with known previous sent versions, or associated
522 Differential Revision IDs. ``oldnode`` and ``Differential diff`` could
523 Differential Revision IDs. ``oldnode`` and ``Differential diff`` could
523 be ``None``.
524 be ``None``.
524
525
525 Examines commit messages like "Differential Revision:" to get the
526 Examines commit messages like "Differential Revision:" to get the
526 association information.
527 association information.
527
528
528 If such commit message line is not found, examines all precursors and their
529 If such commit message line is not found, examines all precursors and their
529 tags. Tags with format like "D1234" are considered a match and the node
530 tags. Tags with format like "D1234" are considered a match and the node
530 with that tag, and the number after "D" (ex. 1234) will be returned.
531 with that tag, and the number after "D" (ex. 1234) will be returned.
531
532
532 The ``old node``, if not None, is guaranteed to be the last diff of
533 The ``old node``, if not None, is guaranteed to be the last diff of
533 corresponding Differential Revision, and exist in the repo.
534 corresponding Differential Revision, and exist in the repo.
534 """
535 """
535 unfi = repo.unfiltered()
536 unfi = repo.unfiltered()
536 has_node = unfi.changelog.index.has_node
537 has_node = unfi.changelog.index.has_node
537
538
538 result = {} # {node: (oldnode?, lastdiff?, drev)}
539 result = {} # {node: (oldnode?, lastdiff?, drev)}
539 # ordered for test stability when printing new -> old mapping below
540 # ordered for test stability when printing new -> old mapping below
540 toconfirm = util.sortdict() # {node: (force, {precnode}, drev)}
541 toconfirm = util.sortdict() # {node: (force, {precnode}, drev)}
541 for node in nodelist:
542 for node in nodelist:
542 ctx = unfi[node]
543 ctx = unfi[node]
543 # For tags like "D123", put them into "toconfirm" to verify later
544 # For tags like "D123", put them into "toconfirm" to verify later
544 precnodes = list(obsutil.allpredecessors(unfi.obsstore, [node]))
545 precnodes = list(obsutil.allpredecessors(unfi.obsstore, [node]))
545 for n in precnodes:
546 for n in precnodes:
546 if has_node(n):
547 if has_node(n):
547 for tag in unfi.nodetags(n):
548 for tag in unfi.nodetags(n):
548 m = _differentialrevisiontagre.match(tag)
549 m = _differentialrevisiontagre.match(tag)
549 if m:
550 if m:
550 toconfirm[node] = (0, set(precnodes), int(m.group(1)))
551 toconfirm[node] = (0, set(precnodes), int(m.group(1)))
551 break
552 break
552 else:
553 else:
553 continue # move to next predecessor
554 continue # move to next predecessor
554 break # found a tag, stop
555 break # found a tag, stop
555 else:
556 else:
556 # Check commit message
557 # Check commit message
557 m = _differentialrevisiondescre.search(ctx.description())
558 m = _differentialrevisiondescre.search(ctx.description())
558 if m:
559 if m:
559 toconfirm[node] = (1, set(precnodes), int(m.group('id')))
560 toconfirm[node] = (1, set(precnodes), int(m.group('id')))
560
561
561 # Double check if tags are genuine by collecting all old nodes from
562 # Double check if tags are genuine by collecting all old nodes from
562 # Phabricator, and expect precursors overlap with it.
563 # Phabricator, and expect precursors overlap with it.
563 if toconfirm:
564 if toconfirm:
564 drevs = [drev for force, precs, drev in toconfirm.values()]
565 drevs = [drev for force, precs, drev in toconfirm.values()]
565 alldiffs = callconduit(
566 alldiffs = callconduit(
566 unfi.ui, b'differential.querydiffs', {b'revisionIDs': drevs}
567 unfi.ui, b'differential.querydiffs', {b'revisionIDs': drevs}
567 )
568 )
568
569
569 def getnodes(d, precset):
570 def getnodes(d, precset):
570 # Ignore other nodes that were combined into the Differential
571 # Ignore other nodes that were combined into the Differential
571 # that aren't predecessors of the current local node.
572 # that aren't predecessors of the current local node.
572 return [n for n in getlocalcommits(d) if n in precset]
573 return [n for n in getlocalcommits(d) if n in precset]
573
574
574 for newnode, (force, precset, drev) in toconfirm.items():
575 for newnode, (force, precset, drev) in toconfirm.items():
575 diffs = [
576 diffs = [
576 d for d in alldiffs.values() if int(d[b'revisionID']) == drev
577 d for d in alldiffs.values() if int(d[b'revisionID']) == drev
577 ]
578 ]
578
579
579 # local predecessors known by Phabricator
580 # local predecessors known by Phabricator
580 phprecset = {n for d in diffs for n in getnodes(d, precset)}
581 phprecset = {n for d in diffs for n in getnodes(d, precset)}
581
582
582 # Ignore if precursors (Phabricator and local repo) do not overlap,
583 # Ignore if precursors (Phabricator and local repo) do not overlap,
583 # and force is not set (when commit message says nothing)
584 # and force is not set (when commit message says nothing)
584 if not force and not phprecset:
585 if not force and not phprecset:
585 tagname = b'D%d' % drev
586 tagname = b'D%d' % drev
586 tags.tag(
587 tags.tag(
587 repo,
588 repo,
588 tagname,
589 tagname,
589 repo.nullid,
590 repo.nullid,
590 message=None,
591 message=None,
591 user=None,
592 user=None,
592 date=None,
593 date=None,
593 local=True,
594 local=True,
594 )
595 )
595 unfi.ui.warn(
596 unfi.ui.warn(
596 _(
597 _(
597 b'D%d: local tag removed - does not match '
598 b'D%d: local tag removed - does not match '
598 b'Differential history\n'
599 b'Differential history\n'
599 )
600 )
600 % drev
601 % drev
601 )
602 )
602 continue
603 continue
603
604
604 # Find the last node using Phabricator metadata, and make sure it
605 # Find the last node using Phabricator metadata, and make sure it
605 # exists in the repo
606 # exists in the repo
606 oldnode = lastdiff = None
607 oldnode = lastdiff = None
607 if diffs:
608 if diffs:
608 lastdiff = max(diffs, key=lambda d: int(d[b'id']))
609 lastdiff = max(diffs, key=lambda d: int(d[b'id']))
609 oldnodes = getnodes(lastdiff, precset)
610 oldnodes = getnodes(lastdiff, precset)
610
611
611 _debug(
612 _debug(
612 unfi.ui,
613 unfi.ui,
613 b"%s mapped to old nodes %s\n"
614 b"%s mapped to old nodes %s\n"
614 % (
615 % (
615 short(newnode),
616 short(newnode),
616 stringutil.pprint([short(n) for n in sorted(oldnodes)]),
617 stringutil.pprint([short(n) for n in sorted(oldnodes)]),
617 ),
618 ),
618 )
619 )
619
620
620 # If this commit was the result of `hg fold` after submission,
621 # If this commit was the result of `hg fold` after submission,
621 # and now resubmitted with --fold, the easiest thing to do is
622 # and now resubmitted with --fold, the easiest thing to do is
622 # to leave the node clear. This only results in creating a new
623 # to leave the node clear. This only results in creating a new
623 # diff for the _same_ Differential Revision if this commit is
624 # diff for the _same_ Differential Revision if this commit is
624 # the first or last in the selected range. If we picked a node
625 # the first or last in the selected range. If we picked a node
625 # from the list instead, it would have to be the lowest if at
626 # from the list instead, it would have to be the lowest if at
626 # the beginning of the --fold range, or the highest at the end.
627 # the beginning of the --fold range, or the highest at the end.
627 # Otherwise, one or more of the nodes wouldn't be considered in
628 # Otherwise, one or more of the nodes wouldn't be considered in
628 # the diff, and the Differential wouldn't be properly updated.
629 # the diff, and the Differential wouldn't be properly updated.
629 # If this commit is the result of `hg split` in the same
630 # If this commit is the result of `hg split` in the same
630 # scenario, there is a single oldnode here (and multiple
631 # scenario, there is a single oldnode here (and multiple
631 # newnodes mapped to it). That makes it the same as the normal
632 # newnodes mapped to it). That makes it the same as the normal
632 # case, as the edges of the newnode range cleanly maps to one
633 # case, as the edges of the newnode range cleanly maps to one
633 # oldnode each.
634 # oldnode each.
634 if len(oldnodes) == 1:
635 if len(oldnodes) == 1:
635 oldnode = oldnodes[0]
636 oldnode = oldnodes[0]
636 if oldnode and not has_node(oldnode):
637 if oldnode and not has_node(oldnode):
637 oldnode = None
638 oldnode = None
638
639
639 result[newnode] = (oldnode, lastdiff, drev)
640 result[newnode] = (oldnode, lastdiff, drev)
640
641
641 return result
642 return result
642
643
643
644
644 def getdrevmap(repo, revs):
645 def getdrevmap(repo, revs):
645 """Return a dict mapping each rev in `revs` to their Differential Revision
646 """Return a dict mapping each rev in `revs` to their Differential Revision
646 ID or None.
647 ID or None.
647 """
648 """
648 result = {}
649 result = {}
649 for rev in revs:
650 for rev in revs:
650 result[rev] = None
651 result[rev] = None
651 ctx = repo[rev]
652 ctx = repo[rev]
652 # Check commit message
653 # Check commit message
653 m = _differentialrevisiondescre.search(ctx.description())
654 m = _differentialrevisiondescre.search(ctx.description())
654 if m:
655 if m:
655 result[rev] = int(m.group('id'))
656 result[rev] = int(m.group('id'))
656 continue
657 continue
657 # Check tags
658 # Check tags
658 for tag in repo.nodetags(ctx.node()):
659 for tag in repo.nodetags(ctx.node()):
659 m = _differentialrevisiontagre.match(tag)
660 m = _differentialrevisiontagre.match(tag)
660 if m:
661 if m:
661 result[rev] = int(m.group(1))
662 result[rev] = int(m.group(1))
662 break
663 break
663
664
664 return result
665 return result
665
666
666
667
667 def getdiff(basectx, ctx, diffopts):
668 def getdiff(basectx, ctx, diffopts):
668 """plain-text diff without header (user, commit message, etc)"""
669 """plain-text diff without header (user, commit message, etc)"""
669 output = util.stringio()
670 output = util.stringio()
670 for chunk, _label in patch.diffui(
671 for chunk, _label in patch.diffui(
671 ctx.repo(), basectx.p1().node(), ctx.node(), None, opts=diffopts
672 ctx.repo(), basectx.p1().node(), ctx.node(), None, opts=diffopts
672 ):
673 ):
673 output.write(chunk)
674 output.write(chunk)
674 return output.getvalue()
675 return output.getvalue()
675
676
676
677
677 class DiffChangeType(object):
678 class DiffChangeType(object):
678 ADD = 1
679 ADD = 1
679 CHANGE = 2
680 CHANGE = 2
680 DELETE = 3
681 DELETE = 3
681 MOVE_AWAY = 4
682 MOVE_AWAY = 4
682 COPY_AWAY = 5
683 COPY_AWAY = 5
683 MOVE_HERE = 6
684 MOVE_HERE = 6
684 COPY_HERE = 7
685 COPY_HERE = 7
685 MULTICOPY = 8
686 MULTICOPY = 8
686
687
687
688
688 class DiffFileType(object):
689 class DiffFileType(object):
689 TEXT = 1
690 TEXT = 1
690 IMAGE = 2
691 IMAGE = 2
691 BINARY = 3
692 BINARY = 3
692
693
693
694
694 @attr.s
695 @attr.s
695 class phabhunk(dict):
696 class phabhunk(dict):
696 """Represents a Differential hunk, which is owned by a Differential change"""
697 """Represents a Differential hunk, which is owned by a Differential change"""
697
698
698 oldOffset = attr.ib(default=0) # camelcase-required
699 oldOffset = attr.ib(default=0) # camelcase-required
699 oldLength = attr.ib(default=0) # camelcase-required
700 oldLength = attr.ib(default=0) # camelcase-required
700 newOffset = attr.ib(default=0) # camelcase-required
701 newOffset = attr.ib(default=0) # camelcase-required
701 newLength = attr.ib(default=0) # camelcase-required
702 newLength = attr.ib(default=0) # camelcase-required
702 corpus = attr.ib(default='')
703 corpus = attr.ib(default='')
703 # These get added to the phabchange's equivalents
704 # These get added to the phabchange's equivalents
704 addLines = attr.ib(default=0) # camelcase-required
705 addLines = attr.ib(default=0) # camelcase-required
705 delLines = attr.ib(default=0) # camelcase-required
706 delLines = attr.ib(default=0) # camelcase-required
706
707
707
708
708 @attr.s
709 @attr.s
709 class phabchange(object):
710 class phabchange(object):
710 """Represents a Differential change, owns Differential hunks and owned by a
711 """Represents a Differential change, owns Differential hunks and owned by a
711 Differential diff. Each one represents one file in a diff.
712 Differential diff. Each one represents one file in a diff.
712 """
713 """
713
714
714 currentPath = attr.ib(default=None) # camelcase-required
715 currentPath = attr.ib(default=None) # camelcase-required
715 oldPath = attr.ib(default=None) # camelcase-required
716 oldPath = attr.ib(default=None) # camelcase-required
716 awayPaths = attr.ib(default=attr.Factory(list)) # camelcase-required
717 awayPaths = attr.ib(default=attr.Factory(list)) # camelcase-required
717 metadata = attr.ib(default=attr.Factory(dict))
718 metadata = attr.ib(default=attr.Factory(dict))
718 oldProperties = attr.ib(default=attr.Factory(dict)) # camelcase-required
719 oldProperties = attr.ib(default=attr.Factory(dict)) # camelcase-required
719 newProperties = attr.ib(default=attr.Factory(dict)) # camelcase-required
720 newProperties = attr.ib(default=attr.Factory(dict)) # camelcase-required
720 type = attr.ib(default=DiffChangeType.CHANGE)
721 type = attr.ib(default=DiffChangeType.CHANGE)
721 fileType = attr.ib(default=DiffFileType.TEXT) # camelcase-required
722 fileType = attr.ib(default=DiffFileType.TEXT) # camelcase-required
722 commitHash = attr.ib(default=None) # camelcase-required
723 commitHash = attr.ib(default=None) # camelcase-required
723 addLines = attr.ib(default=0) # camelcase-required
724 addLines = attr.ib(default=0) # camelcase-required
724 delLines = attr.ib(default=0) # camelcase-required
725 delLines = attr.ib(default=0) # camelcase-required
725 hunks = attr.ib(default=attr.Factory(list))
726 hunks = attr.ib(default=attr.Factory(list))
726
727
727 def copynewmetadatatoold(self):
728 def copynewmetadatatoold(self):
728 for key in list(self.metadata.keys()):
729 for key in list(self.metadata.keys()):
729 newkey = key.replace(b'new:', b'old:')
730 newkey = key.replace(b'new:', b'old:')
730 self.metadata[newkey] = self.metadata[key]
731 self.metadata[newkey] = self.metadata[key]
731
732
732 def addoldmode(self, value):
733 def addoldmode(self, value):
733 self.oldProperties[b'unix:filemode'] = value
734 self.oldProperties[b'unix:filemode'] = value
734
735
735 def addnewmode(self, value):
736 def addnewmode(self, value):
736 self.newProperties[b'unix:filemode'] = value
737 self.newProperties[b'unix:filemode'] = value
737
738
738 def addhunk(self, hunk):
739 def addhunk(self, hunk):
739 if not isinstance(hunk, phabhunk):
740 if not isinstance(hunk, phabhunk):
740 raise error.Abort(b'phabchange.addhunk only takes phabhunks')
741 raise error.Abort(b'phabchange.addhunk only takes phabhunks')
741 self.hunks.append(pycompat.byteskwargs(attr.asdict(hunk)))
742 self.hunks.append(pycompat.byteskwargs(attr.asdict(hunk)))
742 # It's useful to include these stats since the Phab web UI shows them,
743 # It's useful to include these stats since the Phab web UI shows them,
743 # and uses them to estimate how large a change a Revision is. Also used
744 # and uses them to estimate how large a change a Revision is. Also used
744 # in email subjects for the [+++--] bit.
745 # in email subjects for the [+++--] bit.
745 self.addLines += hunk.addLines
746 self.addLines += hunk.addLines
746 self.delLines += hunk.delLines
747 self.delLines += hunk.delLines
747
748
748
749
749 @attr.s
750 @attr.s
750 class phabdiff(object):
751 class phabdiff(object):
751 """Represents a Differential diff, owns Differential changes. Corresponds
752 """Represents a Differential diff, owns Differential changes. Corresponds
752 to a commit.
753 to a commit.
753 """
754 """
754
755
755 # Doesn't seem to be any reason to send this (output of uname -n)
756 # Doesn't seem to be any reason to send this (output of uname -n)
756 sourceMachine = attr.ib(default=b'') # camelcase-required
757 sourceMachine = attr.ib(default=b'') # camelcase-required
757 sourcePath = attr.ib(default=b'/') # camelcase-required
758 sourcePath = attr.ib(default=b'/') # camelcase-required
758 sourceControlBaseRevision = attr.ib(default=b'0' * 40) # camelcase-required
759 sourceControlBaseRevision = attr.ib(default=b'0' * 40) # camelcase-required
759 sourceControlPath = attr.ib(default=b'/') # camelcase-required
760 sourceControlPath = attr.ib(default=b'/') # camelcase-required
760 sourceControlSystem = attr.ib(default=b'hg') # camelcase-required
761 sourceControlSystem = attr.ib(default=b'hg') # camelcase-required
761 branch = attr.ib(default=b'default')
762 branch = attr.ib(default=b'default')
762 bookmark = attr.ib(default=None)
763 bookmark = attr.ib(default=None)
763 creationMethod = attr.ib(default=b'phabsend') # camelcase-required
764 creationMethod = attr.ib(default=b'phabsend') # camelcase-required
764 lintStatus = attr.ib(default=b'none') # camelcase-required
765 lintStatus = attr.ib(default=b'none') # camelcase-required
765 unitStatus = attr.ib(default=b'none') # camelcase-required
766 unitStatus = attr.ib(default=b'none') # camelcase-required
766 changes = attr.ib(default=attr.Factory(dict))
767 changes = attr.ib(default=attr.Factory(dict))
767 repositoryPHID = attr.ib(default=None) # camelcase-required
768 repositoryPHID = attr.ib(default=None) # camelcase-required
768
769
769 def addchange(self, change):
770 def addchange(self, change):
770 if not isinstance(change, phabchange):
771 if not isinstance(change, phabchange):
771 raise error.Abort(b'phabdiff.addchange only takes phabchanges')
772 raise error.Abort(b'phabdiff.addchange only takes phabchanges')
772 self.changes[change.currentPath] = pycompat.byteskwargs(
773 self.changes[change.currentPath] = pycompat.byteskwargs(
773 attr.asdict(change)
774 attr.asdict(change)
774 )
775 )
775
776
776
777
777 def maketext(pchange, basectx, ctx, fname):
778 def maketext(pchange, basectx, ctx, fname):
778 """populate the phabchange for a text file"""
779 """populate the phabchange for a text file"""
779 repo = ctx.repo()
780 repo = ctx.repo()
780 fmatcher = match.exact([fname])
781 fmatcher = match.exact([fname])
781 diffopts = mdiff.diffopts(git=True, context=32767)
782 diffopts = mdiff.diffopts(git=True, context=32767)
782 _pfctx, _fctx, header, fhunks = next(
783 _pfctx, _fctx, header, fhunks = next(
783 patch.diffhunks(repo, basectx.p1(), ctx, fmatcher, opts=diffopts)
784 patch.diffhunks(repo, basectx.p1(), ctx, fmatcher, opts=diffopts)
784 )
785 )
785
786
786 for fhunk in fhunks:
787 for fhunk in fhunks:
787 (oldOffset, oldLength, newOffset, newLength), lines = fhunk
788 (oldOffset, oldLength, newOffset, newLength), lines = fhunk
788 corpus = b''.join(lines[1:])
789 corpus = b''.join(lines[1:])
789 shunk = list(header)
790 shunk = list(header)
790 shunk.extend(lines)
791 shunk.extend(lines)
791 _mf, _mt, addLines, delLines, _hb = patch.diffstatsum(
792 _mf, _mt, addLines, delLines, _hb = patch.diffstatsum(
792 patch.diffstatdata(util.iterlines(shunk))
793 patch.diffstatdata(util.iterlines(shunk))
793 )
794 )
794 pchange.addhunk(
795 pchange.addhunk(
795 phabhunk(
796 phabhunk(
796 oldOffset,
797 oldOffset,
797 oldLength,
798 oldLength,
798 newOffset,
799 newOffset,
799 newLength,
800 newLength,
800 corpus,
801 corpus,
801 addLines,
802 addLines,
802 delLines,
803 delLines,
803 )
804 )
804 )
805 )
805
806
806
807
807 def uploadchunks(fctx, fphid):
808 def uploadchunks(fctx, fphid):
808 """upload large binary files as separate chunks.
809 """upload large binary files as separate chunks.
809 Phab requests chunking over 8MiB, and splits into 4MiB chunks
810 Phab requests chunking over 8MiB, and splits into 4MiB chunks
810 """
811 """
811 ui = fctx.repo().ui
812 ui = fctx.repo().ui
812 chunks = callconduit(ui, b'file.querychunks', {b'filePHID': fphid})
813 chunks = callconduit(ui, b'file.querychunks', {b'filePHID': fphid})
813 with ui.makeprogress(
814 with ui.makeprogress(
814 _(b'uploading file chunks'), unit=_(b'chunks'), total=len(chunks)
815 _(b'uploading file chunks'), unit=_(b'chunks'), total=len(chunks)
815 ) as progress:
816 ) as progress:
816 for chunk in chunks:
817 for chunk in chunks:
817 progress.increment()
818 progress.increment()
818 if chunk[b'complete']:
819 if chunk[b'complete']:
819 continue
820 continue
820 bstart = int(chunk[b'byteStart'])
821 bstart = int(chunk[b'byteStart'])
821 bend = int(chunk[b'byteEnd'])
822 bend = int(chunk[b'byteEnd'])
822 callconduit(
823 callconduit(
823 ui,
824 ui,
824 b'file.uploadchunk',
825 b'file.uploadchunk',
825 {
826 {
826 b'filePHID': fphid,
827 b'filePHID': fphid,
827 b'byteStart': bstart,
828 b'byteStart': bstart,
828 b'data': base64.b64encode(fctx.data()[bstart:bend]),
829 b'data': base64.b64encode(fctx.data()[bstart:bend]),
829 b'dataEncoding': b'base64',
830 b'dataEncoding': b'base64',
830 },
831 },
831 )
832 )
832
833
833
834
834 def uploadfile(fctx):
835 def uploadfile(fctx):
835 """upload binary files to Phabricator"""
836 """upload binary files to Phabricator"""
836 repo = fctx.repo()
837 repo = fctx.repo()
837 ui = repo.ui
838 ui = repo.ui
838 fname = fctx.path()
839 fname = fctx.path()
839 size = fctx.size()
840 size = fctx.size()
840 fhash = pycompat.bytestr(hashlib.sha256(fctx.data()).hexdigest())
841 fhash = pycompat.bytestr(hashlib.sha256(fctx.data()).hexdigest())
841
842
842 # an allocate call is required first to see if an upload is even required
843 # an allocate call is required first to see if an upload is even required
843 # (Phab might already have it) and to determine if chunking is needed
844 # (Phab might already have it) and to determine if chunking is needed
844 allocateparams = {
845 allocateparams = {
845 b'name': fname,
846 b'name': fname,
846 b'contentLength': size,
847 b'contentLength': size,
847 b'contentHash': fhash,
848 b'contentHash': fhash,
848 }
849 }
849 filealloc = callconduit(ui, b'file.allocate', allocateparams)
850 filealloc = callconduit(ui, b'file.allocate', allocateparams)
850 fphid = filealloc[b'filePHID']
851 fphid = filealloc[b'filePHID']
851
852
852 if filealloc[b'upload']:
853 if filealloc[b'upload']:
853 ui.write(_(b'uploading %s\n') % bytes(fctx))
854 ui.write(_(b'uploading %s\n') % bytes(fctx))
854 if not fphid:
855 if not fphid:
855 uploadparams = {
856 uploadparams = {
856 b'name': fname,
857 b'name': fname,
857 b'data_base64': base64.b64encode(fctx.data()),
858 b'data_base64': base64.b64encode(fctx.data()),
858 }
859 }
859 fphid = callconduit(ui, b'file.upload', uploadparams)
860 fphid = callconduit(ui, b'file.upload', uploadparams)
860 else:
861 else:
861 uploadchunks(fctx, fphid)
862 uploadchunks(fctx, fphid)
862 else:
863 else:
863 ui.debug(b'server already has %s\n' % bytes(fctx))
864 ui.debug(b'server already has %s\n' % bytes(fctx))
864
865
865 if not fphid:
866 if not fphid:
866 raise error.Abort(b'Upload of %s failed.' % bytes(fctx))
867 raise error.Abort(b'Upload of %s failed.' % bytes(fctx))
867
868
868 return fphid
869 return fphid
869
870
870
871
871 def addoldbinary(pchange, oldfctx, fctx):
872 def addoldbinary(pchange, oldfctx, fctx):
872 """add the metadata for the previous version of a binary file to the
873 """add the metadata for the previous version of a binary file to the
873 phabchange for the new version
874 phabchange for the new version
874
875
875 ``oldfctx`` is the previous version of the file; ``fctx`` is the new
876 ``oldfctx`` is the previous version of the file; ``fctx`` is the new
876 version of the file, or None if the file is being removed.
877 version of the file, or None if the file is being removed.
877 """
878 """
878 if not fctx or fctx.cmp(oldfctx):
879 if not fctx or fctx.cmp(oldfctx):
879 # Files differ, add the old one
880 # Files differ, add the old one
880 pchange.metadata[b'old:file:size'] = oldfctx.size()
881 pchange.metadata[b'old:file:size'] = oldfctx.size()
881 mimeguess, _enc = mimetypes.guess_type(
882 mimeguess, _enc = mimetypes.guess_type(
882 encoding.unifromlocal(oldfctx.path())
883 encoding.unifromlocal(oldfctx.path())
883 )
884 )
884 if mimeguess:
885 if mimeguess:
885 pchange.metadata[b'old:file:mime-type'] = pycompat.bytestr(
886 pchange.metadata[b'old:file:mime-type'] = pycompat.bytestr(
886 mimeguess
887 mimeguess
887 )
888 )
888 fphid = uploadfile(oldfctx)
889 fphid = uploadfile(oldfctx)
889 pchange.metadata[b'old:binary-phid'] = fphid
890 pchange.metadata[b'old:binary-phid'] = fphid
890 else:
891 else:
891 # If it's left as IMAGE/BINARY web UI might try to display it
892 # If it's left as IMAGE/BINARY web UI might try to display it
892 pchange.fileType = DiffFileType.TEXT
893 pchange.fileType = DiffFileType.TEXT
893 pchange.copynewmetadatatoold()
894 pchange.copynewmetadatatoold()
894
895
895
896
896 def makebinary(pchange, fctx):
897 def makebinary(pchange, fctx):
897 """populate the phabchange for a binary file"""
898 """populate the phabchange for a binary file"""
898 pchange.fileType = DiffFileType.BINARY
899 pchange.fileType = DiffFileType.BINARY
899 fphid = uploadfile(fctx)
900 fphid = uploadfile(fctx)
900 pchange.metadata[b'new:binary-phid'] = fphid
901 pchange.metadata[b'new:binary-phid'] = fphid
901 pchange.metadata[b'new:file:size'] = fctx.size()
902 pchange.metadata[b'new:file:size'] = fctx.size()
902 mimeguess, _enc = mimetypes.guess_type(encoding.unifromlocal(fctx.path()))
903 mimeguess, _enc = mimetypes.guess_type(encoding.unifromlocal(fctx.path()))
903 if mimeguess:
904 if mimeguess:
904 mimeguess = pycompat.bytestr(mimeguess)
905 mimeguess = pycompat.bytestr(mimeguess)
905 pchange.metadata[b'new:file:mime-type'] = mimeguess
906 pchange.metadata[b'new:file:mime-type'] = mimeguess
906 if mimeguess.startswith(b'image/'):
907 if mimeguess.startswith(b'image/'):
907 pchange.fileType = DiffFileType.IMAGE
908 pchange.fileType = DiffFileType.IMAGE
908
909
909
910
910 # Copied from mercurial/patch.py
911 # Copied from mercurial/patch.py
911 gitmode = {b'l': b'120000', b'x': b'100755', b'': b'100644'}
912 gitmode = {b'l': b'120000', b'x': b'100755', b'': b'100644'}
912
913
913
914
914 def notutf8(fctx):
915 def notutf8(fctx):
915 """detect non-UTF-8 text files since Phabricator requires them to be marked
916 """detect non-UTF-8 text files since Phabricator requires them to be marked
916 as binary
917 as binary
917 """
918 """
918 try:
919 try:
919 fctx.data().decode('utf-8')
920 fctx.data().decode('utf-8')
920 return False
921 return False
921 except UnicodeDecodeError:
922 except UnicodeDecodeError:
922 fctx.repo().ui.write(
923 fctx.repo().ui.write(
923 _(b'file %s detected as non-UTF-8, marked as binary\n')
924 _(b'file %s detected as non-UTF-8, marked as binary\n')
924 % fctx.path()
925 % fctx.path()
925 )
926 )
926 return True
927 return True
927
928
928
929
929 def addremoved(pdiff, basectx, ctx, removed):
930 def addremoved(pdiff, basectx, ctx, removed):
930 """add removed files to the phabdiff. Shouldn't include moves"""
931 """add removed files to the phabdiff. Shouldn't include moves"""
931 for fname in removed:
932 for fname in removed:
932 pchange = phabchange(
933 pchange = phabchange(
933 currentPath=fname, oldPath=fname, type=DiffChangeType.DELETE
934 currentPath=fname, oldPath=fname, type=DiffChangeType.DELETE
934 )
935 )
935 oldfctx = basectx.p1()[fname]
936 oldfctx = basectx.p1()[fname]
936 pchange.addoldmode(gitmode[oldfctx.flags()])
937 pchange.addoldmode(gitmode[oldfctx.flags()])
937 if not (oldfctx.isbinary() or notutf8(oldfctx)):
938 if not (oldfctx.isbinary() or notutf8(oldfctx)):
938 maketext(pchange, basectx, ctx, fname)
939 maketext(pchange, basectx, ctx, fname)
939
940
940 pdiff.addchange(pchange)
941 pdiff.addchange(pchange)
941
942
942
943
943 def addmodified(pdiff, basectx, ctx, modified):
944 def addmodified(pdiff, basectx, ctx, modified):
944 """add modified files to the phabdiff"""
945 """add modified files to the phabdiff"""
945 for fname in modified:
946 for fname in modified:
946 fctx = ctx[fname]
947 fctx = ctx[fname]
947 oldfctx = basectx.p1()[fname]
948 oldfctx = basectx.p1()[fname]
948 pchange = phabchange(currentPath=fname, oldPath=fname)
949 pchange = phabchange(currentPath=fname, oldPath=fname)
949 filemode = gitmode[fctx.flags()]
950 filemode = gitmode[fctx.flags()]
950 originalmode = gitmode[oldfctx.flags()]
951 originalmode = gitmode[oldfctx.flags()]
951 if filemode != originalmode:
952 if filemode != originalmode:
952 pchange.addoldmode(originalmode)
953 pchange.addoldmode(originalmode)
953 pchange.addnewmode(filemode)
954 pchange.addnewmode(filemode)
954
955
955 if (
956 if (
956 fctx.isbinary()
957 fctx.isbinary()
957 or notutf8(fctx)
958 or notutf8(fctx)
958 or oldfctx.isbinary()
959 or oldfctx.isbinary()
959 or notutf8(oldfctx)
960 or notutf8(oldfctx)
960 ):
961 ):
961 makebinary(pchange, fctx)
962 makebinary(pchange, fctx)
962 addoldbinary(pchange, oldfctx, fctx)
963 addoldbinary(pchange, oldfctx, fctx)
963 else:
964 else:
964 maketext(pchange, basectx, ctx, fname)
965 maketext(pchange, basectx, ctx, fname)
965
966
966 pdiff.addchange(pchange)
967 pdiff.addchange(pchange)
967
968
968
969
969 def addadded(pdiff, basectx, ctx, added, removed):
970 def addadded(pdiff, basectx, ctx, added, removed):
970 """add file adds to the phabdiff, both new files and copies/moves"""
971 """add file adds to the phabdiff, both new files and copies/moves"""
971 # Keep track of files that've been recorded as moved/copied, so if there are
972 # Keep track of files that've been recorded as moved/copied, so if there are
972 # additional copies we can mark them (moves get removed from removed)
973 # additional copies we can mark them (moves get removed from removed)
973 copiedchanges = {}
974 copiedchanges = {}
974 movedchanges = {}
975 movedchanges = {}
975
976
976 copy = {}
977 copy = {}
977 if basectx != ctx:
978 if basectx != ctx:
978 copy = copies.pathcopies(basectx.p1(), ctx)
979 copy = copies.pathcopies(basectx.p1(), ctx)
979
980
980 for fname in added:
981 for fname in added:
981 fctx = ctx[fname]
982 fctx = ctx[fname]
982 oldfctx = None
983 oldfctx = None
983 pchange = phabchange(currentPath=fname)
984 pchange = phabchange(currentPath=fname)
984
985
985 filemode = gitmode[fctx.flags()]
986 filemode = gitmode[fctx.flags()]
986
987
987 if copy:
988 if copy:
988 originalfname = copy.get(fname, fname)
989 originalfname = copy.get(fname, fname)
989 else:
990 else:
990 originalfname = fname
991 originalfname = fname
991 if fctx.renamed():
992 if fctx.renamed():
992 originalfname = fctx.renamed()[0]
993 originalfname = fctx.renamed()[0]
993
994
994 renamed = fname != originalfname
995 renamed = fname != originalfname
995
996
996 if renamed:
997 if renamed:
997 oldfctx = basectx.p1()[originalfname]
998 oldfctx = basectx.p1()[originalfname]
998 originalmode = gitmode[oldfctx.flags()]
999 originalmode = gitmode[oldfctx.flags()]
999 pchange.oldPath = originalfname
1000 pchange.oldPath = originalfname
1000
1001
1001 if originalfname in removed:
1002 if originalfname in removed:
1002 origpchange = phabchange(
1003 origpchange = phabchange(
1003 currentPath=originalfname,
1004 currentPath=originalfname,
1004 oldPath=originalfname,
1005 oldPath=originalfname,
1005 type=DiffChangeType.MOVE_AWAY,
1006 type=DiffChangeType.MOVE_AWAY,
1006 awayPaths=[fname],
1007 awayPaths=[fname],
1007 )
1008 )
1008 movedchanges[originalfname] = origpchange
1009 movedchanges[originalfname] = origpchange
1009 removed.remove(originalfname)
1010 removed.remove(originalfname)
1010 pchange.type = DiffChangeType.MOVE_HERE
1011 pchange.type = DiffChangeType.MOVE_HERE
1011 elif originalfname in movedchanges:
1012 elif originalfname in movedchanges:
1012 movedchanges[originalfname].type = DiffChangeType.MULTICOPY
1013 movedchanges[originalfname].type = DiffChangeType.MULTICOPY
1013 movedchanges[originalfname].awayPaths.append(fname)
1014 movedchanges[originalfname].awayPaths.append(fname)
1014 pchange.type = DiffChangeType.COPY_HERE
1015 pchange.type = DiffChangeType.COPY_HERE
1015 else: # pure copy
1016 else: # pure copy
1016 if originalfname not in copiedchanges:
1017 if originalfname not in copiedchanges:
1017 origpchange = phabchange(
1018 origpchange = phabchange(
1018 currentPath=originalfname, type=DiffChangeType.COPY_AWAY
1019 currentPath=originalfname, type=DiffChangeType.COPY_AWAY
1019 )
1020 )
1020 copiedchanges[originalfname] = origpchange
1021 copiedchanges[originalfname] = origpchange
1021 else:
1022 else:
1022 origpchange = copiedchanges[originalfname]
1023 origpchange = copiedchanges[originalfname]
1023 origpchange.awayPaths.append(fname)
1024 origpchange.awayPaths.append(fname)
1024 pchange.type = DiffChangeType.COPY_HERE
1025 pchange.type = DiffChangeType.COPY_HERE
1025
1026
1026 if filemode != originalmode:
1027 if filemode != originalmode:
1027 pchange.addoldmode(originalmode)
1028 pchange.addoldmode(originalmode)
1028 pchange.addnewmode(filemode)
1029 pchange.addnewmode(filemode)
1029 else: # Brand-new file
1030 else: # Brand-new file
1030 pchange.addnewmode(gitmode[fctx.flags()])
1031 pchange.addnewmode(gitmode[fctx.flags()])
1031 pchange.type = DiffChangeType.ADD
1032 pchange.type = DiffChangeType.ADD
1032
1033
1033 if (
1034 if (
1034 fctx.isbinary()
1035 fctx.isbinary()
1035 or notutf8(fctx)
1036 or notutf8(fctx)
1036 or (oldfctx and (oldfctx.isbinary() or notutf8(oldfctx)))
1037 or (oldfctx and (oldfctx.isbinary() or notutf8(oldfctx)))
1037 ):
1038 ):
1038 makebinary(pchange, fctx)
1039 makebinary(pchange, fctx)
1039 if renamed:
1040 if renamed:
1040 addoldbinary(pchange, oldfctx, fctx)
1041 addoldbinary(pchange, oldfctx, fctx)
1041 else:
1042 else:
1042 maketext(pchange, basectx, ctx, fname)
1043 maketext(pchange, basectx, ctx, fname)
1043
1044
1044 pdiff.addchange(pchange)
1045 pdiff.addchange(pchange)
1045
1046
1046 for _path, copiedchange in copiedchanges.items():
1047 for _path, copiedchange in copiedchanges.items():
1047 pdiff.addchange(copiedchange)
1048 pdiff.addchange(copiedchange)
1048 for _path, movedchange in movedchanges.items():
1049 for _path, movedchange in movedchanges.items():
1049 pdiff.addchange(movedchange)
1050 pdiff.addchange(movedchange)
1050
1051
1051
1052
1052 def creatediff(basectx, ctx):
1053 def creatediff(basectx, ctx):
1053 """create a Differential Diff"""
1054 """create a Differential Diff"""
1054 repo = ctx.repo()
1055 repo = ctx.repo()
1055 repophid = getrepophid(repo)
1056 repophid = getrepophid(repo)
1056 # Create a "Differential Diff" via "differential.creatediff" API
1057 # Create a "Differential Diff" via "differential.creatediff" API
1057 pdiff = phabdiff(
1058 pdiff = phabdiff(
1058 sourceControlBaseRevision=b'%s' % basectx.p1().hex(),
1059 sourceControlBaseRevision=b'%s' % basectx.p1().hex(),
1059 branch=b'%s' % ctx.branch(),
1060 branch=b'%s' % ctx.branch(),
1060 )
1061 )
1061 modified, added, removed, _d, _u, _i, _c = basectx.p1().status(ctx)
1062 modified, added, removed, _d, _u, _i, _c = basectx.p1().status(ctx)
1062 # addadded will remove moved files from removed, so addremoved won't get
1063 # addadded will remove moved files from removed, so addremoved won't get
1063 # them
1064 # them
1064 addadded(pdiff, basectx, ctx, added, removed)
1065 addadded(pdiff, basectx, ctx, added, removed)
1065 addmodified(pdiff, basectx, ctx, modified)
1066 addmodified(pdiff, basectx, ctx, modified)
1066 addremoved(pdiff, basectx, ctx, removed)
1067 addremoved(pdiff, basectx, ctx, removed)
1067 if repophid:
1068 if repophid:
1068 pdiff.repositoryPHID = repophid
1069 pdiff.repositoryPHID = repophid
1069 diff = callconduit(
1070 diff = callconduit(
1070 repo.ui,
1071 repo.ui,
1071 b'differential.creatediff',
1072 b'differential.creatediff',
1072 pycompat.byteskwargs(attr.asdict(pdiff)),
1073 pycompat.byteskwargs(attr.asdict(pdiff)),
1073 )
1074 )
1074 if not diff:
1075 if not diff:
1075 if basectx != ctx:
1076 if basectx != ctx:
1076 msg = _(b'cannot create diff for %s::%s') % (basectx, ctx)
1077 msg = _(b'cannot create diff for %s::%s') % (basectx, ctx)
1077 else:
1078 else:
1078 msg = _(b'cannot create diff for %s') % ctx
1079 msg = _(b'cannot create diff for %s') % ctx
1079 raise error.Abort(msg)
1080 raise error.Abort(msg)
1080 return diff
1081 return diff
1081
1082
1082
1083
1083 def writediffproperties(ctxs, diff):
1084 def writediffproperties(ctxs, diff):
1084 """write metadata to diff so patches could be applied losslessly
1085 """write metadata to diff so patches could be applied losslessly
1085
1086
1086 ``ctxs`` is the list of commits that created the diff, in ascending order.
1087 ``ctxs`` is the list of commits that created the diff, in ascending order.
1087 The list is generally a single commit, but may be several when using
1088 The list is generally a single commit, but may be several when using
1088 ``phabsend --fold``.
1089 ``phabsend --fold``.
1089 """
1090 """
1090 # creatediff returns with a diffid but query returns with an id
1091 # creatediff returns with a diffid but query returns with an id
1091 diffid = diff.get(b'diffid', diff.get(b'id'))
1092 diffid = diff.get(b'diffid', diff.get(b'id'))
1092 basectx = ctxs[0]
1093 basectx = ctxs[0]
1093 tipctx = ctxs[-1]
1094 tipctx = ctxs[-1]
1094
1095
1095 params = {
1096 params = {
1096 b'diff_id': diffid,
1097 b'diff_id': diffid,
1097 b'name': b'hg:meta',
1098 b'name': b'hg:meta',
1098 b'data': templatefilters.json(
1099 b'data': templatefilters.json(
1099 {
1100 {
1100 b'user': tipctx.user(),
1101 b'user': tipctx.user(),
1101 b'date': b'%d %d' % tipctx.date(),
1102 b'date': b'%d %d' % tipctx.date(),
1102 b'branch': tipctx.branch(),
1103 b'branch': tipctx.branch(),
1103 b'node': tipctx.hex(),
1104 b'node': tipctx.hex(),
1104 b'parent': basectx.p1().hex(),
1105 b'parent': basectx.p1().hex(),
1105 }
1106 }
1106 ),
1107 ),
1107 }
1108 }
1108 callconduit(basectx.repo().ui, b'differential.setdiffproperty', params)
1109 callconduit(basectx.repo().ui, b'differential.setdiffproperty', params)
1109
1110
1110 commits = {}
1111 commits = {}
1111 for ctx in ctxs:
1112 for ctx in ctxs:
1112 commits[ctx.hex()] = {
1113 commits[ctx.hex()] = {
1113 b'author': stringutil.person(ctx.user()),
1114 b'author': stringutil.person(ctx.user()),
1114 b'authorEmail': stringutil.email(ctx.user()),
1115 b'authorEmail': stringutil.email(ctx.user()),
1115 b'time': int(ctx.date()[0]),
1116 b'time': int(ctx.date()[0]),
1116 b'commit': ctx.hex(),
1117 b'commit': ctx.hex(),
1117 b'parents': [ctx.p1().hex()],
1118 b'parents': [ctx.p1().hex()],
1118 b'branch': ctx.branch(),
1119 b'branch': ctx.branch(),
1119 }
1120 }
1120 params = {
1121 params = {
1121 b'diff_id': diffid,
1122 b'diff_id': diffid,
1122 b'name': b'local:commits',
1123 b'name': b'local:commits',
1123 b'data': templatefilters.json(commits),
1124 b'data': templatefilters.json(commits),
1124 }
1125 }
1125 callconduit(basectx.repo().ui, b'differential.setdiffproperty', params)
1126 callconduit(basectx.repo().ui, b'differential.setdiffproperty', params)
1126
1127
1127
1128
1128 def createdifferentialrevision(
1129 def createdifferentialrevision(
1129 ctxs,
1130 ctxs,
1130 revid=None,
1131 revid=None,
1131 parentrevphid=None,
1132 parentrevphid=None,
1132 oldbasenode=None,
1133 oldbasenode=None,
1133 oldnode=None,
1134 oldnode=None,
1134 olddiff=None,
1135 olddiff=None,
1135 actions=None,
1136 actions=None,
1136 comment=None,
1137 comment=None,
1137 ):
1138 ):
1138 """create or update a Differential Revision
1139 """create or update a Differential Revision
1139
1140
1140 If revid is None, create a new Differential Revision, otherwise update
1141 If revid is None, create a new Differential Revision, otherwise update
1141 revid. If parentrevphid is not None, set it as a dependency.
1142 revid. If parentrevphid is not None, set it as a dependency.
1142
1143
1143 If there is a single commit for the new Differential Revision, ``ctxs`` will
1144 If there is a single commit for the new Differential Revision, ``ctxs`` will
1144 be a list of that single context. Otherwise, it is a list that covers the
1145 be a list of that single context. Otherwise, it is a list that covers the
1145 range of changes for the differential, where ``ctxs[0]`` is the first change
1146 range of changes for the differential, where ``ctxs[0]`` is the first change
1146 to include and ``ctxs[-1]`` is the last.
1147 to include and ``ctxs[-1]`` is the last.
1147
1148
1148 If oldnode is not None, check if the patch content (without commit message
1149 If oldnode is not None, check if the patch content (without commit message
1149 and metadata) has changed before creating another diff. For a Revision with
1150 and metadata) has changed before creating another diff. For a Revision with
1150 a single commit, ``oldbasenode`` and ``oldnode`` have the same value. For a
1151 a single commit, ``oldbasenode`` and ``oldnode`` have the same value. For a
1151 Revision covering multiple commits, ``oldbasenode`` corresponds to
1152 Revision covering multiple commits, ``oldbasenode`` corresponds to
1152 ``ctxs[0]`` the previous time this Revision was posted, and ``oldnode``
1153 ``ctxs[0]`` the previous time this Revision was posted, and ``oldnode``
1153 corresponds to ``ctxs[-1]``.
1154 corresponds to ``ctxs[-1]``.
1154
1155
1155 If actions is not None, they will be appended to the transaction.
1156 If actions is not None, they will be appended to the transaction.
1156 """
1157 """
1157 ctx = ctxs[-1]
1158 ctx = ctxs[-1]
1158 basectx = ctxs[0]
1159 basectx = ctxs[0]
1159
1160
1160 repo = ctx.repo()
1161 repo = ctx.repo()
1161 if oldnode:
1162 if oldnode:
1162 diffopts = mdiff.diffopts(git=True, context=32767)
1163 diffopts = mdiff.diffopts(git=True, context=32767)
1163 unfi = repo.unfiltered()
1164 unfi = repo.unfiltered()
1164 oldctx = unfi[oldnode]
1165 oldctx = unfi[oldnode]
1165 oldbasectx = unfi[oldbasenode]
1166 oldbasectx = unfi[oldbasenode]
1166 neednewdiff = getdiff(basectx, ctx, diffopts) != getdiff(
1167 neednewdiff = getdiff(basectx, ctx, diffopts) != getdiff(
1167 oldbasectx, oldctx, diffopts
1168 oldbasectx, oldctx, diffopts
1168 )
1169 )
1169 else:
1170 else:
1170 neednewdiff = True
1171 neednewdiff = True
1171
1172
1172 transactions = []
1173 transactions = []
1173 if neednewdiff:
1174 if neednewdiff:
1174 diff = creatediff(basectx, ctx)
1175 diff = creatediff(basectx, ctx)
1175 transactions.append({b'type': b'update', b'value': diff[b'phid']})
1176 transactions.append({b'type': b'update', b'value': diff[b'phid']})
1176 if comment:
1177 if comment:
1177 transactions.append({b'type': b'comment', b'value': comment})
1178 transactions.append({b'type': b'comment', b'value': comment})
1178 else:
1179 else:
1179 # Even if we don't need to upload a new diff because the patch content
1180 # Even if we don't need to upload a new diff because the patch content
1180 # does not change. We might still need to update its metadata so
1181 # does not change. We might still need to update its metadata so
1181 # pushers could know the correct node metadata.
1182 # pushers could know the correct node metadata.
1182 assert olddiff
1183 assert olddiff
1183 diff = olddiff
1184 diff = olddiff
1184 writediffproperties(ctxs, diff)
1185 writediffproperties(ctxs, diff)
1185
1186
1186 # Set the parent Revision every time, so commit re-ordering is picked-up
1187 # Set the parent Revision every time, so commit re-ordering is picked-up
1187 if parentrevphid:
1188 if parentrevphid:
1188 transactions.append(
1189 transactions.append(
1189 {b'type': b'parents.set', b'value': [parentrevphid]}
1190 {b'type': b'parents.set', b'value': [parentrevphid]}
1190 )
1191 )
1191
1192
1192 if actions:
1193 if actions:
1193 transactions += actions
1194 transactions += actions
1194
1195
1195 # When folding multiple local commits into a single review, arcanist will
1196 # When folding multiple local commits into a single review, arcanist will
1196 # take the summary line of the first commit as the title, and then
1197 # take the summary line of the first commit as the title, and then
1197 # concatenate the rest of the remaining messages (including each of their
1198 # concatenate the rest of the remaining messages (including each of their
1198 # first lines) to the rest of the first commit message (each separated by
1199 # first lines) to the rest of the first commit message (each separated by
1199 # an empty line), and use that as the summary field. Do the same here.
1200 # an empty line), and use that as the summary field. Do the same here.
1200 # For commits with only a one line message, there is no summary field, as
1201 # For commits with only a one line message, there is no summary field, as
1201 # this gets assigned to the title.
1202 # this gets assigned to the title.
1202 fields = util.sortdict() # sorted for stable wire protocol in tests
1203 fields = util.sortdict() # sorted for stable wire protocol in tests
1203
1204
1204 for i, _ctx in enumerate(ctxs):
1205 for i, _ctx in enumerate(ctxs):
1205 # Parse commit message and update related fields.
1206 # Parse commit message and update related fields.
1206 desc = _ctx.description()
1207 desc = _ctx.description()
1207 info = callconduit(
1208 info = callconduit(
1208 repo.ui, b'differential.parsecommitmessage', {b'corpus': desc}
1209 repo.ui, b'differential.parsecommitmessage', {b'corpus': desc}
1209 )
1210 )
1210
1211
1211 for k in [b'title', b'summary', b'testPlan']:
1212 for k in [b'title', b'summary', b'testPlan']:
1212 v = info[b'fields'].get(k)
1213 v = info[b'fields'].get(k)
1213 if not v:
1214 if not v:
1214 continue
1215 continue
1215
1216
1216 if i == 0:
1217 if i == 0:
1217 # Title, summary and test plan (if present) are taken verbatim
1218 # Title, summary and test plan (if present) are taken verbatim
1218 # for the first commit.
1219 # for the first commit.
1219 fields[k] = v.rstrip()
1220 fields[k] = v.rstrip()
1220 continue
1221 continue
1221 elif k == b'title':
1222 elif k == b'title':
1222 # Add subsequent titles (i.e. the first line of the commit
1223 # Add subsequent titles (i.e. the first line of the commit
1223 # message) back to the summary.
1224 # message) back to the summary.
1224 k = b'summary'
1225 k = b'summary'
1225
1226
1226 # Append any current field to the existing composite field
1227 # Append any current field to the existing composite field
1227 fields[k] = b'\n\n'.join(filter(None, [fields.get(k), v.rstrip()]))
1228 fields[k] = b'\n\n'.join(filter(None, [fields.get(k), v.rstrip()]))
1228
1229
1229 for k, v in fields.items():
1230 for k, v in fields.items():
1230 transactions.append({b'type': k, b'value': v})
1231 transactions.append({b'type': k, b'value': v})
1231
1232
1232 params = {b'transactions': transactions}
1233 params = {b'transactions': transactions}
1233 if revid is not None:
1234 if revid is not None:
1234 # Update an existing Differential Revision
1235 # Update an existing Differential Revision
1235 params[b'objectIdentifier'] = revid
1236 params[b'objectIdentifier'] = revid
1236
1237
1237 revision = callconduit(repo.ui, b'differential.revision.edit', params)
1238 revision = callconduit(repo.ui, b'differential.revision.edit', params)
1238 if not revision:
1239 if not revision:
1239 if len(ctxs) == 1:
1240 if len(ctxs) == 1:
1240 msg = _(b'cannot create revision for %s') % ctx
1241 msg = _(b'cannot create revision for %s') % ctx
1241 else:
1242 else:
1242 msg = _(b'cannot create revision for %s::%s') % (basectx, ctx)
1243 msg = _(b'cannot create revision for %s::%s') % (basectx, ctx)
1243 raise error.Abort(msg)
1244 raise error.Abort(msg)
1244
1245
1245 return revision, diff
1246 return revision, diff
1246
1247
1247
1248
1248 def userphids(ui, names):
1249 def userphids(ui, names):
1249 """convert user names to PHIDs"""
1250 """convert user names to PHIDs"""
1250 names = [name.lower() for name in names]
1251 names = [name.lower() for name in names]
1251 query = {b'constraints': {b'usernames': names}}
1252 query = {b'constraints': {b'usernames': names}}
1252 result = callconduit(ui, b'user.search', query)
1253 result = callconduit(ui, b'user.search', query)
1253 # username not found is not an error of the API. So check if we have missed
1254 # username not found is not an error of the API. So check if we have missed
1254 # some names here.
1255 # some names here.
1255 data = result[b'data']
1256 data = result[b'data']
1256 resolved = {entry[b'fields'][b'username'].lower() for entry in data}
1257 resolved = {entry[b'fields'][b'username'].lower() for entry in data}
1257 unresolved = set(names) - resolved
1258 unresolved = set(names) - resolved
1258 if unresolved:
1259 if unresolved:
1259 raise error.Abort(
1260 raise error.Abort(
1260 _(b'unknown username: %s') % b' '.join(sorted(unresolved))
1261 _(b'unknown username: %s') % b' '.join(sorted(unresolved))
1261 )
1262 )
1262 return [entry[b'phid'] for entry in data]
1263 return [entry[b'phid'] for entry in data]
1263
1264
1264
1265
1265 def _print_phabsend_action(ui, ctx, newrevid, action):
1266 def _print_phabsend_action(ui, ctx, newrevid, action):
1266 """print the ``action`` that occurred when posting ``ctx`` for review
1267 """print the ``action`` that occurred when posting ``ctx`` for review
1267
1268
1268 This is a utility function for the sending phase of ``phabsend``, which
1269 This is a utility function for the sending phase of ``phabsend``, which
1269 makes it easier to show a status for all local commits with `--fold``.
1270 makes it easier to show a status for all local commits with `--fold``.
1270 """
1271 """
1271 actiondesc = ui.label(
1272 actiondesc = ui.label(
1272 {
1273 {
1273 b'created': _(b'created'),
1274 b'created': _(b'created'),
1274 b'skipped': _(b'skipped'),
1275 b'skipped': _(b'skipped'),
1275 b'updated': _(b'updated'),
1276 b'updated': _(b'updated'),
1276 }[action],
1277 }[action],
1277 b'phabricator.action.%s' % action,
1278 b'phabricator.action.%s' % action,
1278 )
1279 )
1279 drevdesc = ui.label(b'D%d' % newrevid, b'phabricator.drev')
1280 drevdesc = ui.label(b'D%d' % newrevid, b'phabricator.drev')
1280 summary = cmdutil.format_changeset_summary(ui, ctx, b'phabsend')
1281 summary = cmdutil.format_changeset_summary(ui, ctx, b'phabsend')
1281 ui.write(_(b'%s - %s - %s\n') % (drevdesc, actiondesc, summary))
1282 ui.write(_(b'%s - %s - %s\n') % (drevdesc, actiondesc, summary))
1282
1283
1283
1284
1284 def _amend_diff_properties(unfi, drevid, newnodes, diff):
1285 def _amend_diff_properties(unfi, drevid, newnodes, diff):
1285 """update the local commit list for the ``diff`` associated with ``drevid``
1286 """update the local commit list for the ``diff`` associated with ``drevid``
1286
1287
1287 This is a utility function for the amend phase of ``phabsend``, which
1288 This is a utility function for the amend phase of ``phabsend``, which
1288 converts failures to warning messages.
1289 converts failures to warning messages.
1289 """
1290 """
1290 _debug(
1291 _debug(
1291 unfi.ui,
1292 unfi.ui,
1292 b"new commits: %s\n" % stringutil.pprint([short(n) for n in newnodes]),
1293 b"new commits: %s\n" % stringutil.pprint([short(n) for n in newnodes]),
1293 )
1294 )
1294
1295
1295 try:
1296 try:
1296 writediffproperties([unfi[newnode] for newnode in newnodes], diff)
1297 writediffproperties([unfi[newnode] for newnode in newnodes], diff)
1297 except util.urlerr.urlerror:
1298 except util.urlerr.urlerror:
1298 # If it fails just warn and keep going, otherwise the DREV
1299 # If it fails just warn and keep going, otherwise the DREV
1299 # associations will be lost
1300 # associations will be lost
1300 unfi.ui.warnnoi18n(b'Failed to update metadata for D%d\n' % drevid)
1301 unfi.ui.warnnoi18n(b'Failed to update metadata for D%d\n' % drevid)
1301
1302
1302
1303
1303 @vcrcommand(
1304 @vcrcommand(
1304 b'phabsend',
1305 b'phabsend',
1305 [
1306 [
1306 (b'r', b'rev', [], _(b'revisions to send'), _(b'REV')),
1307 (b'r', b'rev', [], _(b'revisions to send'), _(b'REV')),
1307 (b'', b'amend', True, _(b'update commit messages')),
1308 (b'', b'amend', True, _(b'update commit messages')),
1308 (b'', b'reviewer', [], _(b'specify reviewers')),
1309 (b'', b'reviewer', [], _(b'specify reviewers')),
1309 (b'', b'blocker', [], _(b'specify blocking reviewers')),
1310 (b'', b'blocker', [], _(b'specify blocking reviewers')),
1310 (
1311 (
1311 b'm',
1312 b'm',
1312 b'comment',
1313 b'comment',
1313 b'',
1314 b'',
1314 _(b'add a comment to Revisions with new/updated Diffs'),
1315 _(b'add a comment to Revisions with new/updated Diffs'),
1315 ),
1316 ),
1316 (b'', b'confirm', None, _(b'ask for confirmation before sending')),
1317 (b'', b'confirm', None, _(b'ask for confirmation before sending')),
1317 (b'', b'fold', False, _(b'combine the revisions into one review')),
1318 (b'', b'fold', False, _(b'combine the revisions into one review')),
1318 ],
1319 ],
1319 _(b'REV [OPTIONS]'),
1320 _(b'REV [OPTIONS]'),
1320 helpcategory=command.CATEGORY_IMPORT_EXPORT,
1321 helpcategory=command.CATEGORY_IMPORT_EXPORT,
1321 )
1322 )
1322 def phabsend(ui, repo, *revs, **opts):
1323 def phabsend(ui, repo, *revs, **opts):
1323 """upload changesets to Phabricator
1324 """upload changesets to Phabricator
1324
1325
1325 If there are multiple revisions specified, they will be send as a stack
1326 If there are multiple revisions specified, they will be send as a stack
1326 with a linear dependencies relationship using the order specified by the
1327 with a linear dependencies relationship using the order specified by the
1327 revset.
1328 revset.
1328
1329
1329 For the first time uploading changesets, local tags will be created to
1330 For the first time uploading changesets, local tags will be created to
1330 maintain the association. After the first time, phabsend will check
1331 maintain the association. After the first time, phabsend will check
1331 obsstore and tags information so it can figure out whether to update an
1332 obsstore and tags information so it can figure out whether to update an
1332 existing Differential Revision, or create a new one.
1333 existing Differential Revision, or create a new one.
1333
1334
1334 If --amend is set, update commit messages so they have the
1335 If --amend is set, update commit messages so they have the
1335 ``Differential Revision`` URL, remove related tags. This is similar to what
1336 ``Differential Revision`` URL, remove related tags. This is similar to what
1336 arcanist will do, and is more desired in author-push workflows. Otherwise,
1337 arcanist will do, and is more desired in author-push workflows. Otherwise,
1337 use local tags to record the ``Differential Revision`` association.
1338 use local tags to record the ``Differential Revision`` association.
1338
1339
1339 The --confirm option lets you confirm changesets before sending them. You
1340 The --confirm option lets you confirm changesets before sending them. You
1340 can also add following to your configuration file to make it default
1341 can also add following to your configuration file to make it default
1341 behaviour::
1342 behaviour::
1342
1343
1343 [phabsend]
1344 [phabsend]
1344 confirm = true
1345 confirm = true
1345
1346
1346 By default, a separate review will be created for each commit that is
1347 By default, a separate review will be created for each commit that is
1347 selected, and will have the same parent/child relationship in Phabricator.
1348 selected, and will have the same parent/child relationship in Phabricator.
1348 If ``--fold`` is set, multiple commits are rolled up into a single review
1349 If ``--fold`` is set, multiple commits are rolled up into a single review
1349 as if diffed from the parent of the first revision to the last. The commit
1350 as if diffed from the parent of the first revision to the last. The commit
1350 messages are concatenated in the summary field on Phabricator.
1351 messages are concatenated in the summary field on Phabricator.
1351
1352
1352 phabsend will check obsstore and the above association to decide whether to
1353 phabsend will check obsstore and the above association to decide whether to
1353 update an existing Differential Revision, or create a new one.
1354 update an existing Differential Revision, or create a new one.
1354 """
1355 """
1355 opts = pycompat.byteskwargs(opts)
1356 opts = pycompat.byteskwargs(opts)
1356 revs = list(revs) + opts.get(b'rev', [])
1357 revs = list(revs) + opts.get(b'rev', [])
1357 revs = logcmdutil.revrange(repo, revs)
1358 revs = logcmdutil.revrange(repo, revs)
1358 revs.sort() # ascending order to preserve topological parent/child in phab
1359 revs.sort() # ascending order to preserve topological parent/child in phab
1359
1360
1360 if not revs:
1361 if not revs:
1361 raise error.Abort(_(b'phabsend requires at least one changeset'))
1362 raise error.Abort(_(b'phabsend requires at least one changeset'))
1362 if opts.get(b'amend'):
1363 if opts.get(b'amend'):
1363 cmdutil.checkunfinished(repo)
1364 cmdutil.checkunfinished(repo)
1364
1365
1365 ctxs = [repo[rev] for rev in revs]
1366 ctxs = [repo[rev] for rev in revs]
1366
1367
1367 if any(c for c in ctxs if c.obsolete()):
1368 if any(c for c in ctxs if c.obsolete()):
1368 raise error.Abort(_(b"obsolete commits cannot be posted for review"))
1369 raise error.Abort(_(b"obsolete commits cannot be posted for review"))
1369
1370
1370 # Ensure the local commits are an unbroken range. The semantics of the
1371 # Ensure the local commits are an unbroken range. The semantics of the
1371 # --fold option implies this, and the auto restacking of orphans requires
1372 # --fold option implies this, and the auto restacking of orphans requires
1372 # it. Otherwise A+C in A->B->C will cause B to be orphaned, and C' to
1373 # it. Otherwise A+C in A->B->C will cause B to be orphaned, and C' to
1373 # get A' as a parent.
1374 # get A' as a parent.
1374 def _fail_nonlinear_revs(revs, revtype):
1375 def _fail_nonlinear_revs(revs, revtype):
1375 badnodes = [repo[r].node() for r in revs]
1376 badnodes = [repo[r].node() for r in revs]
1376 raise error.Abort(
1377 raise error.Abort(
1377 _(b"cannot phabsend multiple %s revisions: %s")
1378 _(b"cannot phabsend multiple %s revisions: %s")
1378 % (revtype, scmutil.nodesummaries(repo, badnodes)),
1379 % (revtype, scmutil.nodesummaries(repo, badnodes)),
1379 hint=_(b"the revisions must form a linear chain"),
1380 hint=_(b"the revisions must form a linear chain"),
1380 )
1381 )
1381
1382
1382 heads = repo.revs(b'heads(%ld)', revs)
1383 heads = repo.revs(b'heads(%ld)', revs)
1383 if len(heads) > 1:
1384 if len(heads) > 1:
1384 _fail_nonlinear_revs(heads, b"head")
1385 _fail_nonlinear_revs(heads, b"head")
1385
1386
1386 roots = repo.revs(b'roots(%ld)', revs)
1387 roots = repo.revs(b'roots(%ld)', revs)
1387 if len(roots) > 1:
1388 if len(roots) > 1:
1388 _fail_nonlinear_revs(roots, b"root")
1389 _fail_nonlinear_revs(roots, b"root")
1389
1390
1390 fold = opts.get(b'fold')
1391 fold = opts.get(b'fold')
1391 if fold:
1392 if fold:
1392 if len(revs) == 1:
1393 if len(revs) == 1:
1393 # TODO: just switch to --no-fold instead?
1394 # TODO: just switch to --no-fold instead?
1394 raise error.Abort(_(b"cannot fold a single revision"))
1395 raise error.Abort(_(b"cannot fold a single revision"))
1395
1396
1396 # There's no clear way to manage multiple commits with a Dxxx tag, so
1397 # There's no clear way to manage multiple commits with a Dxxx tag, so
1397 # require the amend option. (We could append "_nnn", but then it
1398 # require the amend option. (We could append "_nnn", but then it
1398 # becomes jumbled if earlier commits are added to an update.) It should
1399 # becomes jumbled if earlier commits are added to an update.) It should
1399 # lock the repo and ensure that the range is editable, but that would
1400 # lock the repo and ensure that the range is editable, but that would
1400 # make the code pretty convoluted. The default behavior of `arc` is to
1401 # make the code pretty convoluted. The default behavior of `arc` is to
1401 # create a new review anyway.
1402 # create a new review anyway.
1402 if not opts.get(b"amend"):
1403 if not opts.get(b"amend"):
1403 raise error.Abort(_(b"cannot fold with --no-amend"))
1404 raise error.Abort(_(b"cannot fold with --no-amend"))
1404
1405
1405 # It might be possible to bucketize the revisions by the DREV value, and
1406 # It might be possible to bucketize the revisions by the DREV value, and
1406 # iterate over those groups when posting, and then again when amending.
1407 # iterate over those groups when posting, and then again when amending.
1407 # But for simplicity, require all selected revisions to be for the same
1408 # But for simplicity, require all selected revisions to be for the same
1408 # DREV (if present). Adding local revisions to an existing DREV is
1409 # DREV (if present). Adding local revisions to an existing DREV is
1409 # acceptable.
1410 # acceptable.
1410 drevmatchers = [
1411 drevmatchers = [
1411 _differentialrevisiondescre.search(ctx.description())
1412 _differentialrevisiondescre.search(ctx.description())
1412 for ctx in ctxs
1413 for ctx in ctxs
1413 ]
1414 ]
1414 if len({m.group('url') for m in drevmatchers if m}) > 1:
1415 if len({m.group('url') for m in drevmatchers if m}) > 1:
1415 raise error.Abort(
1416 raise error.Abort(
1416 _(b"cannot fold revisions with different DREV values")
1417 _(b"cannot fold revisions with different DREV values")
1417 )
1418 )
1418
1419
1419 # {newnode: (oldnode, olddiff, olddrev}
1420 # {newnode: (oldnode, olddiff, olddrev}
1420 oldmap = getoldnodedrevmap(repo, [repo[r].node() for r in revs])
1421 oldmap = getoldnodedrevmap(repo, [repo[r].node() for r in revs])
1421
1422
1422 confirm = ui.configbool(b'phabsend', b'confirm')
1423 confirm = ui.configbool(b'phabsend', b'confirm')
1423 confirm |= bool(opts.get(b'confirm'))
1424 confirm |= bool(opts.get(b'confirm'))
1424 if confirm:
1425 if confirm:
1425 confirmed = _confirmbeforesend(repo, revs, oldmap)
1426 confirmed = _confirmbeforesend(repo, revs, oldmap)
1426 if not confirmed:
1427 if not confirmed:
1427 raise error.Abort(_(b'phabsend cancelled'))
1428 raise error.Abort(_(b'phabsend cancelled'))
1428
1429
1429 actions = []
1430 actions = []
1430 reviewers = opts.get(b'reviewer', [])
1431 reviewers = opts.get(b'reviewer', [])
1431 blockers = opts.get(b'blocker', [])
1432 blockers = opts.get(b'blocker', [])
1432 phids = []
1433 phids = []
1433 if reviewers:
1434 if reviewers:
1434 phids.extend(userphids(repo.ui, reviewers))
1435 phids.extend(userphids(repo.ui, reviewers))
1435 if blockers:
1436 if blockers:
1436 phids.extend(
1437 phids.extend(
1437 map(
1438 map(
1438 lambda phid: b'blocking(%s)' % phid,
1439 lambda phid: b'blocking(%s)' % phid,
1439 userphids(repo.ui, blockers),
1440 userphids(repo.ui, blockers),
1440 )
1441 )
1441 )
1442 )
1442 if phids:
1443 if phids:
1443 actions.append({b'type': b'reviewers.add', b'value': phids})
1444 actions.append({b'type': b'reviewers.add', b'value': phids})
1444
1445
1445 drevids = [] # [int]
1446 drevids = [] # [int]
1446 diffmap = {} # {newnode: diff}
1447 diffmap = {} # {newnode: diff}
1447
1448
1448 # Send patches one by one so we know their Differential Revision PHIDs and
1449 # Send patches one by one so we know their Differential Revision PHIDs and
1449 # can provide dependency relationship
1450 # can provide dependency relationship
1450 lastrevphid = None
1451 lastrevphid = None
1451 for ctx in ctxs:
1452 for ctx in ctxs:
1452 if fold:
1453 if fold:
1453 ui.debug(b'sending rev %d::%d\n' % (ctx.rev(), ctxs[-1].rev()))
1454 ui.debug(b'sending rev %d::%d\n' % (ctx.rev(), ctxs[-1].rev()))
1454 else:
1455 else:
1455 ui.debug(b'sending rev %d\n' % ctx.rev())
1456 ui.debug(b'sending rev %d\n' % ctx.rev())
1456
1457
1457 # Get Differential Revision ID
1458 # Get Differential Revision ID
1458 oldnode, olddiff, revid = oldmap.get(ctx.node(), (None, None, None))
1459 oldnode, olddiff, revid = oldmap.get(ctx.node(), (None, None, None))
1459 oldbasenode, oldbasediff, oldbaserevid = oldnode, olddiff, revid
1460 oldbasenode, oldbasediff, oldbaserevid = oldnode, olddiff, revid
1460
1461
1461 if fold:
1462 if fold:
1462 oldbasenode, oldbasediff, oldbaserevid = oldmap.get(
1463 oldbasenode, oldbasediff, oldbaserevid = oldmap.get(
1463 ctxs[-1].node(), (None, None, None)
1464 ctxs[-1].node(), (None, None, None)
1464 )
1465 )
1465
1466
1466 if oldnode != ctx.node() or opts.get(b'amend'):
1467 if oldnode != ctx.node() or opts.get(b'amend'):
1467 # Create or update Differential Revision
1468 # Create or update Differential Revision
1468 revision, diff = createdifferentialrevision(
1469 revision, diff = createdifferentialrevision(
1469 ctxs if fold else [ctx],
1470 ctxs if fold else [ctx],
1470 revid,
1471 revid,
1471 lastrevphid,
1472 lastrevphid,
1472 oldbasenode,
1473 oldbasenode,
1473 oldnode,
1474 oldnode,
1474 olddiff,
1475 olddiff,
1475 actions,
1476 actions,
1476 opts.get(b'comment'),
1477 opts.get(b'comment'),
1477 )
1478 )
1478
1479
1479 if fold:
1480 if fold:
1480 for ctx in ctxs:
1481 for ctx in ctxs:
1481 diffmap[ctx.node()] = diff
1482 diffmap[ctx.node()] = diff
1482 else:
1483 else:
1483 diffmap[ctx.node()] = diff
1484 diffmap[ctx.node()] = diff
1484
1485
1485 newrevid = int(revision[b'object'][b'id'])
1486 newrevid = int(revision[b'object'][b'id'])
1486 newrevphid = revision[b'object'][b'phid']
1487 newrevphid = revision[b'object'][b'phid']
1487 if revid:
1488 if revid:
1488 action = b'updated'
1489 action = b'updated'
1489 else:
1490 else:
1490 action = b'created'
1491 action = b'created'
1491
1492
1492 # Create a local tag to note the association, if commit message
1493 # Create a local tag to note the association, if commit message
1493 # does not have it already
1494 # does not have it already
1494 if not fold:
1495 if not fold:
1495 m = _differentialrevisiondescre.search(ctx.description())
1496 m = _differentialrevisiondescre.search(ctx.description())
1496 if not m or int(m.group('id')) != newrevid:
1497 if not m or int(m.group('id')) != newrevid:
1497 tagname = b'D%d' % newrevid
1498 tagname = b'D%d' % newrevid
1498 tags.tag(
1499 tags.tag(
1499 repo,
1500 repo,
1500 tagname,
1501 tagname,
1501 ctx.node(),
1502 ctx.node(),
1502 message=None,
1503 message=None,
1503 user=None,
1504 user=None,
1504 date=None,
1505 date=None,
1505 local=True,
1506 local=True,
1506 )
1507 )
1507 else:
1508 else:
1508 # Nothing changed. But still set "newrevphid" so the next revision
1509 # Nothing changed. But still set "newrevphid" so the next revision
1509 # could depend on this one and "newrevid" for the summary line.
1510 # could depend on this one and "newrevid" for the summary line.
1510 newrevphid = querydrev(repo.ui, b'%d' % revid)[0][b'phid']
1511 newrevphid = querydrev(repo.ui, b'%d' % revid)[0][b'phid']
1511 newrevid = revid
1512 newrevid = revid
1512 action = b'skipped'
1513 action = b'skipped'
1513
1514
1514 drevids.append(newrevid)
1515 drevids.append(newrevid)
1515 lastrevphid = newrevphid
1516 lastrevphid = newrevphid
1516
1517
1517 if fold:
1518 if fold:
1518 for c in ctxs:
1519 for c in ctxs:
1519 if oldmap.get(c.node(), (None, None, None))[2]:
1520 if oldmap.get(c.node(), (None, None, None))[2]:
1520 action = b'updated'
1521 action = b'updated'
1521 else:
1522 else:
1522 action = b'created'
1523 action = b'created'
1523 _print_phabsend_action(ui, c, newrevid, action)
1524 _print_phabsend_action(ui, c, newrevid, action)
1524 break
1525 break
1525
1526
1526 _print_phabsend_action(ui, ctx, newrevid, action)
1527 _print_phabsend_action(ui, ctx, newrevid, action)
1527
1528
1528 # Update commit messages and remove tags
1529 # Update commit messages and remove tags
1529 if opts.get(b'amend'):
1530 if opts.get(b'amend'):
1530 unfi = repo.unfiltered()
1531 unfi = repo.unfiltered()
1531 drevs = callconduit(ui, b'differential.query', {b'ids': drevids})
1532 drevs = callconduit(ui, b'differential.query', {b'ids': drevids})
1532 with repo.wlock(), repo.lock(), repo.transaction(b'phabsend'):
1533 with repo.wlock(), repo.lock(), repo.transaction(b'phabsend'):
1533 # Eagerly evaluate commits to restabilize before creating new
1534 # Eagerly evaluate commits to restabilize before creating new
1534 # commits. The selected revisions are excluded because they are
1535 # commits. The selected revisions are excluded because they are
1535 # automatically restacked as part of the submission process.
1536 # automatically restacked as part of the submission process.
1536 restack = [
1537 restack = [
1537 c
1538 c
1538 for c in repo.set(
1539 for c in repo.set(
1539 b"(%ld::) - (%ld) - unstable() - obsolete() - public()",
1540 b"(%ld::) - (%ld) - unstable() - obsolete() - public()",
1540 revs,
1541 revs,
1541 revs,
1542 revs,
1542 )
1543 )
1543 ]
1544 ]
1544 wnode = unfi[b'.'].node()
1545 wnode = unfi[b'.'].node()
1545 mapping = {} # {oldnode: [newnode]}
1546 mapping = {} # {oldnode: [newnode]}
1546 newnodes = []
1547 newnodes = []
1547
1548
1548 drevid = drevids[0]
1549 drevid = drevids[0]
1549
1550
1550 for i, rev in enumerate(revs):
1551 for i, rev in enumerate(revs):
1551 old = unfi[rev]
1552 old = unfi[rev]
1552 if not fold:
1553 if not fold:
1553 drevid = drevids[i]
1554 drevid = drevids[i]
1554 drev = [d for d in drevs if int(d[b'id']) == drevid][0]
1555 drev = [d for d in drevs if int(d[b'id']) == drevid][0]
1555
1556
1556 newdesc = get_amended_desc(drev, old, fold)
1557 newdesc = get_amended_desc(drev, old, fold)
1557 # Make sure commit message contain "Differential Revision"
1558 # Make sure commit message contain "Differential Revision"
1558 if (
1559 if (
1559 old.description() != newdesc
1560 old.description() != newdesc
1560 or old.p1().node() in mapping
1561 or old.p1().node() in mapping
1561 or old.p2().node() in mapping
1562 or old.p2().node() in mapping
1562 ):
1563 ):
1563 if old.phase() == phases.public:
1564 if old.phase() == phases.public:
1564 ui.warn(
1565 ui.warn(
1565 _(b"warning: not updating public commit %s\n")
1566 _(b"warning: not updating public commit %s\n")
1566 % scmutil.formatchangeid(old)
1567 % scmutil.formatchangeid(old)
1567 )
1568 )
1568 continue
1569 continue
1569 parents = [
1570 parents = [
1570 mapping.get(old.p1().node(), (old.p1(),))[0],
1571 mapping.get(old.p1().node(), (old.p1(),))[0],
1571 mapping.get(old.p2().node(), (old.p2(),))[0],
1572 mapping.get(old.p2().node(), (old.p2(),))[0],
1572 ]
1573 ]
1573 newdesc = rewriteutil.update_hash_refs(
1574 newdesc = rewriteutil.update_hash_refs(
1574 repo,
1575 repo,
1575 newdesc,
1576 newdesc,
1576 mapping,
1577 mapping,
1577 )
1578 )
1578 new = context.metadataonlyctx(
1579 new = context.metadataonlyctx(
1579 repo,
1580 repo,
1580 old,
1581 old,
1581 parents=parents,
1582 parents=parents,
1582 text=newdesc,
1583 text=newdesc,
1583 user=old.user(),
1584 user=old.user(),
1584 date=old.date(),
1585 date=old.date(),
1585 extra=old.extra(),
1586 extra=old.extra(),
1586 )
1587 )
1587
1588
1588 newnode = new.commit()
1589 newnode = new.commit()
1589
1590
1590 mapping[old.node()] = [newnode]
1591 mapping[old.node()] = [newnode]
1591
1592
1592 if fold:
1593 if fold:
1593 # Defer updating the (single) Diff until all nodes are
1594 # Defer updating the (single) Diff until all nodes are
1594 # collected. No tags were created, so none need to be
1595 # collected. No tags were created, so none need to be
1595 # removed.
1596 # removed.
1596 newnodes.append(newnode)
1597 newnodes.append(newnode)
1597 continue
1598 continue
1598
1599
1599 _amend_diff_properties(
1600 _amend_diff_properties(
1600 unfi, drevid, [newnode], diffmap[old.node()]
1601 unfi, drevid, [newnode], diffmap[old.node()]
1601 )
1602 )
1602
1603
1603 # Remove local tags since it's no longer necessary
1604 # Remove local tags since it's no longer necessary
1604 tagname = b'D%d' % drevid
1605 tagname = b'D%d' % drevid
1605 if tagname in repo.tags():
1606 if tagname in repo.tags():
1606 tags.tag(
1607 tags.tag(
1607 repo,
1608 repo,
1608 tagname,
1609 tagname,
1609 repo.nullid,
1610 repo.nullid,
1610 message=None,
1611 message=None,
1611 user=None,
1612 user=None,
1612 date=None,
1613 date=None,
1613 local=True,
1614 local=True,
1614 )
1615 )
1615 elif fold:
1616 elif fold:
1616 # When folding multiple commits into one review with
1617 # When folding multiple commits into one review with
1617 # --fold, track even the commits that weren't amended, so
1618 # --fold, track even the commits that weren't amended, so
1618 # that their association isn't lost if the properties are
1619 # that their association isn't lost if the properties are
1619 # rewritten below.
1620 # rewritten below.
1620 newnodes.append(old.node())
1621 newnodes.append(old.node())
1621
1622
1622 # If the submitted commits are public, no amend takes place so
1623 # If the submitted commits are public, no amend takes place so
1623 # there are no newnodes and therefore no diff update to do.
1624 # there are no newnodes and therefore no diff update to do.
1624 if fold and newnodes:
1625 if fold and newnodes:
1625 diff = diffmap[old.node()]
1626 diff = diffmap[old.node()]
1626
1627
1627 # The diff object in diffmap doesn't have the local commits
1628 # The diff object in diffmap doesn't have the local commits
1628 # because that could be returned from differential.creatediff,
1629 # because that could be returned from differential.creatediff,
1629 # not differential.querydiffs. So use the queried diff (if
1630 # not differential.querydiffs. So use the queried diff (if
1630 # present), or force the amend (a new revision is being posted.)
1631 # present), or force the amend (a new revision is being posted.)
1631 if not olddiff or set(newnodes) != getlocalcommits(olddiff):
1632 if not olddiff or set(newnodes) != getlocalcommits(olddiff):
1632 _debug(ui, b"updating local commit list for D%d\n" % drevid)
1633 _debug(ui, b"updating local commit list for D%d\n" % drevid)
1633 _amend_diff_properties(unfi, drevid, newnodes, diff)
1634 _amend_diff_properties(unfi, drevid, newnodes, diff)
1634 else:
1635 else:
1635 _debug(
1636 _debug(
1636 ui,
1637 ui,
1637 b"local commit list for D%d is already up-to-date\n"
1638 b"local commit list for D%d is already up-to-date\n"
1638 % drevid,
1639 % drevid,
1639 )
1640 )
1640 elif fold:
1641 elif fold:
1641 _debug(ui, b"no newnodes to update\n")
1642 _debug(ui, b"no newnodes to update\n")
1642
1643
1643 # Restack any children of first-time submissions that were orphaned
1644 # Restack any children of first-time submissions that were orphaned
1644 # in the process. The ctx won't report that it is an orphan until
1645 # in the process. The ctx won't report that it is an orphan until
1645 # the cleanup takes place below.
1646 # the cleanup takes place below.
1646 for old in restack:
1647 for old in restack:
1647 parents = [
1648 parents = [
1648 mapping.get(old.p1().node(), (old.p1(),))[0],
1649 mapping.get(old.p1().node(), (old.p1(),))[0],
1649 mapping.get(old.p2().node(), (old.p2(),))[0],
1650 mapping.get(old.p2().node(), (old.p2(),))[0],
1650 ]
1651 ]
1651 new = context.metadataonlyctx(
1652 new = context.metadataonlyctx(
1652 repo,
1653 repo,
1653 old,
1654 old,
1654 parents=parents,
1655 parents=parents,
1655 text=rewriteutil.update_hash_refs(
1656 text=rewriteutil.update_hash_refs(
1656 repo, old.description(), mapping
1657 repo, old.description(), mapping
1657 ),
1658 ),
1658 user=old.user(),
1659 user=old.user(),
1659 date=old.date(),
1660 date=old.date(),
1660 extra=old.extra(),
1661 extra=old.extra(),
1661 )
1662 )
1662
1663
1663 newnode = new.commit()
1664 newnode = new.commit()
1664
1665
1665 # Don't obsolete unselected descendants of nodes that have not
1666 # Don't obsolete unselected descendants of nodes that have not
1666 # been changed in this transaction- that results in an error.
1667 # been changed in this transaction- that results in an error.
1667 if newnode != old.node():
1668 if newnode != old.node():
1668 mapping[old.node()] = [newnode]
1669 mapping[old.node()] = [newnode]
1669 _debug(
1670 _debug(
1670 ui,
1671 ui,
1671 b"restabilizing %s as %s\n"
1672 b"restabilizing %s as %s\n"
1672 % (short(old.node()), short(newnode)),
1673 % (short(old.node()), short(newnode)),
1673 )
1674 )
1674 else:
1675 else:
1675 _debug(
1676 _debug(
1676 ui,
1677 ui,
1677 b"not restabilizing unchanged %s\n" % short(old.node()),
1678 b"not restabilizing unchanged %s\n" % short(old.node()),
1678 )
1679 )
1679
1680
1680 scmutil.cleanupnodes(repo, mapping, b'phabsend', fixphase=True)
1681 scmutil.cleanupnodes(repo, mapping, b'phabsend', fixphase=True)
1681 if wnode in mapping:
1682 if wnode in mapping:
1682 unfi.setparents(mapping[wnode][0])
1683 unfi.setparents(mapping[wnode][0])
1683
1684
1684
1685
1685 # Map from "hg:meta" keys to header understood by "hg import". The order is
1686 # Map from "hg:meta" keys to header understood by "hg import". The order is
1686 # consistent with "hg export" output.
1687 # consistent with "hg export" output.
1687 _metanamemap = util.sortdict(
1688 _metanamemap = util.sortdict(
1688 [
1689 [
1689 (b'user', b'User'),
1690 (b'user', b'User'),
1690 (b'date', b'Date'),
1691 (b'date', b'Date'),
1691 (b'branch', b'Branch'),
1692 (b'branch', b'Branch'),
1692 (b'node', b'Node ID'),
1693 (b'node', b'Node ID'),
1693 (b'parent', b'Parent '),
1694 (b'parent', b'Parent '),
1694 ]
1695 ]
1695 )
1696 )
1696
1697
1697
1698
1698 def _confirmbeforesend(repo, revs, oldmap):
1699 def _confirmbeforesend(repo, revs, oldmap):
1699 url, token = readurltoken(repo.ui)
1700 url, token = readurltoken(repo.ui)
1700 ui = repo.ui
1701 ui = repo.ui
1701 for rev in revs:
1702 for rev in revs:
1702 ctx = repo[rev]
1703 ctx = repo[rev]
1703 oldnode, olddiff, drevid = oldmap.get(ctx.node(), (None, None, None))
1704 oldnode, olddiff, drevid = oldmap.get(ctx.node(), (None, None, None))
1704 if drevid:
1705 if drevid:
1705 drevdesc = ui.label(b'D%d' % drevid, b'phabricator.drev')
1706 drevdesc = ui.label(b'D%d' % drevid, b'phabricator.drev')
1706 else:
1707 else:
1707 drevdesc = ui.label(_(b'NEW'), b'phabricator.drev')
1708 drevdesc = ui.label(_(b'NEW'), b'phabricator.drev')
1708
1709
1709 ui.write(
1710 ui.write(
1710 _(b'%s - %s\n')
1711 _(b'%s - %s\n')
1711 % (
1712 % (
1712 drevdesc,
1713 drevdesc,
1713 cmdutil.format_changeset_summary(ui, ctx, b'phabsend'),
1714 cmdutil.format_changeset_summary(ui, ctx, b'phabsend'),
1714 )
1715 )
1715 )
1716 )
1716
1717
1717 if ui.promptchoice(
1718 if ui.promptchoice(
1718 _(b'Send the above changes to %s (Y/n)?$$ &Yes $$ &No') % url
1719 _(b'Send the above changes to %s (Y/n)?$$ &Yes $$ &No') % url
1719 ):
1720 ):
1720 return False
1721 return False
1721
1722
1722 return True
1723 return True
1723
1724
1724
1725
1725 _knownstatusnames = {
1726 _knownstatusnames = {
1726 b'accepted',
1727 b'accepted',
1727 b'needsreview',
1728 b'needsreview',
1728 b'needsrevision',
1729 b'needsrevision',
1729 b'closed',
1730 b'closed',
1730 b'abandoned',
1731 b'abandoned',
1731 b'changesplanned',
1732 b'changesplanned',
1732 }
1733 }
1733
1734
1734
1735
1735 def _getstatusname(drev):
1736 def _getstatusname(drev):
1736 """get normalized status name from a Differential Revision"""
1737 """get normalized status name from a Differential Revision"""
1737 return drev[b'statusName'].replace(b' ', b'').lower()
1738 return drev[b'statusName'].replace(b' ', b'').lower()
1738
1739
1739
1740
1740 # Small language to specify differential revisions. Support symbols: (), :X,
1741 # Small language to specify differential revisions. Support symbols: (), :X,
1741 # +, and -.
1742 # +, and -.
1742
1743
1743 _elements = {
1744 _elements = {
1744 # token-type: binding-strength, primary, prefix, infix, suffix
1745 # token-type: binding-strength, primary, prefix, infix, suffix
1745 b'(': (12, None, (b'group', 1, b')'), None, None),
1746 b'(': (12, None, (b'group', 1, b')'), None, None),
1746 b':': (8, None, (b'ancestors', 8), None, None),
1747 b':': (8, None, (b'ancestors', 8), None, None),
1747 b'&': (5, None, None, (b'and_', 5), None),
1748 b'&': (5, None, None, (b'and_', 5), None),
1748 b'+': (4, None, None, (b'add', 4), None),
1749 b'+': (4, None, None, (b'add', 4), None),
1749 b'-': (4, None, None, (b'sub', 4), None),
1750 b'-': (4, None, None, (b'sub', 4), None),
1750 b')': (0, None, None, None, None),
1751 b')': (0, None, None, None, None),
1751 b'symbol': (0, b'symbol', None, None, None),
1752 b'symbol': (0, b'symbol', None, None, None),
1752 b'end': (0, None, None, None, None),
1753 b'end': (0, None, None, None, None),
1753 }
1754 }
1754
1755
1755
1756
1756 def _tokenize(text):
1757 def _tokenize(text):
1757 view = memoryview(text) # zero-copy slice
1758 view = memoryview(text) # zero-copy slice
1758 special = b'():+-& '
1759 special = b'():+-& '
1759 pos = 0
1760 pos = 0
1760 length = len(text)
1761 length = len(text)
1761 while pos < length:
1762 while pos < length:
1762 symbol = b''.join(
1763 symbol = b''.join(
1763 itertools.takewhile(
1764 itertools.takewhile(
1764 lambda ch: ch not in special, pycompat.iterbytestr(view[pos:])
1765 lambda ch: ch not in special, pycompat.iterbytestr(view[pos:])
1765 )
1766 )
1766 )
1767 )
1767 if symbol:
1768 if symbol:
1768 yield (b'symbol', symbol, pos)
1769 yield (b'symbol', symbol, pos)
1769 pos += len(symbol)
1770 pos += len(symbol)
1770 else: # special char, ignore space
1771 else: # special char, ignore space
1771 if text[pos : pos + 1] != b' ':
1772 if text[pos : pos + 1] != b' ':
1772 yield (text[pos : pos + 1], None, pos)
1773 yield (text[pos : pos + 1], None, pos)
1773 pos += 1
1774 pos += 1
1774 yield (b'end', None, pos)
1775 yield (b'end', None, pos)
1775
1776
1776
1777
1777 def _parse(text):
1778 def _parse(text):
1778 tree, pos = parser.parser(_elements).parse(_tokenize(text))
1779 tree, pos = parser.parser(_elements).parse(_tokenize(text))
1779 if pos != len(text):
1780 if pos != len(text):
1780 raise error.ParseError(b'invalid token', pos)
1781 raise error.ParseError(b'invalid token', pos)
1781 return tree
1782 return tree
1782
1783
1783
1784
1784 def _parsedrev(symbol):
1785 def _parsedrev(symbol):
1785 """str -> int or None, ex. 'D45' -> 45; '12' -> 12; 'x' -> None"""
1786 """str -> int or None, ex. 'D45' -> 45; '12' -> 12; 'x' -> None"""
1786 if symbol.startswith(b'D') and symbol[1:].isdigit():
1787 if symbol.startswith(b'D') and symbol[1:].isdigit():
1787 return int(symbol[1:])
1788 return int(symbol[1:])
1788 if symbol.isdigit():
1789 if symbol.isdigit():
1789 return int(symbol)
1790 return int(symbol)
1790
1791
1791
1792
1792 def _prefetchdrevs(tree):
1793 def _prefetchdrevs(tree):
1793 """return ({single-drev-id}, {ancestor-drev-id}) to prefetch"""
1794 """return ({single-drev-id}, {ancestor-drev-id}) to prefetch"""
1794 drevs = set()
1795 drevs = set()
1795 ancestordrevs = set()
1796 ancestordrevs = set()
1796 op = tree[0]
1797 op = tree[0]
1797 if op == b'symbol':
1798 if op == b'symbol':
1798 r = _parsedrev(tree[1])
1799 r = _parsedrev(tree[1])
1799 if r:
1800 if r:
1800 drevs.add(r)
1801 drevs.add(r)
1801 elif op == b'ancestors':
1802 elif op == b'ancestors':
1802 r, a = _prefetchdrevs(tree[1])
1803 r, a = _prefetchdrevs(tree[1])
1803 drevs.update(r)
1804 drevs.update(r)
1804 ancestordrevs.update(r)
1805 ancestordrevs.update(r)
1805 ancestordrevs.update(a)
1806 ancestordrevs.update(a)
1806 else:
1807 else:
1807 for t in tree[1:]:
1808 for t in tree[1:]:
1808 r, a = _prefetchdrevs(t)
1809 r, a = _prefetchdrevs(t)
1809 drevs.update(r)
1810 drevs.update(r)
1810 ancestordrevs.update(a)
1811 ancestordrevs.update(a)
1811 return drevs, ancestordrevs
1812 return drevs, ancestordrevs
1812
1813
1813
1814
1814 def querydrev(ui, spec):
1815 def querydrev(ui, spec):
1815 """return a list of "Differential Revision" dicts
1816 """return a list of "Differential Revision" dicts
1816
1817
1817 spec is a string using a simple query language, see docstring in phabread
1818 spec is a string using a simple query language, see docstring in phabread
1818 for details.
1819 for details.
1819
1820
1820 A "Differential Revision dict" looks like:
1821 A "Differential Revision dict" looks like:
1821
1822
1822 {
1823 {
1823 "activeDiffPHID": "PHID-DIFF-xoqnjkobbm6k4dk6hi72",
1824 "activeDiffPHID": "PHID-DIFF-xoqnjkobbm6k4dk6hi72",
1824 "authorPHID": "PHID-USER-tv3ohwc4v4jeu34otlye",
1825 "authorPHID": "PHID-USER-tv3ohwc4v4jeu34otlye",
1825 "auxiliary": {
1826 "auxiliary": {
1826 "phabricator:depends-on": [
1827 "phabricator:depends-on": [
1827 "PHID-DREV-gbapp366kutjebt7agcd"
1828 "PHID-DREV-gbapp366kutjebt7agcd"
1828 ]
1829 ]
1829 "phabricator:projects": [],
1830 "phabricator:projects": [],
1830 },
1831 },
1831 "branch": "default",
1832 "branch": "default",
1832 "ccs": [],
1833 "ccs": [],
1833 "commits": [],
1834 "commits": [],
1834 "dateCreated": "1499181406",
1835 "dateCreated": "1499181406",
1835 "dateModified": "1499182103",
1836 "dateModified": "1499182103",
1836 "diffs": [
1837 "diffs": [
1837 "3",
1838 "3",
1838 "4",
1839 "4",
1839 ],
1840 ],
1840 "hashes": [],
1841 "hashes": [],
1841 "id": "2",
1842 "id": "2",
1842 "lineCount": "2",
1843 "lineCount": "2",
1843 "phid": "PHID-DREV-672qvysjcczopag46qty",
1844 "phid": "PHID-DREV-672qvysjcczopag46qty",
1844 "properties": {},
1845 "properties": {},
1845 "repositoryPHID": "PHID-REPO-hub2hx62ieuqeheznasv",
1846 "repositoryPHID": "PHID-REPO-hub2hx62ieuqeheznasv",
1846 "reviewers": [],
1847 "reviewers": [],
1847 "sourcePath": null
1848 "sourcePath": null
1848 "status": "0",
1849 "status": "0",
1849 "statusName": "Needs Review",
1850 "statusName": "Needs Review",
1850 "summary": "",
1851 "summary": "",
1851 "testPlan": "",
1852 "testPlan": "",
1852 "title": "example",
1853 "title": "example",
1853 "uri": "https://phab.example.com/D2",
1854 "uri": "https://phab.example.com/D2",
1854 }
1855 }
1855 """
1856 """
1856 # TODO: replace differential.query and differential.querydiffs with
1857 # TODO: replace differential.query and differential.querydiffs with
1857 # differential.diff.search because the former (and their output) are
1858 # differential.diff.search because the former (and their output) are
1858 # frozen, and planned to be deprecated and removed.
1859 # frozen, and planned to be deprecated and removed.
1859
1860
1860 def fetch(params):
1861 def fetch(params):
1861 """params -> single drev or None"""
1862 """params -> single drev or None"""
1862 key = (params.get(b'ids') or params.get(b'phids') or [None])[0]
1863 key = (params.get(b'ids') or params.get(b'phids') or [None])[0]
1863 if key in prefetched:
1864 if key in prefetched:
1864 return prefetched[key]
1865 return prefetched[key]
1865 drevs = callconduit(ui, b'differential.query', params)
1866 drevs = callconduit(ui, b'differential.query', params)
1866 # Fill prefetched with the result
1867 # Fill prefetched with the result
1867 for drev in drevs:
1868 for drev in drevs:
1868 prefetched[drev[b'phid']] = drev
1869 prefetched[drev[b'phid']] = drev
1869 prefetched[int(drev[b'id'])] = drev
1870 prefetched[int(drev[b'id'])] = drev
1870 if key not in prefetched:
1871 if key not in prefetched:
1871 raise error.Abort(
1872 raise error.Abort(
1872 _(b'cannot get Differential Revision %r') % params
1873 _(b'cannot get Differential Revision %r') % params
1873 )
1874 )
1874 return prefetched[key]
1875 return prefetched[key]
1875
1876
1876 def getstack(topdrevids):
1877 def getstack(topdrevids):
1877 """given a top, get a stack from the bottom, [id] -> [id]"""
1878 """given a top, get a stack from the bottom, [id] -> [id]"""
1878 visited = set()
1879 visited = set()
1879 result = []
1880 result = []
1880 queue = [{b'ids': [i]} for i in topdrevids]
1881 queue = [{b'ids': [i]} for i in topdrevids]
1881 while queue:
1882 while queue:
1882 params = queue.pop()
1883 params = queue.pop()
1883 drev = fetch(params)
1884 drev = fetch(params)
1884 if drev[b'id'] in visited:
1885 if drev[b'id'] in visited:
1885 continue
1886 continue
1886 visited.add(drev[b'id'])
1887 visited.add(drev[b'id'])
1887 result.append(int(drev[b'id']))
1888 result.append(int(drev[b'id']))
1888 auxiliary = drev.get(b'auxiliary', {})
1889 auxiliary = drev.get(b'auxiliary', {})
1889 depends = auxiliary.get(b'phabricator:depends-on', [])
1890 depends = auxiliary.get(b'phabricator:depends-on', [])
1890 for phid in depends:
1891 for phid in depends:
1891 queue.append({b'phids': [phid]})
1892 queue.append({b'phids': [phid]})
1892 result.reverse()
1893 result.reverse()
1893 return smartset.baseset(result)
1894 return smartset.baseset(result)
1894
1895
1895 # Initialize prefetch cache
1896 # Initialize prefetch cache
1896 prefetched = {} # {id or phid: drev}
1897 prefetched = {} # {id or phid: drev}
1897
1898
1898 tree = _parse(spec)
1899 tree = _parse(spec)
1899 drevs, ancestordrevs = _prefetchdrevs(tree)
1900 drevs, ancestordrevs = _prefetchdrevs(tree)
1900
1901
1901 # developer config: phabricator.batchsize
1902 # developer config: phabricator.batchsize
1902 batchsize = ui.configint(b'phabricator', b'batchsize')
1903 batchsize = ui.configint(b'phabricator', b'batchsize')
1903
1904
1904 # Prefetch Differential Revisions in batch
1905 # Prefetch Differential Revisions in batch
1905 tofetch = set(drevs)
1906 tofetch = set(drevs)
1906 for r in ancestordrevs:
1907 for r in ancestordrevs:
1907 tofetch.update(range(max(1, r - batchsize), r + 1))
1908 tofetch.update(range(max(1, r - batchsize), r + 1))
1908 if drevs:
1909 if drevs:
1909 fetch({b'ids': list(tofetch)})
1910 fetch({b'ids': list(tofetch)})
1910 validids = sorted(set(getstack(list(ancestordrevs))) | set(drevs))
1911 validids = sorted(set(getstack(list(ancestordrevs))) | set(drevs))
1911
1912
1912 # Walk through the tree, return smartsets
1913 # Walk through the tree, return smartsets
1913 def walk(tree):
1914 def walk(tree):
1914 op = tree[0]
1915 op = tree[0]
1915 if op == b'symbol':
1916 if op == b'symbol':
1916 drev = _parsedrev(tree[1])
1917 drev = _parsedrev(tree[1])
1917 if drev:
1918 if drev:
1918 return smartset.baseset([drev])
1919 return smartset.baseset([drev])
1919 elif tree[1] in _knownstatusnames:
1920 elif tree[1] in _knownstatusnames:
1920 drevs = [
1921 drevs = [
1921 r
1922 r
1922 for r in validids
1923 for r in validids
1923 if _getstatusname(prefetched[r]) == tree[1]
1924 if _getstatusname(prefetched[r]) == tree[1]
1924 ]
1925 ]
1925 return smartset.baseset(drevs)
1926 return smartset.baseset(drevs)
1926 else:
1927 else:
1927 raise error.Abort(_(b'unknown symbol: %s') % tree[1])
1928 raise error.Abort(_(b'unknown symbol: %s') % tree[1])
1928 elif op in {b'and_', b'add', b'sub'}:
1929 elif op in {b'and_', b'add', b'sub'}:
1929 assert len(tree) == 3
1930 assert len(tree) == 3
1930 return getattr(operator, op)(walk(tree[1]), walk(tree[2]))
1931 return getattr(operator, op)(walk(tree[1]), walk(tree[2]))
1931 elif op == b'group':
1932 elif op == b'group':
1932 return walk(tree[1])
1933 return walk(tree[1])
1933 elif op == b'ancestors':
1934 elif op == b'ancestors':
1934 return getstack(walk(tree[1]))
1935 return getstack(walk(tree[1]))
1935 else:
1936 else:
1936 raise error.ProgrammingError(b'illegal tree: %r' % tree)
1937 raise error.ProgrammingError(b'illegal tree: %r' % tree)
1937
1938
1938 return [prefetched[r] for r in walk(tree)]
1939 return [prefetched[r] for r in walk(tree)]
1939
1940
1940
1941
1941 def getdescfromdrev(drev):
1942 def getdescfromdrev(drev):
1942 """get description (commit message) from "Differential Revision"
1943 """get description (commit message) from "Differential Revision"
1943
1944
1944 This is similar to differential.getcommitmessage API. But we only care
1945 This is similar to differential.getcommitmessage API. But we only care
1945 about limited fields: title, summary, test plan, and URL.
1946 about limited fields: title, summary, test plan, and URL.
1946 """
1947 """
1947 title = drev[b'title']
1948 title = drev[b'title']
1948 summary = drev[b'summary'].rstrip()
1949 summary = drev[b'summary'].rstrip()
1949 testplan = drev[b'testPlan'].rstrip()
1950 testplan = drev[b'testPlan'].rstrip()
1950 if testplan:
1951 if testplan:
1951 testplan = b'Test Plan:\n%s' % testplan
1952 testplan = b'Test Plan:\n%s' % testplan
1952 uri = b'Differential Revision: %s' % drev[b'uri']
1953 uri = b'Differential Revision: %s' % drev[b'uri']
1953 return b'\n\n'.join(filter(None, [title, summary, testplan, uri]))
1954 return b'\n\n'.join(filter(None, [title, summary, testplan, uri]))
1954
1955
1955
1956
1956 def get_amended_desc(drev, ctx, folded):
1957 def get_amended_desc(drev, ctx, folded):
1957 """similar to ``getdescfromdrev``, but supports a folded series of commits
1958 """similar to ``getdescfromdrev``, but supports a folded series of commits
1958
1959
1959 This is used when determining if an individual commit needs to have its
1960 This is used when determining if an individual commit needs to have its
1960 message amended after posting it for review. The determination is made for
1961 message amended after posting it for review. The determination is made for
1961 each individual commit, even when they were folded into one review.
1962 each individual commit, even when they were folded into one review.
1962 """
1963 """
1963 if not folded:
1964 if not folded:
1964 return getdescfromdrev(drev)
1965 return getdescfromdrev(drev)
1965
1966
1966 uri = b'Differential Revision: %s' % drev[b'uri']
1967 uri = b'Differential Revision: %s' % drev[b'uri']
1967
1968
1968 # Since the commit messages were combined when posting multiple commits
1969 # Since the commit messages were combined when posting multiple commits
1969 # with --fold, the fields can't be read from Phabricator here, or *all*
1970 # with --fold, the fields can't be read from Phabricator here, or *all*
1970 # affected local revisions will end up with the same commit message after
1971 # affected local revisions will end up with the same commit message after
1971 # the URI is amended in. Append in the DREV line, or update it if it
1972 # the URI is amended in. Append in the DREV line, or update it if it
1972 # exists. At worst, this means commit message or test plan updates on
1973 # exists. At worst, this means commit message or test plan updates on
1973 # Phabricator aren't propagated back to the repository, but that seems
1974 # Phabricator aren't propagated back to the repository, but that seems
1974 # reasonable for the case where local commits are effectively combined
1975 # reasonable for the case where local commits are effectively combined
1975 # in Phabricator.
1976 # in Phabricator.
1976 m = _differentialrevisiondescre.search(ctx.description())
1977 m = _differentialrevisiondescre.search(ctx.description())
1977 if not m:
1978 if not m:
1978 return b'\n\n'.join([ctx.description(), uri])
1979 return b'\n\n'.join([ctx.description(), uri])
1979
1980
1980 return _differentialrevisiondescre.sub(uri, ctx.description())
1981 return _differentialrevisiondescre.sub(uri, ctx.description())
1981
1982
1982
1983
1983 def getlocalcommits(diff):
1984 def getlocalcommits(diff):
1984 """get the set of local commits from a diff object
1985 """get the set of local commits from a diff object
1985
1986
1986 See ``getdiffmeta()`` for an example diff object.
1987 See ``getdiffmeta()`` for an example diff object.
1987 """
1988 """
1988 props = diff.get(b'properties') or {}
1989 props = diff.get(b'properties') or {}
1989 commits = props.get(b'local:commits') or {}
1990 commits = props.get(b'local:commits') or {}
1990 if len(commits) > 1:
1991 if len(commits) > 1:
1991 return {bin(c) for c in commits.keys()}
1992 return {bin(c) for c in commits.keys()}
1992
1993
1993 # Storing the diff metadata predates storing `local:commits`, so continue
1994 # Storing the diff metadata predates storing `local:commits`, so continue
1994 # to use that in the --no-fold case.
1995 # to use that in the --no-fold case.
1995 return {bin(getdiffmeta(diff).get(b'node', b'')) or None}
1996 return {bin(getdiffmeta(diff).get(b'node', b'')) or None}
1996
1997
1997
1998
1998 def getdiffmeta(diff):
1999 def getdiffmeta(diff):
1999 """get commit metadata (date, node, user, p1) from a diff object
2000 """get commit metadata (date, node, user, p1) from a diff object
2000
2001
2001 The metadata could be "hg:meta", sent by phabsend, like:
2002 The metadata could be "hg:meta", sent by phabsend, like:
2002
2003
2003 "properties": {
2004 "properties": {
2004 "hg:meta": {
2005 "hg:meta": {
2005 "branch": "default",
2006 "branch": "default",
2006 "date": "1499571514 25200",
2007 "date": "1499571514 25200",
2007 "node": "98c08acae292b2faf60a279b4189beb6cff1414d",
2008 "node": "98c08acae292b2faf60a279b4189beb6cff1414d",
2008 "user": "Foo Bar <foo@example.com>",
2009 "user": "Foo Bar <foo@example.com>",
2009 "parent": "6d0abad76b30e4724a37ab8721d630394070fe16"
2010 "parent": "6d0abad76b30e4724a37ab8721d630394070fe16"
2010 }
2011 }
2011 }
2012 }
2012
2013
2013 Or converted from "local:commits", sent by "arc", like:
2014 Or converted from "local:commits", sent by "arc", like:
2014
2015
2015 "properties": {
2016 "properties": {
2016 "local:commits": {
2017 "local:commits": {
2017 "98c08acae292b2faf60a279b4189beb6cff1414d": {
2018 "98c08acae292b2faf60a279b4189beb6cff1414d": {
2018 "author": "Foo Bar",
2019 "author": "Foo Bar",
2019 "authorEmail": "foo@example.com"
2020 "authorEmail": "foo@example.com"
2020 "branch": "default",
2021 "branch": "default",
2021 "commit": "98c08acae292b2faf60a279b4189beb6cff1414d",
2022 "commit": "98c08acae292b2faf60a279b4189beb6cff1414d",
2022 "local": "1000",
2023 "local": "1000",
2023 "message": "...",
2024 "message": "...",
2024 "parents": ["6d0abad76b30e4724a37ab8721d630394070fe16"],
2025 "parents": ["6d0abad76b30e4724a37ab8721d630394070fe16"],
2025 "rev": "98c08acae292b2faf60a279b4189beb6cff1414d",
2026 "rev": "98c08acae292b2faf60a279b4189beb6cff1414d",
2026 "summary": "...",
2027 "summary": "...",
2027 "tag": "",
2028 "tag": "",
2028 "time": 1499546314,
2029 "time": 1499546314,
2029 }
2030 }
2030 }
2031 }
2031 }
2032 }
2032
2033
2033 Note: metadata extracted from "local:commits" will lose time zone
2034 Note: metadata extracted from "local:commits" will lose time zone
2034 information.
2035 information.
2035 """
2036 """
2036 props = diff.get(b'properties') or {}
2037 props = diff.get(b'properties') or {}
2037 meta = props.get(b'hg:meta')
2038 meta = props.get(b'hg:meta')
2038 if not meta:
2039 if not meta:
2039 if props.get(b'local:commits'):
2040 if props.get(b'local:commits'):
2040 commit = sorted(props[b'local:commits'].values())[0]
2041 commit = sorted(props[b'local:commits'].values())[0]
2041 meta = {}
2042 meta = {}
2042 if b'author' in commit and b'authorEmail' in commit:
2043 if b'author' in commit and b'authorEmail' in commit:
2043 meta[b'user'] = b'%s <%s>' % (
2044 meta[b'user'] = b'%s <%s>' % (
2044 commit[b'author'],
2045 commit[b'author'],
2045 commit[b'authorEmail'],
2046 commit[b'authorEmail'],
2046 )
2047 )
2047 if b'time' in commit:
2048 if b'time' in commit:
2048 meta[b'date'] = b'%d 0' % int(commit[b'time'])
2049 meta[b'date'] = b'%d 0' % int(commit[b'time'])
2049 if b'branch' in commit:
2050 if b'branch' in commit:
2050 meta[b'branch'] = commit[b'branch']
2051 meta[b'branch'] = commit[b'branch']
2051 node = commit.get(b'commit', commit.get(b'rev'))
2052 node = commit.get(b'commit', commit.get(b'rev'))
2052 if node:
2053 if node:
2053 meta[b'node'] = node
2054 meta[b'node'] = node
2054 if len(commit.get(b'parents', ())) >= 1:
2055 if len(commit.get(b'parents', ())) >= 1:
2055 meta[b'parent'] = commit[b'parents'][0]
2056 meta[b'parent'] = commit[b'parents'][0]
2056 else:
2057 else:
2057 meta = {}
2058 meta = {}
2058 if b'date' not in meta and b'dateCreated' in diff:
2059 if b'date' not in meta and b'dateCreated' in diff:
2059 meta[b'date'] = b'%s 0' % diff[b'dateCreated']
2060 meta[b'date'] = b'%s 0' % diff[b'dateCreated']
2060 if b'branch' not in meta and diff.get(b'branch'):
2061 if b'branch' not in meta and diff.get(b'branch'):
2061 meta[b'branch'] = diff[b'branch']
2062 meta[b'branch'] = diff[b'branch']
2062 if b'parent' not in meta and diff.get(b'sourceControlBaseRevision'):
2063 if b'parent' not in meta and diff.get(b'sourceControlBaseRevision'):
2063 meta[b'parent'] = diff[b'sourceControlBaseRevision']
2064 meta[b'parent'] = diff[b'sourceControlBaseRevision']
2064 return meta
2065 return meta
2065
2066
2066
2067
2067 def _getdrevs(ui, stack, specs):
2068 def _getdrevs(ui, stack, specs):
2068 """convert user supplied DREVSPECs into "Differential Revision" dicts
2069 """convert user supplied DREVSPECs into "Differential Revision" dicts
2069
2070
2070 See ``hg help phabread`` for how to specify each DREVSPEC.
2071 See ``hg help phabread`` for how to specify each DREVSPEC.
2071 """
2072 """
2072 if len(specs) > 0:
2073 if len(specs) > 0:
2073
2074
2074 def _formatspec(s):
2075 def _formatspec(s):
2075 if stack:
2076 if stack:
2076 s = b':(%s)' % s
2077 s = b':(%s)' % s
2077 return b'(%s)' % s
2078 return b'(%s)' % s
2078
2079
2079 spec = b'+'.join(pycompat.maplist(_formatspec, specs))
2080 spec = b'+'.join(pycompat.maplist(_formatspec, specs))
2080
2081
2081 drevs = querydrev(ui, spec)
2082 drevs = querydrev(ui, spec)
2082 if drevs:
2083 if drevs:
2083 return drevs
2084 return drevs
2084
2085
2085 raise error.Abort(_(b"empty DREVSPEC set"))
2086 raise error.Abort(_(b"empty DREVSPEC set"))
2086
2087
2087
2088
2088 def readpatch(ui, drevs, write):
2089 def readpatch(ui, drevs, write):
2089 """generate plain-text patch readable by 'hg import'
2090 """generate plain-text patch readable by 'hg import'
2090
2091
2091 write takes a list of (DREV, bytes), where DREV is the differential number
2092 write takes a list of (DREV, bytes), where DREV is the differential number
2092 (as bytes, without the "D" prefix) and the bytes are the text of a patch
2093 (as bytes, without the "D" prefix) and the bytes are the text of a patch
2093 to be imported. drevs is what "querydrev" returns, results of
2094 to be imported. drevs is what "querydrev" returns, results of
2094 "differential.query".
2095 "differential.query".
2095 """
2096 """
2096 # Prefetch hg:meta property for all diffs
2097 # Prefetch hg:meta property for all diffs
2097 diffids = sorted({max(int(v) for v in drev[b'diffs']) for drev in drevs})
2098 diffids = sorted({max(int(v) for v in drev[b'diffs']) for drev in drevs})
2098 diffs = callconduit(ui, b'differential.querydiffs', {b'ids': diffids})
2099 diffs = callconduit(ui, b'differential.querydiffs', {b'ids': diffids})
2099
2100
2100 patches = []
2101 patches = []
2101
2102
2102 # Generate patch for each drev
2103 # Generate patch for each drev
2103 for drev in drevs:
2104 for drev in drevs:
2104 ui.note(_(b'reading D%s\n') % drev[b'id'])
2105 ui.note(_(b'reading D%s\n') % drev[b'id'])
2105
2106
2106 diffid = max(int(v) for v in drev[b'diffs'])
2107 diffid = max(int(v) for v in drev[b'diffs'])
2107 body = callconduit(ui, b'differential.getrawdiff', {b'diffID': diffid})
2108 body = callconduit(ui, b'differential.getrawdiff', {b'diffID': diffid})
2108 desc = getdescfromdrev(drev)
2109 desc = getdescfromdrev(drev)
2109 header = b'# HG changeset patch\n'
2110 header = b'# HG changeset patch\n'
2110
2111
2111 # Try to preserve metadata from hg:meta property. Write hg patch
2112 # Try to preserve metadata from hg:meta property. Write hg patch
2112 # headers that can be read by the "import" command. See patchheadermap
2113 # headers that can be read by the "import" command. See patchheadermap
2113 # and extract in mercurial/patch.py for supported headers.
2114 # and extract in mercurial/patch.py for supported headers.
2114 meta = getdiffmeta(diffs[b'%d' % diffid])
2115 meta = getdiffmeta(diffs[b'%d' % diffid])
2115 for k in _metanamemap.keys():
2116 for k in _metanamemap.keys():
2116 if k in meta:
2117 if k in meta:
2117 header += b'# %s %s\n' % (_metanamemap[k], meta[k])
2118 header += b'# %s %s\n' % (_metanamemap[k], meta[k])
2118
2119
2119 content = b'%s%s\n%s' % (header, desc, body)
2120 content = b'%s%s\n%s' % (header, desc, body)
2120 patches.append((drev[b'id'], content))
2121 patches.append((drev[b'id'], content))
2121
2122
2122 # Write patches to the supplied callback
2123 # Write patches to the supplied callback
2123 write(patches)
2124 write(patches)
2124
2125
2125
2126
2126 @vcrcommand(
2127 @vcrcommand(
2127 b'phabread',
2128 b'phabread',
2128 [(b'', b'stack', False, _(b'read dependencies'))],
2129 [(b'', b'stack', False, _(b'read dependencies'))],
2129 _(b'DREVSPEC... [OPTIONS]'),
2130 _(b'DREVSPEC... [OPTIONS]'),
2130 helpcategory=command.CATEGORY_IMPORT_EXPORT,
2131 helpcategory=command.CATEGORY_IMPORT_EXPORT,
2131 optionalrepo=True,
2132 optionalrepo=True,
2132 )
2133 )
2133 def phabread(ui, repo, *specs, **opts):
2134 def phabread(ui, repo, *specs, **opts):
2134 """print patches from Phabricator suitable for importing
2135 """print patches from Phabricator suitable for importing
2135
2136
2136 DREVSPEC could be a Differential Revision identity, like ``D123``, or just
2137 DREVSPEC could be a Differential Revision identity, like ``D123``, or just
2137 the number ``123``. It could also have common operators like ``+``, ``-``,
2138 the number ``123``. It could also have common operators like ``+``, ``-``,
2138 ``&``, ``(``, ``)`` for complex queries. Prefix ``:`` could be used to
2139 ``&``, ``(``, ``)`` for complex queries. Prefix ``:`` could be used to
2139 select a stack. If multiple DREVSPEC values are given, the result is the
2140 select a stack. If multiple DREVSPEC values are given, the result is the
2140 union of each individually evaluated value. No attempt is currently made
2141 union of each individually evaluated value. No attempt is currently made
2141 to reorder the values to run from parent to child.
2142 to reorder the values to run from parent to child.
2142
2143
2143 ``abandoned``, ``accepted``, ``closed``, ``needsreview``, ``needsrevision``
2144 ``abandoned``, ``accepted``, ``closed``, ``needsreview``, ``needsrevision``
2144 could be used to filter patches by status. For performance reason, they
2145 could be used to filter patches by status. For performance reason, they
2145 only represent a subset of non-status selections and cannot be used alone.
2146 only represent a subset of non-status selections and cannot be used alone.
2146
2147
2147 For example, ``:D6+8-(2+D4)`` selects a stack up to D6, plus D8 and exclude
2148 For example, ``:D6+8-(2+D4)`` selects a stack up to D6, plus D8 and exclude
2148 D2 and D4. ``:D9 & needsreview`` selects "Needs Review" revisions in a
2149 D2 and D4. ``:D9 & needsreview`` selects "Needs Review" revisions in a
2149 stack up to D9.
2150 stack up to D9.
2150
2151
2151 If --stack is given, follow dependencies information and read all patches.
2152 If --stack is given, follow dependencies information and read all patches.
2152 It is equivalent to the ``:`` operator.
2153 It is equivalent to the ``:`` operator.
2153 """
2154 """
2154 opts = pycompat.byteskwargs(opts)
2155 opts = pycompat.byteskwargs(opts)
2155 drevs = _getdrevs(ui, opts.get(b'stack'), specs)
2156 drevs = _getdrevs(ui, opts.get(b'stack'), specs)
2156
2157
2157 def _write(patches):
2158 def _write(patches):
2158 for drev, content in patches:
2159 for drev, content in patches:
2159 ui.write(content)
2160 ui.write(content)
2160
2161
2161 readpatch(ui, drevs, _write)
2162 readpatch(ui, drevs, _write)
2162
2163
2163
2164
2164 @vcrcommand(
2165 @vcrcommand(
2165 b'phabimport',
2166 b'phabimport',
2166 [(b'', b'stack', False, _(b'import dependencies as well'))],
2167 [(b'', b'stack', False, _(b'import dependencies as well'))],
2167 _(b'DREVSPEC... [OPTIONS]'),
2168 _(b'DREVSPEC... [OPTIONS]'),
2168 helpcategory=command.CATEGORY_IMPORT_EXPORT,
2169 helpcategory=command.CATEGORY_IMPORT_EXPORT,
2169 )
2170 )
2170 def phabimport(ui, repo, *specs, **opts):
2171 def phabimport(ui, repo, *specs, **opts):
2171 """import patches from Phabricator for the specified Differential Revisions
2172 """import patches from Phabricator for the specified Differential Revisions
2172
2173
2173 The patches are read and applied starting at the parent of the working
2174 The patches are read and applied starting at the parent of the working
2174 directory.
2175 directory.
2175
2176
2176 See ``hg help phabread`` for how to specify DREVSPEC.
2177 See ``hg help phabread`` for how to specify DREVSPEC.
2177 """
2178 """
2178 opts = pycompat.byteskwargs(opts)
2179 opts = pycompat.byteskwargs(opts)
2179
2180
2180 # --bypass avoids losing exec and symlink bits when importing on Windows,
2181 # --bypass avoids losing exec and symlink bits when importing on Windows,
2181 # and allows importing with a dirty wdir. It also aborts instead of leaving
2182 # and allows importing with a dirty wdir. It also aborts instead of leaving
2182 # rejects.
2183 # rejects.
2183 opts[b'bypass'] = True
2184 opts[b'bypass'] = True
2184
2185
2185 # Mandatory default values, synced with commands.import
2186 # Mandatory default values, synced with commands.import
2186 opts[b'strip'] = 1
2187 opts[b'strip'] = 1
2187 opts[b'prefix'] = b''
2188 opts[b'prefix'] = b''
2188 # Evolve 9.3.0 assumes this key is present in cmdutil.tryimportone()
2189 # Evolve 9.3.0 assumes this key is present in cmdutil.tryimportone()
2189 opts[b'obsolete'] = False
2190 opts[b'obsolete'] = False
2190
2191
2191 if ui.configbool(b'phabimport', b'secret'):
2192 if ui.configbool(b'phabimport', b'secret'):
2192 opts[b'secret'] = True
2193 opts[b'secret'] = True
2193 if ui.configbool(b'phabimport', b'obsolete'):
2194 if ui.configbool(b'phabimport', b'obsolete'):
2194 opts[b'obsolete'] = True # Handled by evolve wrapping tryimportone()
2195 opts[b'obsolete'] = True # Handled by evolve wrapping tryimportone()
2195
2196
2196 def _write(patches):
2197 def _write(patches):
2197 parents = repo[None].parents()
2198 parents = repo[None].parents()
2198
2199
2199 with repo.wlock(), repo.lock(), repo.transaction(b'phabimport'):
2200 with repo.wlock(), repo.lock(), repo.transaction(b'phabimport'):
2200 for drev, contents in patches:
2201 for drev, contents in patches:
2201 ui.status(_(b'applying patch from D%s\n') % drev)
2202 ui.status(_(b'applying patch from D%s\n') % drev)
2202
2203
2203 with patch.extract(ui, pycompat.bytesio(contents)) as patchdata:
2204 with patch.extract(ui, io.BytesIO(contents)) as patchdata:
2204 msg, node, rej = cmdutil.tryimportone(
2205 msg, node, rej = cmdutil.tryimportone(
2205 ui,
2206 ui,
2206 repo,
2207 repo,
2207 patchdata,
2208 patchdata,
2208 parents,
2209 parents,
2209 opts,
2210 opts,
2210 [],
2211 [],
2211 None, # Never update wdir to another revision
2212 None, # Never update wdir to another revision
2212 )
2213 )
2213
2214
2214 if not node:
2215 if not node:
2215 raise error.Abort(_(b'D%s: no diffs found') % drev)
2216 raise error.Abort(_(b'D%s: no diffs found') % drev)
2216
2217
2217 ui.note(msg + b'\n')
2218 ui.note(msg + b'\n')
2218 parents = [repo[node]]
2219 parents = [repo[node]]
2219
2220
2220 drevs = _getdrevs(ui, opts.get(b'stack'), specs)
2221 drevs = _getdrevs(ui, opts.get(b'stack'), specs)
2221
2222
2222 readpatch(repo.ui, drevs, _write)
2223 readpatch(repo.ui, drevs, _write)
2223
2224
2224
2225
2225 @vcrcommand(
2226 @vcrcommand(
2226 b'phabupdate',
2227 b'phabupdate',
2227 [
2228 [
2228 (b'', b'accept', False, _(b'accept revisions')),
2229 (b'', b'accept', False, _(b'accept revisions')),
2229 (b'', b'reject', False, _(b'reject revisions')),
2230 (b'', b'reject', False, _(b'reject revisions')),
2230 (b'', b'request-review', False, _(b'request review on revisions')),
2231 (b'', b'request-review', False, _(b'request review on revisions')),
2231 (b'', b'abandon', False, _(b'abandon revisions')),
2232 (b'', b'abandon', False, _(b'abandon revisions')),
2232 (b'', b'reclaim', False, _(b'reclaim revisions')),
2233 (b'', b'reclaim', False, _(b'reclaim revisions')),
2233 (b'', b'close', False, _(b'close revisions')),
2234 (b'', b'close', False, _(b'close revisions')),
2234 (b'', b'reopen', False, _(b'reopen revisions')),
2235 (b'', b'reopen', False, _(b'reopen revisions')),
2235 (b'', b'plan-changes', False, _(b'plan changes for revisions')),
2236 (b'', b'plan-changes', False, _(b'plan changes for revisions')),
2236 (b'', b'resign', False, _(b'resign as a reviewer from revisions')),
2237 (b'', b'resign', False, _(b'resign as a reviewer from revisions')),
2237 (b'', b'commandeer', False, _(b'commandeer revisions')),
2238 (b'', b'commandeer', False, _(b'commandeer revisions')),
2238 (b'm', b'comment', b'', _(b'comment on the last revision')),
2239 (b'm', b'comment', b'', _(b'comment on the last revision')),
2239 (b'r', b'rev', b'', _(b'local revision to update'), _(b'REV')),
2240 (b'r', b'rev', b'', _(b'local revision to update'), _(b'REV')),
2240 ],
2241 ],
2241 _(b'[DREVSPEC...| -r REV...] [OPTIONS]'),
2242 _(b'[DREVSPEC...| -r REV...] [OPTIONS]'),
2242 helpcategory=command.CATEGORY_IMPORT_EXPORT,
2243 helpcategory=command.CATEGORY_IMPORT_EXPORT,
2243 optionalrepo=True,
2244 optionalrepo=True,
2244 )
2245 )
2245 def phabupdate(ui, repo, *specs, **opts):
2246 def phabupdate(ui, repo, *specs, **opts):
2246 """update Differential Revision in batch
2247 """update Differential Revision in batch
2247
2248
2248 DREVSPEC selects revisions. See :hg:`help phabread` for its usage.
2249 DREVSPEC selects revisions. See :hg:`help phabread` for its usage.
2249 """
2250 """
2250 opts = pycompat.byteskwargs(opts)
2251 opts = pycompat.byteskwargs(opts)
2251 transactions = [
2252 transactions = [
2252 b'abandon',
2253 b'abandon',
2253 b'accept',
2254 b'accept',
2254 b'close',
2255 b'close',
2255 b'commandeer',
2256 b'commandeer',
2256 b'plan-changes',
2257 b'plan-changes',
2257 b'reclaim',
2258 b'reclaim',
2258 b'reject',
2259 b'reject',
2259 b'reopen',
2260 b'reopen',
2260 b'request-review',
2261 b'request-review',
2261 b'resign',
2262 b'resign',
2262 ]
2263 ]
2263 flags = [n for n in transactions if opts.get(n.replace(b'-', b'_'))]
2264 flags = [n for n in transactions if opts.get(n.replace(b'-', b'_'))]
2264 if len(flags) > 1:
2265 if len(flags) > 1:
2265 raise error.Abort(_(b'%s cannot be used together') % b', '.join(flags))
2266 raise error.Abort(_(b'%s cannot be used together') % b', '.join(flags))
2266
2267
2267 actions = []
2268 actions = []
2268 for f in flags:
2269 for f in flags:
2269 actions.append({b'type': f, b'value': True})
2270 actions.append({b'type': f, b'value': True})
2270
2271
2271 revs = opts.get(b'rev')
2272 revs = opts.get(b'rev')
2272 if revs:
2273 if revs:
2273 if not repo:
2274 if not repo:
2274 raise error.InputError(_(b'--rev requires a repository'))
2275 raise error.InputError(_(b'--rev requires a repository'))
2275
2276
2276 if specs:
2277 if specs:
2277 raise error.InputError(_(b'cannot specify both DREVSPEC and --rev'))
2278 raise error.InputError(_(b'cannot specify both DREVSPEC and --rev'))
2278
2279
2279 drevmap = getdrevmap(repo, logcmdutil.revrange(repo, [revs]))
2280 drevmap = getdrevmap(repo, logcmdutil.revrange(repo, [revs]))
2280 specs = []
2281 specs = []
2281 unknown = []
2282 unknown = []
2282 for r, d in pycompat.iteritems(drevmap):
2283 for r, d in pycompat.iteritems(drevmap):
2283 if d is None:
2284 if d is None:
2284 unknown.append(repo[r])
2285 unknown.append(repo[r])
2285 else:
2286 else:
2286 specs.append(b'D%d' % d)
2287 specs.append(b'D%d' % d)
2287 if unknown:
2288 if unknown:
2288 raise error.InputError(
2289 raise error.InputError(
2289 _(b'selected revisions without a Differential: %s')
2290 _(b'selected revisions without a Differential: %s')
2290 % scmutil.nodesummaries(repo, unknown)
2291 % scmutil.nodesummaries(repo, unknown)
2291 )
2292 )
2292
2293
2293 drevs = _getdrevs(ui, opts.get(b'stack'), specs)
2294 drevs = _getdrevs(ui, opts.get(b'stack'), specs)
2294 for i, drev in enumerate(drevs):
2295 for i, drev in enumerate(drevs):
2295 if i + 1 == len(drevs) and opts.get(b'comment'):
2296 if i + 1 == len(drevs) and opts.get(b'comment'):
2296 actions.append({b'type': b'comment', b'value': opts[b'comment']})
2297 actions.append({b'type': b'comment', b'value': opts[b'comment']})
2297 if actions:
2298 if actions:
2298 params = {
2299 params = {
2299 b'objectIdentifier': drev[b'phid'],
2300 b'objectIdentifier': drev[b'phid'],
2300 b'transactions': actions,
2301 b'transactions': actions,
2301 }
2302 }
2302 callconduit(ui, b'differential.revision.edit', params)
2303 callconduit(ui, b'differential.revision.edit', params)
2303
2304
2304
2305
2305 @eh.templatekeyword(b'phabreview', requires={b'ctx'})
2306 @eh.templatekeyword(b'phabreview', requires={b'ctx'})
2306 def template_review(context, mapping):
2307 def template_review(context, mapping):
2307 """:phabreview: Object describing the review for this changeset.
2308 """:phabreview: Object describing the review for this changeset.
2308 Has attributes `url` and `id`.
2309 Has attributes `url` and `id`.
2309 """
2310 """
2310 ctx = context.resource(mapping, b'ctx')
2311 ctx = context.resource(mapping, b'ctx')
2311 m = _differentialrevisiondescre.search(ctx.description())
2312 m = _differentialrevisiondescre.search(ctx.description())
2312 if m:
2313 if m:
2313 return templateutil.hybriddict(
2314 return templateutil.hybriddict(
2314 {
2315 {
2315 b'url': m.group('url'),
2316 b'url': m.group('url'),
2316 b'id': b"D%s" % m.group('id'),
2317 b'id': b"D%s" % m.group('id'),
2317 }
2318 }
2318 )
2319 )
2319 else:
2320 else:
2320 tags = ctx.repo().nodetags(ctx.node())
2321 tags = ctx.repo().nodetags(ctx.node())
2321 for t in tags:
2322 for t in tags:
2322 if _differentialrevisiontagre.match(t):
2323 if _differentialrevisiontagre.match(t):
2323 url = ctx.repo().ui.config(b'phabricator', b'url')
2324 url = ctx.repo().ui.config(b'phabricator', b'url')
2324 if not url.endswith(b'/'):
2325 if not url.endswith(b'/'):
2325 url += b'/'
2326 url += b'/'
2326 url += t
2327 url += t
2327
2328
2328 return templateutil.hybriddict(
2329 return templateutil.hybriddict(
2329 {
2330 {
2330 b'url': url,
2331 b'url': url,
2331 b'id': t,
2332 b'id': t,
2332 }
2333 }
2333 )
2334 )
2334 return None
2335 return None
2335
2336
2336
2337
2337 @eh.templatekeyword(b'phabstatus', requires={b'ctx', b'repo', b'ui'})
2338 @eh.templatekeyword(b'phabstatus', requires={b'ctx', b'repo', b'ui'})
2338 def template_status(context, mapping):
2339 def template_status(context, mapping):
2339 """:phabstatus: String. Status of Phabricator differential."""
2340 """:phabstatus: String. Status of Phabricator differential."""
2340 ctx = context.resource(mapping, b'ctx')
2341 ctx = context.resource(mapping, b'ctx')
2341 repo = context.resource(mapping, b'repo')
2342 repo = context.resource(mapping, b'repo')
2342 ui = context.resource(mapping, b'ui')
2343 ui = context.resource(mapping, b'ui')
2343
2344
2344 rev = ctx.rev()
2345 rev = ctx.rev()
2345 try:
2346 try:
2346 drevid = getdrevmap(repo, [rev])[rev]
2347 drevid = getdrevmap(repo, [rev])[rev]
2347 except KeyError:
2348 except KeyError:
2348 return None
2349 return None
2349 drevs = callconduit(ui, b'differential.query', {b'ids': [drevid]})
2350 drevs = callconduit(ui, b'differential.query', {b'ids': [drevid]})
2350 for drev in drevs:
2351 for drev in drevs:
2351 if int(drev[b'id']) == drevid:
2352 if int(drev[b'id']) == drevid:
2352 return templateutil.hybriddict(
2353 return templateutil.hybriddict(
2353 {
2354 {
2354 b'url': drev[b'uri'],
2355 b'url': drev[b'uri'],
2355 b'status': drev[b'statusName'],
2356 b'status': drev[b'statusName'],
2356 }
2357 }
2357 )
2358 )
2358 return None
2359 return None
2359
2360
2360
2361
2361 @show.showview(b'phabstatus', csettopic=b'work')
2362 @show.showview(b'phabstatus', csettopic=b'work')
2362 def phabstatusshowview(ui, repo, displayer):
2363 def phabstatusshowview(ui, repo, displayer):
2363 """Phabricator differiential status"""
2364 """Phabricator differiential status"""
2364 revs = repo.revs('sort(_underway(), topo)')
2365 revs = repo.revs('sort(_underway(), topo)')
2365 drevmap = getdrevmap(repo, revs)
2366 drevmap = getdrevmap(repo, revs)
2366 unknownrevs, drevids, revsbydrevid = [], set(), {}
2367 unknownrevs, drevids, revsbydrevid = [], set(), {}
2367 for rev, drevid in pycompat.iteritems(drevmap):
2368 for rev, drevid in pycompat.iteritems(drevmap):
2368 if drevid is not None:
2369 if drevid is not None:
2369 drevids.add(drevid)
2370 drevids.add(drevid)
2370 revsbydrevid.setdefault(drevid, set()).add(rev)
2371 revsbydrevid.setdefault(drevid, set()).add(rev)
2371 else:
2372 else:
2372 unknownrevs.append(rev)
2373 unknownrevs.append(rev)
2373
2374
2374 drevs = callconduit(ui, b'differential.query', {b'ids': list(drevids)})
2375 drevs = callconduit(ui, b'differential.query', {b'ids': list(drevids)})
2375 drevsbyrev = {}
2376 drevsbyrev = {}
2376 for drev in drevs:
2377 for drev in drevs:
2377 for rev in revsbydrevid[int(drev[b'id'])]:
2378 for rev in revsbydrevid[int(drev[b'id'])]:
2378 drevsbyrev[rev] = drev
2379 drevsbyrev[rev] = drev
2379
2380
2380 def phabstatus(ctx):
2381 def phabstatus(ctx):
2381 drev = drevsbyrev[ctx.rev()]
2382 drev = drevsbyrev[ctx.rev()]
2382 status = ui.label(
2383 status = ui.label(
2383 b'%(statusName)s' % drev,
2384 b'%(statusName)s' % drev,
2384 b'phabricator.status.%s' % _getstatusname(drev),
2385 b'phabricator.status.%s' % _getstatusname(drev),
2385 )
2386 )
2386 ui.write(b"\n%s %s\n" % (drev[b'uri'], status))
2387 ui.write(b"\n%s %s\n" % (drev[b'uri'], status))
2387
2388
2388 revs -= smartset.baseset(unknownrevs)
2389 revs -= smartset.baseset(unknownrevs)
2389 revdag = graphmod.dagwalker(repo, revs)
2390 revdag = graphmod.dagwalker(repo, revs)
2390
2391
2391 ui.setconfig(b'experimental', b'graphshorten', True)
2392 ui.setconfig(b'experimental', b'graphshorten', True)
2392 displayer._exthook = phabstatus
2393 displayer._exthook = phabstatus
2393 nodelen = show.longestshortest(repo, revs)
2394 nodelen = show.longestshortest(repo, revs)
2394 logcmdutil.displaygraph(
2395 logcmdutil.displaygraph(
2395 ui,
2396 ui,
2396 repo,
2397 repo,
2397 revdag,
2398 revdag,
2398 displayer,
2399 displayer,
2399 graphmod.asciiedges,
2400 graphmod.asciiedges,
2400 props={b'nodelen': nodelen},
2401 props={b'nodelen': nodelen},
2401 )
2402 )
@@ -1,135 +1,135 b''
1 # mpatch.py - Python implementation of mpatch.c
1 # mpatch.py - Python implementation of mpatch.c
2 #
2 #
3 # Copyright 2009 Olivia Mackall <olivia@selenic.com> and others
3 # Copyright 2009 Olivia Mackall <olivia@selenic.com> and others
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 from __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 import io
10 import struct
11 import struct
11
12
12 from .. import pycompat
13
13
14 stringio = pycompat.bytesio
14 stringio = io.BytesIO
15
15
16
16
17 class mpatchError(Exception):
17 class mpatchError(Exception):
18 """error raised when a delta cannot be decoded"""
18 """error raised when a delta cannot be decoded"""
19
19
20
20
21 # This attempts to apply a series of patches in time proportional to
21 # This attempts to apply a series of patches in time proportional to
22 # the total size of the patches, rather than patches * len(text). This
22 # the total size of the patches, rather than patches * len(text). This
23 # means rather than shuffling strings around, we shuffle around
23 # means rather than shuffling strings around, we shuffle around
24 # pointers to fragments with fragment lists.
24 # pointers to fragments with fragment lists.
25 #
25 #
26 # When the fragment lists get too long, we collapse them. To do this
26 # When the fragment lists get too long, we collapse them. To do this
27 # efficiently, we do all our operations inside a buffer created by
27 # efficiently, we do all our operations inside a buffer created by
28 # mmap and simply use memmove. This avoids creating a bunch of large
28 # mmap and simply use memmove. This avoids creating a bunch of large
29 # temporary string buffers.
29 # temporary string buffers.
30
30
31
31
32 def _pull(dst, src, l): # pull l bytes from src
32 def _pull(dst, src, l): # pull l bytes from src
33 while l:
33 while l:
34 f = src.pop()
34 f = src.pop()
35 if f[0] > l: # do we need to split?
35 if f[0] > l: # do we need to split?
36 src.append((f[0] - l, f[1] + l))
36 src.append((f[0] - l, f[1] + l))
37 dst.append((l, f[1]))
37 dst.append((l, f[1]))
38 return
38 return
39 dst.append(f)
39 dst.append(f)
40 l -= f[0]
40 l -= f[0]
41
41
42
42
43 def _move(m, dest, src, count):
43 def _move(m, dest, src, count):
44 """move count bytes from src to dest
44 """move count bytes from src to dest
45
45
46 The file pointer is left at the end of dest.
46 The file pointer is left at the end of dest.
47 """
47 """
48 m.seek(src)
48 m.seek(src)
49 buf = m.read(count)
49 buf = m.read(count)
50 m.seek(dest)
50 m.seek(dest)
51 m.write(buf)
51 m.write(buf)
52
52
53
53
54 def _collect(m, buf, list):
54 def _collect(m, buf, list):
55 start = buf
55 start = buf
56 for l, p in reversed(list):
56 for l, p in reversed(list):
57 _move(m, buf, p, l)
57 _move(m, buf, p, l)
58 buf += l
58 buf += l
59 return (buf - start, start)
59 return (buf - start, start)
60
60
61
61
62 def patches(a, bins):
62 def patches(a, bins):
63 if not bins:
63 if not bins:
64 return a
64 return a
65
65
66 plens = [len(x) for x in bins]
66 plens = [len(x) for x in bins]
67 pl = sum(plens)
67 pl = sum(plens)
68 bl = len(a) + pl
68 bl = len(a) + pl
69 tl = bl + bl + pl # enough for the patches and two working texts
69 tl = bl + bl + pl # enough for the patches and two working texts
70 b1, b2 = 0, bl
70 b1, b2 = 0, bl
71
71
72 if not tl:
72 if not tl:
73 return a
73 return a
74
74
75 m = stringio()
75 m = stringio()
76
76
77 # load our original text
77 # load our original text
78 m.write(a)
78 m.write(a)
79 frags = [(len(a), b1)]
79 frags = [(len(a), b1)]
80
80
81 # copy all the patches into our segment so we can memmove from them
81 # copy all the patches into our segment so we can memmove from them
82 pos = b2 + bl
82 pos = b2 + bl
83 m.seek(pos)
83 m.seek(pos)
84 for p in bins:
84 for p in bins:
85 m.write(p)
85 m.write(p)
86
86
87 for plen in plens:
87 for plen in plens:
88 # if our list gets too long, execute it
88 # if our list gets too long, execute it
89 if len(frags) > 128:
89 if len(frags) > 128:
90 b2, b1 = b1, b2
90 b2, b1 = b1, b2
91 frags = [_collect(m, b1, frags)]
91 frags = [_collect(m, b1, frags)]
92
92
93 new = []
93 new = []
94 end = pos + plen
94 end = pos + plen
95 last = 0
95 last = 0
96 while pos < end:
96 while pos < end:
97 m.seek(pos)
97 m.seek(pos)
98 try:
98 try:
99 p1, p2, l = struct.unpack(b">lll", m.read(12))
99 p1, p2, l = struct.unpack(b">lll", m.read(12))
100 except struct.error:
100 except struct.error:
101 raise mpatchError(b"patch cannot be decoded")
101 raise mpatchError(b"patch cannot be decoded")
102 _pull(new, frags, p1 - last) # what didn't change
102 _pull(new, frags, p1 - last) # what didn't change
103 _pull([], frags, p2 - p1) # what got deleted
103 _pull([], frags, p2 - p1) # what got deleted
104 new.append((l, pos + 12)) # what got added
104 new.append((l, pos + 12)) # what got added
105 pos += l + 12
105 pos += l + 12
106 last = p2
106 last = p2
107 frags.extend(reversed(new)) # what was left at the end
107 frags.extend(reversed(new)) # what was left at the end
108
108
109 t = _collect(m, b2, frags)
109 t = _collect(m, b2, frags)
110
110
111 m.seek(t[1])
111 m.seek(t[1])
112 return m.read(t[0])
112 return m.read(t[0])
113
113
114
114
115 def patchedsize(orig, delta):
115 def patchedsize(orig, delta):
116 outlen, last, bin = 0, 0, 0
116 outlen, last, bin = 0, 0, 0
117 binend = len(delta)
117 binend = len(delta)
118 data = 12
118 data = 12
119
119
120 while data <= binend:
120 while data <= binend:
121 decode = delta[bin : bin + 12]
121 decode = delta[bin : bin + 12]
122 start, end, length = struct.unpack(b">lll", decode)
122 start, end, length = struct.unpack(b">lll", decode)
123 if start > end:
123 if start > end:
124 break
124 break
125 bin = data + length
125 bin = data + length
126 data = bin + 12
126 data = bin + 12
127 outlen += start - last
127 outlen += start - last
128 last = end
128 last = end
129 outlen += length
129 outlen += length
130
130
131 if bin != binend:
131 if bin != binend:
132 raise mpatchError(b"patch cannot be decoded")
132 raise mpatchError(b"patch cannot be decoded")
133
133
134 outlen += orig - last
134 outlen += orig - last
135 return outlen
135 return outlen
@@ -1,975 +1,976 b''
1 # parsers.py - Python implementation of parsers.c
1 # parsers.py - Python implementation of parsers.c
2 #
2 #
3 # Copyright 2009 Olivia Mackall <olivia@selenic.com> and others
3 # Copyright 2009 Olivia Mackall <olivia@selenic.com> and others
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 from __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 import io
10 import stat
11 import stat
11 import struct
12 import struct
12 import zlib
13 import zlib
13
14
14 from ..node import (
15 from ..node import (
15 nullrev,
16 nullrev,
16 sha1nodeconstants,
17 sha1nodeconstants,
17 )
18 )
18 from ..thirdparty import attr
19 from ..thirdparty import attr
19 from .. import (
20 from .. import (
20 error,
21 error,
21 pycompat,
22 pycompat,
22 revlogutils,
23 revlogutils,
23 util,
24 util,
24 )
25 )
25
26
26 from ..revlogutils import nodemap as nodemaputil
27 from ..revlogutils import nodemap as nodemaputil
27 from ..revlogutils import constants as revlog_constants
28 from ..revlogutils import constants as revlog_constants
28
29
29 stringio = pycompat.bytesio
30 stringio = io.BytesIO
30
31
31
32
32 _pack = struct.pack
33 _pack = struct.pack
33 _unpack = struct.unpack
34 _unpack = struct.unpack
34 _compress = zlib.compress
35 _compress = zlib.compress
35 _decompress = zlib.decompress
36 _decompress = zlib.decompress
36
37
37
38
38 # a special value used internally for `size` if the file come from the other parent
39 # a special value used internally for `size` if the file come from the other parent
39 FROM_P2 = -2
40 FROM_P2 = -2
40
41
41 # a special value used internally for `size` if the file is modified/merged/added
42 # a special value used internally for `size` if the file is modified/merged/added
42 NONNORMAL = -1
43 NONNORMAL = -1
43
44
44 # a special value used internally for `time` if the time is ambigeous
45 # a special value used internally for `time` if the time is ambigeous
45 AMBIGUOUS_TIME = -1
46 AMBIGUOUS_TIME = -1
46
47
47 # Bits of the `flags` byte inside a node in the file format
48 # Bits of the `flags` byte inside a node in the file format
48 DIRSTATE_V2_WDIR_TRACKED = 1 << 0
49 DIRSTATE_V2_WDIR_TRACKED = 1 << 0
49 DIRSTATE_V2_P1_TRACKED = 1 << 1
50 DIRSTATE_V2_P1_TRACKED = 1 << 1
50 DIRSTATE_V2_P2_INFO = 1 << 2
51 DIRSTATE_V2_P2_INFO = 1 << 2
51 DIRSTATE_V2_MODE_EXEC_PERM = 1 << 3
52 DIRSTATE_V2_MODE_EXEC_PERM = 1 << 3
52 DIRSTATE_V2_MODE_IS_SYMLINK = 1 << 4
53 DIRSTATE_V2_MODE_IS_SYMLINK = 1 << 4
53 DIRSTATE_V2_HAS_FALLBACK_EXEC = 1 << 5
54 DIRSTATE_V2_HAS_FALLBACK_EXEC = 1 << 5
54 DIRSTATE_V2_FALLBACK_EXEC = 1 << 6
55 DIRSTATE_V2_FALLBACK_EXEC = 1 << 6
55 DIRSTATE_V2_HAS_FALLBACK_SYMLINK = 1 << 7
56 DIRSTATE_V2_HAS_FALLBACK_SYMLINK = 1 << 7
56 DIRSTATE_V2_FALLBACK_SYMLINK = 1 << 8
57 DIRSTATE_V2_FALLBACK_SYMLINK = 1 << 8
57 DIRSTATE_V2_EXPECTED_STATE_IS_MODIFIED = 1 << 9
58 DIRSTATE_V2_EXPECTED_STATE_IS_MODIFIED = 1 << 9
58 DIRSTATE_V2_HAS_MODE_AND_SIZE = 1 << 10
59 DIRSTATE_V2_HAS_MODE_AND_SIZE = 1 << 10
59 DIRSTATE_V2_HAS_MTIME = 1 << 11
60 DIRSTATE_V2_HAS_MTIME = 1 << 11
60 DIRSTATE_V2_MTIME_SECOND_AMBIGUOUS = 1 << 12
61 DIRSTATE_V2_MTIME_SECOND_AMBIGUOUS = 1 << 12
61 DIRSTATE_V2_DIRECTORY = 1 << 13
62 DIRSTATE_V2_DIRECTORY = 1 << 13
62 DIRSTATE_V2_ALL_UNKNOWN_RECORDED = 1 << 14
63 DIRSTATE_V2_ALL_UNKNOWN_RECORDED = 1 << 14
63 DIRSTATE_V2_ALL_IGNORED_RECORDED = 1 << 15
64 DIRSTATE_V2_ALL_IGNORED_RECORDED = 1 << 15
64
65
65
66
66 @attr.s(slots=True, init=False)
67 @attr.s(slots=True, init=False)
67 class DirstateItem(object):
68 class DirstateItem(object):
68 """represent a dirstate entry
69 """represent a dirstate entry
69
70
70 It hold multiple attributes
71 It hold multiple attributes
71
72
72 # about file tracking
73 # about file tracking
73 - wc_tracked: is the file tracked by the working copy
74 - wc_tracked: is the file tracked by the working copy
74 - p1_tracked: is the file tracked in working copy first parent
75 - p1_tracked: is the file tracked in working copy first parent
75 - p2_info: the file has been involved in some merge operation. Either
76 - p2_info: the file has been involved in some merge operation. Either
76 because it was actually merged, or because the p2 version was
77 because it was actually merged, or because the p2 version was
77 ahead, or because some rename moved it there. In either case
78 ahead, or because some rename moved it there. In either case
78 `hg status` will want it displayed as modified.
79 `hg status` will want it displayed as modified.
79
80
80 # about the file state expected from p1 manifest:
81 # about the file state expected from p1 manifest:
81 - mode: the file mode in p1
82 - mode: the file mode in p1
82 - size: the file size in p1
83 - size: the file size in p1
83
84
84 These value can be set to None, which mean we don't have a meaningful value
85 These value can be set to None, which mean we don't have a meaningful value
85 to compare with. Either because we don't really care about them as there
86 to compare with. Either because we don't really care about them as there
86 `status` is known without having to look at the disk or because we don't
87 `status` is known without having to look at the disk or because we don't
87 know these right now and a full comparison will be needed to find out if
88 know these right now and a full comparison will be needed to find out if
88 the file is clean.
89 the file is clean.
89
90
90 # about the file state on disk last time we saw it:
91 # about the file state on disk last time we saw it:
91 - mtime: the last known clean mtime for the file.
92 - mtime: the last known clean mtime for the file.
92
93
93 This value can be set to None if no cachable state exist. Either because we
94 This value can be set to None if no cachable state exist. Either because we
94 do not care (see previous section) or because we could not cache something
95 do not care (see previous section) or because we could not cache something
95 yet.
96 yet.
96 """
97 """
97
98
98 _wc_tracked = attr.ib()
99 _wc_tracked = attr.ib()
99 _p1_tracked = attr.ib()
100 _p1_tracked = attr.ib()
100 _p2_info = attr.ib()
101 _p2_info = attr.ib()
101 _mode = attr.ib()
102 _mode = attr.ib()
102 _size = attr.ib()
103 _size = attr.ib()
103 _mtime_s = attr.ib()
104 _mtime_s = attr.ib()
104 _mtime_ns = attr.ib()
105 _mtime_ns = attr.ib()
105 _fallback_exec = attr.ib()
106 _fallback_exec = attr.ib()
106 _fallback_symlink = attr.ib()
107 _fallback_symlink = attr.ib()
107 _mtime_second_ambiguous = attr.ib()
108 _mtime_second_ambiguous = attr.ib()
108
109
109 def __init__(
110 def __init__(
110 self,
111 self,
111 wc_tracked=False,
112 wc_tracked=False,
112 p1_tracked=False,
113 p1_tracked=False,
113 p2_info=False,
114 p2_info=False,
114 has_meaningful_data=True,
115 has_meaningful_data=True,
115 has_meaningful_mtime=True,
116 has_meaningful_mtime=True,
116 parentfiledata=None,
117 parentfiledata=None,
117 fallback_exec=None,
118 fallback_exec=None,
118 fallback_symlink=None,
119 fallback_symlink=None,
119 ):
120 ):
120 self._wc_tracked = wc_tracked
121 self._wc_tracked = wc_tracked
121 self._p1_tracked = p1_tracked
122 self._p1_tracked = p1_tracked
122 self._p2_info = p2_info
123 self._p2_info = p2_info
123
124
124 self._fallback_exec = fallback_exec
125 self._fallback_exec = fallback_exec
125 self._fallback_symlink = fallback_symlink
126 self._fallback_symlink = fallback_symlink
126
127
127 self._mode = None
128 self._mode = None
128 self._size = None
129 self._size = None
129 self._mtime_s = None
130 self._mtime_s = None
130 self._mtime_ns = None
131 self._mtime_ns = None
131 self._mtime_second_ambiguous = False
132 self._mtime_second_ambiguous = False
132 if parentfiledata is None:
133 if parentfiledata is None:
133 has_meaningful_mtime = False
134 has_meaningful_mtime = False
134 has_meaningful_data = False
135 has_meaningful_data = False
135 elif parentfiledata[2] is None:
136 elif parentfiledata[2] is None:
136 has_meaningful_mtime = False
137 has_meaningful_mtime = False
137 if has_meaningful_data:
138 if has_meaningful_data:
138 self._mode = parentfiledata[0]
139 self._mode = parentfiledata[0]
139 self._size = parentfiledata[1]
140 self._size = parentfiledata[1]
140 if has_meaningful_mtime:
141 if has_meaningful_mtime:
141 (
142 (
142 self._mtime_s,
143 self._mtime_s,
143 self._mtime_ns,
144 self._mtime_ns,
144 self._mtime_second_ambiguous,
145 self._mtime_second_ambiguous,
145 ) = parentfiledata[2]
146 ) = parentfiledata[2]
146
147
147 @classmethod
148 @classmethod
148 def from_v2_data(cls, flags, size, mtime_s, mtime_ns):
149 def from_v2_data(cls, flags, size, mtime_s, mtime_ns):
149 """Build a new DirstateItem object from V2 data"""
150 """Build a new DirstateItem object from V2 data"""
150 has_mode_size = bool(flags & DIRSTATE_V2_HAS_MODE_AND_SIZE)
151 has_mode_size = bool(flags & DIRSTATE_V2_HAS_MODE_AND_SIZE)
151 has_meaningful_mtime = bool(flags & DIRSTATE_V2_HAS_MTIME)
152 has_meaningful_mtime = bool(flags & DIRSTATE_V2_HAS_MTIME)
152 mode = None
153 mode = None
153
154
154 if flags & +DIRSTATE_V2_EXPECTED_STATE_IS_MODIFIED:
155 if flags & +DIRSTATE_V2_EXPECTED_STATE_IS_MODIFIED:
155 # we do not have support for this flag in the code yet,
156 # we do not have support for this flag in the code yet,
156 # force a lookup for this file.
157 # force a lookup for this file.
157 has_mode_size = False
158 has_mode_size = False
158 has_meaningful_mtime = False
159 has_meaningful_mtime = False
159
160
160 fallback_exec = None
161 fallback_exec = None
161 if flags & DIRSTATE_V2_HAS_FALLBACK_EXEC:
162 if flags & DIRSTATE_V2_HAS_FALLBACK_EXEC:
162 fallback_exec = flags & DIRSTATE_V2_FALLBACK_EXEC
163 fallback_exec = flags & DIRSTATE_V2_FALLBACK_EXEC
163
164
164 fallback_symlink = None
165 fallback_symlink = None
165 if flags & DIRSTATE_V2_HAS_FALLBACK_SYMLINK:
166 if flags & DIRSTATE_V2_HAS_FALLBACK_SYMLINK:
166 fallback_symlink = flags & DIRSTATE_V2_FALLBACK_SYMLINK
167 fallback_symlink = flags & DIRSTATE_V2_FALLBACK_SYMLINK
167
168
168 if has_mode_size:
169 if has_mode_size:
169 assert stat.S_IXUSR == 0o100
170 assert stat.S_IXUSR == 0o100
170 if flags & DIRSTATE_V2_MODE_EXEC_PERM:
171 if flags & DIRSTATE_V2_MODE_EXEC_PERM:
171 mode = 0o755
172 mode = 0o755
172 else:
173 else:
173 mode = 0o644
174 mode = 0o644
174 if flags & DIRSTATE_V2_MODE_IS_SYMLINK:
175 if flags & DIRSTATE_V2_MODE_IS_SYMLINK:
175 mode |= stat.S_IFLNK
176 mode |= stat.S_IFLNK
176 else:
177 else:
177 mode |= stat.S_IFREG
178 mode |= stat.S_IFREG
178
179
179 second_ambiguous = flags & DIRSTATE_V2_MTIME_SECOND_AMBIGUOUS
180 second_ambiguous = flags & DIRSTATE_V2_MTIME_SECOND_AMBIGUOUS
180 return cls(
181 return cls(
181 wc_tracked=bool(flags & DIRSTATE_V2_WDIR_TRACKED),
182 wc_tracked=bool(flags & DIRSTATE_V2_WDIR_TRACKED),
182 p1_tracked=bool(flags & DIRSTATE_V2_P1_TRACKED),
183 p1_tracked=bool(flags & DIRSTATE_V2_P1_TRACKED),
183 p2_info=bool(flags & DIRSTATE_V2_P2_INFO),
184 p2_info=bool(flags & DIRSTATE_V2_P2_INFO),
184 has_meaningful_data=has_mode_size,
185 has_meaningful_data=has_mode_size,
185 has_meaningful_mtime=has_meaningful_mtime,
186 has_meaningful_mtime=has_meaningful_mtime,
186 parentfiledata=(mode, size, (mtime_s, mtime_ns, second_ambiguous)),
187 parentfiledata=(mode, size, (mtime_s, mtime_ns, second_ambiguous)),
187 fallback_exec=fallback_exec,
188 fallback_exec=fallback_exec,
188 fallback_symlink=fallback_symlink,
189 fallback_symlink=fallback_symlink,
189 )
190 )
190
191
191 @classmethod
192 @classmethod
192 def from_v1_data(cls, state, mode, size, mtime):
193 def from_v1_data(cls, state, mode, size, mtime):
193 """Build a new DirstateItem object from V1 data
194 """Build a new DirstateItem object from V1 data
194
195
195 Since the dirstate-v1 format is frozen, the signature of this function
196 Since the dirstate-v1 format is frozen, the signature of this function
196 is not expected to change, unlike the __init__ one.
197 is not expected to change, unlike the __init__ one.
197 """
198 """
198 if state == b'm':
199 if state == b'm':
199 return cls(wc_tracked=True, p1_tracked=True, p2_info=True)
200 return cls(wc_tracked=True, p1_tracked=True, p2_info=True)
200 elif state == b'a':
201 elif state == b'a':
201 return cls(wc_tracked=True)
202 return cls(wc_tracked=True)
202 elif state == b'r':
203 elif state == b'r':
203 if size == NONNORMAL:
204 if size == NONNORMAL:
204 p1_tracked = True
205 p1_tracked = True
205 p2_info = True
206 p2_info = True
206 elif size == FROM_P2:
207 elif size == FROM_P2:
207 p1_tracked = False
208 p1_tracked = False
208 p2_info = True
209 p2_info = True
209 else:
210 else:
210 p1_tracked = True
211 p1_tracked = True
211 p2_info = False
212 p2_info = False
212 return cls(p1_tracked=p1_tracked, p2_info=p2_info)
213 return cls(p1_tracked=p1_tracked, p2_info=p2_info)
213 elif state == b'n':
214 elif state == b'n':
214 if size == FROM_P2:
215 if size == FROM_P2:
215 return cls(wc_tracked=True, p2_info=True)
216 return cls(wc_tracked=True, p2_info=True)
216 elif size == NONNORMAL:
217 elif size == NONNORMAL:
217 return cls(wc_tracked=True, p1_tracked=True)
218 return cls(wc_tracked=True, p1_tracked=True)
218 elif mtime == AMBIGUOUS_TIME:
219 elif mtime == AMBIGUOUS_TIME:
219 return cls(
220 return cls(
220 wc_tracked=True,
221 wc_tracked=True,
221 p1_tracked=True,
222 p1_tracked=True,
222 has_meaningful_mtime=False,
223 has_meaningful_mtime=False,
223 parentfiledata=(mode, size, (42, 0, False)),
224 parentfiledata=(mode, size, (42, 0, False)),
224 )
225 )
225 else:
226 else:
226 return cls(
227 return cls(
227 wc_tracked=True,
228 wc_tracked=True,
228 p1_tracked=True,
229 p1_tracked=True,
229 parentfiledata=(mode, size, (mtime, 0, False)),
230 parentfiledata=(mode, size, (mtime, 0, False)),
230 )
231 )
231 else:
232 else:
232 raise RuntimeError(b'unknown state: %s' % state)
233 raise RuntimeError(b'unknown state: %s' % state)
233
234
234 def set_possibly_dirty(self):
235 def set_possibly_dirty(self):
235 """Mark a file as "possibly dirty"
236 """Mark a file as "possibly dirty"
236
237
237 This means the next status call will have to actually check its content
238 This means the next status call will have to actually check its content
238 to make sure it is correct.
239 to make sure it is correct.
239 """
240 """
240 self._mtime_s = None
241 self._mtime_s = None
241 self._mtime_ns = None
242 self._mtime_ns = None
242
243
243 def set_clean(self, mode, size, mtime):
244 def set_clean(self, mode, size, mtime):
244 """mark a file as "clean" cancelling potential "possibly dirty call"
245 """mark a file as "clean" cancelling potential "possibly dirty call"
245
246
246 Note: this function is a descendant of `dirstate.normal` and is
247 Note: this function is a descendant of `dirstate.normal` and is
247 currently expected to be call on "normal" entry only. There are not
248 currently expected to be call on "normal" entry only. There are not
248 reason for this to not change in the future as long as the ccode is
249 reason for this to not change in the future as long as the ccode is
249 updated to preserve the proper state of the non-normal files.
250 updated to preserve the proper state of the non-normal files.
250 """
251 """
251 self._wc_tracked = True
252 self._wc_tracked = True
252 self._p1_tracked = True
253 self._p1_tracked = True
253 self._mode = mode
254 self._mode = mode
254 self._size = size
255 self._size = size
255 self._mtime_s, self._mtime_ns, self._mtime_second_ambiguous = mtime
256 self._mtime_s, self._mtime_ns, self._mtime_second_ambiguous = mtime
256
257
257 def set_tracked(self):
258 def set_tracked(self):
258 """mark a file as tracked in the working copy
259 """mark a file as tracked in the working copy
259
260
260 This will ultimately be called by command like `hg add`.
261 This will ultimately be called by command like `hg add`.
261 """
262 """
262 self._wc_tracked = True
263 self._wc_tracked = True
263 # `set_tracked` is replacing various `normallookup` call. So we mark
264 # `set_tracked` is replacing various `normallookup` call. So we mark
264 # the files as needing lookup
265 # the files as needing lookup
265 #
266 #
266 # Consider dropping this in the future in favor of something less broad.
267 # Consider dropping this in the future in favor of something less broad.
267 self._mtime_s = None
268 self._mtime_s = None
268 self._mtime_ns = None
269 self._mtime_ns = None
269
270
270 def set_untracked(self):
271 def set_untracked(self):
271 """mark a file as untracked in the working copy
272 """mark a file as untracked in the working copy
272
273
273 This will ultimately be called by command like `hg remove`.
274 This will ultimately be called by command like `hg remove`.
274 """
275 """
275 self._wc_tracked = False
276 self._wc_tracked = False
276 self._mode = None
277 self._mode = None
277 self._size = None
278 self._size = None
278 self._mtime_s = None
279 self._mtime_s = None
279 self._mtime_ns = None
280 self._mtime_ns = None
280
281
281 def drop_merge_data(self):
282 def drop_merge_data(self):
282 """remove all "merge-only" from a DirstateItem
283 """remove all "merge-only" from a DirstateItem
283
284
284 This is to be call by the dirstatemap code when the second parent is dropped
285 This is to be call by the dirstatemap code when the second parent is dropped
285 """
286 """
286 if self._p2_info:
287 if self._p2_info:
287 self._p2_info = False
288 self._p2_info = False
288 self._mode = None
289 self._mode = None
289 self._size = None
290 self._size = None
290 self._mtime_s = None
291 self._mtime_s = None
291 self._mtime_ns = None
292 self._mtime_ns = None
292
293
293 @property
294 @property
294 def mode(self):
295 def mode(self):
295 return self.v1_mode()
296 return self.v1_mode()
296
297
297 @property
298 @property
298 def size(self):
299 def size(self):
299 return self.v1_size()
300 return self.v1_size()
300
301
301 @property
302 @property
302 def mtime(self):
303 def mtime(self):
303 return self.v1_mtime()
304 return self.v1_mtime()
304
305
305 def mtime_likely_equal_to(self, other_mtime):
306 def mtime_likely_equal_to(self, other_mtime):
306 self_sec = self._mtime_s
307 self_sec = self._mtime_s
307 if self_sec is None:
308 if self_sec is None:
308 return False
309 return False
309 self_ns = self._mtime_ns
310 self_ns = self._mtime_ns
310 other_sec, other_ns, second_ambiguous = other_mtime
311 other_sec, other_ns, second_ambiguous = other_mtime
311 if self_sec != other_sec:
312 if self_sec != other_sec:
312 # seconds are different theses mtime are definitly not equal
313 # seconds are different theses mtime are definitly not equal
313 return False
314 return False
314 elif other_ns == 0 or self_ns == 0:
315 elif other_ns == 0 or self_ns == 0:
315 # at least one side as no nano-seconds information
316 # at least one side as no nano-seconds information
316
317
317 if self._mtime_second_ambiguous:
318 if self._mtime_second_ambiguous:
318 # We cannot trust the mtime in this case
319 # We cannot trust the mtime in this case
319 return False
320 return False
320 else:
321 else:
321 # the "seconds" value was reliable on its own. We are good to go.
322 # the "seconds" value was reliable on its own. We are good to go.
322 return True
323 return True
323 else:
324 else:
324 # We have nano second information, let us use them !
325 # We have nano second information, let us use them !
325 return self_ns == other_ns
326 return self_ns == other_ns
326
327
327 @property
328 @property
328 def state(self):
329 def state(self):
329 """
330 """
330 States are:
331 States are:
331 n normal
332 n normal
332 m needs merging
333 m needs merging
333 r marked for removal
334 r marked for removal
334 a marked for addition
335 a marked for addition
335
336
336 XXX This "state" is a bit obscure and mostly a direct expression of the
337 XXX This "state" is a bit obscure and mostly a direct expression of the
337 dirstatev1 format. It would make sense to ultimately deprecate it in
338 dirstatev1 format. It would make sense to ultimately deprecate it in
338 favor of the more "semantic" attributes.
339 favor of the more "semantic" attributes.
339 """
340 """
340 if not self.any_tracked:
341 if not self.any_tracked:
341 return b'?'
342 return b'?'
342 return self.v1_state()
343 return self.v1_state()
343
344
344 @property
345 @property
345 def has_fallback_exec(self):
346 def has_fallback_exec(self):
346 """True if "fallback" information are available for the "exec" bit
347 """True if "fallback" information are available for the "exec" bit
347
348
348 Fallback information can be stored in the dirstate to keep track of
349 Fallback information can be stored in the dirstate to keep track of
349 filesystem attribute tracked by Mercurial when the underlying file
350 filesystem attribute tracked by Mercurial when the underlying file
350 system or operating system does not support that property, (e.g.
351 system or operating system does not support that property, (e.g.
351 Windows).
352 Windows).
352
353
353 Not all version of the dirstate on-disk storage support preserving this
354 Not all version of the dirstate on-disk storage support preserving this
354 information.
355 information.
355 """
356 """
356 return self._fallback_exec is not None
357 return self._fallback_exec is not None
357
358
358 @property
359 @property
359 def fallback_exec(self):
360 def fallback_exec(self):
360 """ "fallback" information for the executable bit
361 """ "fallback" information for the executable bit
361
362
362 True if the file should be considered executable when we cannot get
363 True if the file should be considered executable when we cannot get
363 this information from the files system. False if it should be
364 this information from the files system. False if it should be
364 considered non-executable.
365 considered non-executable.
365
366
366 See has_fallback_exec for details."""
367 See has_fallback_exec for details."""
367 return self._fallback_exec
368 return self._fallback_exec
368
369
369 @fallback_exec.setter
370 @fallback_exec.setter
370 def set_fallback_exec(self, value):
371 def set_fallback_exec(self, value):
371 """control "fallback" executable bit
372 """control "fallback" executable bit
372
373
373 Set to:
374 Set to:
374 - True if the file should be considered executable,
375 - True if the file should be considered executable,
375 - False if the file should be considered non-executable,
376 - False if the file should be considered non-executable,
376 - None if we do not have valid fallback data.
377 - None if we do not have valid fallback data.
377
378
378 See has_fallback_exec for details."""
379 See has_fallback_exec for details."""
379 if value is None:
380 if value is None:
380 self._fallback_exec = None
381 self._fallback_exec = None
381 else:
382 else:
382 self._fallback_exec = bool(value)
383 self._fallback_exec = bool(value)
383
384
384 @property
385 @property
385 def has_fallback_symlink(self):
386 def has_fallback_symlink(self):
386 """True if "fallback" information are available for symlink status
387 """True if "fallback" information are available for symlink status
387
388
388 Fallback information can be stored in the dirstate to keep track of
389 Fallback information can be stored in the dirstate to keep track of
389 filesystem attribute tracked by Mercurial when the underlying file
390 filesystem attribute tracked by Mercurial when the underlying file
390 system or operating system does not support that property, (e.g.
391 system or operating system does not support that property, (e.g.
391 Windows).
392 Windows).
392
393
393 Not all version of the dirstate on-disk storage support preserving this
394 Not all version of the dirstate on-disk storage support preserving this
394 information."""
395 information."""
395 return self._fallback_symlink is not None
396 return self._fallback_symlink is not None
396
397
397 @property
398 @property
398 def fallback_symlink(self):
399 def fallback_symlink(self):
399 """ "fallback" information for symlink status
400 """ "fallback" information for symlink status
400
401
401 True if the file should be considered executable when we cannot get
402 True if the file should be considered executable when we cannot get
402 this information from the files system. False if it should be
403 this information from the files system. False if it should be
403 considered non-executable.
404 considered non-executable.
404
405
405 See has_fallback_exec for details."""
406 See has_fallback_exec for details."""
406 return self._fallback_symlink
407 return self._fallback_symlink
407
408
408 @fallback_symlink.setter
409 @fallback_symlink.setter
409 def set_fallback_symlink(self, value):
410 def set_fallback_symlink(self, value):
410 """control "fallback" symlink status
411 """control "fallback" symlink status
411
412
412 Set to:
413 Set to:
413 - True if the file should be considered a symlink,
414 - True if the file should be considered a symlink,
414 - False if the file should be considered not a symlink,
415 - False if the file should be considered not a symlink,
415 - None if we do not have valid fallback data.
416 - None if we do not have valid fallback data.
416
417
417 See has_fallback_symlink for details."""
418 See has_fallback_symlink for details."""
418 if value is None:
419 if value is None:
419 self._fallback_symlink = None
420 self._fallback_symlink = None
420 else:
421 else:
421 self._fallback_symlink = bool(value)
422 self._fallback_symlink = bool(value)
422
423
423 @property
424 @property
424 def tracked(self):
425 def tracked(self):
425 """True is the file is tracked in the working copy"""
426 """True is the file is tracked in the working copy"""
426 return self._wc_tracked
427 return self._wc_tracked
427
428
428 @property
429 @property
429 def any_tracked(self):
430 def any_tracked(self):
430 """True is the file is tracked anywhere (wc or parents)"""
431 """True is the file is tracked anywhere (wc or parents)"""
431 return self._wc_tracked or self._p1_tracked or self._p2_info
432 return self._wc_tracked or self._p1_tracked or self._p2_info
432
433
433 @property
434 @property
434 def added(self):
435 def added(self):
435 """True if the file has been added"""
436 """True if the file has been added"""
436 return self._wc_tracked and not (self._p1_tracked or self._p2_info)
437 return self._wc_tracked and not (self._p1_tracked or self._p2_info)
437
438
438 @property
439 @property
439 def maybe_clean(self):
440 def maybe_clean(self):
440 """True if the file has a chance to be in the "clean" state"""
441 """True if the file has a chance to be in the "clean" state"""
441 if not self._wc_tracked:
442 if not self._wc_tracked:
442 return False
443 return False
443 elif not self._p1_tracked:
444 elif not self._p1_tracked:
444 return False
445 return False
445 elif self._p2_info:
446 elif self._p2_info:
446 return False
447 return False
447 return True
448 return True
448
449
449 @property
450 @property
450 def p1_tracked(self):
451 def p1_tracked(self):
451 """True if the file is tracked in the first parent manifest"""
452 """True if the file is tracked in the first parent manifest"""
452 return self._p1_tracked
453 return self._p1_tracked
453
454
454 @property
455 @property
455 def p2_info(self):
456 def p2_info(self):
456 """True if the file needed to merge or apply any input from p2
457 """True if the file needed to merge or apply any input from p2
457
458
458 See the class documentation for details.
459 See the class documentation for details.
459 """
460 """
460 return self._wc_tracked and self._p2_info
461 return self._wc_tracked and self._p2_info
461
462
462 @property
463 @property
463 def removed(self):
464 def removed(self):
464 """True if the file has been removed"""
465 """True if the file has been removed"""
465 return not self._wc_tracked and (self._p1_tracked or self._p2_info)
466 return not self._wc_tracked and (self._p1_tracked or self._p2_info)
466
467
467 def v2_data(self):
468 def v2_data(self):
468 """Returns (flags, mode, size, mtime) for v2 serialization"""
469 """Returns (flags, mode, size, mtime) for v2 serialization"""
469 flags = 0
470 flags = 0
470 if self._wc_tracked:
471 if self._wc_tracked:
471 flags |= DIRSTATE_V2_WDIR_TRACKED
472 flags |= DIRSTATE_V2_WDIR_TRACKED
472 if self._p1_tracked:
473 if self._p1_tracked:
473 flags |= DIRSTATE_V2_P1_TRACKED
474 flags |= DIRSTATE_V2_P1_TRACKED
474 if self._p2_info:
475 if self._p2_info:
475 flags |= DIRSTATE_V2_P2_INFO
476 flags |= DIRSTATE_V2_P2_INFO
476 if self._mode is not None and self._size is not None:
477 if self._mode is not None and self._size is not None:
477 flags |= DIRSTATE_V2_HAS_MODE_AND_SIZE
478 flags |= DIRSTATE_V2_HAS_MODE_AND_SIZE
478 if self.mode & stat.S_IXUSR:
479 if self.mode & stat.S_IXUSR:
479 flags |= DIRSTATE_V2_MODE_EXEC_PERM
480 flags |= DIRSTATE_V2_MODE_EXEC_PERM
480 if stat.S_ISLNK(self.mode):
481 if stat.S_ISLNK(self.mode):
481 flags |= DIRSTATE_V2_MODE_IS_SYMLINK
482 flags |= DIRSTATE_V2_MODE_IS_SYMLINK
482 if self._mtime_s is not None:
483 if self._mtime_s is not None:
483 flags |= DIRSTATE_V2_HAS_MTIME
484 flags |= DIRSTATE_V2_HAS_MTIME
484 if self._mtime_second_ambiguous:
485 if self._mtime_second_ambiguous:
485 flags |= DIRSTATE_V2_MTIME_SECOND_AMBIGUOUS
486 flags |= DIRSTATE_V2_MTIME_SECOND_AMBIGUOUS
486
487
487 if self._fallback_exec is not None:
488 if self._fallback_exec is not None:
488 flags |= DIRSTATE_V2_HAS_FALLBACK_EXEC
489 flags |= DIRSTATE_V2_HAS_FALLBACK_EXEC
489 if self._fallback_exec:
490 if self._fallback_exec:
490 flags |= DIRSTATE_V2_FALLBACK_EXEC
491 flags |= DIRSTATE_V2_FALLBACK_EXEC
491
492
492 if self._fallback_symlink is not None:
493 if self._fallback_symlink is not None:
493 flags |= DIRSTATE_V2_HAS_FALLBACK_SYMLINK
494 flags |= DIRSTATE_V2_HAS_FALLBACK_SYMLINK
494 if self._fallback_symlink:
495 if self._fallback_symlink:
495 flags |= DIRSTATE_V2_FALLBACK_SYMLINK
496 flags |= DIRSTATE_V2_FALLBACK_SYMLINK
496
497
497 # Note: we do not need to do anything regarding
498 # Note: we do not need to do anything regarding
498 # DIRSTATE_V2_ALL_UNKNOWN_RECORDED and DIRSTATE_V2_ALL_IGNORED_RECORDED
499 # DIRSTATE_V2_ALL_UNKNOWN_RECORDED and DIRSTATE_V2_ALL_IGNORED_RECORDED
499 # since we never set _DIRSTATE_V2_HAS_DIRCTORY_MTIME
500 # since we never set _DIRSTATE_V2_HAS_DIRCTORY_MTIME
500 return (flags, self._size or 0, self._mtime_s or 0, self._mtime_ns or 0)
501 return (flags, self._size or 0, self._mtime_s or 0, self._mtime_ns or 0)
501
502
502 def v1_state(self):
503 def v1_state(self):
503 """return a "state" suitable for v1 serialization"""
504 """return a "state" suitable for v1 serialization"""
504 if not self.any_tracked:
505 if not self.any_tracked:
505 # the object has no state to record, this is -currently-
506 # the object has no state to record, this is -currently-
506 # unsupported
507 # unsupported
507 raise RuntimeError('untracked item')
508 raise RuntimeError('untracked item')
508 elif self.removed:
509 elif self.removed:
509 return b'r'
510 return b'r'
510 elif self._p1_tracked and self._p2_info:
511 elif self._p1_tracked and self._p2_info:
511 return b'm'
512 return b'm'
512 elif self.added:
513 elif self.added:
513 return b'a'
514 return b'a'
514 else:
515 else:
515 return b'n'
516 return b'n'
516
517
517 def v1_mode(self):
518 def v1_mode(self):
518 """return a "mode" suitable for v1 serialization"""
519 """return a "mode" suitable for v1 serialization"""
519 return self._mode if self._mode is not None else 0
520 return self._mode if self._mode is not None else 0
520
521
521 def v1_size(self):
522 def v1_size(self):
522 """return a "size" suitable for v1 serialization"""
523 """return a "size" suitable for v1 serialization"""
523 if not self.any_tracked:
524 if not self.any_tracked:
524 # the object has no state to record, this is -currently-
525 # the object has no state to record, this is -currently-
525 # unsupported
526 # unsupported
526 raise RuntimeError('untracked item')
527 raise RuntimeError('untracked item')
527 elif self.removed and self._p1_tracked and self._p2_info:
528 elif self.removed and self._p1_tracked and self._p2_info:
528 return NONNORMAL
529 return NONNORMAL
529 elif self._p2_info:
530 elif self._p2_info:
530 return FROM_P2
531 return FROM_P2
531 elif self.removed:
532 elif self.removed:
532 return 0
533 return 0
533 elif self.added:
534 elif self.added:
534 return NONNORMAL
535 return NONNORMAL
535 elif self._size is None:
536 elif self._size is None:
536 return NONNORMAL
537 return NONNORMAL
537 else:
538 else:
538 return self._size
539 return self._size
539
540
540 def v1_mtime(self):
541 def v1_mtime(self):
541 """return a "mtime" suitable for v1 serialization"""
542 """return a "mtime" suitable for v1 serialization"""
542 if not self.any_tracked:
543 if not self.any_tracked:
543 # the object has no state to record, this is -currently-
544 # the object has no state to record, this is -currently-
544 # unsupported
545 # unsupported
545 raise RuntimeError('untracked item')
546 raise RuntimeError('untracked item')
546 elif self.removed:
547 elif self.removed:
547 return 0
548 return 0
548 elif self._mtime_s is None:
549 elif self._mtime_s is None:
549 return AMBIGUOUS_TIME
550 return AMBIGUOUS_TIME
550 elif self._p2_info:
551 elif self._p2_info:
551 return AMBIGUOUS_TIME
552 return AMBIGUOUS_TIME
552 elif not self._p1_tracked:
553 elif not self._p1_tracked:
553 return AMBIGUOUS_TIME
554 return AMBIGUOUS_TIME
554 elif self._mtime_second_ambiguous:
555 elif self._mtime_second_ambiguous:
555 return AMBIGUOUS_TIME
556 return AMBIGUOUS_TIME
556 else:
557 else:
557 return self._mtime_s
558 return self._mtime_s
558
559
559
560
560 def gettype(q):
561 def gettype(q):
561 return int(q & 0xFFFF)
562 return int(q & 0xFFFF)
562
563
563
564
564 class BaseIndexObject(object):
565 class BaseIndexObject(object):
565 # Can I be passed to an algorithme implemented in Rust ?
566 # Can I be passed to an algorithme implemented in Rust ?
566 rust_ext_compat = 0
567 rust_ext_compat = 0
567 # Format of an index entry according to Python's `struct` language
568 # Format of an index entry according to Python's `struct` language
568 index_format = revlog_constants.INDEX_ENTRY_V1
569 index_format = revlog_constants.INDEX_ENTRY_V1
569 # Size of a C unsigned long long int, platform independent
570 # Size of a C unsigned long long int, platform independent
570 big_int_size = struct.calcsize(b'>Q')
571 big_int_size = struct.calcsize(b'>Q')
571 # Size of a C long int, platform independent
572 # Size of a C long int, platform independent
572 int_size = struct.calcsize(b'>i')
573 int_size = struct.calcsize(b'>i')
573 # An empty index entry, used as a default value to be overridden, or nullrev
574 # An empty index entry, used as a default value to be overridden, or nullrev
574 null_item = (
575 null_item = (
575 0,
576 0,
576 0,
577 0,
577 0,
578 0,
578 -1,
579 -1,
579 -1,
580 -1,
580 -1,
581 -1,
581 -1,
582 -1,
582 sha1nodeconstants.nullid,
583 sha1nodeconstants.nullid,
583 0,
584 0,
584 0,
585 0,
585 revlog_constants.COMP_MODE_INLINE,
586 revlog_constants.COMP_MODE_INLINE,
586 revlog_constants.COMP_MODE_INLINE,
587 revlog_constants.COMP_MODE_INLINE,
587 revlog_constants.RANK_UNKNOWN,
588 revlog_constants.RANK_UNKNOWN,
588 )
589 )
589
590
590 @util.propertycache
591 @util.propertycache
591 def entry_size(self):
592 def entry_size(self):
592 return self.index_format.size
593 return self.index_format.size
593
594
594 @util.propertycache
595 @util.propertycache
595 def _nodemap(self):
596 def _nodemap(self):
596 nodemap = nodemaputil.NodeMap({sha1nodeconstants.nullid: nullrev})
597 nodemap = nodemaputil.NodeMap({sha1nodeconstants.nullid: nullrev})
597 for r in range(0, len(self)):
598 for r in range(0, len(self)):
598 n = self[r][7]
599 n = self[r][7]
599 nodemap[n] = r
600 nodemap[n] = r
600 return nodemap
601 return nodemap
601
602
602 def has_node(self, node):
603 def has_node(self, node):
603 """return True if the node exist in the index"""
604 """return True if the node exist in the index"""
604 return node in self._nodemap
605 return node in self._nodemap
605
606
606 def rev(self, node):
607 def rev(self, node):
607 """return a revision for a node
608 """return a revision for a node
608
609
609 If the node is unknown, raise a RevlogError"""
610 If the node is unknown, raise a RevlogError"""
610 return self._nodemap[node]
611 return self._nodemap[node]
611
612
612 def get_rev(self, node):
613 def get_rev(self, node):
613 """return a revision for a node
614 """return a revision for a node
614
615
615 If the node is unknown, return None"""
616 If the node is unknown, return None"""
616 return self._nodemap.get(node)
617 return self._nodemap.get(node)
617
618
618 def _stripnodes(self, start):
619 def _stripnodes(self, start):
619 if '_nodemap' in vars(self):
620 if '_nodemap' in vars(self):
620 for r in range(start, len(self)):
621 for r in range(start, len(self)):
621 n = self[r][7]
622 n = self[r][7]
622 del self._nodemap[n]
623 del self._nodemap[n]
623
624
624 def clearcaches(self):
625 def clearcaches(self):
625 self.__dict__.pop('_nodemap', None)
626 self.__dict__.pop('_nodemap', None)
626
627
627 def __len__(self):
628 def __len__(self):
628 return self._lgt + len(self._extra)
629 return self._lgt + len(self._extra)
629
630
630 def append(self, tup):
631 def append(self, tup):
631 if '_nodemap' in vars(self):
632 if '_nodemap' in vars(self):
632 self._nodemap[tup[7]] = len(self)
633 self._nodemap[tup[7]] = len(self)
633 data = self._pack_entry(len(self), tup)
634 data = self._pack_entry(len(self), tup)
634 self._extra.append(data)
635 self._extra.append(data)
635
636
636 def _pack_entry(self, rev, entry):
637 def _pack_entry(self, rev, entry):
637 assert entry[8] == 0
638 assert entry[8] == 0
638 assert entry[9] == 0
639 assert entry[9] == 0
639 return self.index_format.pack(*entry[:8])
640 return self.index_format.pack(*entry[:8])
640
641
641 def _check_index(self, i):
642 def _check_index(self, i):
642 if not isinstance(i, int):
643 if not isinstance(i, int):
643 raise TypeError(b"expecting int indexes")
644 raise TypeError(b"expecting int indexes")
644 if i < 0 or i >= len(self):
645 if i < 0 or i >= len(self):
645 raise IndexError(i)
646 raise IndexError(i)
646
647
647 def __getitem__(self, i):
648 def __getitem__(self, i):
648 if i == -1:
649 if i == -1:
649 return self.null_item
650 return self.null_item
650 self._check_index(i)
651 self._check_index(i)
651 if i >= self._lgt:
652 if i >= self._lgt:
652 data = self._extra[i - self._lgt]
653 data = self._extra[i - self._lgt]
653 else:
654 else:
654 index = self._calculate_index(i)
655 index = self._calculate_index(i)
655 data = self._data[index : index + self.entry_size]
656 data = self._data[index : index + self.entry_size]
656 r = self._unpack_entry(i, data)
657 r = self._unpack_entry(i, data)
657 if self._lgt and i == 0:
658 if self._lgt and i == 0:
658 offset = revlogutils.offset_type(0, gettype(r[0]))
659 offset = revlogutils.offset_type(0, gettype(r[0]))
659 r = (offset,) + r[1:]
660 r = (offset,) + r[1:]
660 return r
661 return r
661
662
662 def _unpack_entry(self, rev, data):
663 def _unpack_entry(self, rev, data):
663 r = self.index_format.unpack(data)
664 r = self.index_format.unpack(data)
664 r = r + (
665 r = r + (
665 0,
666 0,
666 0,
667 0,
667 revlog_constants.COMP_MODE_INLINE,
668 revlog_constants.COMP_MODE_INLINE,
668 revlog_constants.COMP_MODE_INLINE,
669 revlog_constants.COMP_MODE_INLINE,
669 revlog_constants.RANK_UNKNOWN,
670 revlog_constants.RANK_UNKNOWN,
670 )
671 )
671 return r
672 return r
672
673
673 def pack_header(self, header):
674 def pack_header(self, header):
674 """pack header information as binary"""
675 """pack header information as binary"""
675 v_fmt = revlog_constants.INDEX_HEADER
676 v_fmt = revlog_constants.INDEX_HEADER
676 return v_fmt.pack(header)
677 return v_fmt.pack(header)
677
678
678 def entry_binary(self, rev):
679 def entry_binary(self, rev):
679 """return the raw binary string representing a revision"""
680 """return the raw binary string representing a revision"""
680 entry = self[rev]
681 entry = self[rev]
681 p = revlog_constants.INDEX_ENTRY_V1.pack(*entry[:8])
682 p = revlog_constants.INDEX_ENTRY_V1.pack(*entry[:8])
682 if rev == 0:
683 if rev == 0:
683 p = p[revlog_constants.INDEX_HEADER.size :]
684 p = p[revlog_constants.INDEX_HEADER.size :]
684 return p
685 return p
685
686
686
687
687 class IndexObject(BaseIndexObject):
688 class IndexObject(BaseIndexObject):
688 def __init__(self, data):
689 def __init__(self, data):
689 assert len(data) % self.entry_size == 0, (
690 assert len(data) % self.entry_size == 0, (
690 len(data),
691 len(data),
691 self.entry_size,
692 self.entry_size,
692 len(data) % self.entry_size,
693 len(data) % self.entry_size,
693 )
694 )
694 self._data = data
695 self._data = data
695 self._lgt = len(data) // self.entry_size
696 self._lgt = len(data) // self.entry_size
696 self._extra = []
697 self._extra = []
697
698
698 def _calculate_index(self, i):
699 def _calculate_index(self, i):
699 return i * self.entry_size
700 return i * self.entry_size
700
701
701 def __delitem__(self, i):
702 def __delitem__(self, i):
702 if not isinstance(i, slice) or not i.stop == -1 or i.step is not None:
703 if not isinstance(i, slice) or not i.stop == -1 or i.step is not None:
703 raise ValueError(b"deleting slices only supports a:-1 with step 1")
704 raise ValueError(b"deleting slices only supports a:-1 with step 1")
704 i = i.start
705 i = i.start
705 self._check_index(i)
706 self._check_index(i)
706 self._stripnodes(i)
707 self._stripnodes(i)
707 if i < self._lgt:
708 if i < self._lgt:
708 self._data = self._data[: i * self.entry_size]
709 self._data = self._data[: i * self.entry_size]
709 self._lgt = i
710 self._lgt = i
710 self._extra = []
711 self._extra = []
711 else:
712 else:
712 self._extra = self._extra[: i - self._lgt]
713 self._extra = self._extra[: i - self._lgt]
713
714
714
715
715 class PersistentNodeMapIndexObject(IndexObject):
716 class PersistentNodeMapIndexObject(IndexObject):
716 """a Debug oriented class to test persistent nodemap
717 """a Debug oriented class to test persistent nodemap
717
718
718 We need a simple python object to test API and higher level behavior. See
719 We need a simple python object to test API and higher level behavior. See
719 the Rust implementation for more serious usage. This should be used only
720 the Rust implementation for more serious usage. This should be used only
720 through the dedicated `devel.persistent-nodemap` config.
721 through the dedicated `devel.persistent-nodemap` config.
721 """
722 """
722
723
723 def nodemap_data_all(self):
724 def nodemap_data_all(self):
724 """Return bytes containing a full serialization of a nodemap
725 """Return bytes containing a full serialization of a nodemap
725
726
726 The nodemap should be valid for the full set of revisions in the
727 The nodemap should be valid for the full set of revisions in the
727 index."""
728 index."""
728 return nodemaputil.persistent_data(self)
729 return nodemaputil.persistent_data(self)
729
730
730 def nodemap_data_incremental(self):
731 def nodemap_data_incremental(self):
731 """Return bytes containing a incremental update to persistent nodemap
732 """Return bytes containing a incremental update to persistent nodemap
732
733
733 This containst the data for an append-only update of the data provided
734 This containst the data for an append-only update of the data provided
734 in the last call to `update_nodemap_data`.
735 in the last call to `update_nodemap_data`.
735 """
736 """
736 if self._nm_root is None:
737 if self._nm_root is None:
737 return None
738 return None
738 docket = self._nm_docket
739 docket = self._nm_docket
739 changed, data = nodemaputil.update_persistent_data(
740 changed, data = nodemaputil.update_persistent_data(
740 self, self._nm_root, self._nm_max_idx, self._nm_docket.tip_rev
741 self, self._nm_root, self._nm_max_idx, self._nm_docket.tip_rev
741 )
742 )
742
743
743 self._nm_root = self._nm_max_idx = self._nm_docket = None
744 self._nm_root = self._nm_max_idx = self._nm_docket = None
744 return docket, changed, data
745 return docket, changed, data
745
746
746 def update_nodemap_data(self, docket, nm_data):
747 def update_nodemap_data(self, docket, nm_data):
747 """provide full block of persisted binary data for a nodemap
748 """provide full block of persisted binary data for a nodemap
748
749
749 The data are expected to come from disk. See `nodemap_data_all` for a
750 The data are expected to come from disk. See `nodemap_data_all` for a
750 produceur of such data."""
751 produceur of such data."""
751 if nm_data is not None:
752 if nm_data is not None:
752 self._nm_root, self._nm_max_idx = nodemaputil.parse_data(nm_data)
753 self._nm_root, self._nm_max_idx = nodemaputil.parse_data(nm_data)
753 if self._nm_root:
754 if self._nm_root:
754 self._nm_docket = docket
755 self._nm_docket = docket
755 else:
756 else:
756 self._nm_root = self._nm_max_idx = self._nm_docket = None
757 self._nm_root = self._nm_max_idx = self._nm_docket = None
757
758
758
759
759 class InlinedIndexObject(BaseIndexObject):
760 class InlinedIndexObject(BaseIndexObject):
760 def __init__(self, data, inline=0):
761 def __init__(self, data, inline=0):
761 self._data = data
762 self._data = data
762 self._lgt = self._inline_scan(None)
763 self._lgt = self._inline_scan(None)
763 self._inline_scan(self._lgt)
764 self._inline_scan(self._lgt)
764 self._extra = []
765 self._extra = []
765
766
766 def _inline_scan(self, lgt):
767 def _inline_scan(self, lgt):
767 off = 0
768 off = 0
768 if lgt is not None:
769 if lgt is not None:
769 self._offsets = [0] * lgt
770 self._offsets = [0] * lgt
770 count = 0
771 count = 0
771 while off <= len(self._data) - self.entry_size:
772 while off <= len(self._data) - self.entry_size:
772 start = off + self.big_int_size
773 start = off + self.big_int_size
773 (s,) = struct.unpack(
774 (s,) = struct.unpack(
774 b'>i',
775 b'>i',
775 self._data[start : start + self.int_size],
776 self._data[start : start + self.int_size],
776 )
777 )
777 if lgt is not None:
778 if lgt is not None:
778 self._offsets[count] = off
779 self._offsets[count] = off
779 count += 1
780 count += 1
780 off += self.entry_size + s
781 off += self.entry_size + s
781 if off != len(self._data):
782 if off != len(self._data):
782 raise ValueError(b"corrupted data")
783 raise ValueError(b"corrupted data")
783 return count
784 return count
784
785
785 def __delitem__(self, i):
786 def __delitem__(self, i):
786 if not isinstance(i, slice) or not i.stop == -1 or i.step is not None:
787 if not isinstance(i, slice) or not i.stop == -1 or i.step is not None:
787 raise ValueError(b"deleting slices only supports a:-1 with step 1")
788 raise ValueError(b"deleting slices only supports a:-1 with step 1")
788 i = i.start
789 i = i.start
789 self._check_index(i)
790 self._check_index(i)
790 self._stripnodes(i)
791 self._stripnodes(i)
791 if i < self._lgt:
792 if i < self._lgt:
792 self._offsets = self._offsets[:i]
793 self._offsets = self._offsets[:i]
793 self._lgt = i
794 self._lgt = i
794 self._extra = []
795 self._extra = []
795 else:
796 else:
796 self._extra = self._extra[: i - self._lgt]
797 self._extra = self._extra[: i - self._lgt]
797
798
798 def _calculate_index(self, i):
799 def _calculate_index(self, i):
799 return self._offsets[i]
800 return self._offsets[i]
800
801
801
802
802 def parse_index2(data, inline, format=revlog_constants.REVLOGV1):
803 def parse_index2(data, inline, format=revlog_constants.REVLOGV1):
803 if format == revlog_constants.CHANGELOGV2:
804 if format == revlog_constants.CHANGELOGV2:
804 return parse_index_cl_v2(data)
805 return parse_index_cl_v2(data)
805 if not inline:
806 if not inline:
806 if format == revlog_constants.REVLOGV2:
807 if format == revlog_constants.REVLOGV2:
807 cls = IndexObject2
808 cls = IndexObject2
808 else:
809 else:
809 cls = IndexObject
810 cls = IndexObject
810 return cls(data), None
811 return cls(data), None
811 cls = InlinedIndexObject
812 cls = InlinedIndexObject
812 return cls(data, inline), (0, data)
813 return cls(data, inline), (0, data)
813
814
814
815
815 def parse_index_cl_v2(data):
816 def parse_index_cl_v2(data):
816 return IndexChangelogV2(data), None
817 return IndexChangelogV2(data), None
817
818
818
819
819 class IndexObject2(IndexObject):
820 class IndexObject2(IndexObject):
820 index_format = revlog_constants.INDEX_ENTRY_V2
821 index_format = revlog_constants.INDEX_ENTRY_V2
821
822
822 def replace_sidedata_info(
823 def replace_sidedata_info(
823 self,
824 self,
824 rev,
825 rev,
825 sidedata_offset,
826 sidedata_offset,
826 sidedata_length,
827 sidedata_length,
827 offset_flags,
828 offset_flags,
828 compression_mode,
829 compression_mode,
829 ):
830 ):
830 """
831 """
831 Replace an existing index entry's sidedata offset and length with new
832 Replace an existing index entry's sidedata offset and length with new
832 ones.
833 ones.
833 This cannot be used outside of the context of sidedata rewriting,
834 This cannot be used outside of the context of sidedata rewriting,
834 inside the transaction that creates the revision `rev`.
835 inside the transaction that creates the revision `rev`.
835 """
836 """
836 if rev < 0:
837 if rev < 0:
837 raise KeyError
838 raise KeyError
838 self._check_index(rev)
839 self._check_index(rev)
839 if rev < self._lgt:
840 if rev < self._lgt:
840 msg = b"cannot rewrite entries outside of this transaction"
841 msg = b"cannot rewrite entries outside of this transaction"
841 raise KeyError(msg)
842 raise KeyError(msg)
842 else:
843 else:
843 entry = list(self[rev])
844 entry = list(self[rev])
844 entry[0] = offset_flags
845 entry[0] = offset_flags
845 entry[8] = sidedata_offset
846 entry[8] = sidedata_offset
846 entry[9] = sidedata_length
847 entry[9] = sidedata_length
847 entry[11] = compression_mode
848 entry[11] = compression_mode
848 entry = tuple(entry)
849 entry = tuple(entry)
849 new = self._pack_entry(rev, entry)
850 new = self._pack_entry(rev, entry)
850 self._extra[rev - self._lgt] = new
851 self._extra[rev - self._lgt] = new
851
852
852 def _unpack_entry(self, rev, data):
853 def _unpack_entry(self, rev, data):
853 data = self.index_format.unpack(data)
854 data = self.index_format.unpack(data)
854 entry = data[:10]
855 entry = data[:10]
855 data_comp = data[10] & 3
856 data_comp = data[10] & 3
856 sidedata_comp = (data[10] & (3 << 2)) >> 2
857 sidedata_comp = (data[10] & (3 << 2)) >> 2
857 return entry + (data_comp, sidedata_comp, revlog_constants.RANK_UNKNOWN)
858 return entry + (data_comp, sidedata_comp, revlog_constants.RANK_UNKNOWN)
858
859
859 def _pack_entry(self, rev, entry):
860 def _pack_entry(self, rev, entry):
860 data = entry[:10]
861 data = entry[:10]
861 data_comp = entry[10] & 3
862 data_comp = entry[10] & 3
862 sidedata_comp = (entry[11] & 3) << 2
863 sidedata_comp = (entry[11] & 3) << 2
863 data += (data_comp | sidedata_comp,)
864 data += (data_comp | sidedata_comp,)
864
865
865 return self.index_format.pack(*data)
866 return self.index_format.pack(*data)
866
867
867 def entry_binary(self, rev):
868 def entry_binary(self, rev):
868 """return the raw binary string representing a revision"""
869 """return the raw binary string representing a revision"""
869 entry = self[rev]
870 entry = self[rev]
870 return self._pack_entry(rev, entry)
871 return self._pack_entry(rev, entry)
871
872
872 def pack_header(self, header):
873 def pack_header(self, header):
873 """pack header information as binary"""
874 """pack header information as binary"""
874 msg = 'version header should go in the docket, not the index: %d'
875 msg = 'version header should go in the docket, not the index: %d'
875 msg %= header
876 msg %= header
876 raise error.ProgrammingError(msg)
877 raise error.ProgrammingError(msg)
877
878
878
879
879 class IndexChangelogV2(IndexObject2):
880 class IndexChangelogV2(IndexObject2):
880 index_format = revlog_constants.INDEX_ENTRY_CL_V2
881 index_format = revlog_constants.INDEX_ENTRY_CL_V2
881
882
882 null_item = (
883 null_item = (
883 IndexObject2.null_item[: revlog_constants.ENTRY_RANK]
884 IndexObject2.null_item[: revlog_constants.ENTRY_RANK]
884 + (0,) # rank of null is 0
885 + (0,) # rank of null is 0
885 + IndexObject2.null_item[revlog_constants.ENTRY_RANK :]
886 + IndexObject2.null_item[revlog_constants.ENTRY_RANK :]
886 )
887 )
887
888
888 def _unpack_entry(self, rev, data, r=True):
889 def _unpack_entry(self, rev, data, r=True):
889 items = self.index_format.unpack(data)
890 items = self.index_format.unpack(data)
890 return (
891 return (
891 items[revlog_constants.INDEX_ENTRY_V2_IDX_OFFSET],
892 items[revlog_constants.INDEX_ENTRY_V2_IDX_OFFSET],
892 items[revlog_constants.INDEX_ENTRY_V2_IDX_COMPRESSED_LENGTH],
893 items[revlog_constants.INDEX_ENTRY_V2_IDX_COMPRESSED_LENGTH],
893 items[revlog_constants.INDEX_ENTRY_V2_IDX_UNCOMPRESSED_LENGTH],
894 items[revlog_constants.INDEX_ENTRY_V2_IDX_UNCOMPRESSED_LENGTH],
894 rev,
895 rev,
895 rev,
896 rev,
896 items[revlog_constants.INDEX_ENTRY_V2_IDX_PARENT_1],
897 items[revlog_constants.INDEX_ENTRY_V2_IDX_PARENT_1],
897 items[revlog_constants.INDEX_ENTRY_V2_IDX_PARENT_2],
898 items[revlog_constants.INDEX_ENTRY_V2_IDX_PARENT_2],
898 items[revlog_constants.INDEX_ENTRY_V2_IDX_NODEID],
899 items[revlog_constants.INDEX_ENTRY_V2_IDX_NODEID],
899 items[revlog_constants.INDEX_ENTRY_V2_IDX_SIDEDATA_OFFSET],
900 items[revlog_constants.INDEX_ENTRY_V2_IDX_SIDEDATA_OFFSET],
900 items[
901 items[
901 revlog_constants.INDEX_ENTRY_V2_IDX_SIDEDATA_COMPRESSED_LENGTH
902 revlog_constants.INDEX_ENTRY_V2_IDX_SIDEDATA_COMPRESSED_LENGTH
902 ],
903 ],
903 items[revlog_constants.INDEX_ENTRY_V2_IDX_COMPRESSION_MODE] & 3,
904 items[revlog_constants.INDEX_ENTRY_V2_IDX_COMPRESSION_MODE] & 3,
904 (items[revlog_constants.INDEX_ENTRY_V2_IDX_COMPRESSION_MODE] >> 2)
905 (items[revlog_constants.INDEX_ENTRY_V2_IDX_COMPRESSION_MODE] >> 2)
905 & 3,
906 & 3,
906 items[revlog_constants.INDEX_ENTRY_V2_IDX_RANK],
907 items[revlog_constants.INDEX_ENTRY_V2_IDX_RANK],
907 )
908 )
908
909
909 def _pack_entry(self, rev, entry):
910 def _pack_entry(self, rev, entry):
910
911
911 base = entry[revlog_constants.ENTRY_DELTA_BASE]
912 base = entry[revlog_constants.ENTRY_DELTA_BASE]
912 link_rev = entry[revlog_constants.ENTRY_LINK_REV]
913 link_rev = entry[revlog_constants.ENTRY_LINK_REV]
913 assert base == rev, (base, rev)
914 assert base == rev, (base, rev)
914 assert link_rev == rev, (link_rev, rev)
915 assert link_rev == rev, (link_rev, rev)
915 data = (
916 data = (
916 entry[revlog_constants.ENTRY_DATA_OFFSET],
917 entry[revlog_constants.ENTRY_DATA_OFFSET],
917 entry[revlog_constants.ENTRY_DATA_COMPRESSED_LENGTH],
918 entry[revlog_constants.ENTRY_DATA_COMPRESSED_LENGTH],
918 entry[revlog_constants.ENTRY_DATA_UNCOMPRESSED_LENGTH],
919 entry[revlog_constants.ENTRY_DATA_UNCOMPRESSED_LENGTH],
919 entry[revlog_constants.ENTRY_PARENT_1],
920 entry[revlog_constants.ENTRY_PARENT_1],
920 entry[revlog_constants.ENTRY_PARENT_2],
921 entry[revlog_constants.ENTRY_PARENT_2],
921 entry[revlog_constants.ENTRY_NODE_ID],
922 entry[revlog_constants.ENTRY_NODE_ID],
922 entry[revlog_constants.ENTRY_SIDEDATA_OFFSET],
923 entry[revlog_constants.ENTRY_SIDEDATA_OFFSET],
923 entry[revlog_constants.ENTRY_SIDEDATA_COMPRESSED_LENGTH],
924 entry[revlog_constants.ENTRY_SIDEDATA_COMPRESSED_LENGTH],
924 entry[revlog_constants.ENTRY_DATA_COMPRESSION_MODE] & 3
925 entry[revlog_constants.ENTRY_DATA_COMPRESSION_MODE] & 3
925 | (entry[revlog_constants.ENTRY_SIDEDATA_COMPRESSION_MODE] & 3)
926 | (entry[revlog_constants.ENTRY_SIDEDATA_COMPRESSION_MODE] & 3)
926 << 2,
927 << 2,
927 entry[revlog_constants.ENTRY_RANK],
928 entry[revlog_constants.ENTRY_RANK],
928 )
929 )
929 return self.index_format.pack(*data)
930 return self.index_format.pack(*data)
930
931
931
932
932 def parse_index_devel_nodemap(data, inline):
933 def parse_index_devel_nodemap(data, inline):
933 """like parse_index2, but alway return a PersistentNodeMapIndexObject"""
934 """like parse_index2, but alway return a PersistentNodeMapIndexObject"""
934 return PersistentNodeMapIndexObject(data), None
935 return PersistentNodeMapIndexObject(data), None
935
936
936
937
937 def parse_dirstate(dmap, copymap, st):
938 def parse_dirstate(dmap, copymap, st):
938 parents = [st[:20], st[20:40]]
939 parents = [st[:20], st[20:40]]
939 # dereference fields so they will be local in loop
940 # dereference fields so they will be local in loop
940 format = b">cllll"
941 format = b">cllll"
941 e_size = struct.calcsize(format)
942 e_size = struct.calcsize(format)
942 pos1 = 40
943 pos1 = 40
943 l = len(st)
944 l = len(st)
944
945
945 # the inner loop
946 # the inner loop
946 while pos1 < l:
947 while pos1 < l:
947 pos2 = pos1 + e_size
948 pos2 = pos1 + e_size
948 e = _unpack(b">cllll", st[pos1:pos2]) # a literal here is faster
949 e = _unpack(b">cllll", st[pos1:pos2]) # a literal here is faster
949 pos1 = pos2 + e[4]
950 pos1 = pos2 + e[4]
950 f = st[pos2:pos1]
951 f = st[pos2:pos1]
951 if b'\0' in f:
952 if b'\0' in f:
952 f, c = f.split(b'\0')
953 f, c = f.split(b'\0')
953 copymap[f] = c
954 copymap[f] = c
954 dmap[f] = DirstateItem.from_v1_data(*e[:4])
955 dmap[f] = DirstateItem.from_v1_data(*e[:4])
955 return parents
956 return parents
956
957
957
958
958 def pack_dirstate(dmap, copymap, pl):
959 def pack_dirstate(dmap, copymap, pl):
959 cs = stringio()
960 cs = stringio()
960 write = cs.write
961 write = cs.write
961 write(b"".join(pl))
962 write(b"".join(pl))
962 for f, e in pycompat.iteritems(dmap):
963 for f, e in pycompat.iteritems(dmap):
963 if f in copymap:
964 if f in copymap:
964 f = b"%s\0%s" % (f, copymap[f])
965 f = b"%s\0%s" % (f, copymap[f])
965 e = _pack(
966 e = _pack(
966 b">cllll",
967 b">cllll",
967 e.v1_state(),
968 e.v1_state(),
968 e.v1_mode(),
969 e.v1_mode(),
969 e.v1_size(),
970 e.v1_size(),
970 e.v1_mtime(),
971 e.v1_mtime(),
971 len(f),
972 len(f),
972 )
973 )
973 write(e)
974 write(e)
974 write(f)
975 write(f)
975 return cs.getvalue()
976 return cs.getvalue()
@@ -1,471 +1,468 b''
1 # pycompat.py - portability shim for python 3
1 # pycompat.py - portability shim for python 3
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 """Mercurial portability shim for python 3.
6 """Mercurial portability shim for python 3.
7
7
8 This contains aliases to hide python version-specific details from the core.
8 This contains aliases to hide python version-specific details from the core.
9 """
9 """
10
10
11 from __future__ import absolute_import
11 from __future__ import absolute_import
12
12
13 import builtins
13 import builtins
14 import codecs
14 import codecs
15 import concurrent.futures as futures
15 import concurrent.futures as futures
16 import functools
16 import functools
17 import getopt
17 import getopt
18 import http.client as httplib
18 import http.client as httplib
19 import http.cookiejar as cookielib
19 import http.cookiejar as cookielib
20 import inspect
20 import inspect
21 import io
21 import io
22 import json
22 import json
23 import os
23 import os
24 import pickle
24 import pickle
25 import queue
25 import queue
26 import shlex
26 import shlex
27 import socketserver
27 import socketserver
28 import struct
28 import struct
29 import sys
29 import sys
30 import tempfile
30 import tempfile
31 import xmlrpc.client as xmlrpclib
31 import xmlrpc.client as xmlrpclib
32
32
33
33
34 ispy3 = sys.version_info[0] >= 3
34 ispy3 = sys.version_info[0] >= 3
35 ispypy = '__pypy__' in sys.builtin_module_names
35 ispypy = '__pypy__' in sys.builtin_module_names
36 TYPE_CHECKING = False
36 TYPE_CHECKING = False
37
37
38 if not globals(): # hide this from non-pytype users
38 if not globals(): # hide this from non-pytype users
39 import typing
39 import typing
40
40
41 TYPE_CHECKING = typing.TYPE_CHECKING
41 TYPE_CHECKING = typing.TYPE_CHECKING
42
42
43
43
44 def future_set_exception_info(f, exc_info):
44 def future_set_exception_info(f, exc_info):
45 f.set_exception(exc_info[0])
45 f.set_exception(exc_info[0])
46
46
47
47
48 FileNotFoundError = builtins.FileNotFoundError
48 FileNotFoundError = builtins.FileNotFoundError
49
49
50
50
51 def identity(a):
51 def identity(a):
52 return a
52 return a
53
53
54
54
55 def _rapply(f, xs):
55 def _rapply(f, xs):
56 if xs is None:
56 if xs is None:
57 # assume None means non-value of optional data
57 # assume None means non-value of optional data
58 return xs
58 return xs
59 if isinstance(xs, (list, set, tuple)):
59 if isinstance(xs, (list, set, tuple)):
60 return type(xs)(_rapply(f, x) for x in xs)
60 return type(xs)(_rapply(f, x) for x in xs)
61 if isinstance(xs, dict):
61 if isinstance(xs, dict):
62 return type(xs)((_rapply(f, k), _rapply(f, v)) for k, v in xs.items())
62 return type(xs)((_rapply(f, k), _rapply(f, v)) for k, v in xs.items())
63 return f(xs)
63 return f(xs)
64
64
65
65
66 def rapply(f, xs):
66 def rapply(f, xs):
67 """Apply function recursively to every item preserving the data structure
67 """Apply function recursively to every item preserving the data structure
68
68
69 >>> def f(x):
69 >>> def f(x):
70 ... return 'f(%s)' % x
70 ... return 'f(%s)' % x
71 >>> rapply(f, None) is None
71 >>> rapply(f, None) is None
72 True
72 True
73 >>> rapply(f, 'a')
73 >>> rapply(f, 'a')
74 'f(a)'
74 'f(a)'
75 >>> rapply(f, {'a'}) == {'f(a)'}
75 >>> rapply(f, {'a'}) == {'f(a)'}
76 True
76 True
77 >>> rapply(f, ['a', 'b', None, {'c': 'd'}, []])
77 >>> rapply(f, ['a', 'b', None, {'c': 'd'}, []])
78 ['f(a)', 'f(b)', None, {'f(c)': 'f(d)'}, []]
78 ['f(a)', 'f(b)', None, {'f(c)': 'f(d)'}, []]
79
79
80 >>> xs = [object()]
80 >>> xs = [object()]
81 >>> rapply(identity, xs) is xs
81 >>> rapply(identity, xs) is xs
82 True
82 True
83 """
83 """
84 if f is identity:
84 if f is identity:
85 # fast path mainly for py2
85 # fast path mainly for py2
86 return xs
86 return xs
87 return _rapply(f, xs)
87 return _rapply(f, xs)
88
88
89
89
90 if os.name == r'nt' and sys.version_info >= (3, 6):
90 if os.name == r'nt' and sys.version_info >= (3, 6):
91 # MBCS (or ANSI) filesystem encoding must be used as before.
91 # MBCS (or ANSI) filesystem encoding must be used as before.
92 # Otherwise non-ASCII filenames in existing repositories would be
92 # Otherwise non-ASCII filenames in existing repositories would be
93 # corrupted.
93 # corrupted.
94 # This must be set once prior to any fsencode/fsdecode calls.
94 # This must be set once prior to any fsencode/fsdecode calls.
95 sys._enablelegacywindowsfsencoding() # pytype: disable=module-attr
95 sys._enablelegacywindowsfsencoding() # pytype: disable=module-attr
96
96
97 fsencode = os.fsencode
97 fsencode = os.fsencode
98 fsdecode = os.fsdecode
98 fsdecode = os.fsdecode
99 oscurdir = os.curdir.encode('ascii')
99 oscurdir = os.curdir.encode('ascii')
100 oslinesep = os.linesep.encode('ascii')
100 oslinesep = os.linesep.encode('ascii')
101 osname = os.name.encode('ascii')
101 osname = os.name.encode('ascii')
102 ospathsep = os.pathsep.encode('ascii')
102 ospathsep = os.pathsep.encode('ascii')
103 ospardir = os.pardir.encode('ascii')
103 ospardir = os.pardir.encode('ascii')
104 ossep = os.sep.encode('ascii')
104 ossep = os.sep.encode('ascii')
105 osaltsep = os.altsep
105 osaltsep = os.altsep
106 if osaltsep:
106 if osaltsep:
107 osaltsep = osaltsep.encode('ascii')
107 osaltsep = osaltsep.encode('ascii')
108 osdevnull = os.devnull.encode('ascii')
108 osdevnull = os.devnull.encode('ascii')
109
109
110 sysplatform = sys.platform.encode('ascii')
110 sysplatform = sys.platform.encode('ascii')
111 sysexecutable = sys.executable
111 sysexecutable = sys.executable
112 if sysexecutable:
112 if sysexecutable:
113 sysexecutable = os.fsencode(sysexecutable)
113 sysexecutable = os.fsencode(sysexecutable)
114 bytesio = io.BytesIO
115 # TODO deprecate stringio name, as it is a lie on Python 3.
116 stringio = bytesio
117
114
118
115
119 def maplist(*args):
116 def maplist(*args):
120 return list(map(*args))
117 return list(map(*args))
121
118
122
119
123 def rangelist(*args):
120 def rangelist(*args):
124 return list(range(*args))
121 return list(range(*args))
125
122
126
123
127 def ziplist(*args):
124 def ziplist(*args):
128 return list(zip(*args))
125 return list(zip(*args))
129
126
130
127
131 rawinput = input
128 rawinput = input
132 getargspec = inspect.getfullargspec
129 getargspec = inspect.getfullargspec
133
130
134 long = int
131 long = int
135
132
136 if getattr(sys, 'argv', None) is not None:
133 if getattr(sys, 'argv', None) is not None:
137 # On POSIX, the char** argv array is converted to Python str using
134 # On POSIX, the char** argv array is converted to Python str using
138 # Py_DecodeLocale(). The inverse of this is Py_EncodeLocale(), which
135 # Py_DecodeLocale(). The inverse of this is Py_EncodeLocale(), which
139 # isn't directly callable from Python code. In practice, os.fsencode()
136 # isn't directly callable from Python code. In practice, os.fsencode()
140 # can be used instead (this is recommended by Python's documentation
137 # can be used instead (this is recommended by Python's documentation
141 # for sys.argv).
138 # for sys.argv).
142 #
139 #
143 # On Windows, the wchar_t **argv is passed into the interpreter as-is.
140 # On Windows, the wchar_t **argv is passed into the interpreter as-is.
144 # Like POSIX, we need to emulate what Py_EncodeLocale() would do. But
141 # Like POSIX, we need to emulate what Py_EncodeLocale() would do. But
145 # there's an additional wrinkle. What we really want to access is the
142 # there's an additional wrinkle. What we really want to access is the
146 # ANSI codepage representation of the arguments, as this is what
143 # ANSI codepage representation of the arguments, as this is what
147 # `int main()` would receive if Python 3 didn't define `int wmain()`
144 # `int main()` would receive if Python 3 didn't define `int wmain()`
148 # (this is how Python 2 worked). To get that, we encode with the mbcs
145 # (this is how Python 2 worked). To get that, we encode with the mbcs
149 # encoding, which will pass CP_ACP to the underlying Windows API to
146 # encoding, which will pass CP_ACP to the underlying Windows API to
150 # produce bytes.
147 # produce bytes.
151 if os.name == r'nt':
148 if os.name == r'nt':
152 sysargv = [a.encode("mbcs", "ignore") for a in sys.argv]
149 sysargv = [a.encode("mbcs", "ignore") for a in sys.argv]
153 else:
150 else:
154 sysargv = [fsencode(a) for a in sys.argv]
151 sysargv = [fsencode(a) for a in sys.argv]
155
152
156 bytechr = struct.Struct('>B').pack
153 bytechr = struct.Struct('>B').pack
157 byterepr = b'%r'.__mod__
154 byterepr = b'%r'.__mod__
158
155
159
156
160 class bytestr(bytes):
157 class bytestr(bytes):
161 """A bytes which mostly acts as a Python 2 str
158 """A bytes which mostly acts as a Python 2 str
162
159
163 >>> bytestr(), bytestr(bytearray(b'foo')), bytestr(u'ascii'), bytestr(1)
160 >>> bytestr(), bytestr(bytearray(b'foo')), bytestr(u'ascii'), bytestr(1)
164 ('', 'foo', 'ascii', '1')
161 ('', 'foo', 'ascii', '1')
165 >>> s = bytestr(b'foo')
162 >>> s = bytestr(b'foo')
166 >>> assert s is bytestr(s)
163 >>> assert s is bytestr(s)
167
164
168 __bytes__() should be called if provided:
165 __bytes__() should be called if provided:
169
166
170 >>> class bytesable(object):
167 >>> class bytesable(object):
171 ... def __bytes__(self):
168 ... def __bytes__(self):
172 ... return b'bytes'
169 ... return b'bytes'
173 >>> bytestr(bytesable())
170 >>> bytestr(bytesable())
174 'bytes'
171 'bytes'
175
172
176 There's no implicit conversion from non-ascii str as its encoding is
173 There's no implicit conversion from non-ascii str as its encoding is
177 unknown:
174 unknown:
178
175
179 >>> bytestr(chr(0x80)) # doctest: +ELLIPSIS
176 >>> bytestr(chr(0x80)) # doctest: +ELLIPSIS
180 Traceback (most recent call last):
177 Traceback (most recent call last):
181 ...
178 ...
182 UnicodeEncodeError: ...
179 UnicodeEncodeError: ...
183
180
184 Comparison between bytestr and bytes should work:
181 Comparison between bytestr and bytes should work:
185
182
186 >>> assert bytestr(b'foo') == b'foo'
183 >>> assert bytestr(b'foo') == b'foo'
187 >>> assert b'foo' == bytestr(b'foo')
184 >>> assert b'foo' == bytestr(b'foo')
188 >>> assert b'f' in bytestr(b'foo')
185 >>> assert b'f' in bytestr(b'foo')
189 >>> assert bytestr(b'f') in b'foo'
186 >>> assert bytestr(b'f') in b'foo'
190
187
191 Sliced elements should be bytes, not integer:
188 Sliced elements should be bytes, not integer:
192
189
193 >>> s[1], s[:2]
190 >>> s[1], s[:2]
194 (b'o', b'fo')
191 (b'o', b'fo')
195 >>> list(s), list(reversed(s))
192 >>> list(s), list(reversed(s))
196 ([b'f', b'o', b'o'], [b'o', b'o', b'f'])
193 ([b'f', b'o', b'o'], [b'o', b'o', b'f'])
197
194
198 As bytestr type isn't propagated across operations, you need to cast
195 As bytestr type isn't propagated across operations, you need to cast
199 bytes to bytestr explicitly:
196 bytes to bytestr explicitly:
200
197
201 >>> s = bytestr(b'foo').upper()
198 >>> s = bytestr(b'foo').upper()
202 >>> t = bytestr(s)
199 >>> t = bytestr(s)
203 >>> s[0], t[0]
200 >>> s[0], t[0]
204 (70, b'F')
201 (70, b'F')
205
202
206 Be careful to not pass a bytestr object to a function which expects
203 Be careful to not pass a bytestr object to a function which expects
207 bytearray-like behavior.
204 bytearray-like behavior.
208
205
209 >>> t = bytes(t) # cast to bytes
206 >>> t = bytes(t) # cast to bytes
210 >>> assert type(t) is bytes
207 >>> assert type(t) is bytes
211 """
208 """
212
209
213 # Trick pytype into not demanding Iterable[int] be passed to __new__(),
210 # Trick pytype into not demanding Iterable[int] be passed to __new__(),
214 # since the appropriate bytes format is done internally.
211 # since the appropriate bytes format is done internally.
215 #
212 #
216 # https://github.com/google/pytype/issues/500
213 # https://github.com/google/pytype/issues/500
217 if TYPE_CHECKING:
214 if TYPE_CHECKING:
218
215
219 def __init__(self, s=b''):
216 def __init__(self, s=b''):
220 pass
217 pass
221
218
222 def __new__(cls, s=b''):
219 def __new__(cls, s=b''):
223 if isinstance(s, bytestr):
220 if isinstance(s, bytestr):
224 return s
221 return s
225 if not isinstance(
222 if not isinstance(
226 s, (bytes, bytearray)
223 s, (bytes, bytearray)
227 ) and not hasattr( # hasattr-py3-only
224 ) and not hasattr( # hasattr-py3-only
228 s, u'__bytes__'
225 s, u'__bytes__'
229 ):
226 ):
230 s = str(s).encode('ascii')
227 s = str(s).encode('ascii')
231 return bytes.__new__(cls, s)
228 return bytes.__new__(cls, s)
232
229
233 def __getitem__(self, key):
230 def __getitem__(self, key):
234 s = bytes.__getitem__(self, key)
231 s = bytes.__getitem__(self, key)
235 if not isinstance(s, bytes):
232 if not isinstance(s, bytes):
236 s = bytechr(s)
233 s = bytechr(s)
237 return s
234 return s
238
235
239 def __iter__(self):
236 def __iter__(self):
240 return iterbytestr(bytes.__iter__(self))
237 return iterbytestr(bytes.__iter__(self))
241
238
242 def __repr__(self):
239 def __repr__(self):
243 return bytes.__repr__(self)[1:] # drop b''
240 return bytes.__repr__(self)[1:] # drop b''
244
241
245
242
246 def iterbytestr(s):
243 def iterbytestr(s):
247 """Iterate bytes as if it were a str object of Python 2"""
244 """Iterate bytes as if it were a str object of Python 2"""
248 return map(bytechr, s)
245 return map(bytechr, s)
249
246
250
247
251 def maybebytestr(s):
248 def maybebytestr(s):
252 """Promote bytes to bytestr"""
249 """Promote bytes to bytestr"""
253 if isinstance(s, bytes):
250 if isinstance(s, bytes):
254 return bytestr(s)
251 return bytestr(s)
255 return s
252 return s
256
253
257
254
258 def sysbytes(s):
255 def sysbytes(s):
259 """Convert an internal str (e.g. keyword, __doc__) back to bytes
256 """Convert an internal str (e.g. keyword, __doc__) back to bytes
260
257
261 This never raises UnicodeEncodeError, but only ASCII characters
258 This never raises UnicodeEncodeError, but only ASCII characters
262 can be round-trip by sysstr(sysbytes(s)).
259 can be round-trip by sysstr(sysbytes(s)).
263 """
260 """
264 if isinstance(s, bytes):
261 if isinstance(s, bytes):
265 return s
262 return s
266 return s.encode('utf-8')
263 return s.encode('utf-8')
267
264
268
265
269 def sysstr(s):
266 def sysstr(s):
270 """Return a keyword str to be passed to Python functions such as
267 """Return a keyword str to be passed to Python functions such as
271 getattr() and str.encode()
268 getattr() and str.encode()
272
269
273 This never raises UnicodeDecodeError. Non-ascii characters are
270 This never raises UnicodeDecodeError. Non-ascii characters are
274 considered invalid and mapped to arbitrary but unique code points
271 considered invalid and mapped to arbitrary but unique code points
275 such that 'sysstr(a) != sysstr(b)' for all 'a != b'.
272 such that 'sysstr(a) != sysstr(b)' for all 'a != b'.
276 """
273 """
277 if isinstance(s, builtins.str):
274 if isinstance(s, builtins.str):
278 return s
275 return s
279 return s.decode('latin-1')
276 return s.decode('latin-1')
280
277
281
278
282 def strurl(url):
279 def strurl(url):
283 """Converts a bytes url back to str"""
280 """Converts a bytes url back to str"""
284 if isinstance(url, bytes):
281 if isinstance(url, bytes):
285 return url.decode('ascii')
282 return url.decode('ascii')
286 return url
283 return url
287
284
288
285
289 def bytesurl(url):
286 def bytesurl(url):
290 """Converts a str url to bytes by encoding in ascii"""
287 """Converts a str url to bytes by encoding in ascii"""
291 if isinstance(url, str):
288 if isinstance(url, str):
292 return url.encode('ascii')
289 return url.encode('ascii')
293 return url
290 return url
294
291
295
292
296 def raisewithtb(exc, tb):
293 def raisewithtb(exc, tb):
297 """Raise exception with the given traceback"""
294 """Raise exception with the given traceback"""
298 raise exc.with_traceback(tb)
295 raise exc.with_traceback(tb)
299
296
300
297
301 def getdoc(obj):
298 def getdoc(obj):
302 """Get docstring as bytes; may be None so gettext() won't confuse it
299 """Get docstring as bytes; may be None so gettext() won't confuse it
303 with _('')"""
300 with _('')"""
304 doc = getattr(obj, '__doc__', None)
301 doc = getattr(obj, '__doc__', None)
305 if doc is None:
302 if doc is None:
306 return doc
303 return doc
307 return sysbytes(doc)
304 return sysbytes(doc)
308
305
309
306
310 def _wrapattrfunc(f):
307 def _wrapattrfunc(f):
311 @functools.wraps(f)
308 @functools.wraps(f)
312 def w(object, name, *args):
309 def w(object, name, *args):
313 return f(object, sysstr(name), *args)
310 return f(object, sysstr(name), *args)
314
311
315 return w
312 return w
316
313
317
314
318 # these wrappers are automagically imported by hgloader
315 # these wrappers are automagically imported by hgloader
319 delattr = _wrapattrfunc(builtins.delattr)
316 delattr = _wrapattrfunc(builtins.delattr)
320 getattr = _wrapattrfunc(builtins.getattr)
317 getattr = _wrapattrfunc(builtins.getattr)
321 hasattr = _wrapattrfunc(builtins.hasattr)
318 hasattr = _wrapattrfunc(builtins.hasattr)
322 setattr = _wrapattrfunc(builtins.setattr)
319 setattr = _wrapattrfunc(builtins.setattr)
323 xrange = builtins.range
320 xrange = builtins.range
324 unicode = str
321 unicode = str
325
322
326
323
327 def open(name, mode=b'r', buffering=-1, encoding=None):
324 def open(name, mode=b'r', buffering=-1, encoding=None):
328 return builtins.open(name, sysstr(mode), buffering, encoding)
325 return builtins.open(name, sysstr(mode), buffering, encoding)
329
326
330
327
331 safehasattr = _wrapattrfunc(builtins.hasattr)
328 safehasattr = _wrapattrfunc(builtins.hasattr)
332
329
333
330
334 def _getoptbwrapper(orig, args, shortlist, namelist):
331 def _getoptbwrapper(orig, args, shortlist, namelist):
335 """
332 """
336 Takes bytes arguments, converts them to unicode, pass them to
333 Takes bytes arguments, converts them to unicode, pass them to
337 getopt.getopt(), convert the returned values back to bytes and then
334 getopt.getopt(), convert the returned values back to bytes and then
338 return them for Python 3 compatibility as getopt.getopt() don't accepts
335 return them for Python 3 compatibility as getopt.getopt() don't accepts
339 bytes on Python 3.
336 bytes on Python 3.
340 """
337 """
341 args = [a.decode('latin-1') for a in args]
338 args = [a.decode('latin-1') for a in args]
342 shortlist = shortlist.decode('latin-1')
339 shortlist = shortlist.decode('latin-1')
343 namelist = [a.decode('latin-1') for a in namelist]
340 namelist = [a.decode('latin-1') for a in namelist]
344 opts, args = orig(args, shortlist, namelist)
341 opts, args = orig(args, shortlist, namelist)
345 opts = [(a[0].encode('latin-1'), a[1].encode('latin-1')) for a in opts]
342 opts = [(a[0].encode('latin-1'), a[1].encode('latin-1')) for a in opts]
346 args = [a.encode('latin-1') for a in args]
343 args = [a.encode('latin-1') for a in args]
347 return opts, args
344 return opts, args
348
345
349
346
350 def strkwargs(dic):
347 def strkwargs(dic):
351 """
348 """
352 Converts the keys of a python dictonary to str i.e. unicodes so that
349 Converts the keys of a python dictonary to str i.e. unicodes so that
353 they can be passed as keyword arguments as dictionaries with bytes keys
350 they can be passed as keyword arguments as dictionaries with bytes keys
354 can't be passed as keyword arguments to functions on Python 3.
351 can't be passed as keyword arguments to functions on Python 3.
355 """
352 """
356 dic = {k.decode('latin-1'): v for k, v in dic.items()}
353 dic = {k.decode('latin-1'): v for k, v in dic.items()}
357 return dic
354 return dic
358
355
359
356
360 def byteskwargs(dic):
357 def byteskwargs(dic):
361 """
358 """
362 Converts keys of python dictionaries to bytes as they were converted to
359 Converts keys of python dictionaries to bytes as they were converted to
363 str to pass that dictonary as a keyword argument on Python 3.
360 str to pass that dictonary as a keyword argument on Python 3.
364 """
361 """
365 dic = {k.encode('latin-1'): v for k, v in dic.items()}
362 dic = {k.encode('latin-1'): v for k, v in dic.items()}
366 return dic
363 return dic
367
364
368
365
369 # TODO: handle shlex.shlex().
366 # TODO: handle shlex.shlex().
370 def shlexsplit(s, comments=False, posix=True):
367 def shlexsplit(s, comments=False, posix=True):
371 """
368 """
372 Takes bytes argument, convert it to str i.e. unicodes, pass that into
369 Takes bytes argument, convert it to str i.e. unicodes, pass that into
373 shlex.split(), convert the returned value to bytes and return that for
370 shlex.split(), convert the returned value to bytes and return that for
374 Python 3 compatibility as shelx.split() don't accept bytes on Python 3.
371 Python 3 compatibility as shelx.split() don't accept bytes on Python 3.
375 """
372 """
376 ret = shlex.split(s.decode('latin-1'), comments, posix)
373 ret = shlex.split(s.decode('latin-1'), comments, posix)
377 return [a.encode('latin-1') for a in ret]
374 return [a.encode('latin-1') for a in ret]
378
375
379
376
380 iteritems = lambda x: x.items()
377 iteritems = lambda x: x.items()
381 itervalues = lambda x: x.values()
378 itervalues = lambda x: x.values()
382
379
383 # Python 3.5's json.load and json.loads require str. We polyfill its
380 # Python 3.5's json.load and json.loads require str. We polyfill its
384 # code for detecting encoding from bytes.
381 # code for detecting encoding from bytes.
385 if sys.version_info[0:2] < (3, 6):
382 if sys.version_info[0:2] < (3, 6):
386
383
387 def _detect_encoding(b):
384 def _detect_encoding(b):
388 bstartswith = b.startswith
385 bstartswith = b.startswith
389 if bstartswith((codecs.BOM_UTF32_BE, codecs.BOM_UTF32_LE)):
386 if bstartswith((codecs.BOM_UTF32_BE, codecs.BOM_UTF32_LE)):
390 return 'utf-32'
387 return 'utf-32'
391 if bstartswith((codecs.BOM_UTF16_BE, codecs.BOM_UTF16_LE)):
388 if bstartswith((codecs.BOM_UTF16_BE, codecs.BOM_UTF16_LE)):
392 return 'utf-16'
389 return 'utf-16'
393 if bstartswith(codecs.BOM_UTF8):
390 if bstartswith(codecs.BOM_UTF8):
394 return 'utf-8-sig'
391 return 'utf-8-sig'
395
392
396 if len(b) >= 4:
393 if len(b) >= 4:
397 if not b[0]:
394 if not b[0]:
398 # 00 00 -- -- - utf-32-be
395 # 00 00 -- -- - utf-32-be
399 # 00 XX -- -- - utf-16-be
396 # 00 XX -- -- - utf-16-be
400 return 'utf-16-be' if b[1] else 'utf-32-be'
397 return 'utf-16-be' if b[1] else 'utf-32-be'
401 if not b[1]:
398 if not b[1]:
402 # XX 00 00 00 - utf-32-le
399 # XX 00 00 00 - utf-32-le
403 # XX 00 00 XX - utf-16-le
400 # XX 00 00 XX - utf-16-le
404 # XX 00 XX -- - utf-16-le
401 # XX 00 XX -- - utf-16-le
405 return 'utf-16-le' if b[2] or b[3] else 'utf-32-le'
402 return 'utf-16-le' if b[2] or b[3] else 'utf-32-le'
406 elif len(b) == 2:
403 elif len(b) == 2:
407 if not b[0]:
404 if not b[0]:
408 # 00 XX - utf-16-be
405 # 00 XX - utf-16-be
409 return 'utf-16-be'
406 return 'utf-16-be'
410 if not b[1]:
407 if not b[1]:
411 # XX 00 - utf-16-le
408 # XX 00 - utf-16-le
412 return 'utf-16-le'
409 return 'utf-16-le'
413 # default
410 # default
414 return 'utf-8'
411 return 'utf-8'
415
412
416 def json_loads(s, *args, **kwargs):
413 def json_loads(s, *args, **kwargs):
417 if isinstance(s, (bytes, bytearray)):
414 if isinstance(s, (bytes, bytearray)):
418 s = s.decode(_detect_encoding(s), 'surrogatepass')
415 s = s.decode(_detect_encoding(s), 'surrogatepass')
419
416
420 return json.loads(s, *args, **kwargs)
417 return json.loads(s, *args, **kwargs)
421
418
422
419
423 else:
420 else:
424 json_loads = json.loads
421 json_loads = json.loads
425
422
426 isjython = sysplatform.startswith(b'java')
423 isjython = sysplatform.startswith(b'java')
427
424
428 isdarwin = sysplatform.startswith(b'darwin')
425 isdarwin = sysplatform.startswith(b'darwin')
429 islinux = sysplatform.startswith(b'linux')
426 islinux = sysplatform.startswith(b'linux')
430 isposix = osname == b'posix'
427 isposix = osname == b'posix'
431 iswindows = osname == b'nt'
428 iswindows = osname == b'nt'
432
429
433
430
434 def getoptb(args, shortlist, namelist):
431 def getoptb(args, shortlist, namelist):
435 return _getoptbwrapper(getopt.getopt, args, shortlist, namelist)
432 return _getoptbwrapper(getopt.getopt, args, shortlist, namelist)
436
433
437
434
438 def gnugetoptb(args, shortlist, namelist):
435 def gnugetoptb(args, shortlist, namelist):
439 return _getoptbwrapper(getopt.gnu_getopt, args, shortlist, namelist)
436 return _getoptbwrapper(getopt.gnu_getopt, args, shortlist, namelist)
440
437
441
438
442 def mkdtemp(suffix=b'', prefix=b'tmp', dir=None):
439 def mkdtemp(suffix=b'', prefix=b'tmp', dir=None):
443 return tempfile.mkdtemp(suffix, prefix, dir)
440 return tempfile.mkdtemp(suffix, prefix, dir)
444
441
445
442
446 # text=True is not supported; use util.from/tonativeeol() instead
443 # text=True is not supported; use util.from/tonativeeol() instead
447 def mkstemp(suffix=b'', prefix=b'tmp', dir=None):
444 def mkstemp(suffix=b'', prefix=b'tmp', dir=None):
448 return tempfile.mkstemp(suffix, prefix, dir)
445 return tempfile.mkstemp(suffix, prefix, dir)
449
446
450
447
451 # TemporaryFile does not support an "encoding=" argument on python2.
448 # TemporaryFile does not support an "encoding=" argument on python2.
452 # This wrapper file are always open in byte mode.
449 # This wrapper file are always open in byte mode.
453 def unnamedtempfile(mode=None, *args, **kwargs):
450 def unnamedtempfile(mode=None, *args, **kwargs):
454 if mode is None:
451 if mode is None:
455 mode = 'w+b'
452 mode = 'w+b'
456 else:
453 else:
457 mode = sysstr(mode)
454 mode = sysstr(mode)
458 assert 'b' in mode
455 assert 'b' in mode
459 return tempfile.TemporaryFile(mode, *args, **kwargs)
456 return tempfile.TemporaryFile(mode, *args, **kwargs)
460
457
461
458
462 # NamedTemporaryFile does not support an "encoding=" argument on python2.
459 # NamedTemporaryFile does not support an "encoding=" argument on python2.
463 # This wrapper file are always open in byte mode.
460 # This wrapper file are always open in byte mode.
464 def namedtempfile(
461 def namedtempfile(
465 mode=b'w+b', bufsize=-1, suffix=b'', prefix=b'tmp', dir=None, delete=True
462 mode=b'w+b', bufsize=-1, suffix=b'', prefix=b'tmp', dir=None, delete=True
466 ):
463 ):
467 mode = sysstr(mode)
464 mode = sysstr(mode)
468 assert 'b' in mode
465 assert 'b' in mode
469 return tempfile.NamedTemporaryFile(
466 return tempfile.NamedTemporaryFile(
470 mode, bufsize, suffix=suffix, prefix=prefix, dir=dir, delete=delete
467 mode, bufsize, suffix=suffix, prefix=prefix, dir=dir, delete=delete
471 )
468 )
@@ -1,3360 +1,3361 b''
1 # util.py - Mercurial utility functions and platform specific implementations
1 # util.py - Mercurial utility functions and platform specific implementations
2 #
2 #
3 # Copyright 2005 K. Thananchayan <thananck@yahoo.com>
3 # Copyright 2005 K. Thananchayan <thananck@yahoo.com>
4 # Copyright 2005-2007 Olivia Mackall <olivia@selenic.com>
4 # Copyright 2005-2007 Olivia Mackall <olivia@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 """Mercurial utility functions and platform specific implementations.
10 """Mercurial utility functions and platform specific implementations.
11
11
12 This contains helper routines that are independent of the SCM core and
12 This contains helper routines that are independent of the SCM core and
13 hide platform-specific details from the core.
13 hide platform-specific details from the core.
14 """
14 """
15
15
16 from __future__ import absolute_import, print_function
16 from __future__ import absolute_import, print_function
17
17
18 import abc
18 import abc
19 import collections
19 import collections
20 import contextlib
20 import contextlib
21 import errno
21 import errno
22 import gc
22 import gc
23 import hashlib
23 import hashlib
24 import io
24 import itertools
25 import itertools
25 import locale
26 import locale
26 import mmap
27 import mmap
27 import os
28 import os
28 import platform as pyplatform
29 import platform as pyplatform
29 import re as remod
30 import re as remod
30 import shutil
31 import shutil
31 import stat
32 import stat
32 import sys
33 import sys
33 import time
34 import time
34 import traceback
35 import traceback
35 import warnings
36 import warnings
36
37
37 from .node import hex
38 from .node import hex
38 from .thirdparty import attr
39 from .thirdparty import attr
39 from .pycompat import (
40 from .pycompat import (
40 delattr,
41 delattr,
41 getattr,
42 getattr,
42 open,
43 open,
43 setattr,
44 setattr,
44 )
45 )
45 from .node import hex
46 from .node import hex
46 from hgdemandimport import tracing
47 from hgdemandimport import tracing
47 from . import (
48 from . import (
48 encoding,
49 encoding,
49 error,
50 error,
50 i18n,
51 i18n,
51 policy,
52 policy,
52 pycompat,
53 pycompat,
53 urllibcompat,
54 urllibcompat,
54 )
55 )
55 from .utils import (
56 from .utils import (
56 compression,
57 compression,
57 hashutil,
58 hashutil,
58 procutil,
59 procutil,
59 stringutil,
60 stringutil,
60 )
61 )
61
62
62 if pycompat.TYPE_CHECKING:
63 if pycompat.TYPE_CHECKING:
63 from typing import (
64 from typing import (
64 Iterator,
65 Iterator,
65 List,
66 List,
66 Optional,
67 Optional,
67 Tuple,
68 Tuple,
68 )
69 )
69
70
70
71
71 base85 = policy.importmod('base85')
72 base85 = policy.importmod('base85')
72 osutil = policy.importmod('osutil')
73 osutil = policy.importmod('osutil')
73
74
74 b85decode = base85.b85decode
75 b85decode = base85.b85decode
75 b85encode = base85.b85encode
76 b85encode = base85.b85encode
76
77
77 cookielib = pycompat.cookielib
78 cookielib = pycompat.cookielib
78 httplib = pycompat.httplib
79 httplib = pycompat.httplib
79 safehasattr = pycompat.safehasattr
80 safehasattr = pycompat.safehasattr
80 socketserver = pycompat.socketserver
81 socketserver = pycompat.socketserver
81 bytesio = pycompat.bytesio
82 bytesio = io.BytesIO
82 # TODO deprecate stringio name, as it is a lie on Python 3.
83 # TODO deprecate stringio name, as it is a lie on Python 3.
83 stringio = bytesio
84 stringio = bytesio
84 xmlrpclib = pycompat.xmlrpclib
85 xmlrpclib = pycompat.xmlrpclib
85
86
86 httpserver = urllibcompat.httpserver
87 httpserver = urllibcompat.httpserver
87 urlerr = urllibcompat.urlerr
88 urlerr = urllibcompat.urlerr
88 urlreq = urllibcompat.urlreq
89 urlreq = urllibcompat.urlreq
89
90
90 # workaround for win32mbcs
91 # workaround for win32mbcs
91 _filenamebytestr = pycompat.bytestr
92 _filenamebytestr = pycompat.bytestr
92
93
93 if pycompat.iswindows:
94 if pycompat.iswindows:
94 from . import windows as platform
95 from . import windows as platform
95 else:
96 else:
96 from . import posix as platform
97 from . import posix as platform
97
98
98 _ = i18n._
99 _ = i18n._
99
100
100 abspath = platform.abspath
101 abspath = platform.abspath
101 bindunixsocket = platform.bindunixsocket
102 bindunixsocket = platform.bindunixsocket
102 cachestat = platform.cachestat
103 cachestat = platform.cachestat
103 checkexec = platform.checkexec
104 checkexec = platform.checkexec
104 checklink = platform.checklink
105 checklink = platform.checklink
105 copymode = platform.copymode
106 copymode = platform.copymode
106 expandglobs = platform.expandglobs
107 expandglobs = platform.expandglobs
107 getfsmountpoint = platform.getfsmountpoint
108 getfsmountpoint = platform.getfsmountpoint
108 getfstype = platform.getfstype
109 getfstype = platform.getfstype
109 get_password = platform.get_password
110 get_password = platform.get_password
110 groupmembers = platform.groupmembers
111 groupmembers = platform.groupmembers
111 groupname = platform.groupname
112 groupname = platform.groupname
112 isexec = platform.isexec
113 isexec = platform.isexec
113 isowner = platform.isowner
114 isowner = platform.isowner
114 listdir = osutil.listdir
115 listdir = osutil.listdir
115 localpath = platform.localpath
116 localpath = platform.localpath
116 lookupreg = platform.lookupreg
117 lookupreg = platform.lookupreg
117 makedir = platform.makedir
118 makedir = platform.makedir
118 nlinks = platform.nlinks
119 nlinks = platform.nlinks
119 normpath = platform.normpath
120 normpath = platform.normpath
120 normcase = platform.normcase
121 normcase = platform.normcase
121 normcasespec = platform.normcasespec
122 normcasespec = platform.normcasespec
122 normcasefallback = platform.normcasefallback
123 normcasefallback = platform.normcasefallback
123 openhardlinks = platform.openhardlinks
124 openhardlinks = platform.openhardlinks
124 oslink = platform.oslink
125 oslink = platform.oslink
125 parsepatchoutput = platform.parsepatchoutput
126 parsepatchoutput = platform.parsepatchoutput
126 pconvert = platform.pconvert
127 pconvert = platform.pconvert
127 poll = platform.poll
128 poll = platform.poll
128 posixfile = platform.posixfile
129 posixfile = platform.posixfile
129 readlink = platform.readlink
130 readlink = platform.readlink
130 rename = platform.rename
131 rename = platform.rename
131 removedirs = platform.removedirs
132 removedirs = platform.removedirs
132 samedevice = platform.samedevice
133 samedevice = platform.samedevice
133 samefile = platform.samefile
134 samefile = platform.samefile
134 samestat = platform.samestat
135 samestat = platform.samestat
135 setflags = platform.setflags
136 setflags = platform.setflags
136 split = platform.split
137 split = platform.split
137 statfiles = getattr(osutil, 'statfiles', platform.statfiles)
138 statfiles = getattr(osutil, 'statfiles', platform.statfiles)
138 statisexec = platform.statisexec
139 statisexec = platform.statisexec
139 statislink = platform.statislink
140 statislink = platform.statislink
140 umask = platform.umask
141 umask = platform.umask
141 unlink = platform.unlink
142 unlink = platform.unlink
142 username = platform.username
143 username = platform.username
143
144
144
145
145 def setumask(val):
146 def setumask(val):
146 # type: (int) -> None
147 # type: (int) -> None
147 '''updates the umask. used by chg server'''
148 '''updates the umask. used by chg server'''
148 if pycompat.iswindows:
149 if pycompat.iswindows:
149 return
150 return
150 os.umask(val)
151 os.umask(val)
151 global umask
152 global umask
152 platform.umask = umask = val & 0o777
153 platform.umask = umask = val & 0o777
153
154
154
155
155 # small compat layer
156 # small compat layer
156 compengines = compression.compengines
157 compengines = compression.compengines
157 SERVERROLE = compression.SERVERROLE
158 SERVERROLE = compression.SERVERROLE
158 CLIENTROLE = compression.CLIENTROLE
159 CLIENTROLE = compression.CLIENTROLE
159
160
160 try:
161 try:
161 recvfds = osutil.recvfds
162 recvfds = osutil.recvfds
162 except AttributeError:
163 except AttributeError:
163 pass
164 pass
164
165
165 # Python compatibility
166 # Python compatibility
166
167
167 _notset = object()
168 _notset = object()
168
169
169
170
170 def bitsfrom(container):
171 def bitsfrom(container):
171 bits = 0
172 bits = 0
172 for bit in container:
173 for bit in container:
173 bits |= bit
174 bits |= bit
174 return bits
175 return bits
175
176
176
177
177 # python 2.6 still have deprecation warning enabled by default. We do not want
178 # python 2.6 still have deprecation warning enabled by default. We do not want
178 # to display anything to standard user so detect if we are running test and
179 # to display anything to standard user so detect if we are running test and
179 # only use python deprecation warning in this case.
180 # only use python deprecation warning in this case.
180 _dowarn = bool(encoding.environ.get(b'HGEMITWARNINGS'))
181 _dowarn = bool(encoding.environ.get(b'HGEMITWARNINGS'))
181 if _dowarn:
182 if _dowarn:
182 # explicitly unfilter our warning for python 2.7
183 # explicitly unfilter our warning for python 2.7
183 #
184 #
184 # The option of setting PYTHONWARNINGS in the test runner was investigated.
185 # The option of setting PYTHONWARNINGS in the test runner was investigated.
185 # However, module name set through PYTHONWARNINGS was exactly matched, so
186 # However, module name set through PYTHONWARNINGS was exactly matched, so
186 # we cannot set 'mercurial' and have it match eg: 'mercurial.scmutil'. This
187 # we cannot set 'mercurial' and have it match eg: 'mercurial.scmutil'. This
187 # makes the whole PYTHONWARNINGS thing useless for our usecase.
188 # makes the whole PYTHONWARNINGS thing useless for our usecase.
188 warnings.filterwarnings('default', '', DeprecationWarning, 'mercurial')
189 warnings.filterwarnings('default', '', DeprecationWarning, 'mercurial')
189 warnings.filterwarnings('default', '', DeprecationWarning, 'hgext')
190 warnings.filterwarnings('default', '', DeprecationWarning, 'hgext')
190 warnings.filterwarnings('default', '', DeprecationWarning, 'hgext3rd')
191 warnings.filterwarnings('default', '', DeprecationWarning, 'hgext3rd')
191 if _dowarn and pycompat.ispy3:
192 if _dowarn and pycompat.ispy3:
192 # silence warning emitted by passing user string to re.sub()
193 # silence warning emitted by passing user string to re.sub()
193 warnings.filterwarnings(
194 warnings.filterwarnings(
194 'ignore', 'bad escape', DeprecationWarning, 'mercurial'
195 'ignore', 'bad escape', DeprecationWarning, 'mercurial'
195 )
196 )
196 warnings.filterwarnings(
197 warnings.filterwarnings(
197 'ignore', 'invalid escape sequence', DeprecationWarning, 'mercurial'
198 'ignore', 'invalid escape sequence', DeprecationWarning, 'mercurial'
198 )
199 )
199 # TODO: reinvent imp.is_frozen()
200 # TODO: reinvent imp.is_frozen()
200 warnings.filterwarnings(
201 warnings.filterwarnings(
201 'ignore',
202 'ignore',
202 'the imp module is deprecated',
203 'the imp module is deprecated',
203 DeprecationWarning,
204 DeprecationWarning,
204 'mercurial',
205 'mercurial',
205 )
206 )
206
207
207
208
208 def nouideprecwarn(msg, version, stacklevel=1):
209 def nouideprecwarn(msg, version, stacklevel=1):
209 """Issue an python native deprecation warning
210 """Issue an python native deprecation warning
210
211
211 This is a noop outside of tests, use 'ui.deprecwarn' when possible.
212 This is a noop outside of tests, use 'ui.deprecwarn' when possible.
212 """
213 """
213 if _dowarn:
214 if _dowarn:
214 msg += (
215 msg += (
215 b"\n(compatibility will be dropped after Mercurial-%s,"
216 b"\n(compatibility will be dropped after Mercurial-%s,"
216 b" update your code.)"
217 b" update your code.)"
217 ) % version
218 ) % version
218 warnings.warn(pycompat.sysstr(msg), DeprecationWarning, stacklevel + 1)
219 warnings.warn(pycompat.sysstr(msg), DeprecationWarning, stacklevel + 1)
219 # on python 3 with chg, we will need to explicitly flush the output
220 # on python 3 with chg, we will need to explicitly flush the output
220 sys.stderr.flush()
221 sys.stderr.flush()
221
222
222
223
223 DIGESTS = {
224 DIGESTS = {
224 b'md5': hashlib.md5,
225 b'md5': hashlib.md5,
225 b'sha1': hashutil.sha1,
226 b'sha1': hashutil.sha1,
226 b'sha512': hashlib.sha512,
227 b'sha512': hashlib.sha512,
227 }
228 }
228 # List of digest types from strongest to weakest
229 # List of digest types from strongest to weakest
229 DIGESTS_BY_STRENGTH = [b'sha512', b'sha1', b'md5']
230 DIGESTS_BY_STRENGTH = [b'sha512', b'sha1', b'md5']
230
231
231 for k in DIGESTS_BY_STRENGTH:
232 for k in DIGESTS_BY_STRENGTH:
232 assert k in DIGESTS
233 assert k in DIGESTS
233
234
234
235
235 class digester(object):
236 class digester(object):
236 """helper to compute digests.
237 """helper to compute digests.
237
238
238 This helper can be used to compute one or more digests given their name.
239 This helper can be used to compute one or more digests given their name.
239
240
240 >>> d = digester([b'md5', b'sha1'])
241 >>> d = digester([b'md5', b'sha1'])
241 >>> d.update(b'foo')
242 >>> d.update(b'foo')
242 >>> [k for k in sorted(d)]
243 >>> [k for k in sorted(d)]
243 ['md5', 'sha1']
244 ['md5', 'sha1']
244 >>> d[b'md5']
245 >>> d[b'md5']
245 'acbd18db4cc2f85cedef654fccc4a4d8'
246 'acbd18db4cc2f85cedef654fccc4a4d8'
246 >>> d[b'sha1']
247 >>> d[b'sha1']
247 '0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33'
248 '0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33'
248 >>> digester.preferred([b'md5', b'sha1'])
249 >>> digester.preferred([b'md5', b'sha1'])
249 'sha1'
250 'sha1'
250 """
251 """
251
252
252 def __init__(self, digests, s=b''):
253 def __init__(self, digests, s=b''):
253 self._hashes = {}
254 self._hashes = {}
254 for k in digests:
255 for k in digests:
255 if k not in DIGESTS:
256 if k not in DIGESTS:
256 raise error.Abort(_(b'unknown digest type: %s') % k)
257 raise error.Abort(_(b'unknown digest type: %s') % k)
257 self._hashes[k] = DIGESTS[k]()
258 self._hashes[k] = DIGESTS[k]()
258 if s:
259 if s:
259 self.update(s)
260 self.update(s)
260
261
261 def update(self, data):
262 def update(self, data):
262 for h in self._hashes.values():
263 for h in self._hashes.values():
263 h.update(data)
264 h.update(data)
264
265
265 def __getitem__(self, key):
266 def __getitem__(self, key):
266 if key not in DIGESTS:
267 if key not in DIGESTS:
267 raise error.Abort(_(b'unknown digest type: %s') % k)
268 raise error.Abort(_(b'unknown digest type: %s') % k)
268 return hex(self._hashes[key].digest())
269 return hex(self._hashes[key].digest())
269
270
270 def __iter__(self):
271 def __iter__(self):
271 return iter(self._hashes)
272 return iter(self._hashes)
272
273
273 @staticmethod
274 @staticmethod
274 def preferred(supported):
275 def preferred(supported):
275 """returns the strongest digest type in both supported and DIGESTS."""
276 """returns the strongest digest type in both supported and DIGESTS."""
276
277
277 for k in DIGESTS_BY_STRENGTH:
278 for k in DIGESTS_BY_STRENGTH:
278 if k in supported:
279 if k in supported:
279 return k
280 return k
280 return None
281 return None
281
282
282
283
283 class digestchecker(object):
284 class digestchecker(object):
284 """file handle wrapper that additionally checks content against a given
285 """file handle wrapper that additionally checks content against a given
285 size and digests.
286 size and digests.
286
287
287 d = digestchecker(fh, size, {'md5': '...'})
288 d = digestchecker(fh, size, {'md5': '...'})
288
289
289 When multiple digests are given, all of them are validated.
290 When multiple digests are given, all of them are validated.
290 """
291 """
291
292
292 def __init__(self, fh, size, digests):
293 def __init__(self, fh, size, digests):
293 self._fh = fh
294 self._fh = fh
294 self._size = size
295 self._size = size
295 self._got = 0
296 self._got = 0
296 self._digests = dict(digests)
297 self._digests = dict(digests)
297 self._digester = digester(self._digests.keys())
298 self._digester = digester(self._digests.keys())
298
299
299 def read(self, length=-1):
300 def read(self, length=-1):
300 content = self._fh.read(length)
301 content = self._fh.read(length)
301 self._digester.update(content)
302 self._digester.update(content)
302 self._got += len(content)
303 self._got += len(content)
303 return content
304 return content
304
305
305 def validate(self):
306 def validate(self):
306 if self._size != self._got:
307 if self._size != self._got:
307 raise error.Abort(
308 raise error.Abort(
308 _(b'size mismatch: expected %d, got %d')
309 _(b'size mismatch: expected %d, got %d')
309 % (self._size, self._got)
310 % (self._size, self._got)
310 )
311 )
311 for k, v in self._digests.items():
312 for k, v in self._digests.items():
312 if v != self._digester[k]:
313 if v != self._digester[k]:
313 # i18n: first parameter is a digest name
314 # i18n: first parameter is a digest name
314 raise error.Abort(
315 raise error.Abort(
315 _(b'%s mismatch: expected %s, got %s')
316 _(b'%s mismatch: expected %s, got %s')
316 % (k, v, self._digester[k])
317 % (k, v, self._digester[k])
317 )
318 )
318
319
319
320
320 try:
321 try:
321 buffer = buffer # pytype: disable=name-error
322 buffer = buffer # pytype: disable=name-error
322 except NameError:
323 except NameError:
323
324
324 def buffer(sliceable, offset=0, length=None):
325 def buffer(sliceable, offset=0, length=None):
325 if length is not None:
326 if length is not None:
326 return memoryview(sliceable)[offset : offset + length]
327 return memoryview(sliceable)[offset : offset + length]
327 return memoryview(sliceable)[offset:]
328 return memoryview(sliceable)[offset:]
328
329
329
330
330 _chunksize = 4096
331 _chunksize = 4096
331
332
332
333
333 class bufferedinputpipe(object):
334 class bufferedinputpipe(object):
334 """a manually buffered input pipe
335 """a manually buffered input pipe
335
336
336 Python will not let us use buffered IO and lazy reading with 'polling' at
337 Python will not let us use buffered IO and lazy reading with 'polling' at
337 the same time. We cannot probe the buffer state and select will not detect
338 the same time. We cannot probe the buffer state and select will not detect
338 that data are ready to read if they are already buffered.
339 that data are ready to read if they are already buffered.
339
340
340 This class let us work around that by implementing its own buffering
341 This class let us work around that by implementing its own buffering
341 (allowing efficient readline) while offering a way to know if the buffer is
342 (allowing efficient readline) while offering a way to know if the buffer is
342 empty from the output (allowing collaboration of the buffer with polling).
343 empty from the output (allowing collaboration of the buffer with polling).
343
344
344 This class lives in the 'util' module because it makes use of the 'os'
345 This class lives in the 'util' module because it makes use of the 'os'
345 module from the python stdlib.
346 module from the python stdlib.
346 """
347 """
347
348
348 def __new__(cls, fh):
349 def __new__(cls, fh):
349 # If we receive a fileobjectproxy, we need to use a variation of this
350 # If we receive a fileobjectproxy, we need to use a variation of this
350 # class that notifies observers about activity.
351 # class that notifies observers about activity.
351 if isinstance(fh, fileobjectproxy):
352 if isinstance(fh, fileobjectproxy):
352 cls = observedbufferedinputpipe
353 cls = observedbufferedinputpipe
353
354
354 return super(bufferedinputpipe, cls).__new__(cls)
355 return super(bufferedinputpipe, cls).__new__(cls)
355
356
356 def __init__(self, input):
357 def __init__(self, input):
357 self._input = input
358 self._input = input
358 self._buffer = []
359 self._buffer = []
359 self._eof = False
360 self._eof = False
360 self._lenbuf = 0
361 self._lenbuf = 0
361
362
362 @property
363 @property
363 def hasbuffer(self):
364 def hasbuffer(self):
364 """True is any data is currently buffered
365 """True is any data is currently buffered
365
366
366 This will be used externally a pre-step for polling IO. If there is
367 This will be used externally a pre-step for polling IO. If there is
367 already data then no polling should be set in place."""
368 already data then no polling should be set in place."""
368 return bool(self._buffer)
369 return bool(self._buffer)
369
370
370 @property
371 @property
371 def closed(self):
372 def closed(self):
372 return self._input.closed
373 return self._input.closed
373
374
374 def fileno(self):
375 def fileno(self):
375 return self._input.fileno()
376 return self._input.fileno()
376
377
377 def close(self):
378 def close(self):
378 return self._input.close()
379 return self._input.close()
379
380
380 def read(self, size):
381 def read(self, size):
381 while (not self._eof) and (self._lenbuf < size):
382 while (not self._eof) and (self._lenbuf < size):
382 self._fillbuffer()
383 self._fillbuffer()
383 return self._frombuffer(size)
384 return self._frombuffer(size)
384
385
385 def unbufferedread(self, size):
386 def unbufferedread(self, size):
386 if not self._eof and self._lenbuf == 0:
387 if not self._eof and self._lenbuf == 0:
387 self._fillbuffer(max(size, _chunksize))
388 self._fillbuffer(max(size, _chunksize))
388 return self._frombuffer(min(self._lenbuf, size))
389 return self._frombuffer(min(self._lenbuf, size))
389
390
390 def readline(self, *args, **kwargs):
391 def readline(self, *args, **kwargs):
391 if len(self._buffer) > 1:
392 if len(self._buffer) > 1:
392 # this should not happen because both read and readline end with a
393 # this should not happen because both read and readline end with a
393 # _frombuffer call that collapse it.
394 # _frombuffer call that collapse it.
394 self._buffer = [b''.join(self._buffer)]
395 self._buffer = [b''.join(self._buffer)]
395 self._lenbuf = len(self._buffer[0])
396 self._lenbuf = len(self._buffer[0])
396 lfi = -1
397 lfi = -1
397 if self._buffer:
398 if self._buffer:
398 lfi = self._buffer[-1].find(b'\n')
399 lfi = self._buffer[-1].find(b'\n')
399 while (not self._eof) and lfi < 0:
400 while (not self._eof) and lfi < 0:
400 self._fillbuffer()
401 self._fillbuffer()
401 if self._buffer:
402 if self._buffer:
402 lfi = self._buffer[-1].find(b'\n')
403 lfi = self._buffer[-1].find(b'\n')
403 size = lfi + 1
404 size = lfi + 1
404 if lfi < 0: # end of file
405 if lfi < 0: # end of file
405 size = self._lenbuf
406 size = self._lenbuf
406 elif len(self._buffer) > 1:
407 elif len(self._buffer) > 1:
407 # we need to take previous chunks into account
408 # we need to take previous chunks into account
408 size += self._lenbuf - len(self._buffer[-1])
409 size += self._lenbuf - len(self._buffer[-1])
409 return self._frombuffer(size)
410 return self._frombuffer(size)
410
411
411 def _frombuffer(self, size):
412 def _frombuffer(self, size):
412 """return at most 'size' data from the buffer
413 """return at most 'size' data from the buffer
413
414
414 The data are removed from the buffer."""
415 The data are removed from the buffer."""
415 if size == 0 or not self._buffer:
416 if size == 0 or not self._buffer:
416 return b''
417 return b''
417 buf = self._buffer[0]
418 buf = self._buffer[0]
418 if len(self._buffer) > 1:
419 if len(self._buffer) > 1:
419 buf = b''.join(self._buffer)
420 buf = b''.join(self._buffer)
420
421
421 data = buf[:size]
422 data = buf[:size]
422 buf = buf[len(data) :]
423 buf = buf[len(data) :]
423 if buf:
424 if buf:
424 self._buffer = [buf]
425 self._buffer = [buf]
425 self._lenbuf = len(buf)
426 self._lenbuf = len(buf)
426 else:
427 else:
427 self._buffer = []
428 self._buffer = []
428 self._lenbuf = 0
429 self._lenbuf = 0
429 return data
430 return data
430
431
431 def _fillbuffer(self, size=_chunksize):
432 def _fillbuffer(self, size=_chunksize):
432 """read data to the buffer"""
433 """read data to the buffer"""
433 data = os.read(self._input.fileno(), size)
434 data = os.read(self._input.fileno(), size)
434 if not data:
435 if not data:
435 self._eof = True
436 self._eof = True
436 else:
437 else:
437 self._lenbuf += len(data)
438 self._lenbuf += len(data)
438 self._buffer.append(data)
439 self._buffer.append(data)
439
440
440 return data
441 return data
441
442
442
443
443 def mmapread(fp, size=None):
444 def mmapread(fp, size=None):
444 if size == 0:
445 if size == 0:
445 # size of 0 to mmap.mmap() means "all data"
446 # size of 0 to mmap.mmap() means "all data"
446 # rather than "zero bytes", so special case that.
447 # rather than "zero bytes", so special case that.
447 return b''
448 return b''
448 elif size is None:
449 elif size is None:
449 size = 0
450 size = 0
450 fd = getattr(fp, 'fileno', lambda: fp)()
451 fd = getattr(fp, 'fileno', lambda: fp)()
451 try:
452 try:
452 return mmap.mmap(fd, size, access=mmap.ACCESS_READ)
453 return mmap.mmap(fd, size, access=mmap.ACCESS_READ)
453 except ValueError:
454 except ValueError:
454 # Empty files cannot be mmapped, but mmapread should still work. Check
455 # Empty files cannot be mmapped, but mmapread should still work. Check
455 # if the file is empty, and if so, return an empty buffer.
456 # if the file is empty, and if so, return an empty buffer.
456 if os.fstat(fd).st_size == 0:
457 if os.fstat(fd).st_size == 0:
457 return b''
458 return b''
458 raise
459 raise
459
460
460
461
461 class fileobjectproxy(object):
462 class fileobjectproxy(object):
462 """A proxy around file objects that tells a watcher when events occur.
463 """A proxy around file objects that tells a watcher when events occur.
463
464
464 This type is intended to only be used for testing purposes. Think hard
465 This type is intended to only be used for testing purposes. Think hard
465 before using it in important code.
466 before using it in important code.
466 """
467 """
467
468
468 __slots__ = (
469 __slots__ = (
469 '_orig',
470 '_orig',
470 '_observer',
471 '_observer',
471 )
472 )
472
473
473 def __init__(self, fh, observer):
474 def __init__(self, fh, observer):
474 object.__setattr__(self, '_orig', fh)
475 object.__setattr__(self, '_orig', fh)
475 object.__setattr__(self, '_observer', observer)
476 object.__setattr__(self, '_observer', observer)
476
477
477 def __getattribute__(self, name):
478 def __getattribute__(self, name):
478 ours = {
479 ours = {
479 '_observer',
480 '_observer',
480 # IOBase
481 # IOBase
481 'close',
482 'close',
482 # closed if a property
483 # closed if a property
483 'fileno',
484 'fileno',
484 'flush',
485 'flush',
485 'isatty',
486 'isatty',
486 'readable',
487 'readable',
487 'readline',
488 'readline',
488 'readlines',
489 'readlines',
489 'seek',
490 'seek',
490 'seekable',
491 'seekable',
491 'tell',
492 'tell',
492 'truncate',
493 'truncate',
493 'writable',
494 'writable',
494 'writelines',
495 'writelines',
495 # RawIOBase
496 # RawIOBase
496 'read',
497 'read',
497 'readall',
498 'readall',
498 'readinto',
499 'readinto',
499 'write',
500 'write',
500 # BufferedIOBase
501 # BufferedIOBase
501 # raw is a property
502 # raw is a property
502 'detach',
503 'detach',
503 # read defined above
504 # read defined above
504 'read1',
505 'read1',
505 # readinto defined above
506 # readinto defined above
506 # write defined above
507 # write defined above
507 }
508 }
508
509
509 # We only observe some methods.
510 # We only observe some methods.
510 if name in ours:
511 if name in ours:
511 return object.__getattribute__(self, name)
512 return object.__getattribute__(self, name)
512
513
513 return getattr(object.__getattribute__(self, '_orig'), name)
514 return getattr(object.__getattribute__(self, '_orig'), name)
514
515
515 def __nonzero__(self):
516 def __nonzero__(self):
516 return bool(object.__getattribute__(self, '_orig'))
517 return bool(object.__getattribute__(self, '_orig'))
517
518
518 __bool__ = __nonzero__
519 __bool__ = __nonzero__
519
520
520 def __delattr__(self, name):
521 def __delattr__(self, name):
521 return delattr(object.__getattribute__(self, '_orig'), name)
522 return delattr(object.__getattribute__(self, '_orig'), name)
522
523
523 def __setattr__(self, name, value):
524 def __setattr__(self, name, value):
524 return setattr(object.__getattribute__(self, '_orig'), name, value)
525 return setattr(object.__getattribute__(self, '_orig'), name, value)
525
526
526 def __iter__(self):
527 def __iter__(self):
527 return object.__getattribute__(self, '_orig').__iter__()
528 return object.__getattribute__(self, '_orig').__iter__()
528
529
529 def _observedcall(self, name, *args, **kwargs):
530 def _observedcall(self, name, *args, **kwargs):
530 # Call the original object.
531 # Call the original object.
531 orig = object.__getattribute__(self, '_orig')
532 orig = object.__getattribute__(self, '_orig')
532 res = getattr(orig, name)(*args, **kwargs)
533 res = getattr(orig, name)(*args, **kwargs)
533
534
534 # Call a method on the observer of the same name with arguments
535 # Call a method on the observer of the same name with arguments
535 # so it can react, log, etc.
536 # so it can react, log, etc.
536 observer = object.__getattribute__(self, '_observer')
537 observer = object.__getattribute__(self, '_observer')
537 fn = getattr(observer, name, None)
538 fn = getattr(observer, name, None)
538 if fn:
539 if fn:
539 fn(res, *args, **kwargs)
540 fn(res, *args, **kwargs)
540
541
541 return res
542 return res
542
543
543 def close(self, *args, **kwargs):
544 def close(self, *args, **kwargs):
544 return object.__getattribute__(self, '_observedcall')(
545 return object.__getattribute__(self, '_observedcall')(
545 'close', *args, **kwargs
546 'close', *args, **kwargs
546 )
547 )
547
548
548 def fileno(self, *args, **kwargs):
549 def fileno(self, *args, **kwargs):
549 return object.__getattribute__(self, '_observedcall')(
550 return object.__getattribute__(self, '_observedcall')(
550 'fileno', *args, **kwargs
551 'fileno', *args, **kwargs
551 )
552 )
552
553
553 def flush(self, *args, **kwargs):
554 def flush(self, *args, **kwargs):
554 return object.__getattribute__(self, '_observedcall')(
555 return object.__getattribute__(self, '_observedcall')(
555 'flush', *args, **kwargs
556 'flush', *args, **kwargs
556 )
557 )
557
558
558 def isatty(self, *args, **kwargs):
559 def isatty(self, *args, **kwargs):
559 return object.__getattribute__(self, '_observedcall')(
560 return object.__getattribute__(self, '_observedcall')(
560 'isatty', *args, **kwargs
561 'isatty', *args, **kwargs
561 )
562 )
562
563
563 def readable(self, *args, **kwargs):
564 def readable(self, *args, **kwargs):
564 return object.__getattribute__(self, '_observedcall')(
565 return object.__getattribute__(self, '_observedcall')(
565 'readable', *args, **kwargs
566 'readable', *args, **kwargs
566 )
567 )
567
568
568 def readline(self, *args, **kwargs):
569 def readline(self, *args, **kwargs):
569 return object.__getattribute__(self, '_observedcall')(
570 return object.__getattribute__(self, '_observedcall')(
570 'readline', *args, **kwargs
571 'readline', *args, **kwargs
571 )
572 )
572
573
573 def readlines(self, *args, **kwargs):
574 def readlines(self, *args, **kwargs):
574 return object.__getattribute__(self, '_observedcall')(
575 return object.__getattribute__(self, '_observedcall')(
575 'readlines', *args, **kwargs
576 'readlines', *args, **kwargs
576 )
577 )
577
578
578 def seek(self, *args, **kwargs):
579 def seek(self, *args, **kwargs):
579 return object.__getattribute__(self, '_observedcall')(
580 return object.__getattribute__(self, '_observedcall')(
580 'seek', *args, **kwargs
581 'seek', *args, **kwargs
581 )
582 )
582
583
583 def seekable(self, *args, **kwargs):
584 def seekable(self, *args, **kwargs):
584 return object.__getattribute__(self, '_observedcall')(
585 return object.__getattribute__(self, '_observedcall')(
585 'seekable', *args, **kwargs
586 'seekable', *args, **kwargs
586 )
587 )
587
588
588 def tell(self, *args, **kwargs):
589 def tell(self, *args, **kwargs):
589 return object.__getattribute__(self, '_observedcall')(
590 return object.__getattribute__(self, '_observedcall')(
590 'tell', *args, **kwargs
591 'tell', *args, **kwargs
591 )
592 )
592
593
593 def truncate(self, *args, **kwargs):
594 def truncate(self, *args, **kwargs):
594 return object.__getattribute__(self, '_observedcall')(
595 return object.__getattribute__(self, '_observedcall')(
595 'truncate', *args, **kwargs
596 'truncate', *args, **kwargs
596 )
597 )
597
598
598 def writable(self, *args, **kwargs):
599 def writable(self, *args, **kwargs):
599 return object.__getattribute__(self, '_observedcall')(
600 return object.__getattribute__(self, '_observedcall')(
600 'writable', *args, **kwargs
601 'writable', *args, **kwargs
601 )
602 )
602
603
603 def writelines(self, *args, **kwargs):
604 def writelines(self, *args, **kwargs):
604 return object.__getattribute__(self, '_observedcall')(
605 return object.__getattribute__(self, '_observedcall')(
605 'writelines', *args, **kwargs
606 'writelines', *args, **kwargs
606 )
607 )
607
608
608 def read(self, *args, **kwargs):
609 def read(self, *args, **kwargs):
609 return object.__getattribute__(self, '_observedcall')(
610 return object.__getattribute__(self, '_observedcall')(
610 'read', *args, **kwargs
611 'read', *args, **kwargs
611 )
612 )
612
613
613 def readall(self, *args, **kwargs):
614 def readall(self, *args, **kwargs):
614 return object.__getattribute__(self, '_observedcall')(
615 return object.__getattribute__(self, '_observedcall')(
615 'readall', *args, **kwargs
616 'readall', *args, **kwargs
616 )
617 )
617
618
618 def readinto(self, *args, **kwargs):
619 def readinto(self, *args, **kwargs):
619 return object.__getattribute__(self, '_observedcall')(
620 return object.__getattribute__(self, '_observedcall')(
620 'readinto', *args, **kwargs
621 'readinto', *args, **kwargs
621 )
622 )
622
623
623 def write(self, *args, **kwargs):
624 def write(self, *args, **kwargs):
624 return object.__getattribute__(self, '_observedcall')(
625 return object.__getattribute__(self, '_observedcall')(
625 'write', *args, **kwargs
626 'write', *args, **kwargs
626 )
627 )
627
628
628 def detach(self, *args, **kwargs):
629 def detach(self, *args, **kwargs):
629 return object.__getattribute__(self, '_observedcall')(
630 return object.__getattribute__(self, '_observedcall')(
630 'detach', *args, **kwargs
631 'detach', *args, **kwargs
631 )
632 )
632
633
633 def read1(self, *args, **kwargs):
634 def read1(self, *args, **kwargs):
634 return object.__getattribute__(self, '_observedcall')(
635 return object.__getattribute__(self, '_observedcall')(
635 'read1', *args, **kwargs
636 'read1', *args, **kwargs
636 )
637 )
637
638
638
639
639 class observedbufferedinputpipe(bufferedinputpipe):
640 class observedbufferedinputpipe(bufferedinputpipe):
640 """A variation of bufferedinputpipe that is aware of fileobjectproxy.
641 """A variation of bufferedinputpipe that is aware of fileobjectproxy.
641
642
642 ``bufferedinputpipe`` makes low-level calls to ``os.read()`` that
643 ``bufferedinputpipe`` makes low-level calls to ``os.read()`` that
643 bypass ``fileobjectproxy``. Because of this, we need to make
644 bypass ``fileobjectproxy``. Because of this, we need to make
644 ``bufferedinputpipe`` aware of these operations.
645 ``bufferedinputpipe`` aware of these operations.
645
646
646 This variation of ``bufferedinputpipe`` can notify observers about
647 This variation of ``bufferedinputpipe`` can notify observers about
647 ``os.read()`` events. It also re-publishes other events, such as
648 ``os.read()`` events. It also re-publishes other events, such as
648 ``read()`` and ``readline()``.
649 ``read()`` and ``readline()``.
649 """
650 """
650
651
651 def _fillbuffer(self):
652 def _fillbuffer(self):
652 res = super(observedbufferedinputpipe, self)._fillbuffer()
653 res = super(observedbufferedinputpipe, self)._fillbuffer()
653
654
654 fn = getattr(self._input._observer, 'osread', None)
655 fn = getattr(self._input._observer, 'osread', None)
655 if fn:
656 if fn:
656 fn(res, _chunksize)
657 fn(res, _chunksize)
657
658
658 return res
659 return res
659
660
660 # We use different observer methods because the operation isn't
661 # We use different observer methods because the operation isn't
661 # performed on the actual file object but on us.
662 # performed on the actual file object but on us.
662 def read(self, size):
663 def read(self, size):
663 res = super(observedbufferedinputpipe, self).read(size)
664 res = super(observedbufferedinputpipe, self).read(size)
664
665
665 fn = getattr(self._input._observer, 'bufferedread', None)
666 fn = getattr(self._input._observer, 'bufferedread', None)
666 if fn:
667 if fn:
667 fn(res, size)
668 fn(res, size)
668
669
669 return res
670 return res
670
671
671 def readline(self, *args, **kwargs):
672 def readline(self, *args, **kwargs):
672 res = super(observedbufferedinputpipe, self).readline(*args, **kwargs)
673 res = super(observedbufferedinputpipe, self).readline(*args, **kwargs)
673
674
674 fn = getattr(self._input._observer, 'bufferedreadline', None)
675 fn = getattr(self._input._observer, 'bufferedreadline', None)
675 if fn:
676 if fn:
676 fn(res)
677 fn(res)
677
678
678 return res
679 return res
679
680
680
681
681 PROXIED_SOCKET_METHODS = {
682 PROXIED_SOCKET_METHODS = {
682 'makefile',
683 'makefile',
683 'recv',
684 'recv',
684 'recvfrom',
685 'recvfrom',
685 'recvfrom_into',
686 'recvfrom_into',
686 'recv_into',
687 'recv_into',
687 'send',
688 'send',
688 'sendall',
689 'sendall',
689 'sendto',
690 'sendto',
690 'setblocking',
691 'setblocking',
691 'settimeout',
692 'settimeout',
692 'gettimeout',
693 'gettimeout',
693 'setsockopt',
694 'setsockopt',
694 }
695 }
695
696
696
697
697 class socketproxy(object):
698 class socketproxy(object):
698 """A proxy around a socket that tells a watcher when events occur.
699 """A proxy around a socket that tells a watcher when events occur.
699
700
700 This is like ``fileobjectproxy`` except for sockets.
701 This is like ``fileobjectproxy`` except for sockets.
701
702
702 This type is intended to only be used for testing purposes. Think hard
703 This type is intended to only be used for testing purposes. Think hard
703 before using it in important code.
704 before using it in important code.
704 """
705 """
705
706
706 __slots__ = (
707 __slots__ = (
707 '_orig',
708 '_orig',
708 '_observer',
709 '_observer',
709 )
710 )
710
711
711 def __init__(self, sock, observer):
712 def __init__(self, sock, observer):
712 object.__setattr__(self, '_orig', sock)
713 object.__setattr__(self, '_orig', sock)
713 object.__setattr__(self, '_observer', observer)
714 object.__setattr__(self, '_observer', observer)
714
715
715 def __getattribute__(self, name):
716 def __getattribute__(self, name):
716 if name in PROXIED_SOCKET_METHODS:
717 if name in PROXIED_SOCKET_METHODS:
717 return object.__getattribute__(self, name)
718 return object.__getattribute__(self, name)
718
719
719 return getattr(object.__getattribute__(self, '_orig'), name)
720 return getattr(object.__getattribute__(self, '_orig'), name)
720
721
721 def __delattr__(self, name):
722 def __delattr__(self, name):
722 return delattr(object.__getattribute__(self, '_orig'), name)
723 return delattr(object.__getattribute__(self, '_orig'), name)
723
724
724 def __setattr__(self, name, value):
725 def __setattr__(self, name, value):
725 return setattr(object.__getattribute__(self, '_orig'), name, value)
726 return setattr(object.__getattribute__(self, '_orig'), name, value)
726
727
727 def __nonzero__(self):
728 def __nonzero__(self):
728 return bool(object.__getattribute__(self, '_orig'))
729 return bool(object.__getattribute__(self, '_orig'))
729
730
730 __bool__ = __nonzero__
731 __bool__ = __nonzero__
731
732
732 def _observedcall(self, name, *args, **kwargs):
733 def _observedcall(self, name, *args, **kwargs):
733 # Call the original object.
734 # Call the original object.
734 orig = object.__getattribute__(self, '_orig')
735 orig = object.__getattribute__(self, '_orig')
735 res = getattr(orig, name)(*args, **kwargs)
736 res = getattr(orig, name)(*args, **kwargs)
736
737
737 # Call a method on the observer of the same name with arguments
738 # Call a method on the observer of the same name with arguments
738 # so it can react, log, etc.
739 # so it can react, log, etc.
739 observer = object.__getattribute__(self, '_observer')
740 observer = object.__getattribute__(self, '_observer')
740 fn = getattr(observer, name, None)
741 fn = getattr(observer, name, None)
741 if fn:
742 if fn:
742 fn(res, *args, **kwargs)
743 fn(res, *args, **kwargs)
743
744
744 return res
745 return res
745
746
746 def makefile(self, *args, **kwargs):
747 def makefile(self, *args, **kwargs):
747 res = object.__getattribute__(self, '_observedcall')(
748 res = object.__getattribute__(self, '_observedcall')(
748 'makefile', *args, **kwargs
749 'makefile', *args, **kwargs
749 )
750 )
750
751
751 # The file object may be used for I/O. So we turn it into a
752 # The file object may be used for I/O. So we turn it into a
752 # proxy using our observer.
753 # proxy using our observer.
753 observer = object.__getattribute__(self, '_observer')
754 observer = object.__getattribute__(self, '_observer')
754 return makeloggingfileobject(
755 return makeloggingfileobject(
755 observer.fh,
756 observer.fh,
756 res,
757 res,
757 observer.name,
758 observer.name,
758 reads=observer.reads,
759 reads=observer.reads,
759 writes=observer.writes,
760 writes=observer.writes,
760 logdata=observer.logdata,
761 logdata=observer.logdata,
761 logdataapis=observer.logdataapis,
762 logdataapis=observer.logdataapis,
762 )
763 )
763
764
764 def recv(self, *args, **kwargs):
765 def recv(self, *args, **kwargs):
765 return object.__getattribute__(self, '_observedcall')(
766 return object.__getattribute__(self, '_observedcall')(
766 'recv', *args, **kwargs
767 'recv', *args, **kwargs
767 )
768 )
768
769
769 def recvfrom(self, *args, **kwargs):
770 def recvfrom(self, *args, **kwargs):
770 return object.__getattribute__(self, '_observedcall')(
771 return object.__getattribute__(self, '_observedcall')(
771 'recvfrom', *args, **kwargs
772 'recvfrom', *args, **kwargs
772 )
773 )
773
774
774 def recvfrom_into(self, *args, **kwargs):
775 def recvfrom_into(self, *args, **kwargs):
775 return object.__getattribute__(self, '_observedcall')(
776 return object.__getattribute__(self, '_observedcall')(
776 'recvfrom_into', *args, **kwargs
777 'recvfrom_into', *args, **kwargs
777 )
778 )
778
779
779 def recv_into(self, *args, **kwargs):
780 def recv_into(self, *args, **kwargs):
780 return object.__getattribute__(self, '_observedcall')(
781 return object.__getattribute__(self, '_observedcall')(
781 'recv_info', *args, **kwargs
782 'recv_info', *args, **kwargs
782 )
783 )
783
784
784 def send(self, *args, **kwargs):
785 def send(self, *args, **kwargs):
785 return object.__getattribute__(self, '_observedcall')(
786 return object.__getattribute__(self, '_observedcall')(
786 'send', *args, **kwargs
787 'send', *args, **kwargs
787 )
788 )
788
789
789 def sendall(self, *args, **kwargs):
790 def sendall(self, *args, **kwargs):
790 return object.__getattribute__(self, '_observedcall')(
791 return object.__getattribute__(self, '_observedcall')(
791 'sendall', *args, **kwargs
792 'sendall', *args, **kwargs
792 )
793 )
793
794
794 def sendto(self, *args, **kwargs):
795 def sendto(self, *args, **kwargs):
795 return object.__getattribute__(self, '_observedcall')(
796 return object.__getattribute__(self, '_observedcall')(
796 'sendto', *args, **kwargs
797 'sendto', *args, **kwargs
797 )
798 )
798
799
799 def setblocking(self, *args, **kwargs):
800 def setblocking(self, *args, **kwargs):
800 return object.__getattribute__(self, '_observedcall')(
801 return object.__getattribute__(self, '_observedcall')(
801 'setblocking', *args, **kwargs
802 'setblocking', *args, **kwargs
802 )
803 )
803
804
804 def settimeout(self, *args, **kwargs):
805 def settimeout(self, *args, **kwargs):
805 return object.__getattribute__(self, '_observedcall')(
806 return object.__getattribute__(self, '_observedcall')(
806 'settimeout', *args, **kwargs
807 'settimeout', *args, **kwargs
807 )
808 )
808
809
809 def gettimeout(self, *args, **kwargs):
810 def gettimeout(self, *args, **kwargs):
810 return object.__getattribute__(self, '_observedcall')(
811 return object.__getattribute__(self, '_observedcall')(
811 'gettimeout', *args, **kwargs
812 'gettimeout', *args, **kwargs
812 )
813 )
813
814
814 def setsockopt(self, *args, **kwargs):
815 def setsockopt(self, *args, **kwargs):
815 return object.__getattribute__(self, '_observedcall')(
816 return object.__getattribute__(self, '_observedcall')(
816 'setsockopt', *args, **kwargs
817 'setsockopt', *args, **kwargs
817 )
818 )
818
819
819
820
820 class baseproxyobserver(object):
821 class baseproxyobserver(object):
821 def __init__(self, fh, name, logdata, logdataapis):
822 def __init__(self, fh, name, logdata, logdataapis):
822 self.fh = fh
823 self.fh = fh
823 self.name = name
824 self.name = name
824 self.logdata = logdata
825 self.logdata = logdata
825 self.logdataapis = logdataapis
826 self.logdataapis = logdataapis
826
827
827 def _writedata(self, data):
828 def _writedata(self, data):
828 if not self.logdata:
829 if not self.logdata:
829 if self.logdataapis:
830 if self.logdataapis:
830 self.fh.write(b'\n')
831 self.fh.write(b'\n')
831 self.fh.flush()
832 self.fh.flush()
832 return
833 return
833
834
834 # Simple case writes all data on a single line.
835 # Simple case writes all data on a single line.
835 if b'\n' not in data:
836 if b'\n' not in data:
836 if self.logdataapis:
837 if self.logdataapis:
837 self.fh.write(b': %s\n' % stringutil.escapestr(data))
838 self.fh.write(b': %s\n' % stringutil.escapestr(data))
838 else:
839 else:
839 self.fh.write(
840 self.fh.write(
840 b'%s> %s\n' % (self.name, stringutil.escapestr(data))
841 b'%s> %s\n' % (self.name, stringutil.escapestr(data))
841 )
842 )
842 self.fh.flush()
843 self.fh.flush()
843 return
844 return
844
845
845 # Data with newlines is written to multiple lines.
846 # Data with newlines is written to multiple lines.
846 if self.logdataapis:
847 if self.logdataapis:
847 self.fh.write(b':\n')
848 self.fh.write(b':\n')
848
849
849 lines = data.splitlines(True)
850 lines = data.splitlines(True)
850 for line in lines:
851 for line in lines:
851 self.fh.write(
852 self.fh.write(
852 b'%s> %s\n' % (self.name, stringutil.escapestr(line))
853 b'%s> %s\n' % (self.name, stringutil.escapestr(line))
853 )
854 )
854 self.fh.flush()
855 self.fh.flush()
855
856
856
857
857 class fileobjectobserver(baseproxyobserver):
858 class fileobjectobserver(baseproxyobserver):
858 """Logs file object activity."""
859 """Logs file object activity."""
859
860
860 def __init__(
861 def __init__(
861 self, fh, name, reads=True, writes=True, logdata=False, logdataapis=True
862 self, fh, name, reads=True, writes=True, logdata=False, logdataapis=True
862 ):
863 ):
863 super(fileobjectobserver, self).__init__(fh, name, logdata, logdataapis)
864 super(fileobjectobserver, self).__init__(fh, name, logdata, logdataapis)
864 self.reads = reads
865 self.reads = reads
865 self.writes = writes
866 self.writes = writes
866
867
867 def read(self, res, size=-1):
868 def read(self, res, size=-1):
868 if not self.reads:
869 if not self.reads:
869 return
870 return
870 # Python 3 can return None from reads at EOF instead of empty strings.
871 # Python 3 can return None from reads at EOF instead of empty strings.
871 if res is None:
872 if res is None:
872 res = b''
873 res = b''
873
874
874 if size == -1 and res == b'':
875 if size == -1 and res == b'':
875 # Suppress pointless read(-1) calls that return
876 # Suppress pointless read(-1) calls that return
876 # nothing. These happen _a lot_ on Python 3, and there
877 # nothing. These happen _a lot_ on Python 3, and there
877 # doesn't seem to be a better workaround to have matching
878 # doesn't seem to be a better workaround to have matching
878 # Python 2 and 3 behavior. :(
879 # Python 2 and 3 behavior. :(
879 return
880 return
880
881
881 if self.logdataapis:
882 if self.logdataapis:
882 self.fh.write(b'%s> read(%d) -> %d' % (self.name, size, len(res)))
883 self.fh.write(b'%s> read(%d) -> %d' % (self.name, size, len(res)))
883
884
884 self._writedata(res)
885 self._writedata(res)
885
886
886 def readline(self, res, limit=-1):
887 def readline(self, res, limit=-1):
887 if not self.reads:
888 if not self.reads:
888 return
889 return
889
890
890 if self.logdataapis:
891 if self.logdataapis:
891 self.fh.write(b'%s> readline() -> %d' % (self.name, len(res)))
892 self.fh.write(b'%s> readline() -> %d' % (self.name, len(res)))
892
893
893 self._writedata(res)
894 self._writedata(res)
894
895
895 def readinto(self, res, dest):
896 def readinto(self, res, dest):
896 if not self.reads:
897 if not self.reads:
897 return
898 return
898
899
899 if self.logdataapis:
900 if self.logdataapis:
900 self.fh.write(
901 self.fh.write(
901 b'%s> readinto(%d) -> %r' % (self.name, len(dest), res)
902 b'%s> readinto(%d) -> %r' % (self.name, len(dest), res)
902 )
903 )
903
904
904 data = dest[0:res] if res is not None else b''
905 data = dest[0:res] if res is not None else b''
905
906
906 # _writedata() uses "in" operator and is confused by memoryview because
907 # _writedata() uses "in" operator and is confused by memoryview because
907 # characters are ints on Python 3.
908 # characters are ints on Python 3.
908 if isinstance(data, memoryview):
909 if isinstance(data, memoryview):
909 data = data.tobytes()
910 data = data.tobytes()
910
911
911 self._writedata(data)
912 self._writedata(data)
912
913
913 def write(self, res, data):
914 def write(self, res, data):
914 if not self.writes:
915 if not self.writes:
915 return
916 return
916
917
917 # Python 2 returns None from some write() calls. Python 3 (reasonably)
918 # Python 2 returns None from some write() calls. Python 3 (reasonably)
918 # returns the integer bytes written.
919 # returns the integer bytes written.
919 if res is None and data:
920 if res is None and data:
920 res = len(data)
921 res = len(data)
921
922
922 if self.logdataapis:
923 if self.logdataapis:
923 self.fh.write(b'%s> write(%d) -> %r' % (self.name, len(data), res))
924 self.fh.write(b'%s> write(%d) -> %r' % (self.name, len(data), res))
924
925
925 self._writedata(data)
926 self._writedata(data)
926
927
927 def flush(self, res):
928 def flush(self, res):
928 if not self.writes:
929 if not self.writes:
929 return
930 return
930
931
931 self.fh.write(b'%s> flush() -> %r\n' % (self.name, res))
932 self.fh.write(b'%s> flush() -> %r\n' % (self.name, res))
932
933
933 # For observedbufferedinputpipe.
934 # For observedbufferedinputpipe.
934 def bufferedread(self, res, size):
935 def bufferedread(self, res, size):
935 if not self.reads:
936 if not self.reads:
936 return
937 return
937
938
938 if self.logdataapis:
939 if self.logdataapis:
939 self.fh.write(
940 self.fh.write(
940 b'%s> bufferedread(%d) -> %d' % (self.name, size, len(res))
941 b'%s> bufferedread(%d) -> %d' % (self.name, size, len(res))
941 )
942 )
942
943
943 self._writedata(res)
944 self._writedata(res)
944
945
945 def bufferedreadline(self, res):
946 def bufferedreadline(self, res):
946 if not self.reads:
947 if not self.reads:
947 return
948 return
948
949
949 if self.logdataapis:
950 if self.logdataapis:
950 self.fh.write(
951 self.fh.write(
951 b'%s> bufferedreadline() -> %d' % (self.name, len(res))
952 b'%s> bufferedreadline() -> %d' % (self.name, len(res))
952 )
953 )
953
954
954 self._writedata(res)
955 self._writedata(res)
955
956
956
957
957 def makeloggingfileobject(
958 def makeloggingfileobject(
958 logh, fh, name, reads=True, writes=True, logdata=False, logdataapis=True
959 logh, fh, name, reads=True, writes=True, logdata=False, logdataapis=True
959 ):
960 ):
960 """Turn a file object into a logging file object."""
961 """Turn a file object into a logging file object."""
961
962
962 observer = fileobjectobserver(
963 observer = fileobjectobserver(
963 logh,
964 logh,
964 name,
965 name,
965 reads=reads,
966 reads=reads,
966 writes=writes,
967 writes=writes,
967 logdata=logdata,
968 logdata=logdata,
968 logdataapis=logdataapis,
969 logdataapis=logdataapis,
969 )
970 )
970 return fileobjectproxy(fh, observer)
971 return fileobjectproxy(fh, observer)
971
972
972
973
973 class socketobserver(baseproxyobserver):
974 class socketobserver(baseproxyobserver):
974 """Logs socket activity."""
975 """Logs socket activity."""
975
976
976 def __init__(
977 def __init__(
977 self,
978 self,
978 fh,
979 fh,
979 name,
980 name,
980 reads=True,
981 reads=True,
981 writes=True,
982 writes=True,
982 states=True,
983 states=True,
983 logdata=False,
984 logdata=False,
984 logdataapis=True,
985 logdataapis=True,
985 ):
986 ):
986 super(socketobserver, self).__init__(fh, name, logdata, logdataapis)
987 super(socketobserver, self).__init__(fh, name, logdata, logdataapis)
987 self.reads = reads
988 self.reads = reads
988 self.writes = writes
989 self.writes = writes
989 self.states = states
990 self.states = states
990
991
991 def makefile(self, res, mode=None, bufsize=None):
992 def makefile(self, res, mode=None, bufsize=None):
992 if not self.states:
993 if not self.states:
993 return
994 return
994
995
995 self.fh.write(b'%s> makefile(%r, %r)\n' % (self.name, mode, bufsize))
996 self.fh.write(b'%s> makefile(%r, %r)\n' % (self.name, mode, bufsize))
996
997
997 def recv(self, res, size, flags=0):
998 def recv(self, res, size, flags=0):
998 if not self.reads:
999 if not self.reads:
999 return
1000 return
1000
1001
1001 if self.logdataapis:
1002 if self.logdataapis:
1002 self.fh.write(
1003 self.fh.write(
1003 b'%s> recv(%d, %d) -> %d' % (self.name, size, flags, len(res))
1004 b'%s> recv(%d, %d) -> %d' % (self.name, size, flags, len(res))
1004 )
1005 )
1005 self._writedata(res)
1006 self._writedata(res)
1006
1007
1007 def recvfrom(self, res, size, flags=0):
1008 def recvfrom(self, res, size, flags=0):
1008 if not self.reads:
1009 if not self.reads:
1009 return
1010 return
1010
1011
1011 if self.logdataapis:
1012 if self.logdataapis:
1012 self.fh.write(
1013 self.fh.write(
1013 b'%s> recvfrom(%d, %d) -> %d'
1014 b'%s> recvfrom(%d, %d) -> %d'
1014 % (self.name, size, flags, len(res[0]))
1015 % (self.name, size, flags, len(res[0]))
1015 )
1016 )
1016
1017
1017 self._writedata(res[0])
1018 self._writedata(res[0])
1018
1019
1019 def recvfrom_into(self, res, buf, size, flags=0):
1020 def recvfrom_into(self, res, buf, size, flags=0):
1020 if not self.reads:
1021 if not self.reads:
1021 return
1022 return
1022
1023
1023 if self.logdataapis:
1024 if self.logdataapis:
1024 self.fh.write(
1025 self.fh.write(
1025 b'%s> recvfrom_into(%d, %d) -> %d'
1026 b'%s> recvfrom_into(%d, %d) -> %d'
1026 % (self.name, size, flags, res[0])
1027 % (self.name, size, flags, res[0])
1027 )
1028 )
1028
1029
1029 self._writedata(buf[0 : res[0]])
1030 self._writedata(buf[0 : res[0]])
1030
1031
1031 def recv_into(self, res, buf, size=0, flags=0):
1032 def recv_into(self, res, buf, size=0, flags=0):
1032 if not self.reads:
1033 if not self.reads:
1033 return
1034 return
1034
1035
1035 if self.logdataapis:
1036 if self.logdataapis:
1036 self.fh.write(
1037 self.fh.write(
1037 b'%s> recv_into(%d, %d) -> %d' % (self.name, size, flags, res)
1038 b'%s> recv_into(%d, %d) -> %d' % (self.name, size, flags, res)
1038 )
1039 )
1039
1040
1040 self._writedata(buf[0:res])
1041 self._writedata(buf[0:res])
1041
1042
1042 def send(self, res, data, flags=0):
1043 def send(self, res, data, flags=0):
1043 if not self.writes:
1044 if not self.writes:
1044 return
1045 return
1045
1046
1046 self.fh.write(
1047 self.fh.write(
1047 b'%s> send(%d, %d) -> %d' % (self.name, len(data), flags, len(res))
1048 b'%s> send(%d, %d) -> %d' % (self.name, len(data), flags, len(res))
1048 )
1049 )
1049 self._writedata(data)
1050 self._writedata(data)
1050
1051
1051 def sendall(self, res, data, flags=0):
1052 def sendall(self, res, data, flags=0):
1052 if not self.writes:
1053 if not self.writes:
1053 return
1054 return
1054
1055
1055 if self.logdataapis:
1056 if self.logdataapis:
1056 # Returns None on success. So don't bother reporting return value.
1057 # Returns None on success. So don't bother reporting return value.
1057 self.fh.write(
1058 self.fh.write(
1058 b'%s> sendall(%d, %d)' % (self.name, len(data), flags)
1059 b'%s> sendall(%d, %d)' % (self.name, len(data), flags)
1059 )
1060 )
1060
1061
1061 self._writedata(data)
1062 self._writedata(data)
1062
1063
1063 def sendto(self, res, data, flagsoraddress, address=None):
1064 def sendto(self, res, data, flagsoraddress, address=None):
1064 if not self.writes:
1065 if not self.writes:
1065 return
1066 return
1066
1067
1067 if address:
1068 if address:
1068 flags = flagsoraddress
1069 flags = flagsoraddress
1069 else:
1070 else:
1070 flags = 0
1071 flags = 0
1071
1072
1072 if self.logdataapis:
1073 if self.logdataapis:
1073 self.fh.write(
1074 self.fh.write(
1074 b'%s> sendto(%d, %d, %r) -> %d'
1075 b'%s> sendto(%d, %d, %r) -> %d'
1075 % (self.name, len(data), flags, address, res)
1076 % (self.name, len(data), flags, address, res)
1076 )
1077 )
1077
1078
1078 self._writedata(data)
1079 self._writedata(data)
1079
1080
1080 def setblocking(self, res, flag):
1081 def setblocking(self, res, flag):
1081 if not self.states:
1082 if not self.states:
1082 return
1083 return
1083
1084
1084 self.fh.write(b'%s> setblocking(%r)\n' % (self.name, flag))
1085 self.fh.write(b'%s> setblocking(%r)\n' % (self.name, flag))
1085
1086
1086 def settimeout(self, res, value):
1087 def settimeout(self, res, value):
1087 if not self.states:
1088 if not self.states:
1088 return
1089 return
1089
1090
1090 self.fh.write(b'%s> settimeout(%r)\n' % (self.name, value))
1091 self.fh.write(b'%s> settimeout(%r)\n' % (self.name, value))
1091
1092
1092 def gettimeout(self, res):
1093 def gettimeout(self, res):
1093 if not self.states:
1094 if not self.states:
1094 return
1095 return
1095
1096
1096 self.fh.write(b'%s> gettimeout() -> %f\n' % (self.name, res))
1097 self.fh.write(b'%s> gettimeout() -> %f\n' % (self.name, res))
1097
1098
1098 def setsockopt(self, res, level, optname, value):
1099 def setsockopt(self, res, level, optname, value):
1099 if not self.states:
1100 if not self.states:
1100 return
1101 return
1101
1102
1102 self.fh.write(
1103 self.fh.write(
1103 b'%s> setsockopt(%r, %r, %r) -> %r\n'
1104 b'%s> setsockopt(%r, %r, %r) -> %r\n'
1104 % (self.name, level, optname, value, res)
1105 % (self.name, level, optname, value, res)
1105 )
1106 )
1106
1107
1107
1108
1108 def makeloggingsocket(
1109 def makeloggingsocket(
1109 logh,
1110 logh,
1110 fh,
1111 fh,
1111 name,
1112 name,
1112 reads=True,
1113 reads=True,
1113 writes=True,
1114 writes=True,
1114 states=True,
1115 states=True,
1115 logdata=False,
1116 logdata=False,
1116 logdataapis=True,
1117 logdataapis=True,
1117 ):
1118 ):
1118 """Turn a socket into a logging socket."""
1119 """Turn a socket into a logging socket."""
1119
1120
1120 observer = socketobserver(
1121 observer = socketobserver(
1121 logh,
1122 logh,
1122 name,
1123 name,
1123 reads=reads,
1124 reads=reads,
1124 writes=writes,
1125 writes=writes,
1125 states=states,
1126 states=states,
1126 logdata=logdata,
1127 logdata=logdata,
1127 logdataapis=logdataapis,
1128 logdataapis=logdataapis,
1128 )
1129 )
1129 return socketproxy(fh, observer)
1130 return socketproxy(fh, observer)
1130
1131
1131
1132
1132 def version():
1133 def version():
1133 """Return version information if available."""
1134 """Return version information if available."""
1134 try:
1135 try:
1135 from . import __version__
1136 from . import __version__
1136
1137
1137 return __version__.version
1138 return __version__.version
1138 except ImportError:
1139 except ImportError:
1139 return b'unknown'
1140 return b'unknown'
1140
1141
1141
1142
1142 def versiontuple(v=None, n=4):
1143 def versiontuple(v=None, n=4):
1143 """Parses a Mercurial version string into an N-tuple.
1144 """Parses a Mercurial version string into an N-tuple.
1144
1145
1145 The version string to be parsed is specified with the ``v`` argument.
1146 The version string to be parsed is specified with the ``v`` argument.
1146 If it isn't defined, the current Mercurial version string will be parsed.
1147 If it isn't defined, the current Mercurial version string will be parsed.
1147
1148
1148 ``n`` can be 2, 3, or 4. Here is how some version strings map to
1149 ``n`` can be 2, 3, or 4. Here is how some version strings map to
1149 returned values:
1150 returned values:
1150
1151
1151 >>> v = b'3.6.1+190-df9b73d2d444'
1152 >>> v = b'3.6.1+190-df9b73d2d444'
1152 >>> versiontuple(v, 2)
1153 >>> versiontuple(v, 2)
1153 (3, 6)
1154 (3, 6)
1154 >>> versiontuple(v, 3)
1155 >>> versiontuple(v, 3)
1155 (3, 6, 1)
1156 (3, 6, 1)
1156 >>> versiontuple(v, 4)
1157 >>> versiontuple(v, 4)
1157 (3, 6, 1, '190-df9b73d2d444')
1158 (3, 6, 1, '190-df9b73d2d444')
1158
1159
1159 >>> versiontuple(b'3.6.1+190-df9b73d2d444+20151118')
1160 >>> versiontuple(b'3.6.1+190-df9b73d2d444+20151118')
1160 (3, 6, 1, '190-df9b73d2d444+20151118')
1161 (3, 6, 1, '190-df9b73d2d444+20151118')
1161
1162
1162 >>> v = b'3.6'
1163 >>> v = b'3.6'
1163 >>> versiontuple(v, 2)
1164 >>> versiontuple(v, 2)
1164 (3, 6)
1165 (3, 6)
1165 >>> versiontuple(v, 3)
1166 >>> versiontuple(v, 3)
1166 (3, 6, None)
1167 (3, 6, None)
1167 >>> versiontuple(v, 4)
1168 >>> versiontuple(v, 4)
1168 (3, 6, None, None)
1169 (3, 6, None, None)
1169
1170
1170 >>> v = b'3.9-rc'
1171 >>> v = b'3.9-rc'
1171 >>> versiontuple(v, 2)
1172 >>> versiontuple(v, 2)
1172 (3, 9)
1173 (3, 9)
1173 >>> versiontuple(v, 3)
1174 >>> versiontuple(v, 3)
1174 (3, 9, None)
1175 (3, 9, None)
1175 >>> versiontuple(v, 4)
1176 >>> versiontuple(v, 4)
1176 (3, 9, None, 'rc')
1177 (3, 9, None, 'rc')
1177
1178
1178 >>> v = b'3.9-rc+2-02a8fea4289b'
1179 >>> v = b'3.9-rc+2-02a8fea4289b'
1179 >>> versiontuple(v, 2)
1180 >>> versiontuple(v, 2)
1180 (3, 9)
1181 (3, 9)
1181 >>> versiontuple(v, 3)
1182 >>> versiontuple(v, 3)
1182 (3, 9, None)
1183 (3, 9, None)
1183 >>> versiontuple(v, 4)
1184 >>> versiontuple(v, 4)
1184 (3, 9, None, 'rc+2-02a8fea4289b')
1185 (3, 9, None, 'rc+2-02a8fea4289b')
1185
1186
1186 >>> versiontuple(b'4.6rc0')
1187 >>> versiontuple(b'4.6rc0')
1187 (4, 6, None, 'rc0')
1188 (4, 6, None, 'rc0')
1188 >>> versiontuple(b'4.6rc0+12-425d55e54f98')
1189 >>> versiontuple(b'4.6rc0+12-425d55e54f98')
1189 (4, 6, None, 'rc0+12-425d55e54f98')
1190 (4, 6, None, 'rc0+12-425d55e54f98')
1190 >>> versiontuple(b'.1.2.3')
1191 >>> versiontuple(b'.1.2.3')
1191 (None, None, None, '.1.2.3')
1192 (None, None, None, '.1.2.3')
1192 >>> versiontuple(b'12.34..5')
1193 >>> versiontuple(b'12.34..5')
1193 (12, 34, None, '..5')
1194 (12, 34, None, '..5')
1194 >>> versiontuple(b'1.2.3.4.5.6')
1195 >>> versiontuple(b'1.2.3.4.5.6')
1195 (1, 2, 3, '.4.5.6')
1196 (1, 2, 3, '.4.5.6')
1196 """
1197 """
1197 if not v:
1198 if not v:
1198 v = version()
1199 v = version()
1199 m = remod.match(br'(\d+(?:\.\d+){,2})[+-]?(.*)', v)
1200 m = remod.match(br'(\d+(?:\.\d+){,2})[+-]?(.*)', v)
1200 if not m:
1201 if not m:
1201 vparts, extra = b'', v
1202 vparts, extra = b'', v
1202 elif m.group(2):
1203 elif m.group(2):
1203 vparts, extra = m.groups()
1204 vparts, extra = m.groups()
1204 else:
1205 else:
1205 vparts, extra = m.group(1), None
1206 vparts, extra = m.group(1), None
1206
1207
1207 assert vparts is not None # help pytype
1208 assert vparts is not None # help pytype
1208
1209
1209 vints = []
1210 vints = []
1210 for i in vparts.split(b'.'):
1211 for i in vparts.split(b'.'):
1211 try:
1212 try:
1212 vints.append(int(i))
1213 vints.append(int(i))
1213 except ValueError:
1214 except ValueError:
1214 break
1215 break
1215 # (3, 6) -> (3, 6, None)
1216 # (3, 6) -> (3, 6, None)
1216 while len(vints) < 3:
1217 while len(vints) < 3:
1217 vints.append(None)
1218 vints.append(None)
1218
1219
1219 if n == 2:
1220 if n == 2:
1220 return (vints[0], vints[1])
1221 return (vints[0], vints[1])
1221 if n == 3:
1222 if n == 3:
1222 return (vints[0], vints[1], vints[2])
1223 return (vints[0], vints[1], vints[2])
1223 if n == 4:
1224 if n == 4:
1224 return (vints[0], vints[1], vints[2], extra)
1225 return (vints[0], vints[1], vints[2], extra)
1225
1226
1226 raise error.ProgrammingError(b"invalid version part request: %d" % n)
1227 raise error.ProgrammingError(b"invalid version part request: %d" % n)
1227
1228
1228
1229
1229 def cachefunc(func):
1230 def cachefunc(func):
1230 '''cache the result of function calls'''
1231 '''cache the result of function calls'''
1231 # XXX doesn't handle keywords args
1232 # XXX doesn't handle keywords args
1232 if func.__code__.co_argcount == 0:
1233 if func.__code__.co_argcount == 0:
1233 listcache = []
1234 listcache = []
1234
1235
1235 def f():
1236 def f():
1236 if len(listcache) == 0:
1237 if len(listcache) == 0:
1237 listcache.append(func())
1238 listcache.append(func())
1238 return listcache[0]
1239 return listcache[0]
1239
1240
1240 return f
1241 return f
1241 cache = {}
1242 cache = {}
1242 if func.__code__.co_argcount == 1:
1243 if func.__code__.co_argcount == 1:
1243 # we gain a small amount of time because
1244 # we gain a small amount of time because
1244 # we don't need to pack/unpack the list
1245 # we don't need to pack/unpack the list
1245 def f(arg):
1246 def f(arg):
1246 if arg not in cache:
1247 if arg not in cache:
1247 cache[arg] = func(arg)
1248 cache[arg] = func(arg)
1248 return cache[arg]
1249 return cache[arg]
1249
1250
1250 else:
1251 else:
1251
1252
1252 def f(*args):
1253 def f(*args):
1253 if args not in cache:
1254 if args not in cache:
1254 cache[args] = func(*args)
1255 cache[args] = func(*args)
1255 return cache[args]
1256 return cache[args]
1256
1257
1257 return f
1258 return f
1258
1259
1259
1260
1260 class cow(object):
1261 class cow(object):
1261 """helper class to make copy-on-write easier
1262 """helper class to make copy-on-write easier
1262
1263
1263 Call preparewrite before doing any writes.
1264 Call preparewrite before doing any writes.
1264 """
1265 """
1265
1266
1266 def preparewrite(self):
1267 def preparewrite(self):
1267 """call this before writes, return self or a copied new object"""
1268 """call this before writes, return self or a copied new object"""
1268 if getattr(self, '_copied', 0):
1269 if getattr(self, '_copied', 0):
1269 self._copied -= 1
1270 self._copied -= 1
1270 # Function cow.__init__ expects 1 arg(s), got 2 [wrong-arg-count]
1271 # Function cow.__init__ expects 1 arg(s), got 2 [wrong-arg-count]
1271 return self.__class__(self) # pytype: disable=wrong-arg-count
1272 return self.__class__(self) # pytype: disable=wrong-arg-count
1272 return self
1273 return self
1273
1274
1274 def copy(self):
1275 def copy(self):
1275 """always do a cheap copy"""
1276 """always do a cheap copy"""
1276 self._copied = getattr(self, '_copied', 0) + 1
1277 self._copied = getattr(self, '_copied', 0) + 1
1277 return self
1278 return self
1278
1279
1279
1280
1280 class sortdict(collections.OrderedDict):
1281 class sortdict(collections.OrderedDict):
1281 """a simple sorted dictionary
1282 """a simple sorted dictionary
1282
1283
1283 >>> d1 = sortdict([(b'a', 0), (b'b', 1)])
1284 >>> d1 = sortdict([(b'a', 0), (b'b', 1)])
1284 >>> d2 = d1.copy()
1285 >>> d2 = d1.copy()
1285 >>> d2
1286 >>> d2
1286 sortdict([('a', 0), ('b', 1)])
1287 sortdict([('a', 0), ('b', 1)])
1287 >>> d2.update([(b'a', 2)])
1288 >>> d2.update([(b'a', 2)])
1288 >>> list(d2.keys()) # should still be in last-set order
1289 >>> list(d2.keys()) # should still be in last-set order
1289 ['b', 'a']
1290 ['b', 'a']
1290 >>> d1.insert(1, b'a.5', 0.5)
1291 >>> d1.insert(1, b'a.5', 0.5)
1291 >>> d1
1292 >>> d1
1292 sortdict([('a', 0), ('a.5', 0.5), ('b', 1)])
1293 sortdict([('a', 0), ('a.5', 0.5), ('b', 1)])
1293 """
1294 """
1294
1295
1295 def __setitem__(self, key, value):
1296 def __setitem__(self, key, value):
1296 if key in self:
1297 if key in self:
1297 del self[key]
1298 del self[key]
1298 super(sortdict, self).__setitem__(key, value)
1299 super(sortdict, self).__setitem__(key, value)
1299
1300
1300 if pycompat.ispypy:
1301 if pycompat.ispypy:
1301 # __setitem__() isn't called as of PyPy 5.8.0
1302 # __setitem__() isn't called as of PyPy 5.8.0
1302 def update(self, src, **f):
1303 def update(self, src, **f):
1303 if isinstance(src, dict):
1304 if isinstance(src, dict):
1304 src = pycompat.iteritems(src)
1305 src = pycompat.iteritems(src)
1305 for k, v in src:
1306 for k, v in src:
1306 self[k] = v
1307 self[k] = v
1307 for k in f:
1308 for k in f:
1308 self[k] = f[k]
1309 self[k] = f[k]
1309
1310
1310 def insert(self, position, key, value):
1311 def insert(self, position, key, value):
1311 for (i, (k, v)) in enumerate(list(self.items())):
1312 for (i, (k, v)) in enumerate(list(self.items())):
1312 if i == position:
1313 if i == position:
1313 self[key] = value
1314 self[key] = value
1314 if i >= position:
1315 if i >= position:
1315 del self[k]
1316 del self[k]
1316 self[k] = v
1317 self[k] = v
1317
1318
1318
1319
1319 class cowdict(cow, dict):
1320 class cowdict(cow, dict):
1320 """copy-on-write dict
1321 """copy-on-write dict
1321
1322
1322 Be sure to call d = d.preparewrite() before writing to d.
1323 Be sure to call d = d.preparewrite() before writing to d.
1323
1324
1324 >>> a = cowdict()
1325 >>> a = cowdict()
1325 >>> a is a.preparewrite()
1326 >>> a is a.preparewrite()
1326 True
1327 True
1327 >>> b = a.copy()
1328 >>> b = a.copy()
1328 >>> b is a
1329 >>> b is a
1329 True
1330 True
1330 >>> c = b.copy()
1331 >>> c = b.copy()
1331 >>> c is a
1332 >>> c is a
1332 True
1333 True
1333 >>> a = a.preparewrite()
1334 >>> a = a.preparewrite()
1334 >>> b is a
1335 >>> b is a
1335 False
1336 False
1336 >>> a is a.preparewrite()
1337 >>> a is a.preparewrite()
1337 True
1338 True
1338 >>> c = c.preparewrite()
1339 >>> c = c.preparewrite()
1339 >>> b is c
1340 >>> b is c
1340 False
1341 False
1341 >>> b is b.preparewrite()
1342 >>> b is b.preparewrite()
1342 True
1343 True
1343 """
1344 """
1344
1345
1345
1346
1346 class cowsortdict(cow, sortdict):
1347 class cowsortdict(cow, sortdict):
1347 """copy-on-write sortdict
1348 """copy-on-write sortdict
1348
1349
1349 Be sure to call d = d.preparewrite() before writing to d.
1350 Be sure to call d = d.preparewrite() before writing to d.
1350 """
1351 """
1351
1352
1352
1353
1353 class transactional(object): # pytype: disable=ignored-metaclass
1354 class transactional(object): # pytype: disable=ignored-metaclass
1354 """Base class for making a transactional type into a context manager."""
1355 """Base class for making a transactional type into a context manager."""
1355
1356
1356 __metaclass__ = abc.ABCMeta
1357 __metaclass__ = abc.ABCMeta
1357
1358
1358 @abc.abstractmethod
1359 @abc.abstractmethod
1359 def close(self):
1360 def close(self):
1360 """Successfully closes the transaction."""
1361 """Successfully closes the transaction."""
1361
1362
1362 @abc.abstractmethod
1363 @abc.abstractmethod
1363 def release(self):
1364 def release(self):
1364 """Marks the end of the transaction.
1365 """Marks the end of the transaction.
1365
1366
1366 If the transaction has not been closed, it will be aborted.
1367 If the transaction has not been closed, it will be aborted.
1367 """
1368 """
1368
1369
1369 def __enter__(self):
1370 def __enter__(self):
1370 return self
1371 return self
1371
1372
1372 def __exit__(self, exc_type, exc_val, exc_tb):
1373 def __exit__(self, exc_type, exc_val, exc_tb):
1373 try:
1374 try:
1374 if exc_type is None:
1375 if exc_type is None:
1375 self.close()
1376 self.close()
1376 finally:
1377 finally:
1377 self.release()
1378 self.release()
1378
1379
1379
1380
1380 @contextlib.contextmanager
1381 @contextlib.contextmanager
1381 def acceptintervention(tr=None):
1382 def acceptintervention(tr=None):
1382 """A context manager that closes the transaction on InterventionRequired
1383 """A context manager that closes the transaction on InterventionRequired
1383
1384
1384 If no transaction was provided, this simply runs the body and returns
1385 If no transaction was provided, this simply runs the body and returns
1385 """
1386 """
1386 if not tr:
1387 if not tr:
1387 yield
1388 yield
1388 return
1389 return
1389 try:
1390 try:
1390 yield
1391 yield
1391 tr.close()
1392 tr.close()
1392 except error.InterventionRequired:
1393 except error.InterventionRequired:
1393 tr.close()
1394 tr.close()
1394 raise
1395 raise
1395 finally:
1396 finally:
1396 tr.release()
1397 tr.release()
1397
1398
1398
1399
1399 @contextlib.contextmanager
1400 @contextlib.contextmanager
1400 def nullcontextmanager(enter_result=None):
1401 def nullcontextmanager(enter_result=None):
1401 yield enter_result
1402 yield enter_result
1402
1403
1403
1404
1404 class _lrucachenode(object):
1405 class _lrucachenode(object):
1405 """A node in a doubly linked list.
1406 """A node in a doubly linked list.
1406
1407
1407 Holds a reference to nodes on either side as well as a key-value
1408 Holds a reference to nodes on either side as well as a key-value
1408 pair for the dictionary entry.
1409 pair for the dictionary entry.
1409 """
1410 """
1410
1411
1411 __slots__ = ('next', 'prev', 'key', 'value', 'cost')
1412 __slots__ = ('next', 'prev', 'key', 'value', 'cost')
1412
1413
1413 def __init__(self):
1414 def __init__(self):
1414 self.next = self
1415 self.next = self
1415 self.prev = self
1416 self.prev = self
1416
1417
1417 self.key = _notset
1418 self.key = _notset
1418 self.value = None
1419 self.value = None
1419 self.cost = 0
1420 self.cost = 0
1420
1421
1421 def markempty(self):
1422 def markempty(self):
1422 """Mark the node as emptied."""
1423 """Mark the node as emptied."""
1423 self.key = _notset
1424 self.key = _notset
1424 self.value = None
1425 self.value = None
1425 self.cost = 0
1426 self.cost = 0
1426
1427
1427
1428
1428 class lrucachedict(object):
1429 class lrucachedict(object):
1429 """Dict that caches most recent accesses and sets.
1430 """Dict that caches most recent accesses and sets.
1430
1431
1431 The dict consists of an actual backing dict - indexed by original
1432 The dict consists of an actual backing dict - indexed by original
1432 key - and a doubly linked circular list defining the order of entries in
1433 key - and a doubly linked circular list defining the order of entries in
1433 the cache.
1434 the cache.
1434
1435
1435 The head node is the newest entry in the cache. If the cache is full,
1436 The head node is the newest entry in the cache. If the cache is full,
1436 we recycle head.prev and make it the new head. Cache accesses result in
1437 we recycle head.prev and make it the new head. Cache accesses result in
1437 the node being moved to before the existing head and being marked as the
1438 the node being moved to before the existing head and being marked as the
1438 new head node.
1439 new head node.
1439
1440
1440 Items in the cache can be inserted with an optional "cost" value. This is
1441 Items in the cache can be inserted with an optional "cost" value. This is
1441 simply an integer that is specified by the caller. The cache can be queried
1442 simply an integer that is specified by the caller. The cache can be queried
1442 for the total cost of all items presently in the cache.
1443 for the total cost of all items presently in the cache.
1443
1444
1444 The cache can also define a maximum cost. If a cache insertion would
1445 The cache can also define a maximum cost. If a cache insertion would
1445 cause the total cost of the cache to go beyond the maximum cost limit,
1446 cause the total cost of the cache to go beyond the maximum cost limit,
1446 nodes will be evicted to make room for the new code. This can be used
1447 nodes will be evicted to make room for the new code. This can be used
1447 to e.g. set a max memory limit and associate an estimated bytes size
1448 to e.g. set a max memory limit and associate an estimated bytes size
1448 cost to each item in the cache. By default, no maximum cost is enforced.
1449 cost to each item in the cache. By default, no maximum cost is enforced.
1449 """
1450 """
1450
1451
1451 def __init__(self, max, maxcost=0):
1452 def __init__(self, max, maxcost=0):
1452 self._cache = {}
1453 self._cache = {}
1453
1454
1454 self._head = _lrucachenode()
1455 self._head = _lrucachenode()
1455 self._size = 1
1456 self._size = 1
1456 self.capacity = max
1457 self.capacity = max
1457 self.totalcost = 0
1458 self.totalcost = 0
1458 self.maxcost = maxcost
1459 self.maxcost = maxcost
1459
1460
1460 def __len__(self):
1461 def __len__(self):
1461 return len(self._cache)
1462 return len(self._cache)
1462
1463
1463 def __contains__(self, k):
1464 def __contains__(self, k):
1464 return k in self._cache
1465 return k in self._cache
1465
1466
1466 def __iter__(self):
1467 def __iter__(self):
1467 # We don't have to iterate in cache order, but why not.
1468 # We don't have to iterate in cache order, but why not.
1468 n = self._head
1469 n = self._head
1469 for i in range(len(self._cache)):
1470 for i in range(len(self._cache)):
1470 yield n.key
1471 yield n.key
1471 n = n.next
1472 n = n.next
1472
1473
1473 def __getitem__(self, k):
1474 def __getitem__(self, k):
1474 node = self._cache[k]
1475 node = self._cache[k]
1475 self._movetohead(node)
1476 self._movetohead(node)
1476 return node.value
1477 return node.value
1477
1478
1478 def insert(self, k, v, cost=0):
1479 def insert(self, k, v, cost=0):
1479 """Insert a new item in the cache with optional cost value."""
1480 """Insert a new item in the cache with optional cost value."""
1480 node = self._cache.get(k)
1481 node = self._cache.get(k)
1481 # Replace existing value and mark as newest.
1482 # Replace existing value and mark as newest.
1482 if node is not None:
1483 if node is not None:
1483 self.totalcost -= node.cost
1484 self.totalcost -= node.cost
1484 node.value = v
1485 node.value = v
1485 node.cost = cost
1486 node.cost = cost
1486 self.totalcost += cost
1487 self.totalcost += cost
1487 self._movetohead(node)
1488 self._movetohead(node)
1488
1489
1489 if self.maxcost:
1490 if self.maxcost:
1490 self._enforcecostlimit()
1491 self._enforcecostlimit()
1491
1492
1492 return
1493 return
1493
1494
1494 if self._size < self.capacity:
1495 if self._size < self.capacity:
1495 node = self._addcapacity()
1496 node = self._addcapacity()
1496 else:
1497 else:
1497 # Grab the last/oldest item.
1498 # Grab the last/oldest item.
1498 node = self._head.prev
1499 node = self._head.prev
1499
1500
1500 # At capacity. Kill the old entry.
1501 # At capacity. Kill the old entry.
1501 if node.key is not _notset:
1502 if node.key is not _notset:
1502 self.totalcost -= node.cost
1503 self.totalcost -= node.cost
1503 del self._cache[node.key]
1504 del self._cache[node.key]
1504
1505
1505 node.key = k
1506 node.key = k
1506 node.value = v
1507 node.value = v
1507 node.cost = cost
1508 node.cost = cost
1508 self.totalcost += cost
1509 self.totalcost += cost
1509 self._cache[k] = node
1510 self._cache[k] = node
1510 # And mark it as newest entry. No need to adjust order since it
1511 # And mark it as newest entry. No need to adjust order since it
1511 # is already self._head.prev.
1512 # is already self._head.prev.
1512 self._head = node
1513 self._head = node
1513
1514
1514 if self.maxcost:
1515 if self.maxcost:
1515 self._enforcecostlimit()
1516 self._enforcecostlimit()
1516
1517
1517 def __setitem__(self, k, v):
1518 def __setitem__(self, k, v):
1518 self.insert(k, v)
1519 self.insert(k, v)
1519
1520
1520 def __delitem__(self, k):
1521 def __delitem__(self, k):
1521 self.pop(k)
1522 self.pop(k)
1522
1523
1523 def pop(self, k, default=_notset):
1524 def pop(self, k, default=_notset):
1524 try:
1525 try:
1525 node = self._cache.pop(k)
1526 node = self._cache.pop(k)
1526 except KeyError:
1527 except KeyError:
1527 if default is _notset:
1528 if default is _notset:
1528 raise
1529 raise
1529 return default
1530 return default
1530
1531
1531 assert node is not None # help pytype
1532 assert node is not None # help pytype
1532 value = node.value
1533 value = node.value
1533 self.totalcost -= node.cost
1534 self.totalcost -= node.cost
1534 node.markempty()
1535 node.markempty()
1535
1536
1536 # Temporarily mark as newest item before re-adjusting head to make
1537 # Temporarily mark as newest item before re-adjusting head to make
1537 # this node the oldest item.
1538 # this node the oldest item.
1538 self._movetohead(node)
1539 self._movetohead(node)
1539 self._head = node.next
1540 self._head = node.next
1540
1541
1541 return value
1542 return value
1542
1543
1543 # Additional dict methods.
1544 # Additional dict methods.
1544
1545
1545 def get(self, k, default=None):
1546 def get(self, k, default=None):
1546 try:
1547 try:
1547 return self.__getitem__(k)
1548 return self.__getitem__(k)
1548 except KeyError:
1549 except KeyError:
1549 return default
1550 return default
1550
1551
1551 def peek(self, k, default=_notset):
1552 def peek(self, k, default=_notset):
1552 """Get the specified item without moving it to the head
1553 """Get the specified item without moving it to the head
1553
1554
1554 Unlike get(), this doesn't mutate the internal state. But be aware
1555 Unlike get(), this doesn't mutate the internal state. But be aware
1555 that it doesn't mean peek() is thread safe.
1556 that it doesn't mean peek() is thread safe.
1556 """
1557 """
1557 try:
1558 try:
1558 node = self._cache[k]
1559 node = self._cache[k]
1559 assert node is not None # help pytype
1560 assert node is not None # help pytype
1560 return node.value
1561 return node.value
1561 except KeyError:
1562 except KeyError:
1562 if default is _notset:
1563 if default is _notset:
1563 raise
1564 raise
1564 return default
1565 return default
1565
1566
1566 def clear(self):
1567 def clear(self):
1567 n = self._head
1568 n = self._head
1568 while n.key is not _notset:
1569 while n.key is not _notset:
1569 self.totalcost -= n.cost
1570 self.totalcost -= n.cost
1570 n.markempty()
1571 n.markempty()
1571 n = n.next
1572 n = n.next
1572
1573
1573 self._cache.clear()
1574 self._cache.clear()
1574
1575
1575 def copy(self, capacity=None, maxcost=0):
1576 def copy(self, capacity=None, maxcost=0):
1576 """Create a new cache as a copy of the current one.
1577 """Create a new cache as a copy of the current one.
1577
1578
1578 By default, the new cache has the same capacity as the existing one.
1579 By default, the new cache has the same capacity as the existing one.
1579 But, the cache capacity can be changed as part of performing the
1580 But, the cache capacity can be changed as part of performing the
1580 copy.
1581 copy.
1581
1582
1582 Items in the copy have an insertion/access order matching this
1583 Items in the copy have an insertion/access order matching this
1583 instance.
1584 instance.
1584 """
1585 """
1585
1586
1586 capacity = capacity or self.capacity
1587 capacity = capacity or self.capacity
1587 maxcost = maxcost or self.maxcost
1588 maxcost = maxcost or self.maxcost
1588 result = lrucachedict(capacity, maxcost=maxcost)
1589 result = lrucachedict(capacity, maxcost=maxcost)
1589
1590
1590 # We copy entries by iterating in oldest-to-newest order so the copy
1591 # We copy entries by iterating in oldest-to-newest order so the copy
1591 # has the correct ordering.
1592 # has the correct ordering.
1592
1593
1593 # Find the first non-empty entry.
1594 # Find the first non-empty entry.
1594 n = self._head.prev
1595 n = self._head.prev
1595 while n.key is _notset and n is not self._head:
1596 while n.key is _notset and n is not self._head:
1596 n = n.prev
1597 n = n.prev
1597
1598
1598 # We could potentially skip the first N items when decreasing capacity.
1599 # We could potentially skip the first N items when decreasing capacity.
1599 # But let's keep it simple unless it is a performance problem.
1600 # But let's keep it simple unless it is a performance problem.
1600 for i in range(len(self._cache)):
1601 for i in range(len(self._cache)):
1601 result.insert(n.key, n.value, cost=n.cost)
1602 result.insert(n.key, n.value, cost=n.cost)
1602 n = n.prev
1603 n = n.prev
1603
1604
1604 return result
1605 return result
1605
1606
1606 def popoldest(self):
1607 def popoldest(self):
1607 """Remove the oldest item from the cache.
1608 """Remove the oldest item from the cache.
1608
1609
1609 Returns the (key, value) describing the removed cache entry.
1610 Returns the (key, value) describing the removed cache entry.
1610 """
1611 """
1611 if not self._cache:
1612 if not self._cache:
1612 return
1613 return
1613
1614
1614 # Walk the linked list backwards starting at tail node until we hit
1615 # Walk the linked list backwards starting at tail node until we hit
1615 # a non-empty node.
1616 # a non-empty node.
1616 n = self._head.prev
1617 n = self._head.prev
1617
1618
1618 assert n is not None # help pytype
1619 assert n is not None # help pytype
1619
1620
1620 while n.key is _notset:
1621 while n.key is _notset:
1621 n = n.prev
1622 n = n.prev
1622
1623
1623 assert n is not None # help pytype
1624 assert n is not None # help pytype
1624
1625
1625 key, value = n.key, n.value
1626 key, value = n.key, n.value
1626
1627
1627 # And remove it from the cache and mark it as empty.
1628 # And remove it from the cache and mark it as empty.
1628 del self._cache[n.key]
1629 del self._cache[n.key]
1629 self.totalcost -= n.cost
1630 self.totalcost -= n.cost
1630 n.markempty()
1631 n.markempty()
1631
1632
1632 return key, value
1633 return key, value
1633
1634
1634 def _movetohead(self, node):
1635 def _movetohead(self, node):
1635 """Mark a node as the newest, making it the new head.
1636 """Mark a node as the newest, making it the new head.
1636
1637
1637 When a node is accessed, it becomes the freshest entry in the LRU
1638 When a node is accessed, it becomes the freshest entry in the LRU
1638 list, which is denoted by self._head.
1639 list, which is denoted by self._head.
1639
1640
1640 Visually, let's make ``N`` the new head node (* denotes head):
1641 Visually, let's make ``N`` the new head node (* denotes head):
1641
1642
1642 previous/oldest <-> head <-> next/next newest
1643 previous/oldest <-> head <-> next/next newest
1643
1644
1644 ----<->--- A* ---<->-----
1645 ----<->--- A* ---<->-----
1645 | |
1646 | |
1646 E <-> D <-> N <-> C <-> B
1647 E <-> D <-> N <-> C <-> B
1647
1648
1648 To:
1649 To:
1649
1650
1650 ----<->--- N* ---<->-----
1651 ----<->--- N* ---<->-----
1651 | |
1652 | |
1652 E <-> D <-> C <-> B <-> A
1653 E <-> D <-> C <-> B <-> A
1653
1654
1654 This requires the following moves:
1655 This requires the following moves:
1655
1656
1656 C.next = D (node.prev.next = node.next)
1657 C.next = D (node.prev.next = node.next)
1657 D.prev = C (node.next.prev = node.prev)
1658 D.prev = C (node.next.prev = node.prev)
1658 E.next = N (head.prev.next = node)
1659 E.next = N (head.prev.next = node)
1659 N.prev = E (node.prev = head.prev)
1660 N.prev = E (node.prev = head.prev)
1660 N.next = A (node.next = head)
1661 N.next = A (node.next = head)
1661 A.prev = N (head.prev = node)
1662 A.prev = N (head.prev = node)
1662 """
1663 """
1663 head = self._head
1664 head = self._head
1664 # C.next = D
1665 # C.next = D
1665 node.prev.next = node.next
1666 node.prev.next = node.next
1666 # D.prev = C
1667 # D.prev = C
1667 node.next.prev = node.prev
1668 node.next.prev = node.prev
1668 # N.prev = E
1669 # N.prev = E
1669 node.prev = head.prev
1670 node.prev = head.prev
1670 # N.next = A
1671 # N.next = A
1671 # It is tempting to do just "head" here, however if node is
1672 # It is tempting to do just "head" here, however if node is
1672 # adjacent to head, this will do bad things.
1673 # adjacent to head, this will do bad things.
1673 node.next = head.prev.next
1674 node.next = head.prev.next
1674 # E.next = N
1675 # E.next = N
1675 node.next.prev = node
1676 node.next.prev = node
1676 # A.prev = N
1677 # A.prev = N
1677 node.prev.next = node
1678 node.prev.next = node
1678
1679
1679 self._head = node
1680 self._head = node
1680
1681
1681 def _addcapacity(self):
1682 def _addcapacity(self):
1682 """Add a node to the circular linked list.
1683 """Add a node to the circular linked list.
1683
1684
1684 The new node is inserted before the head node.
1685 The new node is inserted before the head node.
1685 """
1686 """
1686 head = self._head
1687 head = self._head
1687 node = _lrucachenode()
1688 node = _lrucachenode()
1688 head.prev.next = node
1689 head.prev.next = node
1689 node.prev = head.prev
1690 node.prev = head.prev
1690 node.next = head
1691 node.next = head
1691 head.prev = node
1692 head.prev = node
1692 self._size += 1
1693 self._size += 1
1693 return node
1694 return node
1694
1695
1695 def _enforcecostlimit(self):
1696 def _enforcecostlimit(self):
1696 # This should run after an insertion. It should only be called if total
1697 # This should run after an insertion. It should only be called if total
1697 # cost limits are being enforced.
1698 # cost limits are being enforced.
1698 # The most recently inserted node is never evicted.
1699 # The most recently inserted node is never evicted.
1699 if len(self) <= 1 or self.totalcost <= self.maxcost:
1700 if len(self) <= 1 or self.totalcost <= self.maxcost:
1700 return
1701 return
1701
1702
1702 # This is logically equivalent to calling popoldest() until we
1703 # This is logically equivalent to calling popoldest() until we
1703 # free up enough cost. We don't do that since popoldest() needs
1704 # free up enough cost. We don't do that since popoldest() needs
1704 # to walk the linked list and doing this in a loop would be
1705 # to walk the linked list and doing this in a loop would be
1705 # quadratic. So we find the first non-empty node and then
1706 # quadratic. So we find the first non-empty node and then
1706 # walk nodes until we free up enough capacity.
1707 # walk nodes until we free up enough capacity.
1707 #
1708 #
1708 # If we only removed the minimum number of nodes to free enough
1709 # If we only removed the minimum number of nodes to free enough
1709 # cost at insert time, chances are high that the next insert would
1710 # cost at insert time, chances are high that the next insert would
1710 # also require pruning. This would effectively constitute quadratic
1711 # also require pruning. This would effectively constitute quadratic
1711 # behavior for insert-heavy workloads. To mitigate this, we set a
1712 # behavior for insert-heavy workloads. To mitigate this, we set a
1712 # target cost that is a percentage of the max cost. This will tend
1713 # target cost that is a percentage of the max cost. This will tend
1713 # to free more nodes when the high water mark is reached, which
1714 # to free more nodes when the high water mark is reached, which
1714 # lowers the chances of needing to prune on the subsequent insert.
1715 # lowers the chances of needing to prune on the subsequent insert.
1715 targetcost = int(self.maxcost * 0.75)
1716 targetcost = int(self.maxcost * 0.75)
1716
1717
1717 n = self._head.prev
1718 n = self._head.prev
1718 while n.key is _notset:
1719 while n.key is _notset:
1719 n = n.prev
1720 n = n.prev
1720
1721
1721 while len(self) > 1 and self.totalcost > targetcost:
1722 while len(self) > 1 and self.totalcost > targetcost:
1722 del self._cache[n.key]
1723 del self._cache[n.key]
1723 self.totalcost -= n.cost
1724 self.totalcost -= n.cost
1724 n.markempty()
1725 n.markempty()
1725 n = n.prev
1726 n = n.prev
1726
1727
1727
1728
1728 def lrucachefunc(func):
1729 def lrucachefunc(func):
1729 '''cache most recent results of function calls'''
1730 '''cache most recent results of function calls'''
1730 cache = {}
1731 cache = {}
1731 order = collections.deque()
1732 order = collections.deque()
1732 if func.__code__.co_argcount == 1:
1733 if func.__code__.co_argcount == 1:
1733
1734
1734 def f(arg):
1735 def f(arg):
1735 if arg not in cache:
1736 if arg not in cache:
1736 if len(cache) > 20:
1737 if len(cache) > 20:
1737 del cache[order.popleft()]
1738 del cache[order.popleft()]
1738 cache[arg] = func(arg)
1739 cache[arg] = func(arg)
1739 else:
1740 else:
1740 order.remove(arg)
1741 order.remove(arg)
1741 order.append(arg)
1742 order.append(arg)
1742 return cache[arg]
1743 return cache[arg]
1743
1744
1744 else:
1745 else:
1745
1746
1746 def f(*args):
1747 def f(*args):
1747 if args not in cache:
1748 if args not in cache:
1748 if len(cache) > 20:
1749 if len(cache) > 20:
1749 del cache[order.popleft()]
1750 del cache[order.popleft()]
1750 cache[args] = func(*args)
1751 cache[args] = func(*args)
1751 else:
1752 else:
1752 order.remove(args)
1753 order.remove(args)
1753 order.append(args)
1754 order.append(args)
1754 return cache[args]
1755 return cache[args]
1755
1756
1756 return f
1757 return f
1757
1758
1758
1759
1759 class propertycache(object):
1760 class propertycache(object):
1760 def __init__(self, func):
1761 def __init__(self, func):
1761 self.func = func
1762 self.func = func
1762 self.name = func.__name__
1763 self.name = func.__name__
1763
1764
1764 def __get__(self, obj, type=None):
1765 def __get__(self, obj, type=None):
1765 result = self.func(obj)
1766 result = self.func(obj)
1766 self.cachevalue(obj, result)
1767 self.cachevalue(obj, result)
1767 return result
1768 return result
1768
1769
1769 def cachevalue(self, obj, value):
1770 def cachevalue(self, obj, value):
1770 # __dict__ assignment required to bypass __setattr__ (eg: repoview)
1771 # __dict__ assignment required to bypass __setattr__ (eg: repoview)
1771 obj.__dict__[self.name] = value
1772 obj.__dict__[self.name] = value
1772
1773
1773
1774
1774 def clearcachedproperty(obj, prop):
1775 def clearcachedproperty(obj, prop):
1775 '''clear a cached property value, if one has been set'''
1776 '''clear a cached property value, if one has been set'''
1776 prop = pycompat.sysstr(prop)
1777 prop = pycompat.sysstr(prop)
1777 if prop in obj.__dict__:
1778 if prop in obj.__dict__:
1778 del obj.__dict__[prop]
1779 del obj.__dict__[prop]
1779
1780
1780
1781
1781 def increasingchunks(source, min=1024, max=65536):
1782 def increasingchunks(source, min=1024, max=65536):
1782 """return no less than min bytes per chunk while data remains,
1783 """return no less than min bytes per chunk while data remains,
1783 doubling min after each chunk until it reaches max"""
1784 doubling min after each chunk until it reaches max"""
1784
1785
1785 def log2(x):
1786 def log2(x):
1786 if not x:
1787 if not x:
1787 return 0
1788 return 0
1788 i = 0
1789 i = 0
1789 while x:
1790 while x:
1790 x >>= 1
1791 x >>= 1
1791 i += 1
1792 i += 1
1792 return i - 1
1793 return i - 1
1793
1794
1794 buf = []
1795 buf = []
1795 blen = 0
1796 blen = 0
1796 for chunk in source:
1797 for chunk in source:
1797 buf.append(chunk)
1798 buf.append(chunk)
1798 blen += len(chunk)
1799 blen += len(chunk)
1799 if blen >= min:
1800 if blen >= min:
1800 if min < max:
1801 if min < max:
1801 min = min << 1
1802 min = min << 1
1802 nmin = 1 << log2(blen)
1803 nmin = 1 << log2(blen)
1803 if nmin > min:
1804 if nmin > min:
1804 min = nmin
1805 min = nmin
1805 if min > max:
1806 if min > max:
1806 min = max
1807 min = max
1807 yield b''.join(buf)
1808 yield b''.join(buf)
1808 blen = 0
1809 blen = 0
1809 buf = []
1810 buf = []
1810 if buf:
1811 if buf:
1811 yield b''.join(buf)
1812 yield b''.join(buf)
1812
1813
1813
1814
1814 def always(fn):
1815 def always(fn):
1815 return True
1816 return True
1816
1817
1817
1818
1818 def never(fn):
1819 def never(fn):
1819 return False
1820 return False
1820
1821
1821
1822
1822 def nogc(func):
1823 def nogc(func):
1823 """disable garbage collector
1824 """disable garbage collector
1824
1825
1825 Python's garbage collector triggers a GC each time a certain number of
1826 Python's garbage collector triggers a GC each time a certain number of
1826 container objects (the number being defined by gc.get_threshold()) are
1827 container objects (the number being defined by gc.get_threshold()) are
1827 allocated even when marked not to be tracked by the collector. Tracking has
1828 allocated even when marked not to be tracked by the collector. Tracking has
1828 no effect on when GCs are triggered, only on what objects the GC looks
1829 no effect on when GCs are triggered, only on what objects the GC looks
1829 into. As a workaround, disable GC while building complex (huge)
1830 into. As a workaround, disable GC while building complex (huge)
1830 containers.
1831 containers.
1831
1832
1832 This garbage collector issue have been fixed in 2.7. But it still affect
1833 This garbage collector issue have been fixed in 2.7. But it still affect
1833 CPython's performance.
1834 CPython's performance.
1834 """
1835 """
1835
1836
1836 def wrapper(*args, **kwargs):
1837 def wrapper(*args, **kwargs):
1837 gcenabled = gc.isenabled()
1838 gcenabled = gc.isenabled()
1838 gc.disable()
1839 gc.disable()
1839 try:
1840 try:
1840 return func(*args, **kwargs)
1841 return func(*args, **kwargs)
1841 finally:
1842 finally:
1842 if gcenabled:
1843 if gcenabled:
1843 gc.enable()
1844 gc.enable()
1844
1845
1845 return wrapper
1846 return wrapper
1846
1847
1847
1848
1848 if pycompat.ispypy:
1849 if pycompat.ispypy:
1849 # PyPy runs slower with gc disabled
1850 # PyPy runs slower with gc disabled
1850 nogc = lambda x: x
1851 nogc = lambda x: x
1851
1852
1852
1853
1853 def pathto(root, n1, n2):
1854 def pathto(root, n1, n2):
1854 # type: (bytes, bytes, bytes) -> bytes
1855 # type: (bytes, bytes, bytes) -> bytes
1855 """return the relative path from one place to another.
1856 """return the relative path from one place to another.
1856 root should use os.sep to separate directories
1857 root should use os.sep to separate directories
1857 n1 should use os.sep to separate directories
1858 n1 should use os.sep to separate directories
1858 n2 should use "/" to separate directories
1859 n2 should use "/" to separate directories
1859 returns an os.sep-separated path.
1860 returns an os.sep-separated path.
1860
1861
1861 If n1 is a relative path, it's assumed it's
1862 If n1 is a relative path, it's assumed it's
1862 relative to root.
1863 relative to root.
1863 n2 should always be relative to root.
1864 n2 should always be relative to root.
1864 """
1865 """
1865 if not n1:
1866 if not n1:
1866 return localpath(n2)
1867 return localpath(n2)
1867 if os.path.isabs(n1):
1868 if os.path.isabs(n1):
1868 if os.path.splitdrive(root)[0] != os.path.splitdrive(n1)[0]:
1869 if os.path.splitdrive(root)[0] != os.path.splitdrive(n1)[0]:
1869 return os.path.join(root, localpath(n2))
1870 return os.path.join(root, localpath(n2))
1870 n2 = b'/'.join((pconvert(root), n2))
1871 n2 = b'/'.join((pconvert(root), n2))
1871 a, b = splitpath(n1), n2.split(b'/')
1872 a, b = splitpath(n1), n2.split(b'/')
1872 a.reverse()
1873 a.reverse()
1873 b.reverse()
1874 b.reverse()
1874 while a and b and a[-1] == b[-1]:
1875 while a and b and a[-1] == b[-1]:
1875 a.pop()
1876 a.pop()
1876 b.pop()
1877 b.pop()
1877 b.reverse()
1878 b.reverse()
1878 return pycompat.ossep.join(([b'..'] * len(a)) + b) or b'.'
1879 return pycompat.ossep.join(([b'..'] * len(a)) + b) or b'.'
1879
1880
1880
1881
1881 def checksignature(func, depth=1):
1882 def checksignature(func, depth=1):
1882 '''wrap a function with code to check for calling errors'''
1883 '''wrap a function with code to check for calling errors'''
1883
1884
1884 def check(*args, **kwargs):
1885 def check(*args, **kwargs):
1885 try:
1886 try:
1886 return func(*args, **kwargs)
1887 return func(*args, **kwargs)
1887 except TypeError:
1888 except TypeError:
1888 if len(traceback.extract_tb(sys.exc_info()[2])) == depth:
1889 if len(traceback.extract_tb(sys.exc_info()[2])) == depth:
1889 raise error.SignatureError
1890 raise error.SignatureError
1890 raise
1891 raise
1891
1892
1892 return check
1893 return check
1893
1894
1894
1895
1895 # a whilelist of known filesystems where hardlink works reliably
1896 # a whilelist of known filesystems where hardlink works reliably
1896 _hardlinkfswhitelist = {
1897 _hardlinkfswhitelist = {
1897 b'apfs',
1898 b'apfs',
1898 b'btrfs',
1899 b'btrfs',
1899 b'ext2',
1900 b'ext2',
1900 b'ext3',
1901 b'ext3',
1901 b'ext4',
1902 b'ext4',
1902 b'hfs',
1903 b'hfs',
1903 b'jfs',
1904 b'jfs',
1904 b'NTFS',
1905 b'NTFS',
1905 b'reiserfs',
1906 b'reiserfs',
1906 b'tmpfs',
1907 b'tmpfs',
1907 b'ufs',
1908 b'ufs',
1908 b'xfs',
1909 b'xfs',
1909 b'zfs',
1910 b'zfs',
1910 }
1911 }
1911
1912
1912
1913
1913 def copyfile(
1914 def copyfile(
1914 src,
1915 src,
1915 dest,
1916 dest,
1916 hardlink=False,
1917 hardlink=False,
1917 copystat=False,
1918 copystat=False,
1918 checkambig=False,
1919 checkambig=False,
1919 nb_bytes=None,
1920 nb_bytes=None,
1920 no_hardlink_cb=None,
1921 no_hardlink_cb=None,
1921 check_fs_hardlink=True,
1922 check_fs_hardlink=True,
1922 ):
1923 ):
1923 """copy a file, preserving mode and optionally other stat info like
1924 """copy a file, preserving mode and optionally other stat info like
1924 atime/mtime
1925 atime/mtime
1925
1926
1926 checkambig argument is used with filestat, and is useful only if
1927 checkambig argument is used with filestat, and is useful only if
1927 destination file is guarded by any lock (e.g. repo.lock or
1928 destination file is guarded by any lock (e.g. repo.lock or
1928 repo.wlock).
1929 repo.wlock).
1929
1930
1930 copystat and checkambig should be exclusive.
1931 copystat and checkambig should be exclusive.
1931
1932
1932 nb_bytes: if set only copy the first `nb_bytes` of the source file.
1933 nb_bytes: if set only copy the first `nb_bytes` of the source file.
1933 """
1934 """
1934 assert not (copystat and checkambig)
1935 assert not (copystat and checkambig)
1935 oldstat = None
1936 oldstat = None
1936 if os.path.lexists(dest):
1937 if os.path.lexists(dest):
1937 if checkambig:
1938 if checkambig:
1938 oldstat = checkambig and filestat.frompath(dest)
1939 oldstat = checkambig and filestat.frompath(dest)
1939 unlink(dest)
1940 unlink(dest)
1940 if hardlink and check_fs_hardlink:
1941 if hardlink and check_fs_hardlink:
1941 # Hardlinks are problematic on CIFS (issue4546), do not allow hardlinks
1942 # Hardlinks are problematic on CIFS (issue4546), do not allow hardlinks
1942 # unless we are confident that dest is on a whitelisted filesystem.
1943 # unless we are confident that dest is on a whitelisted filesystem.
1943 try:
1944 try:
1944 fstype = getfstype(os.path.dirname(dest))
1945 fstype = getfstype(os.path.dirname(dest))
1945 except OSError:
1946 except OSError:
1946 fstype = None
1947 fstype = None
1947 if fstype not in _hardlinkfswhitelist:
1948 if fstype not in _hardlinkfswhitelist:
1948 if no_hardlink_cb is not None:
1949 if no_hardlink_cb is not None:
1949 no_hardlink_cb()
1950 no_hardlink_cb()
1950 hardlink = False
1951 hardlink = False
1951 if hardlink:
1952 if hardlink:
1952 try:
1953 try:
1953 oslink(src, dest)
1954 oslink(src, dest)
1954 if nb_bytes is not None:
1955 if nb_bytes is not None:
1955 m = "the `nb_bytes` argument is incompatible with `hardlink`"
1956 m = "the `nb_bytes` argument is incompatible with `hardlink`"
1956 raise error.ProgrammingError(m)
1957 raise error.ProgrammingError(m)
1957 return
1958 return
1958 except (IOError, OSError) as exc:
1959 except (IOError, OSError) as exc:
1959 if exc.errno != errno.EEXIST and no_hardlink_cb is not None:
1960 if exc.errno != errno.EEXIST and no_hardlink_cb is not None:
1960 no_hardlink_cb()
1961 no_hardlink_cb()
1961 # fall back to normal copy
1962 # fall back to normal copy
1962 if os.path.islink(src):
1963 if os.path.islink(src):
1963 os.symlink(os.readlink(src), dest)
1964 os.symlink(os.readlink(src), dest)
1964 # copytime is ignored for symlinks, but in general copytime isn't needed
1965 # copytime is ignored for symlinks, but in general copytime isn't needed
1965 # for them anyway
1966 # for them anyway
1966 if nb_bytes is not None:
1967 if nb_bytes is not None:
1967 m = "cannot use `nb_bytes` on a symlink"
1968 m = "cannot use `nb_bytes` on a symlink"
1968 raise error.ProgrammingError(m)
1969 raise error.ProgrammingError(m)
1969 else:
1970 else:
1970 try:
1971 try:
1971 shutil.copyfile(src, dest)
1972 shutil.copyfile(src, dest)
1972 if copystat:
1973 if copystat:
1973 # copystat also copies mode
1974 # copystat also copies mode
1974 shutil.copystat(src, dest)
1975 shutil.copystat(src, dest)
1975 else:
1976 else:
1976 shutil.copymode(src, dest)
1977 shutil.copymode(src, dest)
1977 if oldstat and oldstat.stat:
1978 if oldstat and oldstat.stat:
1978 newstat = filestat.frompath(dest)
1979 newstat = filestat.frompath(dest)
1979 if newstat.isambig(oldstat):
1980 if newstat.isambig(oldstat):
1980 # stat of copied file is ambiguous to original one
1981 # stat of copied file is ambiguous to original one
1981 advanced = (
1982 advanced = (
1982 oldstat.stat[stat.ST_MTIME] + 1
1983 oldstat.stat[stat.ST_MTIME] + 1
1983 ) & 0x7FFFFFFF
1984 ) & 0x7FFFFFFF
1984 os.utime(dest, (advanced, advanced))
1985 os.utime(dest, (advanced, advanced))
1985 # We could do something smarter using `copy_file_range` call or similar
1986 # We could do something smarter using `copy_file_range` call or similar
1986 if nb_bytes is not None:
1987 if nb_bytes is not None:
1987 with open(dest, mode='r+') as f:
1988 with open(dest, mode='r+') as f:
1988 f.truncate(nb_bytes)
1989 f.truncate(nb_bytes)
1989 except shutil.Error as inst:
1990 except shutil.Error as inst:
1990 raise error.Abort(stringutil.forcebytestr(inst))
1991 raise error.Abort(stringutil.forcebytestr(inst))
1991
1992
1992
1993
1993 def copyfiles(src, dst, hardlink=None, progress=None):
1994 def copyfiles(src, dst, hardlink=None, progress=None):
1994 """Copy a directory tree using hardlinks if possible."""
1995 """Copy a directory tree using hardlinks if possible."""
1995 num = 0
1996 num = 0
1996
1997
1997 def settopic():
1998 def settopic():
1998 if progress:
1999 if progress:
1999 progress.topic = _(b'linking') if hardlink else _(b'copying')
2000 progress.topic = _(b'linking') if hardlink else _(b'copying')
2000
2001
2001 if os.path.isdir(src):
2002 if os.path.isdir(src):
2002 if hardlink is None:
2003 if hardlink is None:
2003 hardlink = (
2004 hardlink = (
2004 os.stat(src).st_dev == os.stat(os.path.dirname(dst)).st_dev
2005 os.stat(src).st_dev == os.stat(os.path.dirname(dst)).st_dev
2005 )
2006 )
2006 settopic()
2007 settopic()
2007 os.mkdir(dst)
2008 os.mkdir(dst)
2008 for name, kind in listdir(src):
2009 for name, kind in listdir(src):
2009 srcname = os.path.join(src, name)
2010 srcname = os.path.join(src, name)
2010 dstname = os.path.join(dst, name)
2011 dstname = os.path.join(dst, name)
2011 hardlink, n = copyfiles(srcname, dstname, hardlink, progress)
2012 hardlink, n = copyfiles(srcname, dstname, hardlink, progress)
2012 num += n
2013 num += n
2013 else:
2014 else:
2014 if hardlink is None:
2015 if hardlink is None:
2015 hardlink = (
2016 hardlink = (
2016 os.stat(os.path.dirname(src)).st_dev
2017 os.stat(os.path.dirname(src)).st_dev
2017 == os.stat(os.path.dirname(dst)).st_dev
2018 == os.stat(os.path.dirname(dst)).st_dev
2018 )
2019 )
2019 settopic()
2020 settopic()
2020
2021
2021 if hardlink:
2022 if hardlink:
2022 try:
2023 try:
2023 oslink(src, dst)
2024 oslink(src, dst)
2024 except (IOError, OSError) as exc:
2025 except (IOError, OSError) as exc:
2025 if exc.errno != errno.EEXIST:
2026 if exc.errno != errno.EEXIST:
2026 hardlink = False
2027 hardlink = False
2027 # XXX maybe try to relink if the file exist ?
2028 # XXX maybe try to relink if the file exist ?
2028 shutil.copy(src, dst)
2029 shutil.copy(src, dst)
2029 else:
2030 else:
2030 shutil.copy(src, dst)
2031 shutil.copy(src, dst)
2031 num += 1
2032 num += 1
2032 if progress:
2033 if progress:
2033 progress.increment()
2034 progress.increment()
2034
2035
2035 return hardlink, num
2036 return hardlink, num
2036
2037
2037
2038
2038 _winreservednames = {
2039 _winreservednames = {
2039 b'con',
2040 b'con',
2040 b'prn',
2041 b'prn',
2041 b'aux',
2042 b'aux',
2042 b'nul',
2043 b'nul',
2043 b'com1',
2044 b'com1',
2044 b'com2',
2045 b'com2',
2045 b'com3',
2046 b'com3',
2046 b'com4',
2047 b'com4',
2047 b'com5',
2048 b'com5',
2048 b'com6',
2049 b'com6',
2049 b'com7',
2050 b'com7',
2050 b'com8',
2051 b'com8',
2051 b'com9',
2052 b'com9',
2052 b'lpt1',
2053 b'lpt1',
2053 b'lpt2',
2054 b'lpt2',
2054 b'lpt3',
2055 b'lpt3',
2055 b'lpt4',
2056 b'lpt4',
2056 b'lpt5',
2057 b'lpt5',
2057 b'lpt6',
2058 b'lpt6',
2058 b'lpt7',
2059 b'lpt7',
2059 b'lpt8',
2060 b'lpt8',
2060 b'lpt9',
2061 b'lpt9',
2061 }
2062 }
2062 _winreservedchars = b':*?"<>|'
2063 _winreservedchars = b':*?"<>|'
2063
2064
2064
2065
2065 def checkwinfilename(path):
2066 def checkwinfilename(path):
2066 # type: (bytes) -> Optional[bytes]
2067 # type: (bytes) -> Optional[bytes]
2067 r"""Check that the base-relative path is a valid filename on Windows.
2068 r"""Check that the base-relative path is a valid filename on Windows.
2068 Returns None if the path is ok, or a UI string describing the problem.
2069 Returns None if the path is ok, or a UI string describing the problem.
2069
2070
2070 >>> checkwinfilename(b"just/a/normal/path")
2071 >>> checkwinfilename(b"just/a/normal/path")
2071 >>> checkwinfilename(b"foo/bar/con.xml")
2072 >>> checkwinfilename(b"foo/bar/con.xml")
2072 "filename contains 'con', which is reserved on Windows"
2073 "filename contains 'con', which is reserved on Windows"
2073 >>> checkwinfilename(b"foo/con.xml/bar")
2074 >>> checkwinfilename(b"foo/con.xml/bar")
2074 "filename contains 'con', which is reserved on Windows"
2075 "filename contains 'con', which is reserved on Windows"
2075 >>> checkwinfilename(b"foo/bar/xml.con")
2076 >>> checkwinfilename(b"foo/bar/xml.con")
2076 >>> checkwinfilename(b"foo/bar/AUX/bla.txt")
2077 >>> checkwinfilename(b"foo/bar/AUX/bla.txt")
2077 "filename contains 'AUX', which is reserved on Windows"
2078 "filename contains 'AUX', which is reserved on Windows"
2078 >>> checkwinfilename(b"foo/bar/bla:.txt")
2079 >>> checkwinfilename(b"foo/bar/bla:.txt")
2079 "filename contains ':', which is reserved on Windows"
2080 "filename contains ':', which is reserved on Windows"
2080 >>> checkwinfilename(b"foo/bar/b\07la.txt")
2081 >>> checkwinfilename(b"foo/bar/b\07la.txt")
2081 "filename contains '\\x07', which is invalid on Windows"
2082 "filename contains '\\x07', which is invalid on Windows"
2082 >>> checkwinfilename(b"foo/bar/bla ")
2083 >>> checkwinfilename(b"foo/bar/bla ")
2083 "filename ends with ' ', which is not allowed on Windows"
2084 "filename ends with ' ', which is not allowed on Windows"
2084 >>> checkwinfilename(b"../bar")
2085 >>> checkwinfilename(b"../bar")
2085 >>> checkwinfilename(b"foo\\")
2086 >>> checkwinfilename(b"foo\\")
2086 "filename ends with '\\', which is invalid on Windows"
2087 "filename ends with '\\', which is invalid on Windows"
2087 >>> checkwinfilename(b"foo\\/bar")
2088 >>> checkwinfilename(b"foo\\/bar")
2088 "directory name ends with '\\', which is invalid on Windows"
2089 "directory name ends with '\\', which is invalid on Windows"
2089 """
2090 """
2090 if path.endswith(b'\\'):
2091 if path.endswith(b'\\'):
2091 return _(b"filename ends with '\\', which is invalid on Windows")
2092 return _(b"filename ends with '\\', which is invalid on Windows")
2092 if b'\\/' in path:
2093 if b'\\/' in path:
2093 return _(b"directory name ends with '\\', which is invalid on Windows")
2094 return _(b"directory name ends with '\\', which is invalid on Windows")
2094 for n in path.replace(b'\\', b'/').split(b'/'):
2095 for n in path.replace(b'\\', b'/').split(b'/'):
2095 if not n:
2096 if not n:
2096 continue
2097 continue
2097 for c in _filenamebytestr(n):
2098 for c in _filenamebytestr(n):
2098 if c in _winreservedchars:
2099 if c in _winreservedchars:
2099 return (
2100 return (
2100 _(
2101 _(
2101 b"filename contains '%s', which is reserved "
2102 b"filename contains '%s', which is reserved "
2102 b"on Windows"
2103 b"on Windows"
2103 )
2104 )
2104 % c
2105 % c
2105 )
2106 )
2106 if ord(c) <= 31:
2107 if ord(c) <= 31:
2107 return _(
2108 return _(
2108 b"filename contains '%s', which is invalid on Windows"
2109 b"filename contains '%s', which is invalid on Windows"
2109 ) % stringutil.escapestr(c)
2110 ) % stringutil.escapestr(c)
2110 base = n.split(b'.')[0]
2111 base = n.split(b'.')[0]
2111 if base and base.lower() in _winreservednames:
2112 if base and base.lower() in _winreservednames:
2112 return (
2113 return (
2113 _(b"filename contains '%s', which is reserved on Windows")
2114 _(b"filename contains '%s', which is reserved on Windows")
2114 % base
2115 % base
2115 )
2116 )
2116 t = n[-1:]
2117 t = n[-1:]
2117 if t in b'. ' and n not in b'..':
2118 if t in b'. ' and n not in b'..':
2118 return (
2119 return (
2119 _(
2120 _(
2120 b"filename ends with '%s', which is not allowed "
2121 b"filename ends with '%s', which is not allowed "
2121 b"on Windows"
2122 b"on Windows"
2122 )
2123 )
2123 % t
2124 % t
2124 )
2125 )
2125
2126
2126
2127
2127 timer = getattr(time, "perf_counter", None)
2128 timer = getattr(time, "perf_counter", None)
2128
2129
2129 if pycompat.iswindows:
2130 if pycompat.iswindows:
2130 checkosfilename = checkwinfilename
2131 checkosfilename = checkwinfilename
2131 if not timer:
2132 if not timer:
2132 timer = time.clock
2133 timer = time.clock
2133 else:
2134 else:
2134 # mercurial.windows doesn't have platform.checkosfilename
2135 # mercurial.windows doesn't have platform.checkosfilename
2135 checkosfilename = platform.checkosfilename # pytype: disable=module-attr
2136 checkosfilename = platform.checkosfilename # pytype: disable=module-attr
2136 if not timer:
2137 if not timer:
2137 timer = time.time
2138 timer = time.time
2138
2139
2139
2140
2140 def makelock(info, pathname):
2141 def makelock(info, pathname):
2141 """Create a lock file atomically if possible
2142 """Create a lock file atomically if possible
2142
2143
2143 This may leave a stale lock file if symlink isn't supported and signal
2144 This may leave a stale lock file if symlink isn't supported and signal
2144 interrupt is enabled.
2145 interrupt is enabled.
2145 """
2146 """
2146 try:
2147 try:
2147 return os.symlink(info, pathname)
2148 return os.symlink(info, pathname)
2148 except OSError as why:
2149 except OSError as why:
2149 if why.errno == errno.EEXIST:
2150 if why.errno == errno.EEXIST:
2150 raise
2151 raise
2151 except AttributeError: # no symlink in os
2152 except AttributeError: # no symlink in os
2152 pass
2153 pass
2153
2154
2154 flags = os.O_CREAT | os.O_WRONLY | os.O_EXCL | getattr(os, 'O_BINARY', 0)
2155 flags = os.O_CREAT | os.O_WRONLY | os.O_EXCL | getattr(os, 'O_BINARY', 0)
2155 ld = os.open(pathname, flags)
2156 ld = os.open(pathname, flags)
2156 os.write(ld, info)
2157 os.write(ld, info)
2157 os.close(ld)
2158 os.close(ld)
2158
2159
2159
2160
2160 def readlock(pathname):
2161 def readlock(pathname):
2161 # type: (bytes) -> bytes
2162 # type: (bytes) -> bytes
2162 try:
2163 try:
2163 return readlink(pathname)
2164 return readlink(pathname)
2164 except OSError as why:
2165 except OSError as why:
2165 if why.errno not in (errno.EINVAL, errno.ENOSYS):
2166 if why.errno not in (errno.EINVAL, errno.ENOSYS):
2166 raise
2167 raise
2167 except AttributeError: # no symlink in os
2168 except AttributeError: # no symlink in os
2168 pass
2169 pass
2169 with posixfile(pathname, b'rb') as fp:
2170 with posixfile(pathname, b'rb') as fp:
2170 return fp.read()
2171 return fp.read()
2171
2172
2172
2173
2173 def fstat(fp):
2174 def fstat(fp):
2174 '''stat file object that may not have fileno method.'''
2175 '''stat file object that may not have fileno method.'''
2175 try:
2176 try:
2176 return os.fstat(fp.fileno())
2177 return os.fstat(fp.fileno())
2177 except AttributeError:
2178 except AttributeError:
2178 return os.stat(fp.name)
2179 return os.stat(fp.name)
2179
2180
2180
2181
2181 # File system features
2182 # File system features
2182
2183
2183
2184
2184 def fscasesensitive(path):
2185 def fscasesensitive(path):
2185 # type: (bytes) -> bool
2186 # type: (bytes) -> bool
2186 """
2187 """
2187 Return true if the given path is on a case-sensitive filesystem
2188 Return true if the given path is on a case-sensitive filesystem
2188
2189
2189 Requires a path (like /foo/.hg) ending with a foldable final
2190 Requires a path (like /foo/.hg) ending with a foldable final
2190 directory component.
2191 directory component.
2191 """
2192 """
2192 s1 = os.lstat(path)
2193 s1 = os.lstat(path)
2193 d, b = os.path.split(path)
2194 d, b = os.path.split(path)
2194 b2 = b.upper()
2195 b2 = b.upper()
2195 if b == b2:
2196 if b == b2:
2196 b2 = b.lower()
2197 b2 = b.lower()
2197 if b == b2:
2198 if b == b2:
2198 return True # no evidence against case sensitivity
2199 return True # no evidence against case sensitivity
2199 p2 = os.path.join(d, b2)
2200 p2 = os.path.join(d, b2)
2200 try:
2201 try:
2201 s2 = os.lstat(p2)
2202 s2 = os.lstat(p2)
2202 if s2 == s1:
2203 if s2 == s1:
2203 return False
2204 return False
2204 return True
2205 return True
2205 except OSError:
2206 except OSError:
2206 return True
2207 return True
2207
2208
2208
2209
2209 _re2_input = lambda x: x
2210 _re2_input = lambda x: x
2210 try:
2211 try:
2211 import re2 # pytype: disable=import-error
2212 import re2 # pytype: disable=import-error
2212
2213
2213 _re2 = None
2214 _re2 = None
2214 except ImportError:
2215 except ImportError:
2215 _re2 = False
2216 _re2 = False
2216
2217
2217
2218
2218 class _re(object):
2219 class _re(object):
2219 def _checkre2(self):
2220 def _checkre2(self):
2220 global _re2
2221 global _re2
2221 global _re2_input
2222 global _re2_input
2222
2223
2223 check_pattern = br'\[([^\[]+)\]'
2224 check_pattern = br'\[([^\[]+)\]'
2224 check_input = b'[ui]'
2225 check_input = b'[ui]'
2225 try:
2226 try:
2226 # check if match works, see issue3964
2227 # check if match works, see issue3964
2227 _re2 = bool(re2.match(check_pattern, check_input))
2228 _re2 = bool(re2.match(check_pattern, check_input))
2228 except ImportError:
2229 except ImportError:
2229 _re2 = False
2230 _re2 = False
2230 except TypeError:
2231 except TypeError:
2231 # the `pyre-2` project provides a re2 module that accept bytes
2232 # the `pyre-2` project provides a re2 module that accept bytes
2232 # the `fb-re2` project provides a re2 module that acccept sysstr
2233 # the `fb-re2` project provides a re2 module that acccept sysstr
2233 check_pattern = pycompat.sysstr(check_pattern)
2234 check_pattern = pycompat.sysstr(check_pattern)
2234 check_input = pycompat.sysstr(check_input)
2235 check_input = pycompat.sysstr(check_input)
2235 _re2 = bool(re2.match(check_pattern, check_input))
2236 _re2 = bool(re2.match(check_pattern, check_input))
2236 _re2_input = pycompat.sysstr
2237 _re2_input = pycompat.sysstr
2237
2238
2238 def compile(self, pat, flags=0):
2239 def compile(self, pat, flags=0):
2239 """Compile a regular expression, using re2 if possible
2240 """Compile a regular expression, using re2 if possible
2240
2241
2241 For best performance, use only re2-compatible regexp features. The
2242 For best performance, use only re2-compatible regexp features. The
2242 only flags from the re module that are re2-compatible are
2243 only flags from the re module that are re2-compatible are
2243 IGNORECASE and MULTILINE."""
2244 IGNORECASE and MULTILINE."""
2244 if _re2 is None:
2245 if _re2 is None:
2245 self._checkre2()
2246 self._checkre2()
2246 if _re2 and (flags & ~(remod.IGNORECASE | remod.MULTILINE)) == 0:
2247 if _re2 and (flags & ~(remod.IGNORECASE | remod.MULTILINE)) == 0:
2247 if flags & remod.IGNORECASE:
2248 if flags & remod.IGNORECASE:
2248 pat = b'(?i)' + pat
2249 pat = b'(?i)' + pat
2249 if flags & remod.MULTILINE:
2250 if flags & remod.MULTILINE:
2250 pat = b'(?m)' + pat
2251 pat = b'(?m)' + pat
2251 try:
2252 try:
2252 return re2.compile(_re2_input(pat))
2253 return re2.compile(_re2_input(pat))
2253 except re2.error:
2254 except re2.error:
2254 pass
2255 pass
2255 return remod.compile(pat, flags)
2256 return remod.compile(pat, flags)
2256
2257
2257 @propertycache
2258 @propertycache
2258 def escape(self):
2259 def escape(self):
2259 """Return the version of escape corresponding to self.compile.
2260 """Return the version of escape corresponding to self.compile.
2260
2261
2261 This is imperfect because whether re2 or re is used for a particular
2262 This is imperfect because whether re2 or re is used for a particular
2262 function depends on the flags, etc, but it's the best we can do.
2263 function depends on the flags, etc, but it's the best we can do.
2263 """
2264 """
2264 global _re2
2265 global _re2
2265 if _re2 is None:
2266 if _re2 is None:
2266 self._checkre2()
2267 self._checkre2()
2267 if _re2:
2268 if _re2:
2268 return re2.escape
2269 return re2.escape
2269 else:
2270 else:
2270 return remod.escape
2271 return remod.escape
2271
2272
2272
2273
2273 re = _re()
2274 re = _re()
2274
2275
2275 _fspathcache = {}
2276 _fspathcache = {}
2276
2277
2277
2278
2278 def fspath(name, root):
2279 def fspath(name, root):
2279 # type: (bytes, bytes) -> bytes
2280 # type: (bytes, bytes) -> bytes
2280 """Get name in the case stored in the filesystem
2281 """Get name in the case stored in the filesystem
2281
2282
2282 The name should be relative to root, and be normcase-ed for efficiency.
2283 The name should be relative to root, and be normcase-ed for efficiency.
2283
2284
2284 Note that this function is unnecessary, and should not be
2285 Note that this function is unnecessary, and should not be
2285 called, for case-sensitive filesystems (simply because it's expensive).
2286 called, for case-sensitive filesystems (simply because it's expensive).
2286
2287
2287 The root should be normcase-ed, too.
2288 The root should be normcase-ed, too.
2288 """
2289 """
2289
2290
2290 def _makefspathcacheentry(dir):
2291 def _makefspathcacheentry(dir):
2291 return {normcase(n): n for n in os.listdir(dir)}
2292 return {normcase(n): n for n in os.listdir(dir)}
2292
2293
2293 seps = pycompat.ossep
2294 seps = pycompat.ossep
2294 if pycompat.osaltsep:
2295 if pycompat.osaltsep:
2295 seps = seps + pycompat.osaltsep
2296 seps = seps + pycompat.osaltsep
2296 # Protect backslashes. This gets silly very quickly.
2297 # Protect backslashes. This gets silly very quickly.
2297 seps.replace(b'\\', b'\\\\')
2298 seps.replace(b'\\', b'\\\\')
2298 pattern = remod.compile(br'([^%s]+)|([%s]+)' % (seps, seps))
2299 pattern = remod.compile(br'([^%s]+)|([%s]+)' % (seps, seps))
2299 dir = os.path.normpath(root)
2300 dir = os.path.normpath(root)
2300 result = []
2301 result = []
2301 for part, sep in pattern.findall(name):
2302 for part, sep in pattern.findall(name):
2302 if sep:
2303 if sep:
2303 result.append(sep)
2304 result.append(sep)
2304 continue
2305 continue
2305
2306
2306 if dir not in _fspathcache:
2307 if dir not in _fspathcache:
2307 _fspathcache[dir] = _makefspathcacheentry(dir)
2308 _fspathcache[dir] = _makefspathcacheentry(dir)
2308 contents = _fspathcache[dir]
2309 contents = _fspathcache[dir]
2309
2310
2310 found = contents.get(part)
2311 found = contents.get(part)
2311 if not found:
2312 if not found:
2312 # retry "once per directory" per "dirstate.walk" which
2313 # retry "once per directory" per "dirstate.walk" which
2313 # may take place for each patches of "hg qpush", for example
2314 # may take place for each patches of "hg qpush", for example
2314 _fspathcache[dir] = contents = _makefspathcacheentry(dir)
2315 _fspathcache[dir] = contents = _makefspathcacheentry(dir)
2315 found = contents.get(part)
2316 found = contents.get(part)
2316
2317
2317 result.append(found or part)
2318 result.append(found or part)
2318 dir = os.path.join(dir, part)
2319 dir = os.path.join(dir, part)
2319
2320
2320 return b''.join(result)
2321 return b''.join(result)
2321
2322
2322
2323
2323 def checknlink(testfile):
2324 def checknlink(testfile):
2324 # type: (bytes) -> bool
2325 # type: (bytes) -> bool
2325 '''check whether hardlink count reporting works properly'''
2326 '''check whether hardlink count reporting works properly'''
2326
2327
2327 # testfile may be open, so we need a separate file for checking to
2328 # testfile may be open, so we need a separate file for checking to
2328 # work around issue2543 (or testfile may get lost on Samba shares)
2329 # work around issue2543 (or testfile may get lost on Samba shares)
2329 f1, f2, fp = None, None, None
2330 f1, f2, fp = None, None, None
2330 try:
2331 try:
2331 fd, f1 = pycompat.mkstemp(
2332 fd, f1 = pycompat.mkstemp(
2332 prefix=b'.%s-' % os.path.basename(testfile),
2333 prefix=b'.%s-' % os.path.basename(testfile),
2333 suffix=b'1~',
2334 suffix=b'1~',
2334 dir=os.path.dirname(testfile),
2335 dir=os.path.dirname(testfile),
2335 )
2336 )
2336 os.close(fd)
2337 os.close(fd)
2337 f2 = b'%s2~' % f1[:-2]
2338 f2 = b'%s2~' % f1[:-2]
2338
2339
2339 oslink(f1, f2)
2340 oslink(f1, f2)
2340 # nlinks() may behave differently for files on Windows shares if
2341 # nlinks() may behave differently for files on Windows shares if
2341 # the file is open.
2342 # the file is open.
2342 fp = posixfile(f2)
2343 fp = posixfile(f2)
2343 return nlinks(f2) > 1
2344 return nlinks(f2) > 1
2344 except OSError:
2345 except OSError:
2345 return False
2346 return False
2346 finally:
2347 finally:
2347 if fp is not None:
2348 if fp is not None:
2348 fp.close()
2349 fp.close()
2349 for f in (f1, f2):
2350 for f in (f1, f2):
2350 try:
2351 try:
2351 if f is not None:
2352 if f is not None:
2352 os.unlink(f)
2353 os.unlink(f)
2353 except OSError:
2354 except OSError:
2354 pass
2355 pass
2355
2356
2356
2357
2357 def endswithsep(path):
2358 def endswithsep(path):
2358 # type: (bytes) -> bool
2359 # type: (bytes) -> bool
2359 '''Check path ends with os.sep or os.altsep.'''
2360 '''Check path ends with os.sep or os.altsep.'''
2360 return bool( # help pytype
2361 return bool( # help pytype
2361 path.endswith(pycompat.ossep)
2362 path.endswith(pycompat.ossep)
2362 or pycompat.osaltsep
2363 or pycompat.osaltsep
2363 and path.endswith(pycompat.osaltsep)
2364 and path.endswith(pycompat.osaltsep)
2364 )
2365 )
2365
2366
2366
2367
2367 def splitpath(path):
2368 def splitpath(path):
2368 # type: (bytes) -> List[bytes]
2369 # type: (bytes) -> List[bytes]
2369 """Split path by os.sep.
2370 """Split path by os.sep.
2370 Note that this function does not use os.altsep because this is
2371 Note that this function does not use os.altsep because this is
2371 an alternative of simple "xxx.split(os.sep)".
2372 an alternative of simple "xxx.split(os.sep)".
2372 It is recommended to use os.path.normpath() before using this
2373 It is recommended to use os.path.normpath() before using this
2373 function if need."""
2374 function if need."""
2374 return path.split(pycompat.ossep)
2375 return path.split(pycompat.ossep)
2375
2376
2376
2377
2377 def mktempcopy(name, emptyok=False, createmode=None, enforcewritable=False):
2378 def mktempcopy(name, emptyok=False, createmode=None, enforcewritable=False):
2378 """Create a temporary file with the same contents from name
2379 """Create a temporary file with the same contents from name
2379
2380
2380 The permission bits are copied from the original file.
2381 The permission bits are copied from the original file.
2381
2382
2382 If the temporary file is going to be truncated immediately, you
2383 If the temporary file is going to be truncated immediately, you
2383 can use emptyok=True as an optimization.
2384 can use emptyok=True as an optimization.
2384
2385
2385 Returns the name of the temporary file.
2386 Returns the name of the temporary file.
2386 """
2387 """
2387 d, fn = os.path.split(name)
2388 d, fn = os.path.split(name)
2388 fd, temp = pycompat.mkstemp(prefix=b'.%s-' % fn, suffix=b'~', dir=d)
2389 fd, temp = pycompat.mkstemp(prefix=b'.%s-' % fn, suffix=b'~', dir=d)
2389 os.close(fd)
2390 os.close(fd)
2390 # Temporary files are created with mode 0600, which is usually not
2391 # Temporary files are created with mode 0600, which is usually not
2391 # what we want. If the original file already exists, just copy
2392 # what we want. If the original file already exists, just copy
2392 # its mode. Otherwise, manually obey umask.
2393 # its mode. Otherwise, manually obey umask.
2393 copymode(name, temp, createmode, enforcewritable)
2394 copymode(name, temp, createmode, enforcewritable)
2394
2395
2395 if emptyok:
2396 if emptyok:
2396 return temp
2397 return temp
2397 try:
2398 try:
2398 try:
2399 try:
2399 ifp = posixfile(name, b"rb")
2400 ifp = posixfile(name, b"rb")
2400 except IOError as inst:
2401 except IOError as inst:
2401 if inst.errno == errno.ENOENT:
2402 if inst.errno == errno.ENOENT:
2402 return temp
2403 return temp
2403 if not getattr(inst, 'filename', None):
2404 if not getattr(inst, 'filename', None):
2404 inst.filename = name
2405 inst.filename = name
2405 raise
2406 raise
2406 ofp = posixfile(temp, b"wb")
2407 ofp = posixfile(temp, b"wb")
2407 for chunk in filechunkiter(ifp):
2408 for chunk in filechunkiter(ifp):
2408 ofp.write(chunk)
2409 ofp.write(chunk)
2409 ifp.close()
2410 ifp.close()
2410 ofp.close()
2411 ofp.close()
2411 except: # re-raises
2412 except: # re-raises
2412 try:
2413 try:
2413 os.unlink(temp)
2414 os.unlink(temp)
2414 except OSError:
2415 except OSError:
2415 pass
2416 pass
2416 raise
2417 raise
2417 return temp
2418 return temp
2418
2419
2419
2420
2420 class filestat(object):
2421 class filestat(object):
2421 """help to exactly detect change of a file
2422 """help to exactly detect change of a file
2422
2423
2423 'stat' attribute is result of 'os.stat()' if specified 'path'
2424 'stat' attribute is result of 'os.stat()' if specified 'path'
2424 exists. Otherwise, it is None. This can avoid preparative
2425 exists. Otherwise, it is None. This can avoid preparative
2425 'exists()' examination on client side of this class.
2426 'exists()' examination on client side of this class.
2426 """
2427 """
2427
2428
2428 def __init__(self, stat):
2429 def __init__(self, stat):
2429 self.stat = stat
2430 self.stat = stat
2430
2431
2431 @classmethod
2432 @classmethod
2432 def frompath(cls, path):
2433 def frompath(cls, path):
2433 try:
2434 try:
2434 stat = os.stat(path)
2435 stat = os.stat(path)
2435 except OSError as err:
2436 except OSError as err:
2436 if err.errno != errno.ENOENT:
2437 if err.errno != errno.ENOENT:
2437 raise
2438 raise
2438 stat = None
2439 stat = None
2439 return cls(stat)
2440 return cls(stat)
2440
2441
2441 @classmethod
2442 @classmethod
2442 def fromfp(cls, fp):
2443 def fromfp(cls, fp):
2443 stat = os.fstat(fp.fileno())
2444 stat = os.fstat(fp.fileno())
2444 return cls(stat)
2445 return cls(stat)
2445
2446
2446 __hash__ = object.__hash__
2447 __hash__ = object.__hash__
2447
2448
2448 def __eq__(self, old):
2449 def __eq__(self, old):
2449 try:
2450 try:
2450 # if ambiguity between stat of new and old file is
2451 # if ambiguity between stat of new and old file is
2451 # avoided, comparison of size, ctime and mtime is enough
2452 # avoided, comparison of size, ctime and mtime is enough
2452 # to exactly detect change of a file regardless of platform
2453 # to exactly detect change of a file regardless of platform
2453 return (
2454 return (
2454 self.stat.st_size == old.stat.st_size
2455 self.stat.st_size == old.stat.st_size
2455 and self.stat[stat.ST_CTIME] == old.stat[stat.ST_CTIME]
2456 and self.stat[stat.ST_CTIME] == old.stat[stat.ST_CTIME]
2456 and self.stat[stat.ST_MTIME] == old.stat[stat.ST_MTIME]
2457 and self.stat[stat.ST_MTIME] == old.stat[stat.ST_MTIME]
2457 )
2458 )
2458 except AttributeError:
2459 except AttributeError:
2459 pass
2460 pass
2460 try:
2461 try:
2461 return self.stat is None and old.stat is None
2462 return self.stat is None and old.stat is None
2462 except AttributeError:
2463 except AttributeError:
2463 return False
2464 return False
2464
2465
2465 def isambig(self, old):
2466 def isambig(self, old):
2466 """Examine whether new (= self) stat is ambiguous against old one
2467 """Examine whether new (= self) stat is ambiguous against old one
2467
2468
2468 "S[N]" below means stat of a file at N-th change:
2469 "S[N]" below means stat of a file at N-th change:
2469
2470
2470 - S[n-1].ctime < S[n].ctime: can detect change of a file
2471 - S[n-1].ctime < S[n].ctime: can detect change of a file
2471 - S[n-1].ctime == S[n].ctime
2472 - S[n-1].ctime == S[n].ctime
2472 - S[n-1].ctime < S[n].mtime: means natural advancing (*1)
2473 - S[n-1].ctime < S[n].mtime: means natural advancing (*1)
2473 - S[n-1].ctime == S[n].mtime: is ambiguous (*2)
2474 - S[n-1].ctime == S[n].mtime: is ambiguous (*2)
2474 - S[n-1].ctime > S[n].mtime: never occurs naturally (don't care)
2475 - S[n-1].ctime > S[n].mtime: never occurs naturally (don't care)
2475 - S[n-1].ctime > S[n].ctime: never occurs naturally (don't care)
2476 - S[n-1].ctime > S[n].ctime: never occurs naturally (don't care)
2476
2477
2477 Case (*2) above means that a file was changed twice or more at
2478 Case (*2) above means that a file was changed twice or more at
2478 same time in sec (= S[n-1].ctime), and comparison of timestamp
2479 same time in sec (= S[n-1].ctime), and comparison of timestamp
2479 is ambiguous.
2480 is ambiguous.
2480
2481
2481 Base idea to avoid such ambiguity is "advance mtime 1 sec, if
2482 Base idea to avoid such ambiguity is "advance mtime 1 sec, if
2482 timestamp is ambiguous".
2483 timestamp is ambiguous".
2483
2484
2484 But advancing mtime only in case (*2) doesn't work as
2485 But advancing mtime only in case (*2) doesn't work as
2485 expected, because naturally advanced S[n].mtime in case (*1)
2486 expected, because naturally advanced S[n].mtime in case (*1)
2486 might be equal to manually advanced S[n-1 or earlier].mtime.
2487 might be equal to manually advanced S[n-1 or earlier].mtime.
2487
2488
2488 Therefore, all "S[n-1].ctime == S[n].ctime" cases should be
2489 Therefore, all "S[n-1].ctime == S[n].ctime" cases should be
2489 treated as ambiguous regardless of mtime, to avoid overlooking
2490 treated as ambiguous regardless of mtime, to avoid overlooking
2490 by confliction between such mtime.
2491 by confliction between such mtime.
2491
2492
2492 Advancing mtime "if isambig(oldstat)" ensures "S[n-1].mtime !=
2493 Advancing mtime "if isambig(oldstat)" ensures "S[n-1].mtime !=
2493 S[n].mtime", even if size of a file isn't changed.
2494 S[n].mtime", even if size of a file isn't changed.
2494 """
2495 """
2495 try:
2496 try:
2496 return self.stat[stat.ST_CTIME] == old.stat[stat.ST_CTIME]
2497 return self.stat[stat.ST_CTIME] == old.stat[stat.ST_CTIME]
2497 except AttributeError:
2498 except AttributeError:
2498 return False
2499 return False
2499
2500
2500 def avoidambig(self, path, old):
2501 def avoidambig(self, path, old):
2501 """Change file stat of specified path to avoid ambiguity
2502 """Change file stat of specified path to avoid ambiguity
2502
2503
2503 'old' should be previous filestat of 'path'.
2504 'old' should be previous filestat of 'path'.
2504
2505
2505 This skips avoiding ambiguity, if a process doesn't have
2506 This skips avoiding ambiguity, if a process doesn't have
2506 appropriate privileges for 'path'. This returns False in this
2507 appropriate privileges for 'path'. This returns False in this
2507 case.
2508 case.
2508
2509
2509 Otherwise, this returns True, as "ambiguity is avoided".
2510 Otherwise, this returns True, as "ambiguity is avoided".
2510 """
2511 """
2511 advanced = (old.stat[stat.ST_MTIME] + 1) & 0x7FFFFFFF
2512 advanced = (old.stat[stat.ST_MTIME] + 1) & 0x7FFFFFFF
2512 try:
2513 try:
2513 os.utime(path, (advanced, advanced))
2514 os.utime(path, (advanced, advanced))
2514 except OSError as inst:
2515 except OSError as inst:
2515 if inst.errno == errno.EPERM:
2516 if inst.errno == errno.EPERM:
2516 # utime() on the file created by another user causes EPERM,
2517 # utime() on the file created by another user causes EPERM,
2517 # if a process doesn't have appropriate privileges
2518 # if a process doesn't have appropriate privileges
2518 return False
2519 return False
2519 raise
2520 raise
2520 return True
2521 return True
2521
2522
2522 def __ne__(self, other):
2523 def __ne__(self, other):
2523 return not self == other
2524 return not self == other
2524
2525
2525
2526
2526 class atomictempfile(object):
2527 class atomictempfile(object):
2527 """writable file object that atomically updates a file
2528 """writable file object that atomically updates a file
2528
2529
2529 All writes will go to a temporary copy of the original file. Call
2530 All writes will go to a temporary copy of the original file. Call
2530 close() when you are done writing, and atomictempfile will rename
2531 close() when you are done writing, and atomictempfile will rename
2531 the temporary copy to the original name, making the changes
2532 the temporary copy to the original name, making the changes
2532 visible. If the object is destroyed without being closed, all your
2533 visible. If the object is destroyed without being closed, all your
2533 writes are discarded.
2534 writes are discarded.
2534
2535
2535 checkambig argument of constructor is used with filestat, and is
2536 checkambig argument of constructor is used with filestat, and is
2536 useful only if target file is guarded by any lock (e.g. repo.lock
2537 useful only if target file is guarded by any lock (e.g. repo.lock
2537 or repo.wlock).
2538 or repo.wlock).
2538 """
2539 """
2539
2540
2540 def __init__(self, name, mode=b'w+b', createmode=None, checkambig=False):
2541 def __init__(self, name, mode=b'w+b', createmode=None, checkambig=False):
2541 self.__name = name # permanent name
2542 self.__name = name # permanent name
2542 self._tempname = mktempcopy(
2543 self._tempname = mktempcopy(
2543 name,
2544 name,
2544 emptyok=(b'w' in mode),
2545 emptyok=(b'w' in mode),
2545 createmode=createmode,
2546 createmode=createmode,
2546 enforcewritable=(b'w' in mode),
2547 enforcewritable=(b'w' in mode),
2547 )
2548 )
2548
2549
2549 self._fp = posixfile(self._tempname, mode)
2550 self._fp = posixfile(self._tempname, mode)
2550 self._checkambig = checkambig
2551 self._checkambig = checkambig
2551
2552
2552 # delegated methods
2553 # delegated methods
2553 self.read = self._fp.read
2554 self.read = self._fp.read
2554 self.write = self._fp.write
2555 self.write = self._fp.write
2555 self.seek = self._fp.seek
2556 self.seek = self._fp.seek
2556 self.tell = self._fp.tell
2557 self.tell = self._fp.tell
2557 self.fileno = self._fp.fileno
2558 self.fileno = self._fp.fileno
2558
2559
2559 def close(self):
2560 def close(self):
2560 if not self._fp.closed:
2561 if not self._fp.closed:
2561 self._fp.close()
2562 self._fp.close()
2562 filename = localpath(self.__name)
2563 filename = localpath(self.__name)
2563 oldstat = self._checkambig and filestat.frompath(filename)
2564 oldstat = self._checkambig and filestat.frompath(filename)
2564 if oldstat and oldstat.stat:
2565 if oldstat and oldstat.stat:
2565 rename(self._tempname, filename)
2566 rename(self._tempname, filename)
2566 newstat = filestat.frompath(filename)
2567 newstat = filestat.frompath(filename)
2567 if newstat.isambig(oldstat):
2568 if newstat.isambig(oldstat):
2568 # stat of changed file is ambiguous to original one
2569 # stat of changed file is ambiguous to original one
2569 advanced = (oldstat.stat[stat.ST_MTIME] + 1) & 0x7FFFFFFF
2570 advanced = (oldstat.stat[stat.ST_MTIME] + 1) & 0x7FFFFFFF
2570 os.utime(filename, (advanced, advanced))
2571 os.utime(filename, (advanced, advanced))
2571 else:
2572 else:
2572 rename(self._tempname, filename)
2573 rename(self._tempname, filename)
2573
2574
2574 def discard(self):
2575 def discard(self):
2575 if not self._fp.closed:
2576 if not self._fp.closed:
2576 try:
2577 try:
2577 os.unlink(self._tempname)
2578 os.unlink(self._tempname)
2578 except OSError:
2579 except OSError:
2579 pass
2580 pass
2580 self._fp.close()
2581 self._fp.close()
2581
2582
2582 def __del__(self):
2583 def __del__(self):
2583 if safehasattr(self, '_fp'): # constructor actually did something
2584 if safehasattr(self, '_fp'): # constructor actually did something
2584 self.discard()
2585 self.discard()
2585
2586
2586 def __enter__(self):
2587 def __enter__(self):
2587 return self
2588 return self
2588
2589
2589 def __exit__(self, exctype, excvalue, traceback):
2590 def __exit__(self, exctype, excvalue, traceback):
2590 if exctype is not None:
2591 if exctype is not None:
2591 self.discard()
2592 self.discard()
2592 else:
2593 else:
2593 self.close()
2594 self.close()
2594
2595
2595
2596
2596 def unlinkpath(f, ignoremissing=False, rmdir=True):
2597 def unlinkpath(f, ignoremissing=False, rmdir=True):
2597 # type: (bytes, bool, bool) -> None
2598 # type: (bytes, bool, bool) -> None
2598 """unlink and remove the directory if it is empty"""
2599 """unlink and remove the directory if it is empty"""
2599 if ignoremissing:
2600 if ignoremissing:
2600 tryunlink(f)
2601 tryunlink(f)
2601 else:
2602 else:
2602 unlink(f)
2603 unlink(f)
2603 if rmdir:
2604 if rmdir:
2604 # try removing directories that might now be empty
2605 # try removing directories that might now be empty
2605 try:
2606 try:
2606 removedirs(os.path.dirname(f))
2607 removedirs(os.path.dirname(f))
2607 except OSError:
2608 except OSError:
2608 pass
2609 pass
2609
2610
2610
2611
2611 def tryunlink(f):
2612 def tryunlink(f):
2612 # type: (bytes) -> None
2613 # type: (bytes) -> None
2613 """Attempt to remove a file, ignoring ENOENT errors."""
2614 """Attempt to remove a file, ignoring ENOENT errors."""
2614 try:
2615 try:
2615 unlink(f)
2616 unlink(f)
2616 except OSError as e:
2617 except OSError as e:
2617 if e.errno != errno.ENOENT:
2618 if e.errno != errno.ENOENT:
2618 raise
2619 raise
2619
2620
2620
2621
2621 def makedirs(name, mode=None, notindexed=False):
2622 def makedirs(name, mode=None, notindexed=False):
2622 # type: (bytes, Optional[int], bool) -> None
2623 # type: (bytes, Optional[int], bool) -> None
2623 """recursive directory creation with parent mode inheritance
2624 """recursive directory creation with parent mode inheritance
2624
2625
2625 Newly created directories are marked as "not to be indexed by
2626 Newly created directories are marked as "not to be indexed by
2626 the content indexing service", if ``notindexed`` is specified
2627 the content indexing service", if ``notindexed`` is specified
2627 for "write" mode access.
2628 for "write" mode access.
2628 """
2629 """
2629 try:
2630 try:
2630 makedir(name, notindexed)
2631 makedir(name, notindexed)
2631 except OSError as err:
2632 except OSError as err:
2632 if err.errno == errno.EEXIST:
2633 if err.errno == errno.EEXIST:
2633 return
2634 return
2634 if err.errno != errno.ENOENT or not name:
2635 if err.errno != errno.ENOENT or not name:
2635 raise
2636 raise
2636 parent = os.path.dirname(abspath(name))
2637 parent = os.path.dirname(abspath(name))
2637 if parent == name:
2638 if parent == name:
2638 raise
2639 raise
2639 makedirs(parent, mode, notindexed)
2640 makedirs(parent, mode, notindexed)
2640 try:
2641 try:
2641 makedir(name, notindexed)
2642 makedir(name, notindexed)
2642 except OSError as err:
2643 except OSError as err:
2643 # Catch EEXIST to handle races
2644 # Catch EEXIST to handle races
2644 if err.errno == errno.EEXIST:
2645 if err.errno == errno.EEXIST:
2645 return
2646 return
2646 raise
2647 raise
2647 if mode is not None:
2648 if mode is not None:
2648 os.chmod(name, mode)
2649 os.chmod(name, mode)
2649
2650
2650
2651
2651 def readfile(path):
2652 def readfile(path):
2652 # type: (bytes) -> bytes
2653 # type: (bytes) -> bytes
2653 with open(path, b'rb') as fp:
2654 with open(path, b'rb') as fp:
2654 return fp.read()
2655 return fp.read()
2655
2656
2656
2657
2657 def writefile(path, text):
2658 def writefile(path, text):
2658 # type: (bytes, bytes) -> None
2659 # type: (bytes, bytes) -> None
2659 with open(path, b'wb') as fp:
2660 with open(path, b'wb') as fp:
2660 fp.write(text)
2661 fp.write(text)
2661
2662
2662
2663
2663 def appendfile(path, text):
2664 def appendfile(path, text):
2664 # type: (bytes, bytes) -> None
2665 # type: (bytes, bytes) -> None
2665 with open(path, b'ab') as fp:
2666 with open(path, b'ab') as fp:
2666 fp.write(text)
2667 fp.write(text)
2667
2668
2668
2669
2669 class chunkbuffer(object):
2670 class chunkbuffer(object):
2670 """Allow arbitrary sized chunks of data to be efficiently read from an
2671 """Allow arbitrary sized chunks of data to be efficiently read from an
2671 iterator over chunks of arbitrary size."""
2672 iterator over chunks of arbitrary size."""
2672
2673
2673 def __init__(self, in_iter):
2674 def __init__(self, in_iter):
2674 """in_iter is the iterator that's iterating over the input chunks."""
2675 """in_iter is the iterator that's iterating over the input chunks."""
2675
2676
2676 def splitbig(chunks):
2677 def splitbig(chunks):
2677 for chunk in chunks:
2678 for chunk in chunks:
2678 if len(chunk) > 2 ** 20:
2679 if len(chunk) > 2 ** 20:
2679 pos = 0
2680 pos = 0
2680 while pos < len(chunk):
2681 while pos < len(chunk):
2681 end = pos + 2 ** 18
2682 end = pos + 2 ** 18
2682 yield chunk[pos:end]
2683 yield chunk[pos:end]
2683 pos = end
2684 pos = end
2684 else:
2685 else:
2685 yield chunk
2686 yield chunk
2686
2687
2687 self.iter = splitbig(in_iter)
2688 self.iter = splitbig(in_iter)
2688 self._queue = collections.deque()
2689 self._queue = collections.deque()
2689 self._chunkoffset = 0
2690 self._chunkoffset = 0
2690
2691
2691 def read(self, l=None):
2692 def read(self, l=None):
2692 """Read L bytes of data from the iterator of chunks of data.
2693 """Read L bytes of data from the iterator of chunks of data.
2693 Returns less than L bytes if the iterator runs dry.
2694 Returns less than L bytes if the iterator runs dry.
2694
2695
2695 If size parameter is omitted, read everything"""
2696 If size parameter is omitted, read everything"""
2696 if l is None:
2697 if l is None:
2697 return b''.join(self.iter)
2698 return b''.join(self.iter)
2698
2699
2699 left = l
2700 left = l
2700 buf = []
2701 buf = []
2701 queue = self._queue
2702 queue = self._queue
2702 while left > 0:
2703 while left > 0:
2703 # refill the queue
2704 # refill the queue
2704 if not queue:
2705 if not queue:
2705 target = 2 ** 18
2706 target = 2 ** 18
2706 for chunk in self.iter:
2707 for chunk in self.iter:
2707 queue.append(chunk)
2708 queue.append(chunk)
2708 target -= len(chunk)
2709 target -= len(chunk)
2709 if target <= 0:
2710 if target <= 0:
2710 break
2711 break
2711 if not queue:
2712 if not queue:
2712 break
2713 break
2713
2714
2714 # The easy way to do this would be to queue.popleft(), modify the
2715 # The easy way to do this would be to queue.popleft(), modify the
2715 # chunk (if necessary), then queue.appendleft(). However, for cases
2716 # chunk (if necessary), then queue.appendleft(). However, for cases
2716 # where we read partial chunk content, this incurs 2 dequeue
2717 # where we read partial chunk content, this incurs 2 dequeue
2717 # mutations and creates a new str for the remaining chunk in the
2718 # mutations and creates a new str for the remaining chunk in the
2718 # queue. Our code below avoids this overhead.
2719 # queue. Our code below avoids this overhead.
2719
2720
2720 chunk = queue[0]
2721 chunk = queue[0]
2721 chunkl = len(chunk)
2722 chunkl = len(chunk)
2722 offset = self._chunkoffset
2723 offset = self._chunkoffset
2723
2724
2724 # Use full chunk.
2725 # Use full chunk.
2725 if offset == 0 and left >= chunkl:
2726 if offset == 0 and left >= chunkl:
2726 left -= chunkl
2727 left -= chunkl
2727 queue.popleft()
2728 queue.popleft()
2728 buf.append(chunk)
2729 buf.append(chunk)
2729 # self._chunkoffset remains at 0.
2730 # self._chunkoffset remains at 0.
2730 continue
2731 continue
2731
2732
2732 chunkremaining = chunkl - offset
2733 chunkremaining = chunkl - offset
2733
2734
2734 # Use all of unconsumed part of chunk.
2735 # Use all of unconsumed part of chunk.
2735 if left >= chunkremaining:
2736 if left >= chunkremaining:
2736 left -= chunkremaining
2737 left -= chunkremaining
2737 queue.popleft()
2738 queue.popleft()
2738 # offset == 0 is enabled by block above, so this won't merely
2739 # offset == 0 is enabled by block above, so this won't merely
2739 # copy via ``chunk[0:]``.
2740 # copy via ``chunk[0:]``.
2740 buf.append(chunk[offset:])
2741 buf.append(chunk[offset:])
2741 self._chunkoffset = 0
2742 self._chunkoffset = 0
2742
2743
2743 # Partial chunk needed.
2744 # Partial chunk needed.
2744 else:
2745 else:
2745 buf.append(chunk[offset : offset + left])
2746 buf.append(chunk[offset : offset + left])
2746 self._chunkoffset += left
2747 self._chunkoffset += left
2747 left -= chunkremaining
2748 left -= chunkremaining
2748
2749
2749 return b''.join(buf)
2750 return b''.join(buf)
2750
2751
2751
2752
2752 def filechunkiter(f, size=131072, limit=None):
2753 def filechunkiter(f, size=131072, limit=None):
2753 """Create a generator that produces the data in the file size
2754 """Create a generator that produces the data in the file size
2754 (default 131072) bytes at a time, up to optional limit (default is
2755 (default 131072) bytes at a time, up to optional limit (default is
2755 to read all data). Chunks may be less than size bytes if the
2756 to read all data). Chunks may be less than size bytes if the
2756 chunk is the last chunk in the file, or the file is a socket or
2757 chunk is the last chunk in the file, or the file is a socket or
2757 some other type of file that sometimes reads less data than is
2758 some other type of file that sometimes reads less data than is
2758 requested."""
2759 requested."""
2759 assert size >= 0
2760 assert size >= 0
2760 assert limit is None or limit >= 0
2761 assert limit is None or limit >= 0
2761 while True:
2762 while True:
2762 if limit is None:
2763 if limit is None:
2763 nbytes = size
2764 nbytes = size
2764 else:
2765 else:
2765 nbytes = min(limit, size)
2766 nbytes = min(limit, size)
2766 s = nbytes and f.read(nbytes)
2767 s = nbytes and f.read(nbytes)
2767 if not s:
2768 if not s:
2768 break
2769 break
2769 if limit:
2770 if limit:
2770 limit -= len(s)
2771 limit -= len(s)
2771 yield s
2772 yield s
2772
2773
2773
2774
2774 class cappedreader(object):
2775 class cappedreader(object):
2775 """A file object proxy that allows reading up to N bytes.
2776 """A file object proxy that allows reading up to N bytes.
2776
2777
2777 Given a source file object, instances of this type allow reading up to
2778 Given a source file object, instances of this type allow reading up to
2778 N bytes from that source file object. Attempts to read past the allowed
2779 N bytes from that source file object. Attempts to read past the allowed
2779 limit are treated as EOF.
2780 limit are treated as EOF.
2780
2781
2781 It is assumed that I/O is not performed on the original file object
2782 It is assumed that I/O is not performed on the original file object
2782 in addition to I/O that is performed by this instance. If there is,
2783 in addition to I/O that is performed by this instance. If there is,
2783 state tracking will get out of sync and unexpected results will ensue.
2784 state tracking will get out of sync and unexpected results will ensue.
2784 """
2785 """
2785
2786
2786 def __init__(self, fh, limit):
2787 def __init__(self, fh, limit):
2787 """Allow reading up to <limit> bytes from <fh>."""
2788 """Allow reading up to <limit> bytes from <fh>."""
2788 self._fh = fh
2789 self._fh = fh
2789 self._left = limit
2790 self._left = limit
2790
2791
2791 def read(self, n=-1):
2792 def read(self, n=-1):
2792 if not self._left:
2793 if not self._left:
2793 return b''
2794 return b''
2794
2795
2795 if n < 0:
2796 if n < 0:
2796 n = self._left
2797 n = self._left
2797
2798
2798 data = self._fh.read(min(n, self._left))
2799 data = self._fh.read(min(n, self._left))
2799 self._left -= len(data)
2800 self._left -= len(data)
2800 assert self._left >= 0
2801 assert self._left >= 0
2801
2802
2802 return data
2803 return data
2803
2804
2804 def readinto(self, b):
2805 def readinto(self, b):
2805 res = self.read(len(b))
2806 res = self.read(len(b))
2806 if res is None:
2807 if res is None:
2807 return None
2808 return None
2808
2809
2809 b[0 : len(res)] = res
2810 b[0 : len(res)] = res
2810 return len(res)
2811 return len(res)
2811
2812
2812
2813
2813 def unitcountfn(*unittable):
2814 def unitcountfn(*unittable):
2814 '''return a function that renders a readable count of some quantity'''
2815 '''return a function that renders a readable count of some quantity'''
2815
2816
2816 def go(count):
2817 def go(count):
2817 for multiplier, divisor, format in unittable:
2818 for multiplier, divisor, format in unittable:
2818 if abs(count) >= divisor * multiplier:
2819 if abs(count) >= divisor * multiplier:
2819 return format % (count / float(divisor))
2820 return format % (count / float(divisor))
2820 return unittable[-1][2] % count
2821 return unittable[-1][2] % count
2821
2822
2822 return go
2823 return go
2823
2824
2824
2825
2825 def processlinerange(fromline, toline):
2826 def processlinerange(fromline, toline):
2826 # type: (int, int) -> Tuple[int, int]
2827 # type: (int, int) -> Tuple[int, int]
2827 """Check that linerange <fromline>:<toline> makes sense and return a
2828 """Check that linerange <fromline>:<toline> makes sense and return a
2828 0-based range.
2829 0-based range.
2829
2830
2830 >>> processlinerange(10, 20)
2831 >>> processlinerange(10, 20)
2831 (9, 20)
2832 (9, 20)
2832 >>> processlinerange(2, 1)
2833 >>> processlinerange(2, 1)
2833 Traceback (most recent call last):
2834 Traceback (most recent call last):
2834 ...
2835 ...
2835 ParseError: line range must be positive
2836 ParseError: line range must be positive
2836 >>> processlinerange(0, 5)
2837 >>> processlinerange(0, 5)
2837 Traceback (most recent call last):
2838 Traceback (most recent call last):
2838 ...
2839 ...
2839 ParseError: fromline must be strictly positive
2840 ParseError: fromline must be strictly positive
2840 """
2841 """
2841 if toline - fromline < 0:
2842 if toline - fromline < 0:
2842 raise error.ParseError(_(b"line range must be positive"))
2843 raise error.ParseError(_(b"line range must be positive"))
2843 if fromline < 1:
2844 if fromline < 1:
2844 raise error.ParseError(_(b"fromline must be strictly positive"))
2845 raise error.ParseError(_(b"fromline must be strictly positive"))
2845 return fromline - 1, toline
2846 return fromline - 1, toline
2846
2847
2847
2848
2848 bytecount = unitcountfn(
2849 bytecount = unitcountfn(
2849 (100, 1 << 30, _(b'%.0f GB')),
2850 (100, 1 << 30, _(b'%.0f GB')),
2850 (10, 1 << 30, _(b'%.1f GB')),
2851 (10, 1 << 30, _(b'%.1f GB')),
2851 (1, 1 << 30, _(b'%.2f GB')),
2852 (1, 1 << 30, _(b'%.2f GB')),
2852 (100, 1 << 20, _(b'%.0f MB')),
2853 (100, 1 << 20, _(b'%.0f MB')),
2853 (10, 1 << 20, _(b'%.1f MB')),
2854 (10, 1 << 20, _(b'%.1f MB')),
2854 (1, 1 << 20, _(b'%.2f MB')),
2855 (1, 1 << 20, _(b'%.2f MB')),
2855 (100, 1 << 10, _(b'%.0f KB')),
2856 (100, 1 << 10, _(b'%.0f KB')),
2856 (10, 1 << 10, _(b'%.1f KB')),
2857 (10, 1 << 10, _(b'%.1f KB')),
2857 (1, 1 << 10, _(b'%.2f KB')),
2858 (1, 1 << 10, _(b'%.2f KB')),
2858 (1, 1, _(b'%.0f bytes')),
2859 (1, 1, _(b'%.0f bytes')),
2859 )
2860 )
2860
2861
2861
2862
2862 class transformingwriter(object):
2863 class transformingwriter(object):
2863 """Writable file wrapper to transform data by function"""
2864 """Writable file wrapper to transform data by function"""
2864
2865
2865 def __init__(self, fp, encode):
2866 def __init__(self, fp, encode):
2866 self._fp = fp
2867 self._fp = fp
2867 self._encode = encode
2868 self._encode = encode
2868
2869
2869 def close(self):
2870 def close(self):
2870 self._fp.close()
2871 self._fp.close()
2871
2872
2872 def flush(self):
2873 def flush(self):
2873 self._fp.flush()
2874 self._fp.flush()
2874
2875
2875 def write(self, data):
2876 def write(self, data):
2876 return self._fp.write(self._encode(data))
2877 return self._fp.write(self._encode(data))
2877
2878
2878
2879
2879 # Matches a single EOL which can either be a CRLF where repeated CR
2880 # Matches a single EOL which can either be a CRLF where repeated CR
2880 # are removed or a LF. We do not care about old Macintosh files, so a
2881 # are removed or a LF. We do not care about old Macintosh files, so a
2881 # stray CR is an error.
2882 # stray CR is an error.
2882 _eolre = remod.compile(br'\r*\n')
2883 _eolre = remod.compile(br'\r*\n')
2883
2884
2884
2885
2885 def tolf(s):
2886 def tolf(s):
2886 # type: (bytes) -> bytes
2887 # type: (bytes) -> bytes
2887 return _eolre.sub(b'\n', s)
2888 return _eolre.sub(b'\n', s)
2888
2889
2889
2890
2890 def tocrlf(s):
2891 def tocrlf(s):
2891 # type: (bytes) -> bytes
2892 # type: (bytes) -> bytes
2892 return _eolre.sub(b'\r\n', s)
2893 return _eolre.sub(b'\r\n', s)
2893
2894
2894
2895
2895 def _crlfwriter(fp):
2896 def _crlfwriter(fp):
2896 return transformingwriter(fp, tocrlf)
2897 return transformingwriter(fp, tocrlf)
2897
2898
2898
2899
2899 if pycompat.oslinesep == b'\r\n':
2900 if pycompat.oslinesep == b'\r\n':
2900 tonativeeol = tocrlf
2901 tonativeeol = tocrlf
2901 fromnativeeol = tolf
2902 fromnativeeol = tolf
2902 nativeeolwriter = _crlfwriter
2903 nativeeolwriter = _crlfwriter
2903 else:
2904 else:
2904 tonativeeol = pycompat.identity
2905 tonativeeol = pycompat.identity
2905 fromnativeeol = pycompat.identity
2906 fromnativeeol = pycompat.identity
2906 nativeeolwriter = pycompat.identity
2907 nativeeolwriter = pycompat.identity
2907
2908
2908 if pyplatform.python_implementation() == b'CPython' and sys.version_info < (
2909 if pyplatform.python_implementation() == b'CPython' and sys.version_info < (
2909 3,
2910 3,
2910 0,
2911 0,
2911 ):
2912 ):
2912 # There is an issue in CPython that some IO methods do not handle EINTR
2913 # There is an issue in CPython that some IO methods do not handle EINTR
2913 # correctly. The following table shows what CPython version (and functions)
2914 # correctly. The following table shows what CPython version (and functions)
2914 # are affected (buggy: has the EINTR bug, okay: otherwise):
2915 # are affected (buggy: has the EINTR bug, okay: otherwise):
2915 #
2916 #
2916 # | < 2.7.4 | 2.7.4 to 2.7.12 | >= 3.0
2917 # | < 2.7.4 | 2.7.4 to 2.7.12 | >= 3.0
2917 # --------------------------------------------------
2918 # --------------------------------------------------
2918 # fp.__iter__ | buggy | buggy | okay
2919 # fp.__iter__ | buggy | buggy | okay
2919 # fp.read* | buggy | okay [1] | okay
2920 # fp.read* | buggy | okay [1] | okay
2920 #
2921 #
2921 # [1]: fixed by changeset 67dc99a989cd in the cpython hg repo.
2922 # [1]: fixed by changeset 67dc99a989cd in the cpython hg repo.
2922 #
2923 #
2923 # Here we workaround the EINTR issue for fileobj.__iter__. Other methods
2924 # Here we workaround the EINTR issue for fileobj.__iter__. Other methods
2924 # like "read*" work fine, as we do not support Python < 2.7.4.
2925 # like "read*" work fine, as we do not support Python < 2.7.4.
2925 #
2926 #
2926 # Although we can workaround the EINTR issue for fp.__iter__, it is slower:
2927 # Although we can workaround the EINTR issue for fp.__iter__, it is slower:
2927 # "for x in fp" is 4x faster than "for x in iter(fp.readline, '')" in
2928 # "for x in fp" is 4x faster than "for x in iter(fp.readline, '')" in
2928 # CPython 2, because CPython 2 maintains an internal readahead buffer for
2929 # CPython 2, because CPython 2 maintains an internal readahead buffer for
2929 # fp.__iter__ but not other fp.read* methods.
2930 # fp.__iter__ but not other fp.read* methods.
2930 #
2931 #
2931 # On modern systems like Linux, the "read" syscall cannot be interrupted
2932 # On modern systems like Linux, the "read" syscall cannot be interrupted
2932 # when reading "fast" files like on-disk files. So the EINTR issue only
2933 # when reading "fast" files like on-disk files. So the EINTR issue only
2933 # affects things like pipes, sockets, ttys etc. We treat "normal" (S_ISREG)
2934 # affects things like pipes, sockets, ttys etc. We treat "normal" (S_ISREG)
2934 # files approximately as "fast" files and use the fast (unsafe) code path,
2935 # files approximately as "fast" files and use the fast (unsafe) code path,
2935 # to minimize the performance impact.
2936 # to minimize the performance impact.
2936
2937
2937 def iterfile(fp):
2938 def iterfile(fp):
2938 fastpath = True
2939 fastpath = True
2939 if type(fp) is file:
2940 if type(fp) is file:
2940 fastpath = stat.S_ISREG(os.fstat(fp.fileno()).st_mode)
2941 fastpath = stat.S_ISREG(os.fstat(fp.fileno()).st_mode)
2941 if fastpath:
2942 if fastpath:
2942 return fp
2943 return fp
2943 else:
2944 else:
2944 # fp.readline deals with EINTR correctly, use it as a workaround.
2945 # fp.readline deals with EINTR correctly, use it as a workaround.
2945 return iter(fp.readline, b'')
2946 return iter(fp.readline, b'')
2946
2947
2947
2948
2948 else:
2949 else:
2949 # PyPy and CPython 3 do not have the EINTR issue thus no workaround needed.
2950 # PyPy and CPython 3 do not have the EINTR issue thus no workaround needed.
2950 def iterfile(fp):
2951 def iterfile(fp):
2951 return fp
2952 return fp
2952
2953
2953
2954
2954 def iterlines(iterator):
2955 def iterlines(iterator):
2955 # type: (Iterator[bytes]) -> Iterator[bytes]
2956 # type: (Iterator[bytes]) -> Iterator[bytes]
2956 for chunk in iterator:
2957 for chunk in iterator:
2957 for line in chunk.splitlines():
2958 for line in chunk.splitlines():
2958 yield line
2959 yield line
2959
2960
2960
2961
2961 def expandpath(path):
2962 def expandpath(path):
2962 # type: (bytes) -> bytes
2963 # type: (bytes) -> bytes
2963 return os.path.expanduser(os.path.expandvars(path))
2964 return os.path.expanduser(os.path.expandvars(path))
2964
2965
2965
2966
2966 def interpolate(prefix, mapping, s, fn=None, escape_prefix=False):
2967 def interpolate(prefix, mapping, s, fn=None, escape_prefix=False):
2967 """Return the result of interpolating items in the mapping into string s.
2968 """Return the result of interpolating items in the mapping into string s.
2968
2969
2969 prefix is a single character string, or a two character string with
2970 prefix is a single character string, or a two character string with
2970 a backslash as the first character if the prefix needs to be escaped in
2971 a backslash as the first character if the prefix needs to be escaped in
2971 a regular expression.
2972 a regular expression.
2972
2973
2973 fn is an optional function that will be applied to the replacement text
2974 fn is an optional function that will be applied to the replacement text
2974 just before replacement.
2975 just before replacement.
2975
2976
2976 escape_prefix is an optional flag that allows using doubled prefix for
2977 escape_prefix is an optional flag that allows using doubled prefix for
2977 its escaping.
2978 its escaping.
2978 """
2979 """
2979 fn = fn or (lambda s: s)
2980 fn = fn or (lambda s: s)
2980 patterns = b'|'.join(mapping.keys())
2981 patterns = b'|'.join(mapping.keys())
2981 if escape_prefix:
2982 if escape_prefix:
2982 patterns += b'|' + prefix
2983 patterns += b'|' + prefix
2983 if len(prefix) > 1:
2984 if len(prefix) > 1:
2984 prefix_char = prefix[1:]
2985 prefix_char = prefix[1:]
2985 else:
2986 else:
2986 prefix_char = prefix
2987 prefix_char = prefix
2987 mapping[prefix_char] = prefix_char
2988 mapping[prefix_char] = prefix_char
2988 r = remod.compile(br'%s(%s)' % (prefix, patterns))
2989 r = remod.compile(br'%s(%s)' % (prefix, patterns))
2989 return r.sub(lambda x: fn(mapping[x.group()[1:]]), s)
2990 return r.sub(lambda x: fn(mapping[x.group()[1:]]), s)
2990
2991
2991
2992
2992 timecount = unitcountfn(
2993 timecount = unitcountfn(
2993 (1, 1e3, _(b'%.0f s')),
2994 (1, 1e3, _(b'%.0f s')),
2994 (100, 1, _(b'%.1f s')),
2995 (100, 1, _(b'%.1f s')),
2995 (10, 1, _(b'%.2f s')),
2996 (10, 1, _(b'%.2f s')),
2996 (1, 1, _(b'%.3f s')),
2997 (1, 1, _(b'%.3f s')),
2997 (100, 0.001, _(b'%.1f ms')),
2998 (100, 0.001, _(b'%.1f ms')),
2998 (10, 0.001, _(b'%.2f ms')),
2999 (10, 0.001, _(b'%.2f ms')),
2999 (1, 0.001, _(b'%.3f ms')),
3000 (1, 0.001, _(b'%.3f ms')),
3000 (100, 0.000001, _(b'%.1f us')),
3001 (100, 0.000001, _(b'%.1f us')),
3001 (10, 0.000001, _(b'%.2f us')),
3002 (10, 0.000001, _(b'%.2f us')),
3002 (1, 0.000001, _(b'%.3f us')),
3003 (1, 0.000001, _(b'%.3f us')),
3003 (100, 0.000000001, _(b'%.1f ns')),
3004 (100, 0.000000001, _(b'%.1f ns')),
3004 (10, 0.000000001, _(b'%.2f ns')),
3005 (10, 0.000000001, _(b'%.2f ns')),
3005 (1, 0.000000001, _(b'%.3f ns')),
3006 (1, 0.000000001, _(b'%.3f ns')),
3006 )
3007 )
3007
3008
3008
3009
3009 @attr.s
3010 @attr.s
3010 class timedcmstats(object):
3011 class timedcmstats(object):
3011 """Stats information produced by the timedcm context manager on entering."""
3012 """Stats information produced by the timedcm context manager on entering."""
3012
3013
3013 # the starting value of the timer as a float (meaning and resulution is
3014 # the starting value of the timer as a float (meaning and resulution is
3014 # platform dependent, see util.timer)
3015 # platform dependent, see util.timer)
3015 start = attr.ib(default=attr.Factory(lambda: timer()))
3016 start = attr.ib(default=attr.Factory(lambda: timer()))
3016 # the number of seconds as a floating point value; starts at 0, updated when
3017 # the number of seconds as a floating point value; starts at 0, updated when
3017 # the context is exited.
3018 # the context is exited.
3018 elapsed = attr.ib(default=0)
3019 elapsed = attr.ib(default=0)
3019 # the number of nested timedcm context managers.
3020 # the number of nested timedcm context managers.
3020 level = attr.ib(default=1)
3021 level = attr.ib(default=1)
3021
3022
3022 def __bytes__(self):
3023 def __bytes__(self):
3023 return timecount(self.elapsed) if self.elapsed else b'<unknown>'
3024 return timecount(self.elapsed) if self.elapsed else b'<unknown>'
3024
3025
3025 __str__ = encoding.strmethod(__bytes__)
3026 __str__ = encoding.strmethod(__bytes__)
3026
3027
3027
3028
3028 @contextlib.contextmanager
3029 @contextlib.contextmanager
3029 def timedcm(whencefmt, *whenceargs):
3030 def timedcm(whencefmt, *whenceargs):
3030 """A context manager that produces timing information for a given context.
3031 """A context manager that produces timing information for a given context.
3031
3032
3032 On entering a timedcmstats instance is produced.
3033 On entering a timedcmstats instance is produced.
3033
3034
3034 This context manager is reentrant.
3035 This context manager is reentrant.
3035
3036
3036 """
3037 """
3037 # track nested context managers
3038 # track nested context managers
3038 timedcm._nested += 1
3039 timedcm._nested += 1
3039 timing_stats = timedcmstats(level=timedcm._nested)
3040 timing_stats = timedcmstats(level=timedcm._nested)
3040 try:
3041 try:
3041 with tracing.log(whencefmt, *whenceargs):
3042 with tracing.log(whencefmt, *whenceargs):
3042 yield timing_stats
3043 yield timing_stats
3043 finally:
3044 finally:
3044 timing_stats.elapsed = timer() - timing_stats.start
3045 timing_stats.elapsed = timer() - timing_stats.start
3045 timedcm._nested -= 1
3046 timedcm._nested -= 1
3046
3047
3047
3048
3048 timedcm._nested = 0
3049 timedcm._nested = 0
3049
3050
3050
3051
3051 def timed(func):
3052 def timed(func):
3052 """Report the execution time of a function call to stderr.
3053 """Report the execution time of a function call to stderr.
3053
3054
3054 During development, use as a decorator when you need to measure
3055 During development, use as a decorator when you need to measure
3055 the cost of a function, e.g. as follows:
3056 the cost of a function, e.g. as follows:
3056
3057
3057 @util.timed
3058 @util.timed
3058 def foo(a, b, c):
3059 def foo(a, b, c):
3059 pass
3060 pass
3060 """
3061 """
3061
3062
3062 def wrapper(*args, **kwargs):
3063 def wrapper(*args, **kwargs):
3063 with timedcm(pycompat.bytestr(func.__name__)) as time_stats:
3064 with timedcm(pycompat.bytestr(func.__name__)) as time_stats:
3064 result = func(*args, **kwargs)
3065 result = func(*args, **kwargs)
3065 stderr = procutil.stderr
3066 stderr = procutil.stderr
3066 stderr.write(
3067 stderr.write(
3067 b'%s%s: %s\n'
3068 b'%s%s: %s\n'
3068 % (
3069 % (
3069 b' ' * time_stats.level * 2,
3070 b' ' * time_stats.level * 2,
3070 pycompat.bytestr(func.__name__),
3071 pycompat.bytestr(func.__name__),
3071 time_stats,
3072 time_stats,
3072 )
3073 )
3073 )
3074 )
3074 return result
3075 return result
3075
3076
3076 return wrapper
3077 return wrapper
3077
3078
3078
3079
3079 _sizeunits = (
3080 _sizeunits = (
3080 (b'm', 2 ** 20),
3081 (b'm', 2 ** 20),
3081 (b'k', 2 ** 10),
3082 (b'k', 2 ** 10),
3082 (b'g', 2 ** 30),
3083 (b'g', 2 ** 30),
3083 (b'kb', 2 ** 10),
3084 (b'kb', 2 ** 10),
3084 (b'mb', 2 ** 20),
3085 (b'mb', 2 ** 20),
3085 (b'gb', 2 ** 30),
3086 (b'gb', 2 ** 30),
3086 (b'b', 1),
3087 (b'b', 1),
3087 )
3088 )
3088
3089
3089
3090
3090 def sizetoint(s):
3091 def sizetoint(s):
3091 # type: (bytes) -> int
3092 # type: (bytes) -> int
3092 """Convert a space specifier to a byte count.
3093 """Convert a space specifier to a byte count.
3093
3094
3094 >>> sizetoint(b'30')
3095 >>> sizetoint(b'30')
3095 30
3096 30
3096 >>> sizetoint(b'2.2kb')
3097 >>> sizetoint(b'2.2kb')
3097 2252
3098 2252
3098 >>> sizetoint(b'6M')
3099 >>> sizetoint(b'6M')
3099 6291456
3100 6291456
3100 """
3101 """
3101 t = s.strip().lower()
3102 t = s.strip().lower()
3102 try:
3103 try:
3103 for k, u in _sizeunits:
3104 for k, u in _sizeunits:
3104 if t.endswith(k):
3105 if t.endswith(k):
3105 return int(float(t[: -len(k)]) * u)
3106 return int(float(t[: -len(k)]) * u)
3106 return int(t)
3107 return int(t)
3107 except ValueError:
3108 except ValueError:
3108 raise error.ParseError(_(b"couldn't parse size: %s") % s)
3109 raise error.ParseError(_(b"couldn't parse size: %s") % s)
3109
3110
3110
3111
3111 class hooks(object):
3112 class hooks(object):
3112 """A collection of hook functions that can be used to extend a
3113 """A collection of hook functions that can be used to extend a
3113 function's behavior. Hooks are called in lexicographic order,
3114 function's behavior. Hooks are called in lexicographic order,
3114 based on the names of their sources."""
3115 based on the names of their sources."""
3115
3116
3116 def __init__(self):
3117 def __init__(self):
3117 self._hooks = []
3118 self._hooks = []
3118
3119
3119 def add(self, source, hook):
3120 def add(self, source, hook):
3120 self._hooks.append((source, hook))
3121 self._hooks.append((source, hook))
3121
3122
3122 def __call__(self, *args):
3123 def __call__(self, *args):
3123 self._hooks.sort(key=lambda x: x[0])
3124 self._hooks.sort(key=lambda x: x[0])
3124 results = []
3125 results = []
3125 for source, hook in self._hooks:
3126 for source, hook in self._hooks:
3126 results.append(hook(*args))
3127 results.append(hook(*args))
3127 return results
3128 return results
3128
3129
3129
3130
3130 def getstackframes(skip=0, line=b' %-*s in %s\n', fileline=b'%s:%d', depth=0):
3131 def getstackframes(skip=0, line=b' %-*s in %s\n', fileline=b'%s:%d', depth=0):
3131 """Yields lines for a nicely formatted stacktrace.
3132 """Yields lines for a nicely formatted stacktrace.
3132 Skips the 'skip' last entries, then return the last 'depth' entries.
3133 Skips the 'skip' last entries, then return the last 'depth' entries.
3133 Each file+linenumber is formatted according to fileline.
3134 Each file+linenumber is formatted according to fileline.
3134 Each line is formatted according to line.
3135 Each line is formatted according to line.
3135 If line is None, it yields:
3136 If line is None, it yields:
3136 length of longest filepath+line number,
3137 length of longest filepath+line number,
3137 filepath+linenumber,
3138 filepath+linenumber,
3138 function
3139 function
3139
3140
3140 Not be used in production code but very convenient while developing.
3141 Not be used in production code but very convenient while developing.
3141 """
3142 """
3142 entries = [
3143 entries = [
3143 (fileline % (pycompat.sysbytes(fn), ln), pycompat.sysbytes(func))
3144 (fileline % (pycompat.sysbytes(fn), ln), pycompat.sysbytes(func))
3144 for fn, ln, func, _text in traceback.extract_stack()[: -skip - 1]
3145 for fn, ln, func, _text in traceback.extract_stack()[: -skip - 1]
3145 ][-depth:]
3146 ][-depth:]
3146 if entries:
3147 if entries:
3147 fnmax = max(len(entry[0]) for entry in entries)
3148 fnmax = max(len(entry[0]) for entry in entries)
3148 for fnln, func in entries:
3149 for fnln, func in entries:
3149 if line is None:
3150 if line is None:
3150 yield (fnmax, fnln, func)
3151 yield (fnmax, fnln, func)
3151 else:
3152 else:
3152 yield line % (fnmax, fnln, func)
3153 yield line % (fnmax, fnln, func)
3153
3154
3154
3155
3155 def debugstacktrace(
3156 def debugstacktrace(
3156 msg=b'stacktrace',
3157 msg=b'stacktrace',
3157 skip=0,
3158 skip=0,
3158 f=procutil.stderr,
3159 f=procutil.stderr,
3159 otherf=procutil.stdout,
3160 otherf=procutil.stdout,
3160 depth=0,
3161 depth=0,
3161 prefix=b'',
3162 prefix=b'',
3162 ):
3163 ):
3163 """Writes a message to f (stderr) with a nicely formatted stacktrace.
3164 """Writes a message to f (stderr) with a nicely formatted stacktrace.
3164 Skips the 'skip' entries closest to the call, then show 'depth' entries.
3165 Skips the 'skip' entries closest to the call, then show 'depth' entries.
3165 By default it will flush stdout first.
3166 By default it will flush stdout first.
3166 It can be used everywhere and intentionally does not require an ui object.
3167 It can be used everywhere and intentionally does not require an ui object.
3167 Not be used in production code but very convenient while developing.
3168 Not be used in production code but very convenient while developing.
3168 """
3169 """
3169 if otherf:
3170 if otherf:
3170 otherf.flush()
3171 otherf.flush()
3171 f.write(b'%s%s at:\n' % (prefix, msg.rstrip()))
3172 f.write(b'%s%s at:\n' % (prefix, msg.rstrip()))
3172 for line in getstackframes(skip + 1, depth=depth):
3173 for line in getstackframes(skip + 1, depth=depth):
3173 f.write(prefix + line)
3174 f.write(prefix + line)
3174 f.flush()
3175 f.flush()
3175
3176
3176
3177
3177 # convenient shortcut
3178 # convenient shortcut
3178 dst = debugstacktrace
3179 dst = debugstacktrace
3179
3180
3180
3181
3181 def safename(f, tag, ctx, others=None):
3182 def safename(f, tag, ctx, others=None):
3182 """
3183 """
3183 Generate a name that it is safe to rename f to in the given context.
3184 Generate a name that it is safe to rename f to in the given context.
3184
3185
3185 f: filename to rename
3186 f: filename to rename
3186 tag: a string tag that will be included in the new name
3187 tag: a string tag that will be included in the new name
3187 ctx: a context, in which the new name must not exist
3188 ctx: a context, in which the new name must not exist
3188 others: a set of other filenames that the new name must not be in
3189 others: a set of other filenames that the new name must not be in
3189
3190
3190 Returns a file name of the form oldname~tag[~number] which does not exist
3191 Returns a file name of the form oldname~tag[~number] which does not exist
3191 in the provided context and is not in the set of other names.
3192 in the provided context and is not in the set of other names.
3192 """
3193 """
3193 if others is None:
3194 if others is None:
3194 others = set()
3195 others = set()
3195
3196
3196 fn = b'%s~%s' % (f, tag)
3197 fn = b'%s~%s' % (f, tag)
3197 if fn not in ctx and fn not in others:
3198 if fn not in ctx and fn not in others:
3198 return fn
3199 return fn
3199 for n in itertools.count(1):
3200 for n in itertools.count(1):
3200 fn = b'%s~%s~%s' % (f, tag, n)
3201 fn = b'%s~%s~%s' % (f, tag, n)
3201 if fn not in ctx and fn not in others:
3202 if fn not in ctx and fn not in others:
3202 return fn
3203 return fn
3203
3204
3204
3205
3205 def readexactly(stream, n):
3206 def readexactly(stream, n):
3206 '''read n bytes from stream.read and abort if less was available'''
3207 '''read n bytes from stream.read and abort if less was available'''
3207 s = stream.read(n)
3208 s = stream.read(n)
3208 if len(s) < n:
3209 if len(s) < n:
3209 raise error.Abort(
3210 raise error.Abort(
3210 _(b"stream ended unexpectedly (got %d bytes, expected %d)")
3211 _(b"stream ended unexpectedly (got %d bytes, expected %d)")
3211 % (len(s), n)
3212 % (len(s), n)
3212 )
3213 )
3213 return s
3214 return s
3214
3215
3215
3216
3216 def uvarintencode(value):
3217 def uvarintencode(value):
3217 """Encode an unsigned integer value to a varint.
3218 """Encode an unsigned integer value to a varint.
3218
3219
3219 A varint is a variable length integer of 1 or more bytes. Each byte
3220 A varint is a variable length integer of 1 or more bytes. Each byte
3220 except the last has the most significant bit set. The lower 7 bits of
3221 except the last has the most significant bit set. The lower 7 bits of
3221 each byte store the 2's complement representation, least significant group
3222 each byte store the 2's complement representation, least significant group
3222 first.
3223 first.
3223
3224
3224 >>> uvarintencode(0)
3225 >>> uvarintencode(0)
3225 '\\x00'
3226 '\\x00'
3226 >>> uvarintencode(1)
3227 >>> uvarintencode(1)
3227 '\\x01'
3228 '\\x01'
3228 >>> uvarintencode(127)
3229 >>> uvarintencode(127)
3229 '\\x7f'
3230 '\\x7f'
3230 >>> uvarintencode(1337)
3231 >>> uvarintencode(1337)
3231 '\\xb9\\n'
3232 '\\xb9\\n'
3232 >>> uvarintencode(65536)
3233 >>> uvarintencode(65536)
3233 '\\x80\\x80\\x04'
3234 '\\x80\\x80\\x04'
3234 >>> uvarintencode(-1)
3235 >>> uvarintencode(-1)
3235 Traceback (most recent call last):
3236 Traceback (most recent call last):
3236 ...
3237 ...
3237 ProgrammingError: negative value for uvarint: -1
3238 ProgrammingError: negative value for uvarint: -1
3238 """
3239 """
3239 if value < 0:
3240 if value < 0:
3240 raise error.ProgrammingError(b'negative value for uvarint: %d' % value)
3241 raise error.ProgrammingError(b'negative value for uvarint: %d' % value)
3241 bits = value & 0x7F
3242 bits = value & 0x7F
3242 value >>= 7
3243 value >>= 7
3243 bytes = []
3244 bytes = []
3244 while value:
3245 while value:
3245 bytes.append(pycompat.bytechr(0x80 | bits))
3246 bytes.append(pycompat.bytechr(0x80 | bits))
3246 bits = value & 0x7F
3247 bits = value & 0x7F
3247 value >>= 7
3248 value >>= 7
3248 bytes.append(pycompat.bytechr(bits))
3249 bytes.append(pycompat.bytechr(bits))
3249
3250
3250 return b''.join(bytes)
3251 return b''.join(bytes)
3251
3252
3252
3253
3253 def uvarintdecodestream(fh):
3254 def uvarintdecodestream(fh):
3254 """Decode an unsigned variable length integer from a stream.
3255 """Decode an unsigned variable length integer from a stream.
3255
3256
3256 The passed argument is anything that has a ``.read(N)`` method.
3257 The passed argument is anything that has a ``.read(N)`` method.
3257
3258
3258 >>> try:
3259 >>> try:
3259 ... from StringIO import StringIO as BytesIO
3260 ... from StringIO import StringIO as BytesIO
3260 ... except ImportError:
3261 ... except ImportError:
3261 ... from io import BytesIO
3262 ... from io import BytesIO
3262 >>> uvarintdecodestream(BytesIO(b'\\x00'))
3263 >>> uvarintdecodestream(BytesIO(b'\\x00'))
3263 0
3264 0
3264 >>> uvarintdecodestream(BytesIO(b'\\x01'))
3265 >>> uvarintdecodestream(BytesIO(b'\\x01'))
3265 1
3266 1
3266 >>> uvarintdecodestream(BytesIO(b'\\x7f'))
3267 >>> uvarintdecodestream(BytesIO(b'\\x7f'))
3267 127
3268 127
3268 >>> uvarintdecodestream(BytesIO(b'\\xb9\\n'))
3269 >>> uvarintdecodestream(BytesIO(b'\\xb9\\n'))
3269 1337
3270 1337
3270 >>> uvarintdecodestream(BytesIO(b'\\x80\\x80\\x04'))
3271 >>> uvarintdecodestream(BytesIO(b'\\x80\\x80\\x04'))
3271 65536
3272 65536
3272 >>> uvarintdecodestream(BytesIO(b'\\x80'))
3273 >>> uvarintdecodestream(BytesIO(b'\\x80'))
3273 Traceback (most recent call last):
3274 Traceback (most recent call last):
3274 ...
3275 ...
3275 Abort: stream ended unexpectedly (got 0 bytes, expected 1)
3276 Abort: stream ended unexpectedly (got 0 bytes, expected 1)
3276 """
3277 """
3277 result = 0
3278 result = 0
3278 shift = 0
3279 shift = 0
3279 while True:
3280 while True:
3280 byte = ord(readexactly(fh, 1))
3281 byte = ord(readexactly(fh, 1))
3281 result |= (byte & 0x7F) << shift
3282 result |= (byte & 0x7F) << shift
3282 if not (byte & 0x80):
3283 if not (byte & 0x80):
3283 return result
3284 return result
3284 shift += 7
3285 shift += 7
3285
3286
3286
3287
3287 # Passing the '' locale means that the locale should be set according to the
3288 # Passing the '' locale means that the locale should be set according to the
3288 # user settings (environment variables).
3289 # user settings (environment variables).
3289 # Python sometimes avoids setting the global locale settings. When interfacing
3290 # Python sometimes avoids setting the global locale settings. When interfacing
3290 # with C code (e.g. the curses module or the Subversion bindings), the global
3291 # with C code (e.g. the curses module or the Subversion bindings), the global
3291 # locale settings must be initialized correctly. Python 2 does not initialize
3292 # locale settings must be initialized correctly. Python 2 does not initialize
3292 # the global locale settings on interpreter startup. Python 3 sometimes
3293 # the global locale settings on interpreter startup. Python 3 sometimes
3293 # initializes LC_CTYPE, but not consistently at least on Windows. Therefore we
3294 # initializes LC_CTYPE, but not consistently at least on Windows. Therefore we
3294 # explicitly initialize it to get consistent behavior if it's not already
3295 # explicitly initialize it to get consistent behavior if it's not already
3295 # initialized. Since CPython commit 177d921c8c03d30daa32994362023f777624b10d,
3296 # initialized. Since CPython commit 177d921c8c03d30daa32994362023f777624b10d,
3296 # LC_CTYPE is always initialized. If we require Python 3.8+, we should re-check
3297 # LC_CTYPE is always initialized. If we require Python 3.8+, we should re-check
3297 # if we can remove this code.
3298 # if we can remove this code.
3298 @contextlib.contextmanager
3299 @contextlib.contextmanager
3299 def with_lc_ctype():
3300 def with_lc_ctype():
3300 oldloc = locale.setlocale(locale.LC_CTYPE, None)
3301 oldloc = locale.setlocale(locale.LC_CTYPE, None)
3301 if oldloc == 'C':
3302 if oldloc == 'C':
3302 try:
3303 try:
3303 try:
3304 try:
3304 locale.setlocale(locale.LC_CTYPE, '')
3305 locale.setlocale(locale.LC_CTYPE, '')
3305 except locale.Error:
3306 except locale.Error:
3306 # The likely case is that the locale from the environment
3307 # The likely case is that the locale from the environment
3307 # variables is unknown.
3308 # variables is unknown.
3308 pass
3309 pass
3309 yield
3310 yield
3310 finally:
3311 finally:
3311 locale.setlocale(locale.LC_CTYPE, oldloc)
3312 locale.setlocale(locale.LC_CTYPE, oldloc)
3312 else:
3313 else:
3313 yield
3314 yield
3314
3315
3315
3316
3316 def _estimatememory():
3317 def _estimatememory():
3317 # type: () -> Optional[int]
3318 # type: () -> Optional[int]
3318 """Provide an estimate for the available system memory in Bytes.
3319 """Provide an estimate for the available system memory in Bytes.
3319
3320
3320 If no estimate can be provided on the platform, returns None.
3321 If no estimate can be provided on the platform, returns None.
3321 """
3322 """
3322 if pycompat.sysplatform.startswith(b'win'):
3323 if pycompat.sysplatform.startswith(b'win'):
3323 # On Windows, use the GlobalMemoryStatusEx kernel function directly.
3324 # On Windows, use the GlobalMemoryStatusEx kernel function directly.
3324 from ctypes import c_long as DWORD, c_ulonglong as DWORDLONG
3325 from ctypes import c_long as DWORD, c_ulonglong as DWORDLONG
3325 from ctypes.wintypes import ( # pytype: disable=import-error
3326 from ctypes.wintypes import ( # pytype: disable=import-error
3326 Structure,
3327 Structure,
3327 byref,
3328 byref,
3328 sizeof,
3329 sizeof,
3329 windll,
3330 windll,
3330 )
3331 )
3331
3332
3332 class MEMORYSTATUSEX(Structure):
3333 class MEMORYSTATUSEX(Structure):
3333 _fields_ = [
3334 _fields_ = [
3334 ('dwLength', DWORD),
3335 ('dwLength', DWORD),
3335 ('dwMemoryLoad', DWORD),
3336 ('dwMemoryLoad', DWORD),
3336 ('ullTotalPhys', DWORDLONG),
3337 ('ullTotalPhys', DWORDLONG),
3337 ('ullAvailPhys', DWORDLONG),
3338 ('ullAvailPhys', DWORDLONG),
3338 ('ullTotalPageFile', DWORDLONG),
3339 ('ullTotalPageFile', DWORDLONG),
3339 ('ullAvailPageFile', DWORDLONG),
3340 ('ullAvailPageFile', DWORDLONG),
3340 ('ullTotalVirtual', DWORDLONG),
3341 ('ullTotalVirtual', DWORDLONG),
3341 ('ullAvailVirtual', DWORDLONG),
3342 ('ullAvailVirtual', DWORDLONG),
3342 ('ullExtendedVirtual', DWORDLONG),
3343 ('ullExtendedVirtual', DWORDLONG),
3343 ]
3344 ]
3344
3345
3345 x = MEMORYSTATUSEX()
3346 x = MEMORYSTATUSEX()
3346 x.dwLength = sizeof(x)
3347 x.dwLength = sizeof(x)
3347 windll.kernel32.GlobalMemoryStatusEx(byref(x))
3348 windll.kernel32.GlobalMemoryStatusEx(byref(x))
3348 return x.ullAvailPhys
3349 return x.ullAvailPhys
3349
3350
3350 # On newer Unix-like systems and Mac OSX, the sysconf interface
3351 # On newer Unix-like systems and Mac OSX, the sysconf interface
3351 # can be used. _SC_PAGE_SIZE is part of POSIX; _SC_PHYS_PAGES
3352 # can be used. _SC_PAGE_SIZE is part of POSIX; _SC_PHYS_PAGES
3352 # seems to be implemented on most systems.
3353 # seems to be implemented on most systems.
3353 try:
3354 try:
3354 pagesize = os.sysconf(os.sysconf_names['SC_PAGE_SIZE'])
3355 pagesize = os.sysconf(os.sysconf_names['SC_PAGE_SIZE'])
3355 pages = os.sysconf(os.sysconf_names['SC_PHYS_PAGES'])
3356 pages = os.sysconf(os.sysconf_names['SC_PHYS_PAGES'])
3356 return pagesize * pages
3357 return pagesize * pages
3357 except OSError: # sysconf can fail
3358 except OSError: # sysconf can fail
3358 pass
3359 pass
3359 except KeyError: # unknown parameter
3360 except KeyError: # unknown parameter
3360 pass
3361 pass
@@ -1,258 +1,259 b''
1 Create a repository:
1 Create a repository:
2
2
3 #if no-extraextensions
3 #if no-extraextensions
4 $ hg config
4 $ hg config
5 chgserver.idletimeout=60
5 chgserver.idletimeout=60
6 devel.all-warnings=true
6 devel.all-warnings=true
7 devel.default-date=0 0
7 devel.default-date=0 0
8 extensions.fsmonitor= (fsmonitor !)
8 extensions.fsmonitor= (fsmonitor !)
9 format.use-dirstate-v2=1 (dirstate-v2 !)
9 format.use-dirstate-v2=1 (dirstate-v2 !)
10 largefiles.usercache=$TESTTMP/.cache/largefiles
10 largefiles.usercache=$TESTTMP/.cache/largefiles
11 lfs.usercache=$TESTTMP/.cache/lfs
11 lfs.usercache=$TESTTMP/.cache/lfs
12 ui.slash=True
12 ui.slash=True
13 ui.interactive=False
13 ui.interactive=False
14 ui.detailed-exit-code=True
14 ui.detailed-exit-code=True
15 ui.merge=internal:merge
15 ui.merge=internal:merge
16 ui.mergemarkers=detailed
16 ui.mergemarkers=detailed
17 ui.promptecho=True
17 ui.promptecho=True
18 ui.ssh=* (glob)
18 ui.ssh=* (glob)
19 ui.timeout.warn=15
19 ui.timeout.warn=15
20 web.address=localhost
20 web.address=localhost
21 web\.ipv6=(?:True|False) (re)
21 web\.ipv6=(?:True|False) (re)
22 web.server-header=testing stub value
22 web.server-header=testing stub value
23 #endif
23 #endif
24
24
25 $ hg init t
25 $ hg init t
26 $ cd t
26 $ cd t
27
27
28 Prepare a changeset:
28 Prepare a changeset:
29
29
30 $ echo a > a
30 $ echo a > a
31 $ hg add a
31 $ hg add a
32
32
33 $ hg status
33 $ hg status
34 A a
34 A a
35
35
36 Writes to stdio succeed and fail appropriately
36 Writes to stdio succeed and fail appropriately
37
37
38 #if devfull
38 #if devfull
39 $ hg status 2>/dev/full
39 $ hg status 2>/dev/full
40 A a
40 A a
41
41
42 $ hg status >/dev/full
42 $ hg status >/dev/full
43 abort: No space left on device* (glob)
43 abort: No space left on device* (glob)
44 [255]
44 [255]
45 #endif
45 #endif
46
46
47 #if devfull
47 #if devfull
48 $ hg status >/dev/full 2>&1
48 $ hg status >/dev/full 2>&1
49 [255]
49 [255]
50
50
51 $ hg status ENOENT 2>/dev/full
51 $ hg status ENOENT 2>/dev/full
52 [255]
52 [255]
53 #endif
53 #endif
54
54
55 On Python 3, stdio may be None:
55 On Python 3, stdio may be None:
56
56
57 $ hg debuguiprompt --config ui.interactive=true 0<&-
57 $ hg debuguiprompt --config ui.interactive=true 0<&-
58 abort: Bad file descriptor (no-rhg !)
58 abort: Bad file descriptor (no-rhg !)
59 abort: response expected (rhg !)
59 abort: response expected (rhg !)
60 [255]
60 [255]
61 $ hg version -q 0<&-
61 $ hg version -q 0<&-
62 Mercurial Distributed SCM * (glob)
62 Mercurial Distributed SCM * (glob)
63
63
64 #if py3 no-rhg
64 #if py3 no-rhg
65 $ hg version -q 1>&-
65 $ hg version -q 1>&-
66 abort: Bad file descriptor
66 abort: Bad file descriptor
67 [255]
67 [255]
68 #else
68 #else
69 $ hg version -q 1>&-
69 $ hg version -q 1>&-
70 #endif
70 #endif
71 $ hg unknown -q 1>&-
71 $ hg unknown -q 1>&-
72 hg: unknown command 'unknown'
72 hg: unknown command 'unknown'
73 (did you mean debugknown?)
73 (did you mean debugknown?)
74 [10]
74 [10]
75
75
76 $ hg version -q 2>&-
76 $ hg version -q 2>&-
77 Mercurial Distributed SCM * (glob)
77 Mercurial Distributed SCM * (glob)
78 $ hg unknown -q 2>&-
78 $ hg unknown -q 2>&-
79 [10]
79 [10]
80
80
81 $ hg commit -m test
81 $ hg commit -m test
82
82
83 This command is ancient:
83 This command is ancient:
84
84
85 $ hg history
85 $ hg history
86 changeset: 0:acb14030fe0a
86 changeset: 0:acb14030fe0a
87 tag: tip
87 tag: tip
88 user: test
88 user: test
89 date: Thu Jan 01 00:00:00 1970 +0000
89 date: Thu Jan 01 00:00:00 1970 +0000
90 summary: test
90 summary: test
91
91
92
92
93 Verify that updating to revision 0 via commands.update() works properly
93 Verify that updating to revision 0 via commands.update() works properly
94
94
95 $ cat <<EOF > update_to_rev0.py
95 $ cat <<EOF > update_to_rev0.py
96 > from mercurial import commands, hg, ui as uimod
96 > from mercurial import commands, hg, ui as uimod
97 > myui = uimod.ui.load()
97 > myui = uimod.ui.load()
98 > repo = hg.repository(myui, path=b'.')
98 > repo = hg.repository(myui, path=b'.')
99 > commands.update(myui, repo, rev=b"0")
99 > commands.update(myui, repo, rev=b"0")
100 > EOF
100 > EOF
101 $ hg up null
101 $ hg up null
102 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
102 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
103 $ "$PYTHON" ./update_to_rev0.py
103 $ "$PYTHON" ./update_to_rev0.py
104 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
104 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
105 $ hg identify -n
105 $ hg identify -n
106 0
106 0
107
107
108
108
109 Poke around at hashes:
109 Poke around at hashes:
110
110
111 $ hg manifest --debug
111 $ hg manifest --debug
112 b789fdd96dc2f3bd229c1dd8eedf0fc60e2b68e3 644 a
112 b789fdd96dc2f3bd229c1dd8eedf0fc60e2b68e3 644 a
113
113
114 $ hg cat a
114 $ hg cat a
115 a
115 a
116
116
117 Verify should succeed:
117 Verify should succeed:
118
118
119 $ hg verify
119 $ hg verify
120 checking changesets
120 checking changesets
121 checking manifests
121 checking manifests
122 crosschecking files in changesets and manifests
122 crosschecking files in changesets and manifests
123 checking files
123 checking files
124 checked 1 changesets with 1 changes to 1 files
124 checked 1 changesets with 1 changes to 1 files
125
125
126 Repository root:
126 Repository root:
127
127
128 $ hg root
128 $ hg root
129 $TESTTMP/t
129 $TESTTMP/t
130 $ hg log -l1 -T '{reporoot}\n'
130 $ hg log -l1 -T '{reporoot}\n'
131 $TESTTMP/t
131 $TESTTMP/t
132 $ hg root -Tjson | sed 's|\\\\|\\|g'
132 $ hg root -Tjson | sed 's|\\\\|\\|g'
133 [
133 [
134 {
134 {
135 "hgpath": "$TESTTMP/t/.hg",
135 "hgpath": "$TESTTMP/t/.hg",
136 "reporoot": "$TESTTMP/t",
136 "reporoot": "$TESTTMP/t",
137 "storepath": "$TESTTMP/t/.hg/store"
137 "storepath": "$TESTTMP/t/.hg/store"
138 }
138 }
139 ]
139 ]
140
140
141 At the end...
141 At the end...
142
142
143 $ cd ..
143 $ cd ..
144
144
145 Status message redirection:
145 Status message redirection:
146
146
147 $ hg init empty
147 $ hg init empty
148
148
149 status messages are sent to stdout by default:
149 status messages are sent to stdout by default:
150
150
151 $ hg outgoing -R t empty -Tjson 2>/dev/null
151 $ hg outgoing -R t empty -Tjson 2>/dev/null
152 comparing with empty
152 comparing with empty
153 searching for changes
153 searching for changes
154 [
154 [
155 {
155 {
156 "bookmarks": [],
156 "bookmarks": [],
157 "branch": "default",
157 "branch": "default",
158 "date": [0, 0],
158 "date": [0, 0],
159 "desc": "test",
159 "desc": "test",
160 "node": "acb14030fe0a21b60322c440ad2d20cf7685a376",
160 "node": "acb14030fe0a21b60322c440ad2d20cf7685a376",
161 "parents": ["0000000000000000000000000000000000000000"],
161 "parents": ["0000000000000000000000000000000000000000"],
162 "phase": "draft",
162 "phase": "draft",
163 "rev": 0,
163 "rev": 0,
164 "tags": ["tip"],
164 "tags": ["tip"],
165 "user": "test"
165 "user": "test"
166 }
166 }
167 ]
167 ]
168
168
169 which can be configured to send to stderr, so the output wouldn't be
169 which can be configured to send to stderr, so the output wouldn't be
170 interleaved:
170 interleaved:
171
171
172 $ cat <<'EOF' >> "$HGRCPATH"
172 $ cat <<'EOF' >> "$HGRCPATH"
173 > [ui]
173 > [ui]
174 > message-output = stderr
174 > message-output = stderr
175 > EOF
175 > EOF
176 $ hg outgoing -R t empty -Tjson 2>/dev/null
176 $ hg outgoing -R t empty -Tjson 2>/dev/null
177 [
177 [
178 {
178 {
179 "bookmarks": [],
179 "bookmarks": [],
180 "branch": "default",
180 "branch": "default",
181 "date": [0, 0],
181 "date": [0, 0],
182 "desc": "test",
182 "desc": "test",
183 "node": "acb14030fe0a21b60322c440ad2d20cf7685a376",
183 "node": "acb14030fe0a21b60322c440ad2d20cf7685a376",
184 "parents": ["0000000000000000000000000000000000000000"],
184 "parents": ["0000000000000000000000000000000000000000"],
185 "phase": "draft",
185 "phase": "draft",
186 "rev": 0,
186 "rev": 0,
187 "tags": ["tip"],
187 "tags": ["tip"],
188 "user": "test"
188 "user": "test"
189 }
189 }
190 ]
190 ]
191 $ hg outgoing -R t empty -Tjson >/dev/null
191 $ hg outgoing -R t empty -Tjson >/dev/null
192 comparing with empty
192 comparing with empty
193 searching for changes
193 searching for changes
194
194
195 this option should be turned off by HGPLAIN= since it may break scripting use:
195 this option should be turned off by HGPLAIN= since it may break scripting use:
196
196
197 $ HGPLAIN= hg outgoing -R t empty -Tjson 2>/dev/null
197 $ HGPLAIN= hg outgoing -R t empty -Tjson 2>/dev/null
198 comparing with empty
198 comparing with empty
199 searching for changes
199 searching for changes
200 [
200 [
201 {
201 {
202 "bookmarks": [],
202 "bookmarks": [],
203 "branch": "default",
203 "branch": "default",
204 "date": [0, 0],
204 "date": [0, 0],
205 "desc": "test",
205 "desc": "test",
206 "node": "acb14030fe0a21b60322c440ad2d20cf7685a376",
206 "node": "acb14030fe0a21b60322c440ad2d20cf7685a376",
207 "parents": ["0000000000000000000000000000000000000000"],
207 "parents": ["0000000000000000000000000000000000000000"],
208 "phase": "draft",
208 "phase": "draft",
209 "rev": 0,
209 "rev": 0,
210 "tags": ["tip"],
210 "tags": ["tip"],
211 "user": "test"
211 "user": "test"
212 }
212 }
213 ]
213 ]
214
214
215 but still overridden by --config:
215 but still overridden by --config:
216
216
217 $ HGPLAIN= hg outgoing -R t empty -Tjson --config ui.message-output=stderr \
217 $ HGPLAIN= hg outgoing -R t empty -Tjson --config ui.message-output=stderr \
218 > 2>/dev/null
218 > 2>/dev/null
219 [
219 [
220 {
220 {
221 "bookmarks": [],
221 "bookmarks": [],
222 "branch": "default",
222 "branch": "default",
223 "date": [0, 0],
223 "date": [0, 0],
224 "desc": "test",
224 "desc": "test",
225 "node": "acb14030fe0a21b60322c440ad2d20cf7685a376",
225 "node": "acb14030fe0a21b60322c440ad2d20cf7685a376",
226 "parents": ["0000000000000000000000000000000000000000"],
226 "parents": ["0000000000000000000000000000000000000000"],
227 "phase": "draft",
227 "phase": "draft",
228 "rev": 0,
228 "rev": 0,
229 "tags": ["tip"],
229 "tags": ["tip"],
230 "user": "test"
230 "user": "test"
231 }
231 }
232 ]
232 ]
233
233
234 Invalid ui.message-output option:
234 Invalid ui.message-output option:
235
235
236 $ hg log -R t --config ui.message-output=bad
236 $ hg log -R t --config ui.message-output=bad
237 abort: invalid ui.message-output destination: bad
237 abort: invalid ui.message-output destination: bad
238 [255]
238 [255]
239
239
240 Underlying message streams should be updated when ui.fout/ferr are set:
240 Underlying message streams should be updated when ui.fout/ferr are set:
241
241
242 $ cat <<'EOF' > capui.py
242 $ cat <<'EOF' > capui.py
243 > from mercurial import pycompat, registrar
243 > import io
244 > from mercurial import registrar
244 > cmdtable = {}
245 > cmdtable = {}
245 > command = registrar.command(cmdtable)
246 > command = registrar.command(cmdtable)
246 > @command(b'capui', norepo=True)
247 > @command(b'capui', norepo=True)
247 > def capui(ui):
248 > def capui(ui):
248 > out = ui.fout
249 > out = ui.fout
249 > ui.fout = pycompat.bytesio()
250 > ui.fout = io.BytesIO()
250 > ui.status(b'status\n')
251 > ui.status(b'status\n')
251 > ui.ferr = pycompat.bytesio()
252 > ui.ferr = io.BytesIO()
252 > ui.warn(b'warn\n')
253 > ui.warn(b'warn\n')
253 > out.write(b'stdout: %s' % ui.fout.getvalue())
254 > out.write(b'stdout: %s' % ui.fout.getvalue())
254 > out.write(b'stderr: %s' % ui.ferr.getvalue())
255 > out.write(b'stderr: %s' % ui.ferr.getvalue())
255 > EOF
256 > EOF
256 $ hg --config extensions.capui=capui.py --config ui.message-output=stdio capui
257 $ hg --config extensions.capui=capui.py --config ui.message-output=stdio capui
257 stdout: status
258 stdout: status
258 stderr: warn
259 stderr: warn
@@ -1,143 +1,144 b''
1 # unit tests for mercuril.util utilities
1 # unit tests for mercuril.util utilities
2 from __future__ import absolute_import
2 from __future__ import absolute_import
3
3
4 import contextlib
4 import contextlib
5 import io
5 import itertools
6 import itertools
6 import unittest
7 import unittest
7
8
8 from mercurial import pycompat, util, utils
9 from mercurial import pycompat, util, utils
9
10
10
11
11 @contextlib.contextmanager
12 @contextlib.contextmanager
12 def mocktimer(incr=0.1, *additional_targets):
13 def mocktimer(incr=0.1, *additional_targets):
13 """Replaces util.timer and additional_targets with a mock
14 """Replaces util.timer and additional_targets with a mock
14
15
15 The timer starts at 0. On each call the time incremented by the value
16 The timer starts at 0. On each call the time incremented by the value
16 of incr. If incr is an iterable, then the time is incremented by the
17 of incr. If incr is an iterable, then the time is incremented by the
17 next value from that iterable, looping in a cycle when reaching the end.
18 next value from that iterable, looping in a cycle when reaching the end.
18
19
19 additional_targets must be a sequence of (object, attribute_name) tuples;
20 additional_targets must be a sequence of (object, attribute_name) tuples;
20 the mock is set with setattr(object, attribute_name, mock).
21 the mock is set with setattr(object, attribute_name, mock).
21
22
22 """
23 """
23 time = [0]
24 time = [0]
24 try:
25 try:
25 incr = itertools.cycle(incr)
26 incr = itertools.cycle(incr)
26 except TypeError:
27 except TypeError:
27 incr = itertools.repeat(incr)
28 incr = itertools.repeat(incr)
28
29
29 def timer():
30 def timer():
30 time[0] += next(incr)
31 time[0] += next(incr)
31 return time[0]
32 return time[0]
32
33
33 # record original values
34 # record original values
34 orig = util.timer
35 orig = util.timer
35 additional_origs = [(o, a, getattr(o, a)) for o, a in additional_targets]
36 additional_origs = [(o, a, getattr(o, a)) for o, a in additional_targets]
36
37
37 # mock out targets
38 # mock out targets
38 util.timer = timer
39 util.timer = timer
39 for obj, attr in additional_targets:
40 for obj, attr in additional_targets:
40 setattr(obj, attr, timer)
41 setattr(obj, attr, timer)
41
42
42 try:
43 try:
43 yield
44 yield
44 finally:
45 finally:
45 # restore originals
46 # restore originals
46 util.timer = orig
47 util.timer = orig
47 for args in additional_origs:
48 for args in additional_origs:
48 setattr(*args)
49 setattr(*args)
49
50
50
51
51 # attr.s default factory for util.timedstats.start binds the timer we
52 # attr.s default factory for util.timedstats.start binds the timer we
52 # need to mock out.
53 # need to mock out.
53 _start_default = (util.timedcmstats.start.default, 'factory')
54 _start_default = (util.timedcmstats.start.default, 'factory')
54
55
55
56
56 @contextlib.contextmanager
57 @contextlib.contextmanager
57 def capturestderr():
58 def capturestderr():
58 """Replace utils.procutil.stderr with a pycompat.bytesio instance
59 """Replace utils.procutil.stderr with an io.BytesIO instance
59
60
60 The instance is made available as the return value of __enter__.
61 The instance is made available as the return value of __enter__.
61
62
62 This contextmanager is reentrant.
63 This contextmanager is reentrant.
63
64
64 """
65 """
65 orig = utils.procutil.stderr
66 orig = utils.procutil.stderr
66 utils.procutil.stderr = pycompat.bytesio()
67 utils.procutil.stderr = io.BytesIO()
67 try:
68 try:
68 yield utils.procutil.stderr
69 yield utils.procutil.stderr
69 finally:
70 finally:
70 utils.procutil.stderr = orig
71 utils.procutil.stderr = orig
71
72
72
73
73 class timedtests(unittest.TestCase):
74 class timedtests(unittest.TestCase):
74 def testtimedcmstatsstr(self):
75 def testtimedcmstatsstr(self):
75 stats = util.timedcmstats()
76 stats = util.timedcmstats()
76 self.assertEqual(str(stats), '<unknown>')
77 self.assertEqual(str(stats), '<unknown>')
77 self.assertEqual(bytes(stats), b'<unknown>')
78 self.assertEqual(bytes(stats), b'<unknown>')
78 stats.elapsed = 12.34
79 stats.elapsed = 12.34
79 self.assertEqual(str(stats), pycompat.sysstr(util.timecount(12.34)))
80 self.assertEqual(str(stats), pycompat.sysstr(util.timecount(12.34)))
80 self.assertEqual(bytes(stats), util.timecount(12.34))
81 self.assertEqual(bytes(stats), util.timecount(12.34))
81
82
82 def testtimedcmcleanexit(self):
83 def testtimedcmcleanexit(self):
83 # timestamps 1, 4, elapsed time of 4 - 1 = 3
84 # timestamps 1, 4, elapsed time of 4 - 1 = 3
84 with mocktimer([1, 3], _start_default):
85 with mocktimer([1, 3], _start_default):
85 with util.timedcm('pass') as stats:
86 with util.timedcm('pass') as stats:
86 # actual context doesn't matter
87 # actual context doesn't matter
87 pass
88 pass
88
89
89 self.assertEqual(stats.start, 1)
90 self.assertEqual(stats.start, 1)
90 self.assertEqual(stats.elapsed, 3)
91 self.assertEqual(stats.elapsed, 3)
91 self.assertEqual(stats.level, 1)
92 self.assertEqual(stats.level, 1)
92
93
93 def testtimedcmnested(self):
94 def testtimedcmnested(self):
94 # timestamps 1, 3, 6, 10, elapsed times of 6 - 3 = 3 and 10 - 1 = 9
95 # timestamps 1, 3, 6, 10, elapsed times of 6 - 3 = 3 and 10 - 1 = 9
95 with mocktimer([1, 2, 3, 4], _start_default):
96 with mocktimer([1, 2, 3, 4], _start_default):
96 with util.timedcm('outer') as outer_stats:
97 with util.timedcm('outer') as outer_stats:
97 with util.timedcm('inner') as inner_stats:
98 with util.timedcm('inner') as inner_stats:
98 # actual context doesn't matter
99 # actual context doesn't matter
99 pass
100 pass
100
101
101 self.assertEqual(outer_stats.start, 1)
102 self.assertEqual(outer_stats.start, 1)
102 self.assertEqual(outer_stats.elapsed, 9)
103 self.assertEqual(outer_stats.elapsed, 9)
103 self.assertEqual(outer_stats.level, 1)
104 self.assertEqual(outer_stats.level, 1)
104
105
105 self.assertEqual(inner_stats.start, 3)
106 self.assertEqual(inner_stats.start, 3)
106 self.assertEqual(inner_stats.elapsed, 3)
107 self.assertEqual(inner_stats.elapsed, 3)
107 self.assertEqual(inner_stats.level, 2)
108 self.assertEqual(inner_stats.level, 2)
108
109
109 def testtimedcmexception(self):
110 def testtimedcmexception(self):
110 # timestamps 1, 4, elapsed time of 4 - 1 = 3
111 # timestamps 1, 4, elapsed time of 4 - 1 = 3
111 with mocktimer([1, 3], _start_default):
112 with mocktimer([1, 3], _start_default):
112 try:
113 try:
113 with util.timedcm('exceptional') as stats:
114 with util.timedcm('exceptional') as stats:
114 raise ValueError()
115 raise ValueError()
115 except ValueError:
116 except ValueError:
116 pass
117 pass
117
118
118 self.assertEqual(stats.start, 1)
119 self.assertEqual(stats.start, 1)
119 self.assertEqual(stats.elapsed, 3)
120 self.assertEqual(stats.elapsed, 3)
120 self.assertEqual(stats.level, 1)
121 self.assertEqual(stats.level, 1)
121
122
122 def testtimeddecorator(self):
123 def testtimeddecorator(self):
123 @util.timed
124 @util.timed
124 def testfunc(callcount=1):
125 def testfunc(callcount=1):
125 callcount -= 1
126 callcount -= 1
126 if callcount:
127 if callcount:
127 testfunc(callcount)
128 testfunc(callcount)
128
129
129 # timestamps 1, 2, 3, 4, elapsed time of 3 - 2 = 1 and 4 - 1 = 3
130 # timestamps 1, 2, 3, 4, elapsed time of 3 - 2 = 1 and 4 - 1 = 3
130 with mocktimer(1, _start_default):
131 with mocktimer(1, _start_default):
131 with capturestderr() as out:
132 with capturestderr() as out:
132 testfunc(2)
133 testfunc(2)
133
134
134 self.assertEqual(
135 self.assertEqual(
135 out.getvalue(),
136 out.getvalue(),
136 (b' testfunc: 1.000 s\n' b' testfunc: 3.000 s\n'),
137 (b' testfunc: 1.000 s\n' b' testfunc: 3.000 s\n'),
137 )
138 )
138
139
139
140
140 if __name__ == '__main__':
141 if __name__ == '__main__':
141 import silenttestrunner
142 import silenttestrunner
142
143
143 silenttestrunner.main(__name__)
144 silenttestrunner.main(__name__)
General Comments 0
You need to be logged in to leave comments. Login now