##// END OF EJS Templates
dirstate: Removed unused instances of `DirsMultiset`...
Simon Sapin -
r48271:eb416759 default
parent child Browse files
Show More
@@ -1,3971 +1,3980
1 # perf.py - performance test routines
1 # perf.py - performance test routines
2 '''helper extension to measure performance
2 '''helper extension to measure performance
3
3
4 Configurations
4 Configurations
5 ==============
5 ==============
6
6
7 ``perf``
7 ``perf``
8 --------
8 --------
9
9
10 ``all-timing``
10 ``all-timing``
11 When set, additional statistics will be reported for each benchmark: best,
11 When set, additional statistics will be reported for each benchmark: best,
12 worst, median average. If not set only the best timing is reported
12 worst, median average. If not set only the best timing is reported
13 (default: off).
13 (default: off).
14
14
15 ``presleep``
15 ``presleep``
16 number of second to wait before any group of runs (default: 1)
16 number of second to wait before any group of runs (default: 1)
17
17
18 ``pre-run``
18 ``pre-run``
19 number of run to perform before starting measurement.
19 number of run to perform before starting measurement.
20
20
21 ``profile-benchmark``
21 ``profile-benchmark``
22 Enable profiling for the benchmarked section.
22 Enable profiling for the benchmarked section.
23 (The first iteration is benchmarked)
23 (The first iteration is benchmarked)
24
24
25 ``run-limits``
25 ``run-limits``
26 Control the number of runs each benchmark will perform. The option value
26 Control the number of runs each benchmark will perform. The option value
27 should be a list of `<time>-<numberofrun>` pairs. After each run the
27 should be a list of `<time>-<numberofrun>` pairs. After each run the
28 conditions are considered in order with the following logic:
28 conditions are considered in order with the following logic:
29
29
30 If benchmark has been running for <time> seconds, and we have performed
30 If benchmark has been running for <time> seconds, and we have performed
31 <numberofrun> iterations, stop the benchmark,
31 <numberofrun> iterations, stop the benchmark,
32
32
33 The default value is: `3.0-100, 10.0-3`
33 The default value is: `3.0-100, 10.0-3`
34
34
35 ``stub``
35 ``stub``
36 When set, benchmarks will only be run once, useful for testing
36 When set, benchmarks will only be run once, useful for testing
37 (default: off)
37 (default: off)
38 '''
38 '''
39
39
40 # "historical portability" policy of perf.py:
40 # "historical portability" policy of perf.py:
41 #
41 #
42 # We have to do:
42 # We have to do:
43 # - make perf.py "loadable" with as wide Mercurial version as possible
43 # - make perf.py "loadable" with as wide Mercurial version as possible
44 # This doesn't mean that perf commands work correctly with that Mercurial.
44 # This doesn't mean that perf commands work correctly with that Mercurial.
45 # BTW, perf.py itself has been available since 1.1 (or eb240755386d).
45 # BTW, perf.py itself has been available since 1.1 (or eb240755386d).
46 # - make historical perf command work correctly with as wide Mercurial
46 # - make historical perf command work correctly with as wide Mercurial
47 # version as possible
47 # version as possible
48 #
48 #
49 # We have to do, if possible with reasonable cost:
49 # We have to do, if possible with reasonable cost:
50 # - make recent perf command for historical feature work correctly
50 # - make recent perf command for historical feature work correctly
51 # with early Mercurial
51 # with early Mercurial
52 #
52 #
53 # We don't have to do:
53 # We don't have to do:
54 # - make perf command for recent feature work correctly with early
54 # - make perf command for recent feature work correctly with early
55 # Mercurial
55 # Mercurial
56
56
57 from __future__ import absolute_import
57 from __future__ import absolute_import
58 import contextlib
58 import contextlib
59 import functools
59 import functools
60 import gc
60 import gc
61 import os
61 import os
62 import random
62 import random
63 import shutil
63 import shutil
64 import struct
64 import struct
65 import sys
65 import sys
66 import tempfile
66 import tempfile
67 import threading
67 import threading
68 import time
68 import time
69
69
70 import mercurial.revlog
70 import mercurial.revlog
71 from mercurial import (
71 from mercurial import (
72 changegroup,
72 changegroup,
73 cmdutil,
73 cmdutil,
74 commands,
74 commands,
75 copies,
75 copies,
76 error,
76 error,
77 extensions,
77 extensions,
78 hg,
78 hg,
79 mdiff,
79 mdiff,
80 merge,
80 merge,
81 util,
81 util,
82 )
82 )
83
83
84 # for "historical portability":
84 # for "historical portability":
85 # try to import modules separately (in dict order), and ignore
85 # try to import modules separately (in dict order), and ignore
86 # failure, because these aren't available with early Mercurial
86 # failure, because these aren't available with early Mercurial
87 try:
87 try:
88 from mercurial import branchmap # since 2.5 (or bcee63733aad)
88 from mercurial import branchmap # since 2.5 (or bcee63733aad)
89 except ImportError:
89 except ImportError:
90 pass
90 pass
91 try:
91 try:
92 from mercurial import obsolete # since 2.3 (or ad0d6c2b3279)
92 from mercurial import obsolete # since 2.3 (or ad0d6c2b3279)
93 except ImportError:
93 except ImportError:
94 pass
94 pass
95 try:
95 try:
96 from mercurial import registrar # since 3.7 (or 37d50250b696)
96 from mercurial import registrar # since 3.7 (or 37d50250b696)
97
97
98 dir(registrar) # forcibly load it
98 dir(registrar) # forcibly load it
99 except ImportError:
99 except ImportError:
100 registrar = None
100 registrar = None
101 try:
101 try:
102 from mercurial import repoview # since 2.5 (or 3a6ddacb7198)
102 from mercurial import repoview # since 2.5 (or 3a6ddacb7198)
103 except ImportError:
103 except ImportError:
104 pass
104 pass
105 try:
105 try:
106 from mercurial.utils import repoviewutil # since 5.0
106 from mercurial.utils import repoviewutil # since 5.0
107 except ImportError:
107 except ImportError:
108 repoviewutil = None
108 repoviewutil = None
109 try:
109 try:
110 from mercurial import scmutil # since 1.9 (or 8b252e826c68)
110 from mercurial import scmutil # since 1.9 (or 8b252e826c68)
111 except ImportError:
111 except ImportError:
112 pass
112 pass
113 try:
113 try:
114 from mercurial import setdiscovery # since 1.9 (or cb98fed52495)
114 from mercurial import setdiscovery # since 1.9 (or cb98fed52495)
115 except ImportError:
115 except ImportError:
116 pass
116 pass
117
117
118 try:
118 try:
119 from mercurial import profiling
119 from mercurial import profiling
120 except ImportError:
120 except ImportError:
121 profiling = None
121 profiling = None
122
122
123 try:
123 try:
124 from mercurial.revlogutils import constants as revlog_constants
124 from mercurial.revlogutils import constants as revlog_constants
125
125
126 perf_rl_kind = (revlog_constants.KIND_OTHER, b'created-by-perf')
126 perf_rl_kind = (revlog_constants.KIND_OTHER, b'created-by-perf')
127
127
128 def revlog(opener, *args, **kwargs):
128 def revlog(opener, *args, **kwargs):
129 return mercurial.revlog.revlog(opener, perf_rl_kind, *args, **kwargs)
129 return mercurial.revlog.revlog(opener, perf_rl_kind, *args, **kwargs)
130
130
131
131
132 except (ImportError, AttributeError):
132 except (ImportError, AttributeError):
133 perf_rl_kind = None
133 perf_rl_kind = None
134
134
135 def revlog(opener, *args, **kwargs):
135 def revlog(opener, *args, **kwargs):
136 return mercurial.revlog.revlog(opener, *args, **kwargs)
136 return mercurial.revlog.revlog(opener, *args, **kwargs)
137
137
138
138
139 def identity(a):
139 def identity(a):
140 return a
140 return a
141
141
142
142
143 try:
143 try:
144 from mercurial import pycompat
144 from mercurial import pycompat
145
145
146 getargspec = pycompat.getargspec # added to module after 4.5
146 getargspec = pycompat.getargspec # added to module after 4.5
147 _byteskwargs = pycompat.byteskwargs # since 4.1 (or fbc3f73dc802)
147 _byteskwargs = pycompat.byteskwargs # since 4.1 (or fbc3f73dc802)
148 _sysstr = pycompat.sysstr # since 4.0 (or 2219f4f82ede)
148 _sysstr = pycompat.sysstr # since 4.0 (or 2219f4f82ede)
149 _bytestr = pycompat.bytestr # since 4.2 (or b70407bd84d5)
149 _bytestr = pycompat.bytestr # since 4.2 (or b70407bd84d5)
150 _xrange = pycompat.xrange # since 4.8 (or 7eba8f83129b)
150 _xrange = pycompat.xrange # since 4.8 (or 7eba8f83129b)
151 fsencode = pycompat.fsencode # since 3.9 (or f4a5e0e86a7e)
151 fsencode = pycompat.fsencode # since 3.9 (or f4a5e0e86a7e)
152 if pycompat.ispy3:
152 if pycompat.ispy3:
153 _maxint = sys.maxsize # per py3 docs for replacing maxint
153 _maxint = sys.maxsize # per py3 docs for replacing maxint
154 else:
154 else:
155 _maxint = sys.maxint
155 _maxint = sys.maxint
156 except (NameError, ImportError, AttributeError):
156 except (NameError, ImportError, AttributeError):
157 import inspect
157 import inspect
158
158
159 getargspec = inspect.getargspec
159 getargspec = inspect.getargspec
160 _byteskwargs = identity
160 _byteskwargs = identity
161 _bytestr = str
161 _bytestr = str
162 fsencode = identity # no py3 support
162 fsencode = identity # no py3 support
163 _maxint = sys.maxint # no py3 support
163 _maxint = sys.maxint # no py3 support
164 _sysstr = lambda x: x # no py3 support
164 _sysstr = lambda x: x # no py3 support
165 _xrange = xrange
165 _xrange = xrange
166
166
167 try:
167 try:
168 # 4.7+
168 # 4.7+
169 queue = pycompat.queue.Queue
169 queue = pycompat.queue.Queue
170 except (NameError, AttributeError, ImportError):
170 except (NameError, AttributeError, ImportError):
171 # <4.7.
171 # <4.7.
172 try:
172 try:
173 queue = pycompat.queue
173 queue = pycompat.queue
174 except (NameError, AttributeError, ImportError):
174 except (NameError, AttributeError, ImportError):
175 import Queue as queue
175 import Queue as queue
176
176
177 try:
177 try:
178 from mercurial import logcmdutil
178 from mercurial import logcmdutil
179
179
180 makelogtemplater = logcmdutil.maketemplater
180 makelogtemplater = logcmdutil.maketemplater
181 except (AttributeError, ImportError):
181 except (AttributeError, ImportError):
182 try:
182 try:
183 makelogtemplater = cmdutil.makelogtemplater
183 makelogtemplater = cmdutil.makelogtemplater
184 except (AttributeError, ImportError):
184 except (AttributeError, ImportError):
185 makelogtemplater = None
185 makelogtemplater = None
186
186
187 # for "historical portability":
187 # for "historical portability":
188 # define util.safehasattr forcibly, because util.safehasattr has been
188 # define util.safehasattr forcibly, because util.safehasattr has been
189 # available since 1.9.3 (or 94b200a11cf7)
189 # available since 1.9.3 (or 94b200a11cf7)
190 _undefined = object()
190 _undefined = object()
191
191
192
192
193 def safehasattr(thing, attr):
193 def safehasattr(thing, attr):
194 return getattr(thing, _sysstr(attr), _undefined) is not _undefined
194 return getattr(thing, _sysstr(attr), _undefined) is not _undefined
195
195
196
196
197 setattr(util, 'safehasattr', safehasattr)
197 setattr(util, 'safehasattr', safehasattr)
198
198
199 # for "historical portability":
199 # for "historical portability":
200 # define util.timer forcibly, because util.timer has been available
200 # define util.timer forcibly, because util.timer has been available
201 # since ae5d60bb70c9
201 # since ae5d60bb70c9
202 if safehasattr(time, 'perf_counter'):
202 if safehasattr(time, 'perf_counter'):
203 util.timer = time.perf_counter
203 util.timer = time.perf_counter
204 elif os.name == b'nt':
204 elif os.name == b'nt':
205 util.timer = time.clock
205 util.timer = time.clock
206 else:
206 else:
207 util.timer = time.time
207 util.timer = time.time
208
208
209 # for "historical portability":
209 # for "historical portability":
210 # use locally defined empty option list, if formatteropts isn't
210 # use locally defined empty option list, if formatteropts isn't
211 # available, because commands.formatteropts has been available since
211 # available, because commands.formatteropts has been available since
212 # 3.2 (or 7a7eed5176a4), even though formatting itself has been
212 # 3.2 (or 7a7eed5176a4), even though formatting itself has been
213 # available since 2.2 (or ae5f92e154d3)
213 # available since 2.2 (or ae5f92e154d3)
214 formatteropts = getattr(
214 formatteropts = getattr(
215 cmdutil, "formatteropts", getattr(commands, "formatteropts", [])
215 cmdutil, "formatteropts", getattr(commands, "formatteropts", [])
216 )
216 )
217
217
218 # for "historical portability":
218 # for "historical portability":
219 # use locally defined option list, if debugrevlogopts isn't available,
219 # use locally defined option list, if debugrevlogopts isn't available,
220 # because commands.debugrevlogopts has been available since 3.7 (or
220 # because commands.debugrevlogopts has been available since 3.7 (or
221 # 5606f7d0d063), even though cmdutil.openrevlog() has been available
221 # 5606f7d0d063), even though cmdutil.openrevlog() has been available
222 # since 1.9 (or a79fea6b3e77).
222 # since 1.9 (or a79fea6b3e77).
223 revlogopts = getattr(
223 revlogopts = getattr(
224 cmdutil,
224 cmdutil,
225 "debugrevlogopts",
225 "debugrevlogopts",
226 getattr(
226 getattr(
227 commands,
227 commands,
228 "debugrevlogopts",
228 "debugrevlogopts",
229 [
229 [
230 (b'c', b'changelog', False, b'open changelog'),
230 (b'c', b'changelog', False, b'open changelog'),
231 (b'm', b'manifest', False, b'open manifest'),
231 (b'm', b'manifest', False, b'open manifest'),
232 (b'', b'dir', False, b'open directory manifest'),
232 (b'', b'dir', False, b'open directory manifest'),
233 ],
233 ],
234 ),
234 ),
235 )
235 )
236
236
237 cmdtable = {}
237 cmdtable = {}
238
238
239 # for "historical portability":
239 # for "historical portability":
240 # define parsealiases locally, because cmdutil.parsealiases has been
240 # define parsealiases locally, because cmdutil.parsealiases has been
241 # available since 1.5 (or 6252852b4332)
241 # available since 1.5 (or 6252852b4332)
242 def parsealiases(cmd):
242 def parsealiases(cmd):
243 return cmd.split(b"|")
243 return cmd.split(b"|")
244
244
245
245
246 if safehasattr(registrar, 'command'):
246 if safehasattr(registrar, 'command'):
247 command = registrar.command(cmdtable)
247 command = registrar.command(cmdtable)
248 elif safehasattr(cmdutil, 'command'):
248 elif safehasattr(cmdutil, 'command'):
249 command = cmdutil.command(cmdtable)
249 command = cmdutil.command(cmdtable)
250 if 'norepo' not in getargspec(command).args:
250 if 'norepo' not in getargspec(command).args:
251 # for "historical portability":
251 # for "historical portability":
252 # wrap original cmdutil.command, because "norepo" option has
252 # wrap original cmdutil.command, because "norepo" option has
253 # been available since 3.1 (or 75a96326cecb)
253 # been available since 3.1 (or 75a96326cecb)
254 _command = command
254 _command = command
255
255
256 def command(name, options=(), synopsis=None, norepo=False):
256 def command(name, options=(), synopsis=None, norepo=False):
257 if norepo:
257 if norepo:
258 commands.norepo += b' %s' % b' '.join(parsealiases(name))
258 commands.norepo += b' %s' % b' '.join(parsealiases(name))
259 return _command(name, list(options), synopsis)
259 return _command(name, list(options), synopsis)
260
260
261
261
262 else:
262 else:
263 # for "historical portability":
263 # for "historical portability":
264 # define "@command" annotation locally, because cmdutil.command
264 # define "@command" annotation locally, because cmdutil.command
265 # has been available since 1.9 (or 2daa5179e73f)
265 # has been available since 1.9 (or 2daa5179e73f)
266 def command(name, options=(), synopsis=None, norepo=False):
266 def command(name, options=(), synopsis=None, norepo=False):
267 def decorator(func):
267 def decorator(func):
268 if synopsis:
268 if synopsis:
269 cmdtable[name] = func, list(options), synopsis
269 cmdtable[name] = func, list(options), synopsis
270 else:
270 else:
271 cmdtable[name] = func, list(options)
271 cmdtable[name] = func, list(options)
272 if norepo:
272 if norepo:
273 commands.norepo += b' %s' % b' '.join(parsealiases(name))
273 commands.norepo += b' %s' % b' '.join(parsealiases(name))
274 return func
274 return func
275
275
276 return decorator
276 return decorator
277
277
278
278
279 try:
279 try:
280 import mercurial.registrar
280 import mercurial.registrar
281 import mercurial.configitems
281 import mercurial.configitems
282
282
283 configtable = {}
283 configtable = {}
284 configitem = mercurial.registrar.configitem(configtable)
284 configitem = mercurial.registrar.configitem(configtable)
285 configitem(
285 configitem(
286 b'perf',
286 b'perf',
287 b'presleep',
287 b'presleep',
288 default=mercurial.configitems.dynamicdefault,
288 default=mercurial.configitems.dynamicdefault,
289 experimental=True,
289 experimental=True,
290 )
290 )
291 configitem(
291 configitem(
292 b'perf',
292 b'perf',
293 b'stub',
293 b'stub',
294 default=mercurial.configitems.dynamicdefault,
294 default=mercurial.configitems.dynamicdefault,
295 experimental=True,
295 experimental=True,
296 )
296 )
297 configitem(
297 configitem(
298 b'perf',
298 b'perf',
299 b'parentscount',
299 b'parentscount',
300 default=mercurial.configitems.dynamicdefault,
300 default=mercurial.configitems.dynamicdefault,
301 experimental=True,
301 experimental=True,
302 )
302 )
303 configitem(
303 configitem(
304 b'perf',
304 b'perf',
305 b'all-timing',
305 b'all-timing',
306 default=mercurial.configitems.dynamicdefault,
306 default=mercurial.configitems.dynamicdefault,
307 experimental=True,
307 experimental=True,
308 )
308 )
309 configitem(
309 configitem(
310 b'perf',
310 b'perf',
311 b'pre-run',
311 b'pre-run',
312 default=mercurial.configitems.dynamicdefault,
312 default=mercurial.configitems.dynamicdefault,
313 )
313 )
314 configitem(
314 configitem(
315 b'perf',
315 b'perf',
316 b'profile-benchmark',
316 b'profile-benchmark',
317 default=mercurial.configitems.dynamicdefault,
317 default=mercurial.configitems.dynamicdefault,
318 )
318 )
319 configitem(
319 configitem(
320 b'perf',
320 b'perf',
321 b'run-limits',
321 b'run-limits',
322 default=mercurial.configitems.dynamicdefault,
322 default=mercurial.configitems.dynamicdefault,
323 experimental=True,
323 experimental=True,
324 )
324 )
325 except (ImportError, AttributeError):
325 except (ImportError, AttributeError):
326 pass
326 pass
327 except TypeError:
327 except TypeError:
328 # compatibility fix for a11fd395e83f
328 # compatibility fix for a11fd395e83f
329 # hg version: 5.2
329 # hg version: 5.2
330 configitem(
330 configitem(
331 b'perf',
331 b'perf',
332 b'presleep',
332 b'presleep',
333 default=mercurial.configitems.dynamicdefault,
333 default=mercurial.configitems.dynamicdefault,
334 )
334 )
335 configitem(
335 configitem(
336 b'perf',
336 b'perf',
337 b'stub',
337 b'stub',
338 default=mercurial.configitems.dynamicdefault,
338 default=mercurial.configitems.dynamicdefault,
339 )
339 )
340 configitem(
340 configitem(
341 b'perf',
341 b'perf',
342 b'parentscount',
342 b'parentscount',
343 default=mercurial.configitems.dynamicdefault,
343 default=mercurial.configitems.dynamicdefault,
344 )
344 )
345 configitem(
345 configitem(
346 b'perf',
346 b'perf',
347 b'all-timing',
347 b'all-timing',
348 default=mercurial.configitems.dynamicdefault,
348 default=mercurial.configitems.dynamicdefault,
349 )
349 )
350 configitem(
350 configitem(
351 b'perf',
351 b'perf',
352 b'pre-run',
352 b'pre-run',
353 default=mercurial.configitems.dynamicdefault,
353 default=mercurial.configitems.dynamicdefault,
354 )
354 )
355 configitem(
355 configitem(
356 b'perf',
356 b'perf',
357 b'profile-benchmark',
357 b'profile-benchmark',
358 default=mercurial.configitems.dynamicdefault,
358 default=mercurial.configitems.dynamicdefault,
359 )
359 )
360 configitem(
360 configitem(
361 b'perf',
361 b'perf',
362 b'run-limits',
362 b'run-limits',
363 default=mercurial.configitems.dynamicdefault,
363 default=mercurial.configitems.dynamicdefault,
364 )
364 )
365
365
366
366
367 def getlen(ui):
367 def getlen(ui):
368 if ui.configbool(b"perf", b"stub", False):
368 if ui.configbool(b"perf", b"stub", False):
369 return lambda x: 1
369 return lambda x: 1
370 return len
370 return len
371
371
372
372
373 class noop(object):
373 class noop(object):
374 """dummy context manager"""
374 """dummy context manager"""
375
375
376 def __enter__(self):
376 def __enter__(self):
377 pass
377 pass
378
378
379 def __exit__(self, *args):
379 def __exit__(self, *args):
380 pass
380 pass
381
381
382
382
383 NOOPCTX = noop()
383 NOOPCTX = noop()
384
384
385
385
386 def gettimer(ui, opts=None):
386 def gettimer(ui, opts=None):
387 """return a timer function and formatter: (timer, formatter)
387 """return a timer function and formatter: (timer, formatter)
388
388
389 This function exists to gather the creation of formatter in a single
389 This function exists to gather the creation of formatter in a single
390 place instead of duplicating it in all performance commands."""
390 place instead of duplicating it in all performance commands."""
391
391
392 # enforce an idle period before execution to counteract power management
392 # enforce an idle period before execution to counteract power management
393 # experimental config: perf.presleep
393 # experimental config: perf.presleep
394 time.sleep(getint(ui, b"perf", b"presleep", 1))
394 time.sleep(getint(ui, b"perf", b"presleep", 1))
395
395
396 if opts is None:
396 if opts is None:
397 opts = {}
397 opts = {}
398 # redirect all to stderr unless buffer api is in use
398 # redirect all to stderr unless buffer api is in use
399 if not ui._buffers:
399 if not ui._buffers:
400 ui = ui.copy()
400 ui = ui.copy()
401 uifout = safeattrsetter(ui, b'fout', ignoremissing=True)
401 uifout = safeattrsetter(ui, b'fout', ignoremissing=True)
402 if uifout:
402 if uifout:
403 # for "historical portability":
403 # for "historical portability":
404 # ui.fout/ferr have been available since 1.9 (or 4e1ccd4c2b6d)
404 # ui.fout/ferr have been available since 1.9 (or 4e1ccd4c2b6d)
405 uifout.set(ui.ferr)
405 uifout.set(ui.ferr)
406
406
407 # get a formatter
407 # get a formatter
408 uiformatter = getattr(ui, 'formatter', None)
408 uiformatter = getattr(ui, 'formatter', None)
409 if uiformatter:
409 if uiformatter:
410 fm = uiformatter(b'perf', opts)
410 fm = uiformatter(b'perf', opts)
411 else:
411 else:
412 # for "historical portability":
412 # for "historical portability":
413 # define formatter locally, because ui.formatter has been
413 # define formatter locally, because ui.formatter has been
414 # available since 2.2 (or ae5f92e154d3)
414 # available since 2.2 (or ae5f92e154d3)
415 from mercurial import node
415 from mercurial import node
416
416
417 class defaultformatter(object):
417 class defaultformatter(object):
418 """Minimized composition of baseformatter and plainformatter"""
418 """Minimized composition of baseformatter and plainformatter"""
419
419
420 def __init__(self, ui, topic, opts):
420 def __init__(self, ui, topic, opts):
421 self._ui = ui
421 self._ui = ui
422 if ui.debugflag:
422 if ui.debugflag:
423 self.hexfunc = node.hex
423 self.hexfunc = node.hex
424 else:
424 else:
425 self.hexfunc = node.short
425 self.hexfunc = node.short
426
426
427 def __nonzero__(self):
427 def __nonzero__(self):
428 return False
428 return False
429
429
430 __bool__ = __nonzero__
430 __bool__ = __nonzero__
431
431
432 def startitem(self):
432 def startitem(self):
433 pass
433 pass
434
434
435 def data(self, **data):
435 def data(self, **data):
436 pass
436 pass
437
437
438 def write(self, fields, deftext, *fielddata, **opts):
438 def write(self, fields, deftext, *fielddata, **opts):
439 self._ui.write(deftext % fielddata, **opts)
439 self._ui.write(deftext % fielddata, **opts)
440
440
441 def condwrite(self, cond, fields, deftext, *fielddata, **opts):
441 def condwrite(self, cond, fields, deftext, *fielddata, **opts):
442 if cond:
442 if cond:
443 self._ui.write(deftext % fielddata, **opts)
443 self._ui.write(deftext % fielddata, **opts)
444
444
445 def plain(self, text, **opts):
445 def plain(self, text, **opts):
446 self._ui.write(text, **opts)
446 self._ui.write(text, **opts)
447
447
448 def end(self):
448 def end(self):
449 pass
449 pass
450
450
451 fm = defaultformatter(ui, b'perf', opts)
451 fm = defaultformatter(ui, b'perf', opts)
452
452
453 # stub function, runs code only once instead of in a loop
453 # stub function, runs code only once instead of in a loop
454 # experimental config: perf.stub
454 # experimental config: perf.stub
455 if ui.configbool(b"perf", b"stub", False):
455 if ui.configbool(b"perf", b"stub", False):
456 return functools.partial(stub_timer, fm), fm
456 return functools.partial(stub_timer, fm), fm
457
457
458 # experimental config: perf.all-timing
458 # experimental config: perf.all-timing
459 displayall = ui.configbool(b"perf", b"all-timing", False)
459 displayall = ui.configbool(b"perf", b"all-timing", False)
460
460
461 # experimental config: perf.run-limits
461 # experimental config: perf.run-limits
462 limitspec = ui.configlist(b"perf", b"run-limits", [])
462 limitspec = ui.configlist(b"perf", b"run-limits", [])
463 limits = []
463 limits = []
464 for item in limitspec:
464 for item in limitspec:
465 parts = item.split(b'-', 1)
465 parts = item.split(b'-', 1)
466 if len(parts) < 2:
466 if len(parts) < 2:
467 ui.warn((b'malformatted run limit entry, missing "-": %s\n' % item))
467 ui.warn((b'malformatted run limit entry, missing "-": %s\n' % item))
468 continue
468 continue
469 try:
469 try:
470 time_limit = float(_sysstr(parts[0]))
470 time_limit = float(_sysstr(parts[0]))
471 except ValueError as e:
471 except ValueError as e:
472 ui.warn(
472 ui.warn(
473 (
473 (
474 b'malformatted run limit entry, %s: %s\n'
474 b'malformatted run limit entry, %s: %s\n'
475 % (_bytestr(e), item)
475 % (_bytestr(e), item)
476 )
476 )
477 )
477 )
478 continue
478 continue
479 try:
479 try:
480 run_limit = int(_sysstr(parts[1]))
480 run_limit = int(_sysstr(parts[1]))
481 except ValueError as e:
481 except ValueError as e:
482 ui.warn(
482 ui.warn(
483 (
483 (
484 b'malformatted run limit entry, %s: %s\n'
484 b'malformatted run limit entry, %s: %s\n'
485 % (_bytestr(e), item)
485 % (_bytestr(e), item)
486 )
486 )
487 )
487 )
488 continue
488 continue
489 limits.append((time_limit, run_limit))
489 limits.append((time_limit, run_limit))
490 if not limits:
490 if not limits:
491 limits = DEFAULTLIMITS
491 limits = DEFAULTLIMITS
492
492
493 profiler = None
493 profiler = None
494 if profiling is not None:
494 if profiling is not None:
495 if ui.configbool(b"perf", b"profile-benchmark", False):
495 if ui.configbool(b"perf", b"profile-benchmark", False):
496 profiler = profiling.profile(ui)
496 profiler = profiling.profile(ui)
497
497
498 prerun = getint(ui, b"perf", b"pre-run", 0)
498 prerun = getint(ui, b"perf", b"pre-run", 0)
499 t = functools.partial(
499 t = functools.partial(
500 _timer,
500 _timer,
501 fm,
501 fm,
502 displayall=displayall,
502 displayall=displayall,
503 limits=limits,
503 limits=limits,
504 prerun=prerun,
504 prerun=prerun,
505 profiler=profiler,
505 profiler=profiler,
506 )
506 )
507 return t, fm
507 return t, fm
508
508
509
509
510 def stub_timer(fm, func, setup=None, title=None):
510 def stub_timer(fm, func, setup=None, title=None):
511 if setup is not None:
511 if setup is not None:
512 setup()
512 setup()
513 func()
513 func()
514
514
515
515
516 @contextlib.contextmanager
516 @contextlib.contextmanager
517 def timeone():
517 def timeone():
518 r = []
518 r = []
519 ostart = os.times()
519 ostart = os.times()
520 cstart = util.timer()
520 cstart = util.timer()
521 yield r
521 yield r
522 cstop = util.timer()
522 cstop = util.timer()
523 ostop = os.times()
523 ostop = os.times()
524 a, b = ostart, ostop
524 a, b = ostart, ostop
525 r.append((cstop - cstart, b[0] - a[0], b[1] - a[1]))
525 r.append((cstop - cstart, b[0] - a[0], b[1] - a[1]))
526
526
527
527
528 # list of stop condition (elapsed time, minimal run count)
528 # list of stop condition (elapsed time, minimal run count)
529 DEFAULTLIMITS = (
529 DEFAULTLIMITS = (
530 (3.0, 100),
530 (3.0, 100),
531 (10.0, 3),
531 (10.0, 3),
532 )
532 )
533
533
534
534
535 def _timer(
535 def _timer(
536 fm,
536 fm,
537 func,
537 func,
538 setup=None,
538 setup=None,
539 title=None,
539 title=None,
540 displayall=False,
540 displayall=False,
541 limits=DEFAULTLIMITS,
541 limits=DEFAULTLIMITS,
542 prerun=0,
542 prerun=0,
543 profiler=None,
543 profiler=None,
544 ):
544 ):
545 gc.collect()
545 gc.collect()
546 results = []
546 results = []
547 begin = util.timer()
547 begin = util.timer()
548 count = 0
548 count = 0
549 if profiler is None:
549 if profiler is None:
550 profiler = NOOPCTX
550 profiler = NOOPCTX
551 for i in range(prerun):
551 for i in range(prerun):
552 if setup is not None:
552 if setup is not None:
553 setup()
553 setup()
554 func()
554 func()
555 keepgoing = True
555 keepgoing = True
556 while keepgoing:
556 while keepgoing:
557 if setup is not None:
557 if setup is not None:
558 setup()
558 setup()
559 with profiler:
559 with profiler:
560 with timeone() as item:
560 with timeone() as item:
561 r = func()
561 r = func()
562 profiler = NOOPCTX
562 profiler = NOOPCTX
563 count += 1
563 count += 1
564 results.append(item[0])
564 results.append(item[0])
565 cstop = util.timer()
565 cstop = util.timer()
566 # Look for a stop condition.
566 # Look for a stop condition.
567 elapsed = cstop - begin
567 elapsed = cstop - begin
568 for t, mincount in limits:
568 for t, mincount in limits:
569 if elapsed >= t and count >= mincount:
569 if elapsed >= t and count >= mincount:
570 keepgoing = False
570 keepgoing = False
571 break
571 break
572
572
573 formatone(fm, results, title=title, result=r, displayall=displayall)
573 formatone(fm, results, title=title, result=r, displayall=displayall)
574
574
575
575
576 def formatone(fm, timings, title=None, result=None, displayall=False):
576 def formatone(fm, timings, title=None, result=None, displayall=False):
577
577
578 count = len(timings)
578 count = len(timings)
579
579
580 fm.startitem()
580 fm.startitem()
581
581
582 if title:
582 if title:
583 fm.write(b'title', b'! %s\n', title)
583 fm.write(b'title', b'! %s\n', title)
584 if result:
584 if result:
585 fm.write(b'result', b'! result: %s\n', result)
585 fm.write(b'result', b'! result: %s\n', result)
586
586
587 def display(role, entry):
587 def display(role, entry):
588 prefix = b''
588 prefix = b''
589 if role != b'best':
589 if role != b'best':
590 prefix = b'%s.' % role
590 prefix = b'%s.' % role
591 fm.plain(b'!')
591 fm.plain(b'!')
592 fm.write(prefix + b'wall', b' wall %f', entry[0])
592 fm.write(prefix + b'wall', b' wall %f', entry[0])
593 fm.write(prefix + b'comb', b' comb %f', entry[1] + entry[2])
593 fm.write(prefix + b'comb', b' comb %f', entry[1] + entry[2])
594 fm.write(prefix + b'user', b' user %f', entry[1])
594 fm.write(prefix + b'user', b' user %f', entry[1])
595 fm.write(prefix + b'sys', b' sys %f', entry[2])
595 fm.write(prefix + b'sys', b' sys %f', entry[2])
596 fm.write(prefix + b'count', b' (%s of %%d)' % role, count)
596 fm.write(prefix + b'count', b' (%s of %%d)' % role, count)
597 fm.plain(b'\n')
597 fm.plain(b'\n')
598
598
599 timings.sort()
599 timings.sort()
600 min_val = timings[0]
600 min_val = timings[0]
601 display(b'best', min_val)
601 display(b'best', min_val)
602 if displayall:
602 if displayall:
603 max_val = timings[-1]
603 max_val = timings[-1]
604 display(b'max', max_val)
604 display(b'max', max_val)
605 avg = tuple([sum(x) / count for x in zip(*timings)])
605 avg = tuple([sum(x) / count for x in zip(*timings)])
606 display(b'avg', avg)
606 display(b'avg', avg)
607 median = timings[len(timings) // 2]
607 median = timings[len(timings) // 2]
608 display(b'median', median)
608 display(b'median', median)
609
609
610
610
611 # utilities for historical portability
611 # utilities for historical portability
612
612
613
613
614 def getint(ui, section, name, default):
614 def getint(ui, section, name, default):
615 # for "historical portability":
615 # for "historical portability":
616 # ui.configint has been available since 1.9 (or fa2b596db182)
616 # ui.configint has been available since 1.9 (or fa2b596db182)
617 v = ui.config(section, name, None)
617 v = ui.config(section, name, None)
618 if v is None:
618 if v is None:
619 return default
619 return default
620 try:
620 try:
621 return int(v)
621 return int(v)
622 except ValueError:
622 except ValueError:
623 raise error.ConfigError(
623 raise error.ConfigError(
624 b"%s.%s is not an integer ('%s')" % (section, name, v)
624 b"%s.%s is not an integer ('%s')" % (section, name, v)
625 )
625 )
626
626
627
627
628 def safeattrsetter(obj, name, ignoremissing=False):
628 def safeattrsetter(obj, name, ignoremissing=False):
629 """Ensure that 'obj' has 'name' attribute before subsequent setattr
629 """Ensure that 'obj' has 'name' attribute before subsequent setattr
630
630
631 This function is aborted, if 'obj' doesn't have 'name' attribute
631 This function is aborted, if 'obj' doesn't have 'name' attribute
632 at runtime. This avoids overlooking removal of an attribute, which
632 at runtime. This avoids overlooking removal of an attribute, which
633 breaks assumption of performance measurement, in the future.
633 breaks assumption of performance measurement, in the future.
634
634
635 This function returns the object to (1) assign a new value, and
635 This function returns the object to (1) assign a new value, and
636 (2) restore an original value to the attribute.
636 (2) restore an original value to the attribute.
637
637
638 If 'ignoremissing' is true, missing 'name' attribute doesn't cause
638 If 'ignoremissing' is true, missing 'name' attribute doesn't cause
639 abortion, and this function returns None. This is useful to
639 abortion, and this function returns None. This is useful to
640 examine an attribute, which isn't ensured in all Mercurial
640 examine an attribute, which isn't ensured in all Mercurial
641 versions.
641 versions.
642 """
642 """
643 if not util.safehasattr(obj, name):
643 if not util.safehasattr(obj, name):
644 if ignoremissing:
644 if ignoremissing:
645 return None
645 return None
646 raise error.Abort(
646 raise error.Abort(
647 (
647 (
648 b"missing attribute %s of %s might break assumption"
648 b"missing attribute %s of %s might break assumption"
649 b" of performance measurement"
649 b" of performance measurement"
650 )
650 )
651 % (name, obj)
651 % (name, obj)
652 )
652 )
653
653
654 origvalue = getattr(obj, _sysstr(name))
654 origvalue = getattr(obj, _sysstr(name))
655
655
656 class attrutil(object):
656 class attrutil(object):
657 def set(self, newvalue):
657 def set(self, newvalue):
658 setattr(obj, _sysstr(name), newvalue)
658 setattr(obj, _sysstr(name), newvalue)
659
659
660 def restore(self):
660 def restore(self):
661 setattr(obj, _sysstr(name), origvalue)
661 setattr(obj, _sysstr(name), origvalue)
662
662
663 return attrutil()
663 return attrutil()
664
664
665
665
666 # utilities to examine each internal API changes
666 # utilities to examine each internal API changes
667
667
668
668
669 def getbranchmapsubsettable():
669 def getbranchmapsubsettable():
670 # for "historical portability":
670 # for "historical portability":
671 # subsettable is defined in:
671 # subsettable is defined in:
672 # - branchmap since 2.9 (or 175c6fd8cacc)
672 # - branchmap since 2.9 (or 175c6fd8cacc)
673 # - repoview since 2.5 (or 59a9f18d4587)
673 # - repoview since 2.5 (or 59a9f18d4587)
674 # - repoviewutil since 5.0
674 # - repoviewutil since 5.0
675 for mod in (branchmap, repoview, repoviewutil):
675 for mod in (branchmap, repoview, repoviewutil):
676 subsettable = getattr(mod, 'subsettable', None)
676 subsettable = getattr(mod, 'subsettable', None)
677 if subsettable:
677 if subsettable:
678 return subsettable
678 return subsettable
679
679
680 # bisecting in bcee63733aad::59a9f18d4587 can reach here (both
680 # bisecting in bcee63733aad::59a9f18d4587 can reach here (both
681 # branchmap and repoview modules exist, but subsettable attribute
681 # branchmap and repoview modules exist, but subsettable attribute
682 # doesn't)
682 # doesn't)
683 raise error.Abort(
683 raise error.Abort(
684 b"perfbranchmap not available with this Mercurial",
684 b"perfbranchmap not available with this Mercurial",
685 hint=b"use 2.5 or later",
685 hint=b"use 2.5 or later",
686 )
686 )
687
687
688
688
689 def getsvfs(repo):
689 def getsvfs(repo):
690 """Return appropriate object to access files under .hg/store"""
690 """Return appropriate object to access files under .hg/store"""
691 # for "historical portability":
691 # for "historical portability":
692 # repo.svfs has been available since 2.3 (or 7034365089bf)
692 # repo.svfs has been available since 2.3 (or 7034365089bf)
693 svfs = getattr(repo, 'svfs', None)
693 svfs = getattr(repo, 'svfs', None)
694 if svfs:
694 if svfs:
695 return svfs
695 return svfs
696 else:
696 else:
697 return getattr(repo, 'sopener')
697 return getattr(repo, 'sopener')
698
698
699
699
700 def getvfs(repo):
700 def getvfs(repo):
701 """Return appropriate object to access files under .hg"""
701 """Return appropriate object to access files under .hg"""
702 # for "historical portability":
702 # for "historical portability":
703 # repo.vfs has been available since 2.3 (or 7034365089bf)
703 # repo.vfs has been available since 2.3 (or 7034365089bf)
704 vfs = getattr(repo, 'vfs', None)
704 vfs = getattr(repo, 'vfs', None)
705 if vfs:
705 if vfs:
706 return vfs
706 return vfs
707 else:
707 else:
708 return getattr(repo, 'opener')
708 return getattr(repo, 'opener')
709
709
710
710
711 def repocleartagscachefunc(repo):
711 def repocleartagscachefunc(repo):
712 """Return the function to clear tags cache according to repo internal API"""
712 """Return the function to clear tags cache according to repo internal API"""
713 if util.safehasattr(repo, b'_tagscache'): # since 2.0 (or 9dca7653b525)
713 if util.safehasattr(repo, b'_tagscache'): # since 2.0 (or 9dca7653b525)
714 # in this case, setattr(repo, '_tagscache', None) or so isn't
714 # in this case, setattr(repo, '_tagscache', None) or so isn't
715 # correct way to clear tags cache, because existing code paths
715 # correct way to clear tags cache, because existing code paths
716 # expect _tagscache to be a structured object.
716 # expect _tagscache to be a structured object.
717 def clearcache():
717 def clearcache():
718 # _tagscache has been filteredpropertycache since 2.5 (or
718 # _tagscache has been filteredpropertycache since 2.5 (or
719 # 98c867ac1330), and delattr() can't work in such case
719 # 98c867ac1330), and delattr() can't work in such case
720 if '_tagscache' in vars(repo):
720 if '_tagscache' in vars(repo):
721 del repo.__dict__['_tagscache']
721 del repo.__dict__['_tagscache']
722
722
723 return clearcache
723 return clearcache
724
724
725 repotags = safeattrsetter(repo, b'_tags', ignoremissing=True)
725 repotags = safeattrsetter(repo, b'_tags', ignoremissing=True)
726 if repotags: # since 1.4 (or 5614a628d173)
726 if repotags: # since 1.4 (or 5614a628d173)
727 return lambda: repotags.set(None)
727 return lambda: repotags.set(None)
728
728
729 repotagscache = safeattrsetter(repo, b'tagscache', ignoremissing=True)
729 repotagscache = safeattrsetter(repo, b'tagscache', ignoremissing=True)
730 if repotagscache: # since 0.6 (or d7df759d0e97)
730 if repotagscache: # since 0.6 (or d7df759d0e97)
731 return lambda: repotagscache.set(None)
731 return lambda: repotagscache.set(None)
732
732
733 # Mercurial earlier than 0.6 (or d7df759d0e97) logically reaches
733 # Mercurial earlier than 0.6 (or d7df759d0e97) logically reaches
734 # this point, but it isn't so problematic, because:
734 # this point, but it isn't so problematic, because:
735 # - repo.tags of such Mercurial isn't "callable", and repo.tags()
735 # - repo.tags of such Mercurial isn't "callable", and repo.tags()
736 # in perftags() causes failure soon
736 # in perftags() causes failure soon
737 # - perf.py itself has been available since 1.1 (or eb240755386d)
737 # - perf.py itself has been available since 1.1 (or eb240755386d)
738 raise error.Abort(b"tags API of this hg command is unknown")
738 raise error.Abort(b"tags API of this hg command is unknown")
739
739
740
740
741 # utilities to clear cache
741 # utilities to clear cache
742
742
743
743
744 def clearfilecache(obj, attrname):
744 def clearfilecache(obj, attrname):
745 unfiltered = getattr(obj, 'unfiltered', None)
745 unfiltered = getattr(obj, 'unfiltered', None)
746 if unfiltered is not None:
746 if unfiltered is not None:
747 obj = obj.unfiltered()
747 obj = obj.unfiltered()
748 if attrname in vars(obj):
748 if attrname in vars(obj):
749 delattr(obj, attrname)
749 delattr(obj, attrname)
750 obj._filecache.pop(attrname, None)
750 obj._filecache.pop(attrname, None)
751
751
752
752
753 def clearchangelog(repo):
753 def clearchangelog(repo):
754 if repo is not repo.unfiltered():
754 if repo is not repo.unfiltered():
755 object.__setattr__(repo, '_clcachekey', None)
755 object.__setattr__(repo, '_clcachekey', None)
756 object.__setattr__(repo, '_clcache', None)
756 object.__setattr__(repo, '_clcache', None)
757 clearfilecache(repo.unfiltered(), 'changelog')
757 clearfilecache(repo.unfiltered(), 'changelog')
758
758
759
759
760 # perf commands
760 # perf commands
761
761
762
762
763 @command(b'perf::walk|perfwalk', formatteropts)
763 @command(b'perf::walk|perfwalk', formatteropts)
764 def perfwalk(ui, repo, *pats, **opts):
764 def perfwalk(ui, repo, *pats, **opts):
765 opts = _byteskwargs(opts)
765 opts = _byteskwargs(opts)
766 timer, fm = gettimer(ui, opts)
766 timer, fm = gettimer(ui, opts)
767 m = scmutil.match(repo[None], pats, {})
767 m = scmutil.match(repo[None], pats, {})
768 timer(
768 timer(
769 lambda: len(
769 lambda: len(
770 list(
770 list(
771 repo.dirstate.walk(m, subrepos=[], unknown=True, ignored=False)
771 repo.dirstate.walk(m, subrepos=[], unknown=True, ignored=False)
772 )
772 )
773 )
773 )
774 )
774 )
775 fm.end()
775 fm.end()
776
776
777
777
778 @command(b'perf::annotate|perfannotate', formatteropts)
778 @command(b'perf::annotate|perfannotate', formatteropts)
779 def perfannotate(ui, repo, f, **opts):
779 def perfannotate(ui, repo, f, **opts):
780 opts = _byteskwargs(opts)
780 opts = _byteskwargs(opts)
781 timer, fm = gettimer(ui, opts)
781 timer, fm = gettimer(ui, opts)
782 fc = repo[b'.'][f]
782 fc = repo[b'.'][f]
783 timer(lambda: len(fc.annotate(True)))
783 timer(lambda: len(fc.annotate(True)))
784 fm.end()
784 fm.end()
785
785
786
786
787 @command(
787 @command(
788 b'perf::status|perfstatus',
788 b'perf::status|perfstatus',
789 [
789 [
790 (b'u', b'unknown', False, b'ask status to look for unknown files'),
790 (b'u', b'unknown', False, b'ask status to look for unknown files'),
791 (b'', b'dirstate', False, b'benchmark the internal dirstate call'),
791 (b'', b'dirstate', False, b'benchmark the internal dirstate call'),
792 ]
792 ]
793 + formatteropts,
793 + formatteropts,
794 )
794 )
795 def perfstatus(ui, repo, **opts):
795 def perfstatus(ui, repo, **opts):
796 """benchmark the performance of a single status call
796 """benchmark the performance of a single status call
797
797
798 The repository data are preserved between each call.
798 The repository data are preserved between each call.
799
799
800 By default, only the status of the tracked file are requested. If
800 By default, only the status of the tracked file are requested. If
801 `--unknown` is passed, the "unknown" files are also tracked.
801 `--unknown` is passed, the "unknown" files are also tracked.
802 """
802 """
803 opts = _byteskwargs(opts)
803 opts = _byteskwargs(opts)
804 # m = match.always(repo.root, repo.getcwd())
804 # m = match.always(repo.root, repo.getcwd())
805 # timer(lambda: sum(map(len, repo.dirstate.status(m, [], False, False,
805 # timer(lambda: sum(map(len, repo.dirstate.status(m, [], False, False,
806 # False))))
806 # False))))
807 timer, fm = gettimer(ui, opts)
807 timer, fm = gettimer(ui, opts)
808 if opts[b'dirstate']:
808 if opts[b'dirstate']:
809 dirstate = repo.dirstate
809 dirstate = repo.dirstate
810 m = scmutil.matchall(repo)
810 m = scmutil.matchall(repo)
811 unknown = opts[b'unknown']
811 unknown = opts[b'unknown']
812
812
813 def status_dirstate():
813 def status_dirstate():
814 s = dirstate.status(
814 s = dirstate.status(
815 m, subrepos=[], ignored=False, clean=False, unknown=unknown
815 m, subrepos=[], ignored=False, clean=False, unknown=unknown
816 )
816 )
817 sum(map(bool, s))
817 sum(map(bool, s))
818
818
819 timer(status_dirstate)
819 timer(status_dirstate)
820 else:
820 else:
821 timer(lambda: sum(map(len, repo.status(unknown=opts[b'unknown']))))
821 timer(lambda: sum(map(len, repo.status(unknown=opts[b'unknown']))))
822 fm.end()
822 fm.end()
823
823
824
824
825 @command(b'perf::addremove|perfaddremove', formatteropts)
825 @command(b'perf::addremove|perfaddremove', formatteropts)
826 def perfaddremove(ui, repo, **opts):
826 def perfaddremove(ui, repo, **opts):
827 opts = _byteskwargs(opts)
827 opts = _byteskwargs(opts)
828 timer, fm = gettimer(ui, opts)
828 timer, fm = gettimer(ui, opts)
829 try:
829 try:
830 oldquiet = repo.ui.quiet
830 oldquiet = repo.ui.quiet
831 repo.ui.quiet = True
831 repo.ui.quiet = True
832 matcher = scmutil.match(repo[None])
832 matcher = scmutil.match(repo[None])
833 opts[b'dry_run'] = True
833 opts[b'dry_run'] = True
834 if 'uipathfn' in getargspec(scmutil.addremove).args:
834 if 'uipathfn' in getargspec(scmutil.addremove).args:
835 uipathfn = scmutil.getuipathfn(repo)
835 uipathfn = scmutil.getuipathfn(repo)
836 timer(lambda: scmutil.addremove(repo, matcher, b"", uipathfn, opts))
836 timer(lambda: scmutil.addremove(repo, matcher, b"", uipathfn, opts))
837 else:
837 else:
838 timer(lambda: scmutil.addremove(repo, matcher, b"", opts))
838 timer(lambda: scmutil.addremove(repo, matcher, b"", opts))
839 finally:
839 finally:
840 repo.ui.quiet = oldquiet
840 repo.ui.quiet = oldquiet
841 fm.end()
841 fm.end()
842
842
843
843
844 def clearcaches(cl):
844 def clearcaches(cl):
845 # behave somewhat consistently across internal API changes
845 # behave somewhat consistently across internal API changes
846 if util.safehasattr(cl, b'clearcaches'):
846 if util.safehasattr(cl, b'clearcaches'):
847 cl.clearcaches()
847 cl.clearcaches()
848 elif util.safehasattr(cl, b'_nodecache'):
848 elif util.safehasattr(cl, b'_nodecache'):
849 # <= hg-5.2
849 # <= hg-5.2
850 from mercurial.node import nullid, nullrev
850 from mercurial.node import nullid, nullrev
851
851
852 cl._nodecache = {nullid: nullrev}
852 cl._nodecache = {nullid: nullrev}
853 cl._nodepos = None
853 cl._nodepos = None
854
854
855
855
856 @command(b'perf::heads|perfheads', formatteropts)
856 @command(b'perf::heads|perfheads', formatteropts)
857 def perfheads(ui, repo, **opts):
857 def perfheads(ui, repo, **opts):
858 """benchmark the computation of a changelog heads"""
858 """benchmark the computation of a changelog heads"""
859 opts = _byteskwargs(opts)
859 opts = _byteskwargs(opts)
860 timer, fm = gettimer(ui, opts)
860 timer, fm = gettimer(ui, opts)
861 cl = repo.changelog
861 cl = repo.changelog
862
862
863 def s():
863 def s():
864 clearcaches(cl)
864 clearcaches(cl)
865
865
866 def d():
866 def d():
867 len(cl.headrevs())
867 len(cl.headrevs())
868
868
869 timer(d, setup=s)
869 timer(d, setup=s)
870 fm.end()
870 fm.end()
871
871
872
872
873 @command(
873 @command(
874 b'perf::tags|perftags',
874 b'perf::tags|perftags',
875 formatteropts
875 formatteropts
876 + [
876 + [
877 (b'', b'clear-revlogs', False, b'refresh changelog and manifest'),
877 (b'', b'clear-revlogs', False, b'refresh changelog and manifest'),
878 ],
878 ],
879 )
879 )
880 def perftags(ui, repo, **opts):
880 def perftags(ui, repo, **opts):
881 opts = _byteskwargs(opts)
881 opts = _byteskwargs(opts)
882 timer, fm = gettimer(ui, opts)
882 timer, fm = gettimer(ui, opts)
883 repocleartagscache = repocleartagscachefunc(repo)
883 repocleartagscache = repocleartagscachefunc(repo)
884 clearrevlogs = opts[b'clear_revlogs']
884 clearrevlogs = opts[b'clear_revlogs']
885
885
886 def s():
886 def s():
887 if clearrevlogs:
887 if clearrevlogs:
888 clearchangelog(repo)
888 clearchangelog(repo)
889 clearfilecache(repo.unfiltered(), 'manifest')
889 clearfilecache(repo.unfiltered(), 'manifest')
890 repocleartagscache()
890 repocleartagscache()
891
891
892 def t():
892 def t():
893 return len(repo.tags())
893 return len(repo.tags())
894
894
895 timer(t, setup=s)
895 timer(t, setup=s)
896 fm.end()
896 fm.end()
897
897
898
898
899 @command(b'perf::ancestors|perfancestors', formatteropts)
899 @command(b'perf::ancestors|perfancestors', formatteropts)
900 def perfancestors(ui, repo, **opts):
900 def perfancestors(ui, repo, **opts):
901 opts = _byteskwargs(opts)
901 opts = _byteskwargs(opts)
902 timer, fm = gettimer(ui, opts)
902 timer, fm = gettimer(ui, opts)
903 heads = repo.changelog.headrevs()
903 heads = repo.changelog.headrevs()
904
904
905 def d():
905 def d():
906 for a in repo.changelog.ancestors(heads):
906 for a in repo.changelog.ancestors(heads):
907 pass
907 pass
908
908
909 timer(d)
909 timer(d)
910 fm.end()
910 fm.end()
911
911
912
912
913 @command(b'perf::ancestorset|perfancestorset', formatteropts)
913 @command(b'perf::ancestorset|perfancestorset', formatteropts)
914 def perfancestorset(ui, repo, revset, **opts):
914 def perfancestorset(ui, repo, revset, **opts):
915 opts = _byteskwargs(opts)
915 opts = _byteskwargs(opts)
916 timer, fm = gettimer(ui, opts)
916 timer, fm = gettimer(ui, opts)
917 revs = repo.revs(revset)
917 revs = repo.revs(revset)
918 heads = repo.changelog.headrevs()
918 heads = repo.changelog.headrevs()
919
919
920 def d():
920 def d():
921 s = repo.changelog.ancestors(heads)
921 s = repo.changelog.ancestors(heads)
922 for rev in revs:
922 for rev in revs:
923 rev in s
923 rev in s
924
924
925 timer(d)
925 timer(d)
926 fm.end()
926 fm.end()
927
927
928
928
929 @command(b'perf::discovery|perfdiscovery', formatteropts, b'PATH')
929 @command(b'perf::discovery|perfdiscovery', formatteropts, b'PATH')
930 def perfdiscovery(ui, repo, path, **opts):
930 def perfdiscovery(ui, repo, path, **opts):
931 """benchmark discovery between local repo and the peer at given path"""
931 """benchmark discovery between local repo and the peer at given path"""
932 repos = [repo, None]
932 repos = [repo, None]
933 timer, fm = gettimer(ui, opts)
933 timer, fm = gettimer(ui, opts)
934
934
935 try:
935 try:
936 from mercurial.utils.urlutil import get_unique_pull_path
936 from mercurial.utils.urlutil import get_unique_pull_path
937
937
938 path = get_unique_pull_path(b'perfdiscovery', repo, ui, path)[0]
938 path = get_unique_pull_path(b'perfdiscovery', repo, ui, path)[0]
939 except ImportError:
939 except ImportError:
940 path = ui.expandpath(path)
940 path = ui.expandpath(path)
941
941
942 def s():
942 def s():
943 repos[1] = hg.peer(ui, opts, path)
943 repos[1] = hg.peer(ui, opts, path)
944
944
945 def d():
945 def d():
946 setdiscovery.findcommonheads(ui, *repos)
946 setdiscovery.findcommonheads(ui, *repos)
947
947
948 timer(d, setup=s)
948 timer(d, setup=s)
949 fm.end()
949 fm.end()
950
950
951
951
952 @command(
952 @command(
953 b'perf::bookmarks|perfbookmarks',
953 b'perf::bookmarks|perfbookmarks',
954 formatteropts
954 formatteropts
955 + [
955 + [
956 (b'', b'clear-revlogs', False, b'refresh changelog and manifest'),
956 (b'', b'clear-revlogs', False, b'refresh changelog and manifest'),
957 ],
957 ],
958 )
958 )
959 def perfbookmarks(ui, repo, **opts):
959 def perfbookmarks(ui, repo, **opts):
960 """benchmark parsing bookmarks from disk to memory"""
960 """benchmark parsing bookmarks from disk to memory"""
961 opts = _byteskwargs(opts)
961 opts = _byteskwargs(opts)
962 timer, fm = gettimer(ui, opts)
962 timer, fm = gettimer(ui, opts)
963
963
964 clearrevlogs = opts[b'clear_revlogs']
964 clearrevlogs = opts[b'clear_revlogs']
965
965
966 def s():
966 def s():
967 if clearrevlogs:
967 if clearrevlogs:
968 clearchangelog(repo)
968 clearchangelog(repo)
969 clearfilecache(repo, b'_bookmarks')
969 clearfilecache(repo, b'_bookmarks')
970
970
971 def d():
971 def d():
972 repo._bookmarks
972 repo._bookmarks
973
973
974 timer(d, setup=s)
974 timer(d, setup=s)
975 fm.end()
975 fm.end()
976
976
977
977
978 @command(b'perf::bundleread|perfbundleread', formatteropts, b'BUNDLE')
978 @command(b'perf::bundleread|perfbundleread', formatteropts, b'BUNDLE')
979 def perfbundleread(ui, repo, bundlepath, **opts):
979 def perfbundleread(ui, repo, bundlepath, **opts):
980 """Benchmark reading of bundle files.
980 """Benchmark reading of bundle files.
981
981
982 This command is meant to isolate the I/O part of bundle reading as
982 This command is meant to isolate the I/O part of bundle reading as
983 much as possible.
983 much as possible.
984 """
984 """
985 from mercurial import (
985 from mercurial import (
986 bundle2,
986 bundle2,
987 exchange,
987 exchange,
988 streamclone,
988 streamclone,
989 )
989 )
990
990
991 opts = _byteskwargs(opts)
991 opts = _byteskwargs(opts)
992
992
993 def makebench(fn):
993 def makebench(fn):
994 def run():
994 def run():
995 with open(bundlepath, b'rb') as fh:
995 with open(bundlepath, b'rb') as fh:
996 bundle = exchange.readbundle(ui, fh, bundlepath)
996 bundle = exchange.readbundle(ui, fh, bundlepath)
997 fn(bundle)
997 fn(bundle)
998
998
999 return run
999 return run
1000
1000
1001 def makereadnbytes(size):
1001 def makereadnbytes(size):
1002 def run():
1002 def run():
1003 with open(bundlepath, b'rb') as fh:
1003 with open(bundlepath, b'rb') as fh:
1004 bundle = exchange.readbundle(ui, fh, bundlepath)
1004 bundle = exchange.readbundle(ui, fh, bundlepath)
1005 while bundle.read(size):
1005 while bundle.read(size):
1006 pass
1006 pass
1007
1007
1008 return run
1008 return run
1009
1009
1010 def makestdioread(size):
1010 def makestdioread(size):
1011 def run():
1011 def run():
1012 with open(bundlepath, b'rb') as fh:
1012 with open(bundlepath, b'rb') as fh:
1013 while fh.read(size):
1013 while fh.read(size):
1014 pass
1014 pass
1015
1015
1016 return run
1016 return run
1017
1017
1018 # bundle1
1018 # bundle1
1019
1019
1020 def deltaiter(bundle):
1020 def deltaiter(bundle):
1021 for delta in bundle.deltaiter():
1021 for delta in bundle.deltaiter():
1022 pass
1022 pass
1023
1023
1024 def iterchunks(bundle):
1024 def iterchunks(bundle):
1025 for chunk in bundle.getchunks():
1025 for chunk in bundle.getchunks():
1026 pass
1026 pass
1027
1027
1028 # bundle2
1028 # bundle2
1029
1029
1030 def forwardchunks(bundle):
1030 def forwardchunks(bundle):
1031 for chunk in bundle._forwardchunks():
1031 for chunk in bundle._forwardchunks():
1032 pass
1032 pass
1033
1033
1034 def iterparts(bundle):
1034 def iterparts(bundle):
1035 for part in bundle.iterparts():
1035 for part in bundle.iterparts():
1036 pass
1036 pass
1037
1037
1038 def iterpartsseekable(bundle):
1038 def iterpartsseekable(bundle):
1039 for part in bundle.iterparts(seekable=True):
1039 for part in bundle.iterparts(seekable=True):
1040 pass
1040 pass
1041
1041
1042 def seek(bundle):
1042 def seek(bundle):
1043 for part in bundle.iterparts(seekable=True):
1043 for part in bundle.iterparts(seekable=True):
1044 part.seek(0, os.SEEK_END)
1044 part.seek(0, os.SEEK_END)
1045
1045
1046 def makepartreadnbytes(size):
1046 def makepartreadnbytes(size):
1047 def run():
1047 def run():
1048 with open(bundlepath, b'rb') as fh:
1048 with open(bundlepath, b'rb') as fh:
1049 bundle = exchange.readbundle(ui, fh, bundlepath)
1049 bundle = exchange.readbundle(ui, fh, bundlepath)
1050 for part in bundle.iterparts():
1050 for part in bundle.iterparts():
1051 while part.read(size):
1051 while part.read(size):
1052 pass
1052 pass
1053
1053
1054 return run
1054 return run
1055
1055
1056 benches = [
1056 benches = [
1057 (makestdioread(8192), b'read(8k)'),
1057 (makestdioread(8192), b'read(8k)'),
1058 (makestdioread(16384), b'read(16k)'),
1058 (makestdioread(16384), b'read(16k)'),
1059 (makestdioread(32768), b'read(32k)'),
1059 (makestdioread(32768), b'read(32k)'),
1060 (makestdioread(131072), b'read(128k)'),
1060 (makestdioread(131072), b'read(128k)'),
1061 ]
1061 ]
1062
1062
1063 with open(bundlepath, b'rb') as fh:
1063 with open(bundlepath, b'rb') as fh:
1064 bundle = exchange.readbundle(ui, fh, bundlepath)
1064 bundle = exchange.readbundle(ui, fh, bundlepath)
1065
1065
1066 if isinstance(bundle, changegroup.cg1unpacker):
1066 if isinstance(bundle, changegroup.cg1unpacker):
1067 benches.extend(
1067 benches.extend(
1068 [
1068 [
1069 (makebench(deltaiter), b'cg1 deltaiter()'),
1069 (makebench(deltaiter), b'cg1 deltaiter()'),
1070 (makebench(iterchunks), b'cg1 getchunks()'),
1070 (makebench(iterchunks), b'cg1 getchunks()'),
1071 (makereadnbytes(8192), b'cg1 read(8k)'),
1071 (makereadnbytes(8192), b'cg1 read(8k)'),
1072 (makereadnbytes(16384), b'cg1 read(16k)'),
1072 (makereadnbytes(16384), b'cg1 read(16k)'),
1073 (makereadnbytes(32768), b'cg1 read(32k)'),
1073 (makereadnbytes(32768), b'cg1 read(32k)'),
1074 (makereadnbytes(131072), b'cg1 read(128k)'),
1074 (makereadnbytes(131072), b'cg1 read(128k)'),
1075 ]
1075 ]
1076 )
1076 )
1077 elif isinstance(bundle, bundle2.unbundle20):
1077 elif isinstance(bundle, bundle2.unbundle20):
1078 benches.extend(
1078 benches.extend(
1079 [
1079 [
1080 (makebench(forwardchunks), b'bundle2 forwardchunks()'),
1080 (makebench(forwardchunks), b'bundle2 forwardchunks()'),
1081 (makebench(iterparts), b'bundle2 iterparts()'),
1081 (makebench(iterparts), b'bundle2 iterparts()'),
1082 (
1082 (
1083 makebench(iterpartsseekable),
1083 makebench(iterpartsseekable),
1084 b'bundle2 iterparts() seekable',
1084 b'bundle2 iterparts() seekable',
1085 ),
1085 ),
1086 (makebench(seek), b'bundle2 part seek()'),
1086 (makebench(seek), b'bundle2 part seek()'),
1087 (makepartreadnbytes(8192), b'bundle2 part read(8k)'),
1087 (makepartreadnbytes(8192), b'bundle2 part read(8k)'),
1088 (makepartreadnbytes(16384), b'bundle2 part read(16k)'),
1088 (makepartreadnbytes(16384), b'bundle2 part read(16k)'),
1089 (makepartreadnbytes(32768), b'bundle2 part read(32k)'),
1089 (makepartreadnbytes(32768), b'bundle2 part read(32k)'),
1090 (makepartreadnbytes(131072), b'bundle2 part read(128k)'),
1090 (makepartreadnbytes(131072), b'bundle2 part read(128k)'),
1091 ]
1091 ]
1092 )
1092 )
1093 elif isinstance(bundle, streamclone.streamcloneapplier):
1093 elif isinstance(bundle, streamclone.streamcloneapplier):
1094 raise error.Abort(b'stream clone bundles not supported')
1094 raise error.Abort(b'stream clone bundles not supported')
1095 else:
1095 else:
1096 raise error.Abort(b'unhandled bundle type: %s' % type(bundle))
1096 raise error.Abort(b'unhandled bundle type: %s' % type(bundle))
1097
1097
1098 for fn, title in benches:
1098 for fn, title in benches:
1099 timer, fm = gettimer(ui, opts)
1099 timer, fm = gettimer(ui, opts)
1100 timer(fn, title=title)
1100 timer(fn, title=title)
1101 fm.end()
1101 fm.end()
1102
1102
1103
1103
1104 @command(
1104 @command(
1105 b'perf::changegroupchangelog|perfchangegroupchangelog',
1105 b'perf::changegroupchangelog|perfchangegroupchangelog',
1106 formatteropts
1106 formatteropts
1107 + [
1107 + [
1108 (b'', b'cgversion', b'02', b'changegroup version'),
1108 (b'', b'cgversion', b'02', b'changegroup version'),
1109 (b'r', b'rev', b'', b'revisions to add to changegroup'),
1109 (b'r', b'rev', b'', b'revisions to add to changegroup'),
1110 ],
1110 ],
1111 )
1111 )
1112 def perfchangegroupchangelog(ui, repo, cgversion=b'02', rev=None, **opts):
1112 def perfchangegroupchangelog(ui, repo, cgversion=b'02', rev=None, **opts):
1113 """Benchmark producing a changelog group for a changegroup.
1113 """Benchmark producing a changelog group for a changegroup.
1114
1114
1115 This measures the time spent processing the changelog during a
1115 This measures the time spent processing the changelog during a
1116 bundle operation. This occurs during `hg bundle` and on a server
1116 bundle operation. This occurs during `hg bundle` and on a server
1117 processing a `getbundle` wire protocol request (handles clones
1117 processing a `getbundle` wire protocol request (handles clones
1118 and pull requests).
1118 and pull requests).
1119
1119
1120 By default, all revisions are added to the changegroup.
1120 By default, all revisions are added to the changegroup.
1121 """
1121 """
1122 opts = _byteskwargs(opts)
1122 opts = _byteskwargs(opts)
1123 cl = repo.changelog
1123 cl = repo.changelog
1124 nodes = [cl.lookup(r) for r in repo.revs(rev or b'all()')]
1124 nodes = [cl.lookup(r) for r in repo.revs(rev or b'all()')]
1125 bundler = changegroup.getbundler(cgversion, repo)
1125 bundler = changegroup.getbundler(cgversion, repo)
1126
1126
1127 def d():
1127 def d():
1128 state, chunks = bundler._generatechangelog(cl, nodes)
1128 state, chunks = bundler._generatechangelog(cl, nodes)
1129 for chunk in chunks:
1129 for chunk in chunks:
1130 pass
1130 pass
1131
1131
1132 timer, fm = gettimer(ui, opts)
1132 timer, fm = gettimer(ui, opts)
1133
1133
1134 # Terminal printing can interfere with timing. So disable it.
1134 # Terminal printing can interfere with timing. So disable it.
1135 with ui.configoverride({(b'progress', b'disable'): True}):
1135 with ui.configoverride({(b'progress', b'disable'): True}):
1136 timer(d)
1136 timer(d)
1137
1137
1138 fm.end()
1138 fm.end()
1139
1139
1140
1140
1141 @command(b'perf::dirs|perfdirs', formatteropts)
1141 @command(b'perf::dirs|perfdirs', formatteropts)
1142 def perfdirs(ui, repo, **opts):
1142 def perfdirs(ui, repo, **opts):
1143 opts = _byteskwargs(opts)
1143 opts = _byteskwargs(opts)
1144 timer, fm = gettimer(ui, opts)
1144 timer, fm = gettimer(ui, opts)
1145 dirstate = repo.dirstate
1145 dirstate = repo.dirstate
1146 b'a' in dirstate
1146 b'a' in dirstate
1147
1147
1148 def d():
1148 def d():
1149 dirstate.hasdir(b'a')
1149 dirstate.hasdir(b'a')
1150 del dirstate._map._dirs
1150 try:
1151 del dirstate._map._dirs
1152 except AttributeError:
1153 pass
1151
1154
1152 timer(d)
1155 timer(d)
1153 fm.end()
1156 fm.end()
1154
1157
1155
1158
1156 @command(
1159 @command(
1157 b'perf::dirstate|perfdirstate',
1160 b'perf::dirstate|perfdirstate',
1158 [
1161 [
1159 (
1162 (
1160 b'',
1163 b'',
1161 b'iteration',
1164 b'iteration',
1162 None,
1165 None,
1163 b'benchmark a full iteration for the dirstate',
1166 b'benchmark a full iteration for the dirstate',
1164 ),
1167 ),
1165 (
1168 (
1166 b'',
1169 b'',
1167 b'contains',
1170 b'contains',
1168 None,
1171 None,
1169 b'benchmark a large amount of `nf in dirstate` calls',
1172 b'benchmark a large amount of `nf in dirstate` calls',
1170 ),
1173 ),
1171 ]
1174 ]
1172 + formatteropts,
1175 + formatteropts,
1173 )
1176 )
1174 def perfdirstate(ui, repo, **opts):
1177 def perfdirstate(ui, repo, **opts):
1175 """benchmap the time of various distate operations
1178 """benchmap the time of various distate operations
1176
1179
1177 By default benchmark the time necessary to load a dirstate from scratch.
1180 By default benchmark the time necessary to load a dirstate from scratch.
1178 The dirstate is loaded to the point were a "contains" request can be
1181 The dirstate is loaded to the point were a "contains" request can be
1179 answered.
1182 answered.
1180 """
1183 """
1181 opts = _byteskwargs(opts)
1184 opts = _byteskwargs(opts)
1182 timer, fm = gettimer(ui, opts)
1185 timer, fm = gettimer(ui, opts)
1183 b"a" in repo.dirstate
1186 b"a" in repo.dirstate
1184
1187
1185 if opts[b'iteration'] and opts[b'contains']:
1188 if opts[b'iteration'] and opts[b'contains']:
1186 msg = b'only specify one of --iteration or --contains'
1189 msg = b'only specify one of --iteration or --contains'
1187 raise error.Abort(msg)
1190 raise error.Abort(msg)
1188
1191
1189 if opts[b'iteration']:
1192 if opts[b'iteration']:
1190 setup = None
1193 setup = None
1191 dirstate = repo.dirstate
1194 dirstate = repo.dirstate
1192
1195
1193 def d():
1196 def d():
1194 for f in dirstate:
1197 for f in dirstate:
1195 pass
1198 pass
1196
1199
1197 elif opts[b'contains']:
1200 elif opts[b'contains']:
1198 setup = None
1201 setup = None
1199 dirstate = repo.dirstate
1202 dirstate = repo.dirstate
1200 allfiles = list(dirstate)
1203 allfiles = list(dirstate)
1201 # also add file path that will be "missing" from the dirstate
1204 # also add file path that will be "missing" from the dirstate
1202 allfiles.extend([f[::-1] for f in allfiles])
1205 allfiles.extend([f[::-1] for f in allfiles])
1203
1206
1204 def d():
1207 def d():
1205 for f in allfiles:
1208 for f in allfiles:
1206 f in dirstate
1209 f in dirstate
1207
1210
1208 else:
1211 else:
1209
1212
1210 def setup():
1213 def setup():
1211 repo.dirstate.invalidate()
1214 repo.dirstate.invalidate()
1212
1215
1213 def d():
1216 def d():
1214 b"a" in repo.dirstate
1217 b"a" in repo.dirstate
1215
1218
1216 timer(d, setup=setup)
1219 timer(d, setup=setup)
1217 fm.end()
1220 fm.end()
1218
1221
1219
1222
1220 @command(b'perf::dirstatedirs|perfdirstatedirs', formatteropts)
1223 @command(b'perf::dirstatedirs|perfdirstatedirs', formatteropts)
1221 def perfdirstatedirs(ui, repo, **opts):
1224 def perfdirstatedirs(ui, repo, **opts):
1222 """benchmap a 'dirstate.hasdir' call from an empty `dirs` cache"""
1225 """benchmap a 'dirstate.hasdir' call from an empty `dirs` cache"""
1223 opts = _byteskwargs(opts)
1226 opts = _byteskwargs(opts)
1224 timer, fm = gettimer(ui, opts)
1227 timer, fm = gettimer(ui, opts)
1225 repo.dirstate.hasdir(b"a")
1228 repo.dirstate.hasdir(b"a")
1226
1229
1227 def setup():
1230 def setup():
1228 del repo.dirstate._map._dirs
1231 try:
1232 del repo.dirstate._map._dirs
1233 except AttributeError:
1234 pass
1229
1235
1230 def d():
1236 def d():
1231 repo.dirstate.hasdir(b"a")
1237 repo.dirstate.hasdir(b"a")
1232
1238
1233 timer(d, setup=setup)
1239 timer(d, setup=setup)
1234 fm.end()
1240 fm.end()
1235
1241
1236
1242
1237 @command(b'perf::dirstatefoldmap|perfdirstatefoldmap', formatteropts)
1243 @command(b'perf::dirstatefoldmap|perfdirstatefoldmap', formatteropts)
1238 def perfdirstatefoldmap(ui, repo, **opts):
1244 def perfdirstatefoldmap(ui, repo, **opts):
1239 """benchmap a `dirstate._map.filefoldmap.get()` request
1245 """benchmap a `dirstate._map.filefoldmap.get()` request
1240
1246
1241 The dirstate filefoldmap cache is dropped between every request.
1247 The dirstate filefoldmap cache is dropped between every request.
1242 """
1248 """
1243 opts = _byteskwargs(opts)
1249 opts = _byteskwargs(opts)
1244 timer, fm = gettimer(ui, opts)
1250 timer, fm = gettimer(ui, opts)
1245 dirstate = repo.dirstate
1251 dirstate = repo.dirstate
1246 dirstate._map.filefoldmap.get(b'a')
1252 dirstate._map.filefoldmap.get(b'a')
1247
1253
1248 def setup():
1254 def setup():
1249 del dirstate._map.filefoldmap
1255 del dirstate._map.filefoldmap
1250
1256
1251 def d():
1257 def d():
1252 dirstate._map.filefoldmap.get(b'a')
1258 dirstate._map.filefoldmap.get(b'a')
1253
1259
1254 timer(d, setup=setup)
1260 timer(d, setup=setup)
1255 fm.end()
1261 fm.end()
1256
1262
1257
1263
1258 @command(b'perf::dirfoldmap|perfdirfoldmap', formatteropts)
1264 @command(b'perf::dirfoldmap|perfdirfoldmap', formatteropts)
1259 def perfdirfoldmap(ui, repo, **opts):
1265 def perfdirfoldmap(ui, repo, **opts):
1260 """benchmap a `dirstate._map.dirfoldmap.get()` request
1266 """benchmap a `dirstate._map.dirfoldmap.get()` request
1261
1267
1262 The dirstate dirfoldmap cache is dropped between every request.
1268 The dirstate dirfoldmap cache is dropped between every request.
1263 """
1269 """
1264 opts = _byteskwargs(opts)
1270 opts = _byteskwargs(opts)
1265 timer, fm = gettimer(ui, opts)
1271 timer, fm = gettimer(ui, opts)
1266 dirstate = repo.dirstate
1272 dirstate = repo.dirstate
1267 dirstate._map.dirfoldmap.get(b'a')
1273 dirstate._map.dirfoldmap.get(b'a')
1268
1274
1269 def setup():
1275 def setup():
1270 del dirstate._map.dirfoldmap
1276 del dirstate._map.dirfoldmap
1271 del dirstate._map._dirs
1277 try:
1278 del dirstate._map._dirs
1279 except AttributeError:
1280 pass
1272
1281
1273 def d():
1282 def d():
1274 dirstate._map.dirfoldmap.get(b'a')
1283 dirstate._map.dirfoldmap.get(b'a')
1275
1284
1276 timer(d, setup=setup)
1285 timer(d, setup=setup)
1277 fm.end()
1286 fm.end()
1278
1287
1279
1288
1280 @command(b'perf::dirstatewrite|perfdirstatewrite', formatteropts)
1289 @command(b'perf::dirstatewrite|perfdirstatewrite', formatteropts)
1281 def perfdirstatewrite(ui, repo, **opts):
1290 def perfdirstatewrite(ui, repo, **opts):
1282 """benchmap the time it take to write a dirstate on disk"""
1291 """benchmap the time it take to write a dirstate on disk"""
1283 opts = _byteskwargs(opts)
1292 opts = _byteskwargs(opts)
1284 timer, fm = gettimer(ui, opts)
1293 timer, fm = gettimer(ui, opts)
1285 ds = repo.dirstate
1294 ds = repo.dirstate
1286 b"a" in ds
1295 b"a" in ds
1287
1296
1288 def setup():
1297 def setup():
1289 ds._dirty = True
1298 ds._dirty = True
1290
1299
1291 def d():
1300 def d():
1292 ds.write(repo.currenttransaction())
1301 ds.write(repo.currenttransaction())
1293
1302
1294 timer(d, setup=setup)
1303 timer(d, setup=setup)
1295 fm.end()
1304 fm.end()
1296
1305
1297
1306
1298 def _getmergerevs(repo, opts):
1307 def _getmergerevs(repo, opts):
1299 """parse command argument to return rev involved in merge
1308 """parse command argument to return rev involved in merge
1300
1309
1301 input: options dictionnary with `rev`, `from` and `bse`
1310 input: options dictionnary with `rev`, `from` and `bse`
1302 output: (localctx, otherctx, basectx)
1311 output: (localctx, otherctx, basectx)
1303 """
1312 """
1304 if opts[b'from']:
1313 if opts[b'from']:
1305 fromrev = scmutil.revsingle(repo, opts[b'from'])
1314 fromrev = scmutil.revsingle(repo, opts[b'from'])
1306 wctx = repo[fromrev]
1315 wctx = repo[fromrev]
1307 else:
1316 else:
1308 wctx = repo[None]
1317 wctx = repo[None]
1309 # we don't want working dir files to be stat'd in the benchmark, so
1318 # we don't want working dir files to be stat'd in the benchmark, so
1310 # prime that cache
1319 # prime that cache
1311 wctx.dirty()
1320 wctx.dirty()
1312 rctx = scmutil.revsingle(repo, opts[b'rev'], opts[b'rev'])
1321 rctx = scmutil.revsingle(repo, opts[b'rev'], opts[b'rev'])
1313 if opts[b'base']:
1322 if opts[b'base']:
1314 fromrev = scmutil.revsingle(repo, opts[b'base'])
1323 fromrev = scmutil.revsingle(repo, opts[b'base'])
1315 ancestor = repo[fromrev]
1324 ancestor = repo[fromrev]
1316 else:
1325 else:
1317 ancestor = wctx.ancestor(rctx)
1326 ancestor = wctx.ancestor(rctx)
1318 return (wctx, rctx, ancestor)
1327 return (wctx, rctx, ancestor)
1319
1328
1320
1329
1321 @command(
1330 @command(
1322 b'perf::mergecalculate|perfmergecalculate',
1331 b'perf::mergecalculate|perfmergecalculate',
1323 [
1332 [
1324 (b'r', b'rev', b'.', b'rev to merge against'),
1333 (b'r', b'rev', b'.', b'rev to merge against'),
1325 (b'', b'from', b'', b'rev to merge from'),
1334 (b'', b'from', b'', b'rev to merge from'),
1326 (b'', b'base', b'', b'the revision to use as base'),
1335 (b'', b'base', b'', b'the revision to use as base'),
1327 ]
1336 ]
1328 + formatteropts,
1337 + formatteropts,
1329 )
1338 )
1330 def perfmergecalculate(ui, repo, **opts):
1339 def perfmergecalculate(ui, repo, **opts):
1331 opts = _byteskwargs(opts)
1340 opts = _byteskwargs(opts)
1332 timer, fm = gettimer(ui, opts)
1341 timer, fm = gettimer(ui, opts)
1333
1342
1334 wctx, rctx, ancestor = _getmergerevs(repo, opts)
1343 wctx, rctx, ancestor = _getmergerevs(repo, opts)
1335
1344
1336 def d():
1345 def d():
1337 # acceptremote is True because we don't want prompts in the middle of
1346 # acceptremote is True because we don't want prompts in the middle of
1338 # our benchmark
1347 # our benchmark
1339 merge.calculateupdates(
1348 merge.calculateupdates(
1340 repo,
1349 repo,
1341 wctx,
1350 wctx,
1342 rctx,
1351 rctx,
1343 [ancestor],
1352 [ancestor],
1344 branchmerge=False,
1353 branchmerge=False,
1345 force=False,
1354 force=False,
1346 acceptremote=True,
1355 acceptremote=True,
1347 followcopies=True,
1356 followcopies=True,
1348 )
1357 )
1349
1358
1350 timer(d)
1359 timer(d)
1351 fm.end()
1360 fm.end()
1352
1361
1353
1362
1354 @command(
1363 @command(
1355 b'perf::mergecopies|perfmergecopies',
1364 b'perf::mergecopies|perfmergecopies',
1356 [
1365 [
1357 (b'r', b'rev', b'.', b'rev to merge against'),
1366 (b'r', b'rev', b'.', b'rev to merge against'),
1358 (b'', b'from', b'', b'rev to merge from'),
1367 (b'', b'from', b'', b'rev to merge from'),
1359 (b'', b'base', b'', b'the revision to use as base'),
1368 (b'', b'base', b'', b'the revision to use as base'),
1360 ]
1369 ]
1361 + formatteropts,
1370 + formatteropts,
1362 )
1371 )
1363 def perfmergecopies(ui, repo, **opts):
1372 def perfmergecopies(ui, repo, **opts):
1364 """measure runtime of `copies.mergecopies`"""
1373 """measure runtime of `copies.mergecopies`"""
1365 opts = _byteskwargs(opts)
1374 opts = _byteskwargs(opts)
1366 timer, fm = gettimer(ui, opts)
1375 timer, fm = gettimer(ui, opts)
1367 wctx, rctx, ancestor = _getmergerevs(repo, opts)
1376 wctx, rctx, ancestor = _getmergerevs(repo, opts)
1368
1377
1369 def d():
1378 def d():
1370 # acceptremote is True because we don't want prompts in the middle of
1379 # acceptremote is True because we don't want prompts in the middle of
1371 # our benchmark
1380 # our benchmark
1372 copies.mergecopies(repo, wctx, rctx, ancestor)
1381 copies.mergecopies(repo, wctx, rctx, ancestor)
1373
1382
1374 timer(d)
1383 timer(d)
1375 fm.end()
1384 fm.end()
1376
1385
1377
1386
1378 @command(b'perf::pathcopies|perfpathcopies', [], b"REV REV")
1387 @command(b'perf::pathcopies|perfpathcopies', [], b"REV REV")
1379 def perfpathcopies(ui, repo, rev1, rev2, **opts):
1388 def perfpathcopies(ui, repo, rev1, rev2, **opts):
1380 """benchmark the copy tracing logic"""
1389 """benchmark the copy tracing logic"""
1381 opts = _byteskwargs(opts)
1390 opts = _byteskwargs(opts)
1382 timer, fm = gettimer(ui, opts)
1391 timer, fm = gettimer(ui, opts)
1383 ctx1 = scmutil.revsingle(repo, rev1, rev1)
1392 ctx1 = scmutil.revsingle(repo, rev1, rev1)
1384 ctx2 = scmutil.revsingle(repo, rev2, rev2)
1393 ctx2 = scmutil.revsingle(repo, rev2, rev2)
1385
1394
1386 def d():
1395 def d():
1387 copies.pathcopies(ctx1, ctx2)
1396 copies.pathcopies(ctx1, ctx2)
1388
1397
1389 timer(d)
1398 timer(d)
1390 fm.end()
1399 fm.end()
1391
1400
1392
1401
1393 @command(
1402 @command(
1394 b'perf::phases|perfphases',
1403 b'perf::phases|perfphases',
1395 [
1404 [
1396 (b'', b'full', False, b'include file reading time too'),
1405 (b'', b'full', False, b'include file reading time too'),
1397 ],
1406 ],
1398 b"",
1407 b"",
1399 )
1408 )
1400 def perfphases(ui, repo, **opts):
1409 def perfphases(ui, repo, **opts):
1401 """benchmark phasesets computation"""
1410 """benchmark phasesets computation"""
1402 opts = _byteskwargs(opts)
1411 opts = _byteskwargs(opts)
1403 timer, fm = gettimer(ui, opts)
1412 timer, fm = gettimer(ui, opts)
1404 _phases = repo._phasecache
1413 _phases = repo._phasecache
1405 full = opts.get(b'full')
1414 full = opts.get(b'full')
1406
1415
1407 def d():
1416 def d():
1408 phases = _phases
1417 phases = _phases
1409 if full:
1418 if full:
1410 clearfilecache(repo, b'_phasecache')
1419 clearfilecache(repo, b'_phasecache')
1411 phases = repo._phasecache
1420 phases = repo._phasecache
1412 phases.invalidate()
1421 phases.invalidate()
1413 phases.loadphaserevs(repo)
1422 phases.loadphaserevs(repo)
1414
1423
1415 timer(d)
1424 timer(d)
1416 fm.end()
1425 fm.end()
1417
1426
1418
1427
1419 @command(b'perf::phasesremote|perfphasesremote', [], b"[DEST]")
1428 @command(b'perf::phasesremote|perfphasesremote', [], b"[DEST]")
1420 def perfphasesremote(ui, repo, dest=None, **opts):
1429 def perfphasesremote(ui, repo, dest=None, **opts):
1421 """benchmark time needed to analyse phases of the remote server"""
1430 """benchmark time needed to analyse phases of the remote server"""
1422 from mercurial.node import bin
1431 from mercurial.node import bin
1423 from mercurial import (
1432 from mercurial import (
1424 exchange,
1433 exchange,
1425 hg,
1434 hg,
1426 phases,
1435 phases,
1427 )
1436 )
1428
1437
1429 opts = _byteskwargs(opts)
1438 opts = _byteskwargs(opts)
1430 timer, fm = gettimer(ui, opts)
1439 timer, fm = gettimer(ui, opts)
1431
1440
1432 path = ui.getpath(dest, default=(b'default-push', b'default'))
1441 path = ui.getpath(dest, default=(b'default-push', b'default'))
1433 if not path:
1442 if not path:
1434 raise error.Abort(
1443 raise error.Abort(
1435 b'default repository not configured!',
1444 b'default repository not configured!',
1436 hint=b"see 'hg help config.paths'",
1445 hint=b"see 'hg help config.paths'",
1437 )
1446 )
1438 dest = path.pushloc or path.loc
1447 dest = path.pushloc or path.loc
1439 ui.statusnoi18n(b'analysing phase of %s\n' % util.hidepassword(dest))
1448 ui.statusnoi18n(b'analysing phase of %s\n' % util.hidepassword(dest))
1440 other = hg.peer(repo, opts, dest)
1449 other = hg.peer(repo, opts, dest)
1441
1450
1442 # easier to perform discovery through the operation
1451 # easier to perform discovery through the operation
1443 op = exchange.pushoperation(repo, other)
1452 op = exchange.pushoperation(repo, other)
1444 exchange._pushdiscoverychangeset(op)
1453 exchange._pushdiscoverychangeset(op)
1445
1454
1446 remotesubset = op.fallbackheads
1455 remotesubset = op.fallbackheads
1447
1456
1448 with other.commandexecutor() as e:
1457 with other.commandexecutor() as e:
1449 remotephases = e.callcommand(
1458 remotephases = e.callcommand(
1450 b'listkeys', {b'namespace': b'phases'}
1459 b'listkeys', {b'namespace': b'phases'}
1451 ).result()
1460 ).result()
1452 del other
1461 del other
1453 publishing = remotephases.get(b'publishing', False)
1462 publishing = remotephases.get(b'publishing', False)
1454 if publishing:
1463 if publishing:
1455 ui.statusnoi18n(b'publishing: yes\n')
1464 ui.statusnoi18n(b'publishing: yes\n')
1456 else:
1465 else:
1457 ui.statusnoi18n(b'publishing: no\n')
1466 ui.statusnoi18n(b'publishing: no\n')
1458
1467
1459 has_node = getattr(repo.changelog.index, 'has_node', None)
1468 has_node = getattr(repo.changelog.index, 'has_node', None)
1460 if has_node is None:
1469 if has_node is None:
1461 has_node = repo.changelog.nodemap.__contains__
1470 has_node = repo.changelog.nodemap.__contains__
1462 nonpublishroots = 0
1471 nonpublishroots = 0
1463 for nhex, phase in remotephases.iteritems():
1472 for nhex, phase in remotephases.iteritems():
1464 if nhex == b'publishing': # ignore data related to publish option
1473 if nhex == b'publishing': # ignore data related to publish option
1465 continue
1474 continue
1466 node = bin(nhex)
1475 node = bin(nhex)
1467 if has_node(node) and int(phase):
1476 if has_node(node) and int(phase):
1468 nonpublishroots += 1
1477 nonpublishroots += 1
1469 ui.statusnoi18n(b'number of roots: %d\n' % len(remotephases))
1478 ui.statusnoi18n(b'number of roots: %d\n' % len(remotephases))
1470 ui.statusnoi18n(b'number of known non public roots: %d\n' % nonpublishroots)
1479 ui.statusnoi18n(b'number of known non public roots: %d\n' % nonpublishroots)
1471
1480
1472 def d():
1481 def d():
1473 phases.remotephasessummary(repo, remotesubset, remotephases)
1482 phases.remotephasessummary(repo, remotesubset, remotephases)
1474
1483
1475 timer(d)
1484 timer(d)
1476 fm.end()
1485 fm.end()
1477
1486
1478
1487
1479 @command(
1488 @command(
1480 b'perf::manifest|perfmanifest',
1489 b'perf::manifest|perfmanifest',
1481 [
1490 [
1482 (b'm', b'manifest-rev', False, b'Look up a manifest node revision'),
1491 (b'm', b'manifest-rev', False, b'Look up a manifest node revision'),
1483 (b'', b'clear-disk', False, b'clear on-disk caches too'),
1492 (b'', b'clear-disk', False, b'clear on-disk caches too'),
1484 ]
1493 ]
1485 + formatteropts,
1494 + formatteropts,
1486 b'REV|NODE',
1495 b'REV|NODE',
1487 )
1496 )
1488 def perfmanifest(ui, repo, rev, manifest_rev=False, clear_disk=False, **opts):
1497 def perfmanifest(ui, repo, rev, manifest_rev=False, clear_disk=False, **opts):
1489 """benchmark the time to read a manifest from disk and return a usable
1498 """benchmark the time to read a manifest from disk and return a usable
1490 dict-like object
1499 dict-like object
1491
1500
1492 Manifest caches are cleared before retrieval."""
1501 Manifest caches are cleared before retrieval."""
1493 opts = _byteskwargs(opts)
1502 opts = _byteskwargs(opts)
1494 timer, fm = gettimer(ui, opts)
1503 timer, fm = gettimer(ui, opts)
1495 if not manifest_rev:
1504 if not manifest_rev:
1496 ctx = scmutil.revsingle(repo, rev, rev)
1505 ctx = scmutil.revsingle(repo, rev, rev)
1497 t = ctx.manifestnode()
1506 t = ctx.manifestnode()
1498 else:
1507 else:
1499 from mercurial.node import bin
1508 from mercurial.node import bin
1500
1509
1501 if len(rev) == 40:
1510 if len(rev) == 40:
1502 t = bin(rev)
1511 t = bin(rev)
1503 else:
1512 else:
1504 try:
1513 try:
1505 rev = int(rev)
1514 rev = int(rev)
1506
1515
1507 if util.safehasattr(repo.manifestlog, b'getstorage'):
1516 if util.safehasattr(repo.manifestlog, b'getstorage'):
1508 t = repo.manifestlog.getstorage(b'').node(rev)
1517 t = repo.manifestlog.getstorage(b'').node(rev)
1509 else:
1518 else:
1510 t = repo.manifestlog._revlog.lookup(rev)
1519 t = repo.manifestlog._revlog.lookup(rev)
1511 except ValueError:
1520 except ValueError:
1512 raise error.Abort(
1521 raise error.Abort(
1513 b'manifest revision must be integer or full node'
1522 b'manifest revision must be integer or full node'
1514 )
1523 )
1515
1524
1516 def d():
1525 def d():
1517 repo.manifestlog.clearcaches(clear_persisted_data=clear_disk)
1526 repo.manifestlog.clearcaches(clear_persisted_data=clear_disk)
1518 repo.manifestlog[t].read()
1527 repo.manifestlog[t].read()
1519
1528
1520 timer(d)
1529 timer(d)
1521 fm.end()
1530 fm.end()
1522
1531
1523
1532
1524 @command(b'perf::changeset|perfchangeset', formatteropts)
1533 @command(b'perf::changeset|perfchangeset', formatteropts)
1525 def perfchangeset(ui, repo, rev, **opts):
1534 def perfchangeset(ui, repo, rev, **opts):
1526 opts = _byteskwargs(opts)
1535 opts = _byteskwargs(opts)
1527 timer, fm = gettimer(ui, opts)
1536 timer, fm = gettimer(ui, opts)
1528 n = scmutil.revsingle(repo, rev).node()
1537 n = scmutil.revsingle(repo, rev).node()
1529
1538
1530 def d():
1539 def d():
1531 repo.changelog.read(n)
1540 repo.changelog.read(n)
1532 # repo.changelog._cache = None
1541 # repo.changelog._cache = None
1533
1542
1534 timer(d)
1543 timer(d)
1535 fm.end()
1544 fm.end()
1536
1545
1537
1546
1538 @command(b'perf::ignore|perfignore', formatteropts)
1547 @command(b'perf::ignore|perfignore', formatteropts)
1539 def perfignore(ui, repo, **opts):
1548 def perfignore(ui, repo, **opts):
1540 """benchmark operation related to computing ignore"""
1549 """benchmark operation related to computing ignore"""
1541 opts = _byteskwargs(opts)
1550 opts = _byteskwargs(opts)
1542 timer, fm = gettimer(ui, opts)
1551 timer, fm = gettimer(ui, opts)
1543 dirstate = repo.dirstate
1552 dirstate = repo.dirstate
1544
1553
1545 def setupone():
1554 def setupone():
1546 dirstate.invalidate()
1555 dirstate.invalidate()
1547 clearfilecache(dirstate, b'_ignore')
1556 clearfilecache(dirstate, b'_ignore')
1548
1557
1549 def runone():
1558 def runone():
1550 dirstate._ignore
1559 dirstate._ignore
1551
1560
1552 timer(runone, setup=setupone, title=b"load")
1561 timer(runone, setup=setupone, title=b"load")
1553 fm.end()
1562 fm.end()
1554
1563
1555
1564
1556 @command(
1565 @command(
1557 b'perf::index|perfindex',
1566 b'perf::index|perfindex',
1558 [
1567 [
1559 (b'', b'rev', [], b'revision to be looked up (default tip)'),
1568 (b'', b'rev', [], b'revision to be looked up (default tip)'),
1560 (b'', b'no-lookup', None, b'do not revision lookup post creation'),
1569 (b'', b'no-lookup', None, b'do not revision lookup post creation'),
1561 ]
1570 ]
1562 + formatteropts,
1571 + formatteropts,
1563 )
1572 )
1564 def perfindex(ui, repo, **opts):
1573 def perfindex(ui, repo, **opts):
1565 """benchmark index creation time followed by a lookup
1574 """benchmark index creation time followed by a lookup
1566
1575
1567 The default is to look `tip` up. Depending on the index implementation,
1576 The default is to look `tip` up. Depending on the index implementation,
1568 the revision looked up can matters. For example, an implementation
1577 the revision looked up can matters. For example, an implementation
1569 scanning the index will have a faster lookup time for `--rev tip` than for
1578 scanning the index will have a faster lookup time for `--rev tip` than for
1570 `--rev 0`. The number of looked up revisions and their order can also
1579 `--rev 0`. The number of looked up revisions and their order can also
1571 matters.
1580 matters.
1572
1581
1573 Example of useful set to test:
1582 Example of useful set to test:
1574
1583
1575 * tip
1584 * tip
1576 * 0
1585 * 0
1577 * -10:
1586 * -10:
1578 * :10
1587 * :10
1579 * -10: + :10
1588 * -10: + :10
1580 * :10: + -10:
1589 * :10: + -10:
1581 * -10000:
1590 * -10000:
1582 * -10000: + 0
1591 * -10000: + 0
1583
1592
1584 It is not currently possible to check for lookup of a missing node. For
1593 It is not currently possible to check for lookup of a missing node. For
1585 deeper lookup benchmarking, checkout the `perfnodemap` command."""
1594 deeper lookup benchmarking, checkout the `perfnodemap` command."""
1586 import mercurial.revlog
1595 import mercurial.revlog
1587
1596
1588 opts = _byteskwargs(opts)
1597 opts = _byteskwargs(opts)
1589 timer, fm = gettimer(ui, opts)
1598 timer, fm = gettimer(ui, opts)
1590 mercurial.revlog._prereadsize = 2 ** 24 # disable lazy parser in old hg
1599 mercurial.revlog._prereadsize = 2 ** 24 # disable lazy parser in old hg
1591 if opts[b'no_lookup']:
1600 if opts[b'no_lookup']:
1592 if opts['rev']:
1601 if opts['rev']:
1593 raise error.Abort('--no-lookup and --rev are mutually exclusive')
1602 raise error.Abort('--no-lookup and --rev are mutually exclusive')
1594 nodes = []
1603 nodes = []
1595 elif not opts[b'rev']:
1604 elif not opts[b'rev']:
1596 nodes = [repo[b"tip"].node()]
1605 nodes = [repo[b"tip"].node()]
1597 else:
1606 else:
1598 revs = scmutil.revrange(repo, opts[b'rev'])
1607 revs = scmutil.revrange(repo, opts[b'rev'])
1599 cl = repo.changelog
1608 cl = repo.changelog
1600 nodes = [cl.node(r) for r in revs]
1609 nodes = [cl.node(r) for r in revs]
1601
1610
1602 unfi = repo.unfiltered()
1611 unfi = repo.unfiltered()
1603 # find the filecache func directly
1612 # find the filecache func directly
1604 # This avoid polluting the benchmark with the filecache logic
1613 # This avoid polluting the benchmark with the filecache logic
1605 makecl = unfi.__class__.changelog.func
1614 makecl = unfi.__class__.changelog.func
1606
1615
1607 def setup():
1616 def setup():
1608 # probably not necessary, but for good measure
1617 # probably not necessary, but for good measure
1609 clearchangelog(unfi)
1618 clearchangelog(unfi)
1610
1619
1611 def d():
1620 def d():
1612 cl = makecl(unfi)
1621 cl = makecl(unfi)
1613 for n in nodes:
1622 for n in nodes:
1614 cl.rev(n)
1623 cl.rev(n)
1615
1624
1616 timer(d, setup=setup)
1625 timer(d, setup=setup)
1617 fm.end()
1626 fm.end()
1618
1627
1619
1628
1620 @command(
1629 @command(
1621 b'perf::nodemap|perfnodemap',
1630 b'perf::nodemap|perfnodemap',
1622 [
1631 [
1623 (b'', b'rev', [], b'revision to be looked up (default tip)'),
1632 (b'', b'rev', [], b'revision to be looked up (default tip)'),
1624 (b'', b'clear-caches', True, b'clear revlog cache between calls'),
1633 (b'', b'clear-caches', True, b'clear revlog cache between calls'),
1625 ]
1634 ]
1626 + formatteropts,
1635 + formatteropts,
1627 )
1636 )
1628 def perfnodemap(ui, repo, **opts):
1637 def perfnodemap(ui, repo, **opts):
1629 """benchmark the time necessary to look up revision from a cold nodemap
1638 """benchmark the time necessary to look up revision from a cold nodemap
1630
1639
1631 Depending on the implementation, the amount and order of revision we look
1640 Depending on the implementation, the amount and order of revision we look
1632 up can varies. Example of useful set to test:
1641 up can varies. Example of useful set to test:
1633 * tip
1642 * tip
1634 * 0
1643 * 0
1635 * -10:
1644 * -10:
1636 * :10
1645 * :10
1637 * -10: + :10
1646 * -10: + :10
1638 * :10: + -10:
1647 * :10: + -10:
1639 * -10000:
1648 * -10000:
1640 * -10000: + 0
1649 * -10000: + 0
1641
1650
1642 The command currently focus on valid binary lookup. Benchmarking for
1651 The command currently focus on valid binary lookup. Benchmarking for
1643 hexlookup, prefix lookup and missing lookup would also be valuable.
1652 hexlookup, prefix lookup and missing lookup would also be valuable.
1644 """
1653 """
1645 import mercurial.revlog
1654 import mercurial.revlog
1646
1655
1647 opts = _byteskwargs(opts)
1656 opts = _byteskwargs(opts)
1648 timer, fm = gettimer(ui, opts)
1657 timer, fm = gettimer(ui, opts)
1649 mercurial.revlog._prereadsize = 2 ** 24 # disable lazy parser in old hg
1658 mercurial.revlog._prereadsize = 2 ** 24 # disable lazy parser in old hg
1650
1659
1651 unfi = repo.unfiltered()
1660 unfi = repo.unfiltered()
1652 clearcaches = opts[b'clear_caches']
1661 clearcaches = opts[b'clear_caches']
1653 # find the filecache func directly
1662 # find the filecache func directly
1654 # This avoid polluting the benchmark with the filecache logic
1663 # This avoid polluting the benchmark with the filecache logic
1655 makecl = unfi.__class__.changelog.func
1664 makecl = unfi.__class__.changelog.func
1656 if not opts[b'rev']:
1665 if not opts[b'rev']:
1657 raise error.Abort(b'use --rev to specify revisions to look up')
1666 raise error.Abort(b'use --rev to specify revisions to look up')
1658 revs = scmutil.revrange(repo, opts[b'rev'])
1667 revs = scmutil.revrange(repo, opts[b'rev'])
1659 cl = repo.changelog
1668 cl = repo.changelog
1660 nodes = [cl.node(r) for r in revs]
1669 nodes = [cl.node(r) for r in revs]
1661
1670
1662 # use a list to pass reference to a nodemap from one closure to the next
1671 # use a list to pass reference to a nodemap from one closure to the next
1663 nodeget = [None]
1672 nodeget = [None]
1664
1673
1665 def setnodeget():
1674 def setnodeget():
1666 # probably not necessary, but for good measure
1675 # probably not necessary, but for good measure
1667 clearchangelog(unfi)
1676 clearchangelog(unfi)
1668 cl = makecl(unfi)
1677 cl = makecl(unfi)
1669 if util.safehasattr(cl.index, 'get_rev'):
1678 if util.safehasattr(cl.index, 'get_rev'):
1670 nodeget[0] = cl.index.get_rev
1679 nodeget[0] = cl.index.get_rev
1671 else:
1680 else:
1672 nodeget[0] = cl.nodemap.get
1681 nodeget[0] = cl.nodemap.get
1673
1682
1674 def d():
1683 def d():
1675 get = nodeget[0]
1684 get = nodeget[0]
1676 for n in nodes:
1685 for n in nodes:
1677 get(n)
1686 get(n)
1678
1687
1679 setup = None
1688 setup = None
1680 if clearcaches:
1689 if clearcaches:
1681
1690
1682 def setup():
1691 def setup():
1683 setnodeget()
1692 setnodeget()
1684
1693
1685 else:
1694 else:
1686 setnodeget()
1695 setnodeget()
1687 d() # prewarm the data structure
1696 d() # prewarm the data structure
1688 timer(d, setup=setup)
1697 timer(d, setup=setup)
1689 fm.end()
1698 fm.end()
1690
1699
1691
1700
1692 @command(b'perf::startup|perfstartup', formatteropts)
1701 @command(b'perf::startup|perfstartup', formatteropts)
1693 def perfstartup(ui, repo, **opts):
1702 def perfstartup(ui, repo, **opts):
1694 opts = _byteskwargs(opts)
1703 opts = _byteskwargs(opts)
1695 timer, fm = gettimer(ui, opts)
1704 timer, fm = gettimer(ui, opts)
1696
1705
1697 def d():
1706 def d():
1698 if os.name != 'nt':
1707 if os.name != 'nt':
1699 os.system(
1708 os.system(
1700 b"HGRCPATH= %s version -q > /dev/null" % fsencode(sys.argv[0])
1709 b"HGRCPATH= %s version -q > /dev/null" % fsencode(sys.argv[0])
1701 )
1710 )
1702 else:
1711 else:
1703 os.environ['HGRCPATH'] = r' '
1712 os.environ['HGRCPATH'] = r' '
1704 os.system("%s version -q > NUL" % sys.argv[0])
1713 os.system("%s version -q > NUL" % sys.argv[0])
1705
1714
1706 timer(d)
1715 timer(d)
1707 fm.end()
1716 fm.end()
1708
1717
1709
1718
1710 @command(b'perf::parents|perfparents', formatteropts)
1719 @command(b'perf::parents|perfparents', formatteropts)
1711 def perfparents(ui, repo, **opts):
1720 def perfparents(ui, repo, **opts):
1712 """benchmark the time necessary to fetch one changeset's parents.
1721 """benchmark the time necessary to fetch one changeset's parents.
1713
1722
1714 The fetch is done using the `node identifier`, traversing all object layers
1723 The fetch is done using the `node identifier`, traversing all object layers
1715 from the repository object. The first N revisions will be used for this
1724 from the repository object. The first N revisions will be used for this
1716 benchmark. N is controlled by the ``perf.parentscount`` config option
1725 benchmark. N is controlled by the ``perf.parentscount`` config option
1717 (default: 1000).
1726 (default: 1000).
1718 """
1727 """
1719 opts = _byteskwargs(opts)
1728 opts = _byteskwargs(opts)
1720 timer, fm = gettimer(ui, opts)
1729 timer, fm = gettimer(ui, opts)
1721 # control the number of commits perfparents iterates over
1730 # control the number of commits perfparents iterates over
1722 # experimental config: perf.parentscount
1731 # experimental config: perf.parentscount
1723 count = getint(ui, b"perf", b"parentscount", 1000)
1732 count = getint(ui, b"perf", b"parentscount", 1000)
1724 if len(repo.changelog) < count:
1733 if len(repo.changelog) < count:
1725 raise error.Abort(b"repo needs %d commits for this test" % count)
1734 raise error.Abort(b"repo needs %d commits for this test" % count)
1726 repo = repo.unfiltered()
1735 repo = repo.unfiltered()
1727 nl = [repo.changelog.node(i) for i in _xrange(count)]
1736 nl = [repo.changelog.node(i) for i in _xrange(count)]
1728
1737
1729 def d():
1738 def d():
1730 for n in nl:
1739 for n in nl:
1731 repo.changelog.parents(n)
1740 repo.changelog.parents(n)
1732
1741
1733 timer(d)
1742 timer(d)
1734 fm.end()
1743 fm.end()
1735
1744
1736
1745
1737 @command(b'perf::ctxfiles|perfctxfiles', formatteropts)
1746 @command(b'perf::ctxfiles|perfctxfiles', formatteropts)
1738 def perfctxfiles(ui, repo, x, **opts):
1747 def perfctxfiles(ui, repo, x, **opts):
1739 opts = _byteskwargs(opts)
1748 opts = _byteskwargs(opts)
1740 x = int(x)
1749 x = int(x)
1741 timer, fm = gettimer(ui, opts)
1750 timer, fm = gettimer(ui, opts)
1742
1751
1743 def d():
1752 def d():
1744 len(repo[x].files())
1753 len(repo[x].files())
1745
1754
1746 timer(d)
1755 timer(d)
1747 fm.end()
1756 fm.end()
1748
1757
1749
1758
1750 @command(b'perf::rawfiles|perfrawfiles', formatteropts)
1759 @command(b'perf::rawfiles|perfrawfiles', formatteropts)
1751 def perfrawfiles(ui, repo, x, **opts):
1760 def perfrawfiles(ui, repo, x, **opts):
1752 opts = _byteskwargs(opts)
1761 opts = _byteskwargs(opts)
1753 x = int(x)
1762 x = int(x)
1754 timer, fm = gettimer(ui, opts)
1763 timer, fm = gettimer(ui, opts)
1755 cl = repo.changelog
1764 cl = repo.changelog
1756
1765
1757 def d():
1766 def d():
1758 len(cl.read(x)[3])
1767 len(cl.read(x)[3])
1759
1768
1760 timer(d)
1769 timer(d)
1761 fm.end()
1770 fm.end()
1762
1771
1763
1772
1764 @command(b'perf::lookup|perflookup', formatteropts)
1773 @command(b'perf::lookup|perflookup', formatteropts)
1765 def perflookup(ui, repo, rev, **opts):
1774 def perflookup(ui, repo, rev, **opts):
1766 opts = _byteskwargs(opts)
1775 opts = _byteskwargs(opts)
1767 timer, fm = gettimer(ui, opts)
1776 timer, fm = gettimer(ui, opts)
1768 timer(lambda: len(repo.lookup(rev)))
1777 timer(lambda: len(repo.lookup(rev)))
1769 fm.end()
1778 fm.end()
1770
1779
1771
1780
1772 @command(
1781 @command(
1773 b'perf::linelogedits|perflinelogedits',
1782 b'perf::linelogedits|perflinelogedits',
1774 [
1783 [
1775 (b'n', b'edits', 10000, b'number of edits'),
1784 (b'n', b'edits', 10000, b'number of edits'),
1776 (b'', b'max-hunk-lines', 10, b'max lines in a hunk'),
1785 (b'', b'max-hunk-lines', 10, b'max lines in a hunk'),
1777 ],
1786 ],
1778 norepo=True,
1787 norepo=True,
1779 )
1788 )
1780 def perflinelogedits(ui, **opts):
1789 def perflinelogedits(ui, **opts):
1781 from mercurial import linelog
1790 from mercurial import linelog
1782
1791
1783 opts = _byteskwargs(opts)
1792 opts = _byteskwargs(opts)
1784
1793
1785 edits = opts[b'edits']
1794 edits = opts[b'edits']
1786 maxhunklines = opts[b'max_hunk_lines']
1795 maxhunklines = opts[b'max_hunk_lines']
1787
1796
1788 maxb1 = 100000
1797 maxb1 = 100000
1789 random.seed(0)
1798 random.seed(0)
1790 randint = random.randint
1799 randint = random.randint
1791 currentlines = 0
1800 currentlines = 0
1792 arglist = []
1801 arglist = []
1793 for rev in _xrange(edits):
1802 for rev in _xrange(edits):
1794 a1 = randint(0, currentlines)
1803 a1 = randint(0, currentlines)
1795 a2 = randint(a1, min(currentlines, a1 + maxhunklines))
1804 a2 = randint(a1, min(currentlines, a1 + maxhunklines))
1796 b1 = randint(0, maxb1)
1805 b1 = randint(0, maxb1)
1797 b2 = randint(b1, b1 + maxhunklines)
1806 b2 = randint(b1, b1 + maxhunklines)
1798 currentlines += (b2 - b1) - (a2 - a1)
1807 currentlines += (b2 - b1) - (a2 - a1)
1799 arglist.append((rev, a1, a2, b1, b2))
1808 arglist.append((rev, a1, a2, b1, b2))
1800
1809
1801 def d():
1810 def d():
1802 ll = linelog.linelog()
1811 ll = linelog.linelog()
1803 for args in arglist:
1812 for args in arglist:
1804 ll.replacelines(*args)
1813 ll.replacelines(*args)
1805
1814
1806 timer, fm = gettimer(ui, opts)
1815 timer, fm = gettimer(ui, opts)
1807 timer(d)
1816 timer(d)
1808 fm.end()
1817 fm.end()
1809
1818
1810
1819
1811 @command(b'perf::revrange|perfrevrange', formatteropts)
1820 @command(b'perf::revrange|perfrevrange', formatteropts)
1812 def perfrevrange(ui, repo, *specs, **opts):
1821 def perfrevrange(ui, repo, *specs, **opts):
1813 opts = _byteskwargs(opts)
1822 opts = _byteskwargs(opts)
1814 timer, fm = gettimer(ui, opts)
1823 timer, fm = gettimer(ui, opts)
1815 revrange = scmutil.revrange
1824 revrange = scmutil.revrange
1816 timer(lambda: len(revrange(repo, specs)))
1825 timer(lambda: len(revrange(repo, specs)))
1817 fm.end()
1826 fm.end()
1818
1827
1819
1828
1820 @command(b'perf::nodelookup|perfnodelookup', formatteropts)
1829 @command(b'perf::nodelookup|perfnodelookup', formatteropts)
1821 def perfnodelookup(ui, repo, rev, **opts):
1830 def perfnodelookup(ui, repo, rev, **opts):
1822 opts = _byteskwargs(opts)
1831 opts = _byteskwargs(opts)
1823 timer, fm = gettimer(ui, opts)
1832 timer, fm = gettimer(ui, opts)
1824 import mercurial.revlog
1833 import mercurial.revlog
1825
1834
1826 mercurial.revlog._prereadsize = 2 ** 24 # disable lazy parser in old hg
1835 mercurial.revlog._prereadsize = 2 ** 24 # disable lazy parser in old hg
1827 n = scmutil.revsingle(repo, rev).node()
1836 n = scmutil.revsingle(repo, rev).node()
1828
1837
1829 try:
1838 try:
1830 cl = revlog(getsvfs(repo), radix=b"00changelog")
1839 cl = revlog(getsvfs(repo), radix=b"00changelog")
1831 except TypeError:
1840 except TypeError:
1832 cl = revlog(getsvfs(repo), indexfile=b"00changelog.i")
1841 cl = revlog(getsvfs(repo), indexfile=b"00changelog.i")
1833
1842
1834 def d():
1843 def d():
1835 cl.rev(n)
1844 cl.rev(n)
1836 clearcaches(cl)
1845 clearcaches(cl)
1837
1846
1838 timer(d)
1847 timer(d)
1839 fm.end()
1848 fm.end()
1840
1849
1841
1850
1842 @command(
1851 @command(
1843 b'perf::log|perflog',
1852 b'perf::log|perflog',
1844 [(b'', b'rename', False, b'ask log to follow renames')] + formatteropts,
1853 [(b'', b'rename', False, b'ask log to follow renames')] + formatteropts,
1845 )
1854 )
1846 def perflog(ui, repo, rev=None, **opts):
1855 def perflog(ui, repo, rev=None, **opts):
1847 opts = _byteskwargs(opts)
1856 opts = _byteskwargs(opts)
1848 if rev is None:
1857 if rev is None:
1849 rev = []
1858 rev = []
1850 timer, fm = gettimer(ui, opts)
1859 timer, fm = gettimer(ui, opts)
1851 ui.pushbuffer()
1860 ui.pushbuffer()
1852 timer(
1861 timer(
1853 lambda: commands.log(
1862 lambda: commands.log(
1854 ui, repo, rev=rev, date=b'', user=b'', copies=opts.get(b'rename')
1863 ui, repo, rev=rev, date=b'', user=b'', copies=opts.get(b'rename')
1855 )
1864 )
1856 )
1865 )
1857 ui.popbuffer()
1866 ui.popbuffer()
1858 fm.end()
1867 fm.end()
1859
1868
1860
1869
1861 @command(b'perf::moonwalk|perfmoonwalk', formatteropts)
1870 @command(b'perf::moonwalk|perfmoonwalk', formatteropts)
1862 def perfmoonwalk(ui, repo, **opts):
1871 def perfmoonwalk(ui, repo, **opts):
1863 """benchmark walking the changelog backwards
1872 """benchmark walking the changelog backwards
1864
1873
1865 This also loads the changelog data for each revision in the changelog.
1874 This also loads the changelog data for each revision in the changelog.
1866 """
1875 """
1867 opts = _byteskwargs(opts)
1876 opts = _byteskwargs(opts)
1868 timer, fm = gettimer(ui, opts)
1877 timer, fm = gettimer(ui, opts)
1869
1878
1870 def moonwalk():
1879 def moonwalk():
1871 for i in repo.changelog.revs(start=(len(repo) - 1), stop=-1):
1880 for i in repo.changelog.revs(start=(len(repo) - 1), stop=-1):
1872 ctx = repo[i]
1881 ctx = repo[i]
1873 ctx.branch() # read changelog data (in addition to the index)
1882 ctx.branch() # read changelog data (in addition to the index)
1874
1883
1875 timer(moonwalk)
1884 timer(moonwalk)
1876 fm.end()
1885 fm.end()
1877
1886
1878
1887
1879 @command(
1888 @command(
1880 b'perf::templating|perftemplating',
1889 b'perf::templating|perftemplating',
1881 [
1890 [
1882 (b'r', b'rev', [], b'revisions to run the template on'),
1891 (b'r', b'rev', [], b'revisions to run the template on'),
1883 ]
1892 ]
1884 + formatteropts,
1893 + formatteropts,
1885 )
1894 )
1886 def perftemplating(ui, repo, testedtemplate=None, **opts):
1895 def perftemplating(ui, repo, testedtemplate=None, **opts):
1887 """test the rendering time of a given template"""
1896 """test the rendering time of a given template"""
1888 if makelogtemplater is None:
1897 if makelogtemplater is None:
1889 raise error.Abort(
1898 raise error.Abort(
1890 b"perftemplating not available with this Mercurial",
1899 b"perftemplating not available with this Mercurial",
1891 hint=b"use 4.3 or later",
1900 hint=b"use 4.3 or later",
1892 )
1901 )
1893
1902
1894 opts = _byteskwargs(opts)
1903 opts = _byteskwargs(opts)
1895
1904
1896 nullui = ui.copy()
1905 nullui = ui.copy()
1897 nullui.fout = open(os.devnull, 'wb')
1906 nullui.fout = open(os.devnull, 'wb')
1898 nullui.disablepager()
1907 nullui.disablepager()
1899 revs = opts.get(b'rev')
1908 revs = opts.get(b'rev')
1900 if not revs:
1909 if not revs:
1901 revs = [b'all()']
1910 revs = [b'all()']
1902 revs = list(scmutil.revrange(repo, revs))
1911 revs = list(scmutil.revrange(repo, revs))
1903
1912
1904 defaulttemplate = (
1913 defaulttemplate = (
1905 b'{date|shortdate} [{rev}:{node|short}]'
1914 b'{date|shortdate} [{rev}:{node|short}]'
1906 b' {author|person}: {desc|firstline}\n'
1915 b' {author|person}: {desc|firstline}\n'
1907 )
1916 )
1908 if testedtemplate is None:
1917 if testedtemplate is None:
1909 testedtemplate = defaulttemplate
1918 testedtemplate = defaulttemplate
1910 displayer = makelogtemplater(nullui, repo, testedtemplate)
1919 displayer = makelogtemplater(nullui, repo, testedtemplate)
1911
1920
1912 def format():
1921 def format():
1913 for r in revs:
1922 for r in revs:
1914 ctx = repo[r]
1923 ctx = repo[r]
1915 displayer.show(ctx)
1924 displayer.show(ctx)
1916 displayer.flush(ctx)
1925 displayer.flush(ctx)
1917
1926
1918 timer, fm = gettimer(ui, opts)
1927 timer, fm = gettimer(ui, opts)
1919 timer(format)
1928 timer(format)
1920 fm.end()
1929 fm.end()
1921
1930
1922
1931
1923 def _displaystats(ui, opts, entries, data):
1932 def _displaystats(ui, opts, entries, data):
1924 # use a second formatter because the data are quite different, not sure
1933 # use a second formatter because the data are quite different, not sure
1925 # how it flies with the templater.
1934 # how it flies with the templater.
1926 fm = ui.formatter(b'perf-stats', opts)
1935 fm = ui.formatter(b'perf-stats', opts)
1927 for key, title in entries:
1936 for key, title in entries:
1928 values = data[key]
1937 values = data[key]
1929 nbvalues = len(data)
1938 nbvalues = len(data)
1930 values.sort()
1939 values.sort()
1931 stats = {
1940 stats = {
1932 'key': key,
1941 'key': key,
1933 'title': title,
1942 'title': title,
1934 'nbitems': len(values),
1943 'nbitems': len(values),
1935 'min': values[0][0],
1944 'min': values[0][0],
1936 '10%': values[(nbvalues * 10) // 100][0],
1945 '10%': values[(nbvalues * 10) // 100][0],
1937 '25%': values[(nbvalues * 25) // 100][0],
1946 '25%': values[(nbvalues * 25) // 100][0],
1938 '50%': values[(nbvalues * 50) // 100][0],
1947 '50%': values[(nbvalues * 50) // 100][0],
1939 '75%': values[(nbvalues * 75) // 100][0],
1948 '75%': values[(nbvalues * 75) // 100][0],
1940 '80%': values[(nbvalues * 80) // 100][0],
1949 '80%': values[(nbvalues * 80) // 100][0],
1941 '85%': values[(nbvalues * 85) // 100][0],
1950 '85%': values[(nbvalues * 85) // 100][0],
1942 '90%': values[(nbvalues * 90) // 100][0],
1951 '90%': values[(nbvalues * 90) // 100][0],
1943 '95%': values[(nbvalues * 95) // 100][0],
1952 '95%': values[(nbvalues * 95) // 100][0],
1944 '99%': values[(nbvalues * 99) // 100][0],
1953 '99%': values[(nbvalues * 99) // 100][0],
1945 'max': values[-1][0],
1954 'max': values[-1][0],
1946 }
1955 }
1947 fm.startitem()
1956 fm.startitem()
1948 fm.data(**stats)
1957 fm.data(**stats)
1949 # make node pretty for the human output
1958 # make node pretty for the human output
1950 fm.plain('### %s (%d items)\n' % (title, len(values)))
1959 fm.plain('### %s (%d items)\n' % (title, len(values)))
1951 lines = [
1960 lines = [
1952 'min',
1961 'min',
1953 '10%',
1962 '10%',
1954 '25%',
1963 '25%',
1955 '50%',
1964 '50%',
1956 '75%',
1965 '75%',
1957 '80%',
1966 '80%',
1958 '85%',
1967 '85%',
1959 '90%',
1968 '90%',
1960 '95%',
1969 '95%',
1961 '99%',
1970 '99%',
1962 'max',
1971 'max',
1963 ]
1972 ]
1964 for l in lines:
1973 for l in lines:
1965 fm.plain('%s: %s\n' % (l, stats[l]))
1974 fm.plain('%s: %s\n' % (l, stats[l]))
1966 fm.end()
1975 fm.end()
1967
1976
1968
1977
1969 @command(
1978 @command(
1970 b'perf::helper-mergecopies|perfhelper-mergecopies',
1979 b'perf::helper-mergecopies|perfhelper-mergecopies',
1971 formatteropts
1980 formatteropts
1972 + [
1981 + [
1973 (b'r', b'revs', [], b'restrict search to these revisions'),
1982 (b'r', b'revs', [], b'restrict search to these revisions'),
1974 (b'', b'timing', False, b'provides extra data (costly)'),
1983 (b'', b'timing', False, b'provides extra data (costly)'),
1975 (b'', b'stats', False, b'provides statistic about the measured data'),
1984 (b'', b'stats', False, b'provides statistic about the measured data'),
1976 ],
1985 ],
1977 )
1986 )
1978 def perfhelpermergecopies(ui, repo, revs=[], **opts):
1987 def perfhelpermergecopies(ui, repo, revs=[], **opts):
1979 """find statistics about potential parameters for `perfmergecopies`
1988 """find statistics about potential parameters for `perfmergecopies`
1980
1989
1981 This command find (base, p1, p2) triplet relevant for copytracing
1990 This command find (base, p1, p2) triplet relevant for copytracing
1982 benchmarking in the context of a merge. It reports values for some of the
1991 benchmarking in the context of a merge. It reports values for some of the
1983 parameters that impact merge copy tracing time during merge.
1992 parameters that impact merge copy tracing time during merge.
1984
1993
1985 If `--timing` is set, rename detection is run and the associated timing
1994 If `--timing` is set, rename detection is run and the associated timing
1986 will be reported. The extra details come at the cost of slower command
1995 will be reported. The extra details come at the cost of slower command
1987 execution.
1996 execution.
1988
1997
1989 Since rename detection is only run once, other factors might easily
1998 Since rename detection is only run once, other factors might easily
1990 affect the precision of the timing. However it should give a good
1999 affect the precision of the timing. However it should give a good
1991 approximation of which revision triplets are very costly.
2000 approximation of which revision triplets are very costly.
1992 """
2001 """
1993 opts = _byteskwargs(opts)
2002 opts = _byteskwargs(opts)
1994 fm = ui.formatter(b'perf', opts)
2003 fm = ui.formatter(b'perf', opts)
1995 dotiming = opts[b'timing']
2004 dotiming = opts[b'timing']
1996 dostats = opts[b'stats']
2005 dostats = opts[b'stats']
1997
2006
1998 output_template = [
2007 output_template = [
1999 ("base", "%(base)12s"),
2008 ("base", "%(base)12s"),
2000 ("p1", "%(p1.node)12s"),
2009 ("p1", "%(p1.node)12s"),
2001 ("p2", "%(p2.node)12s"),
2010 ("p2", "%(p2.node)12s"),
2002 ("p1.nb-revs", "%(p1.nbrevs)12d"),
2011 ("p1.nb-revs", "%(p1.nbrevs)12d"),
2003 ("p1.nb-files", "%(p1.nbmissingfiles)12d"),
2012 ("p1.nb-files", "%(p1.nbmissingfiles)12d"),
2004 ("p1.renames", "%(p1.renamedfiles)12d"),
2013 ("p1.renames", "%(p1.renamedfiles)12d"),
2005 ("p1.time", "%(p1.time)12.3f"),
2014 ("p1.time", "%(p1.time)12.3f"),
2006 ("p2.nb-revs", "%(p2.nbrevs)12d"),
2015 ("p2.nb-revs", "%(p2.nbrevs)12d"),
2007 ("p2.nb-files", "%(p2.nbmissingfiles)12d"),
2016 ("p2.nb-files", "%(p2.nbmissingfiles)12d"),
2008 ("p2.renames", "%(p2.renamedfiles)12d"),
2017 ("p2.renames", "%(p2.renamedfiles)12d"),
2009 ("p2.time", "%(p2.time)12.3f"),
2018 ("p2.time", "%(p2.time)12.3f"),
2010 ("renames", "%(nbrenamedfiles)12d"),
2019 ("renames", "%(nbrenamedfiles)12d"),
2011 ("total.time", "%(time)12.3f"),
2020 ("total.time", "%(time)12.3f"),
2012 ]
2021 ]
2013 if not dotiming:
2022 if not dotiming:
2014 output_template = [
2023 output_template = [
2015 i
2024 i
2016 for i in output_template
2025 for i in output_template
2017 if not ('time' in i[0] or 'renames' in i[0])
2026 if not ('time' in i[0] or 'renames' in i[0])
2018 ]
2027 ]
2019 header_names = [h for (h, v) in output_template]
2028 header_names = [h for (h, v) in output_template]
2020 output = ' '.join([v for (h, v) in output_template]) + '\n'
2029 output = ' '.join([v for (h, v) in output_template]) + '\n'
2021 header = ' '.join(['%12s'] * len(header_names)) + '\n'
2030 header = ' '.join(['%12s'] * len(header_names)) + '\n'
2022 fm.plain(header % tuple(header_names))
2031 fm.plain(header % tuple(header_names))
2023
2032
2024 if not revs:
2033 if not revs:
2025 revs = ['all()']
2034 revs = ['all()']
2026 revs = scmutil.revrange(repo, revs)
2035 revs = scmutil.revrange(repo, revs)
2027
2036
2028 if dostats:
2037 if dostats:
2029 alldata = {
2038 alldata = {
2030 'nbrevs': [],
2039 'nbrevs': [],
2031 'nbmissingfiles': [],
2040 'nbmissingfiles': [],
2032 }
2041 }
2033 if dotiming:
2042 if dotiming:
2034 alldata['parentnbrenames'] = []
2043 alldata['parentnbrenames'] = []
2035 alldata['totalnbrenames'] = []
2044 alldata['totalnbrenames'] = []
2036 alldata['parenttime'] = []
2045 alldata['parenttime'] = []
2037 alldata['totaltime'] = []
2046 alldata['totaltime'] = []
2038
2047
2039 roi = repo.revs('merge() and %ld', revs)
2048 roi = repo.revs('merge() and %ld', revs)
2040 for r in roi:
2049 for r in roi:
2041 ctx = repo[r]
2050 ctx = repo[r]
2042 p1 = ctx.p1()
2051 p1 = ctx.p1()
2043 p2 = ctx.p2()
2052 p2 = ctx.p2()
2044 bases = repo.changelog._commonancestorsheads(p1.rev(), p2.rev())
2053 bases = repo.changelog._commonancestorsheads(p1.rev(), p2.rev())
2045 for b in bases:
2054 for b in bases:
2046 b = repo[b]
2055 b = repo[b]
2047 p1missing = copies._computeforwardmissing(b, p1)
2056 p1missing = copies._computeforwardmissing(b, p1)
2048 p2missing = copies._computeforwardmissing(b, p2)
2057 p2missing = copies._computeforwardmissing(b, p2)
2049 data = {
2058 data = {
2050 b'base': b.hex(),
2059 b'base': b.hex(),
2051 b'p1.node': p1.hex(),
2060 b'p1.node': p1.hex(),
2052 b'p1.nbrevs': len(repo.revs('only(%d, %d)', p1.rev(), b.rev())),
2061 b'p1.nbrevs': len(repo.revs('only(%d, %d)', p1.rev(), b.rev())),
2053 b'p1.nbmissingfiles': len(p1missing),
2062 b'p1.nbmissingfiles': len(p1missing),
2054 b'p2.node': p2.hex(),
2063 b'p2.node': p2.hex(),
2055 b'p2.nbrevs': len(repo.revs('only(%d, %d)', p2.rev(), b.rev())),
2064 b'p2.nbrevs': len(repo.revs('only(%d, %d)', p2.rev(), b.rev())),
2056 b'p2.nbmissingfiles': len(p2missing),
2065 b'p2.nbmissingfiles': len(p2missing),
2057 }
2066 }
2058 if dostats:
2067 if dostats:
2059 if p1missing:
2068 if p1missing:
2060 alldata['nbrevs'].append(
2069 alldata['nbrevs'].append(
2061 (data['p1.nbrevs'], b.hex(), p1.hex())
2070 (data['p1.nbrevs'], b.hex(), p1.hex())
2062 )
2071 )
2063 alldata['nbmissingfiles'].append(
2072 alldata['nbmissingfiles'].append(
2064 (data['p1.nbmissingfiles'], b.hex(), p1.hex())
2073 (data['p1.nbmissingfiles'], b.hex(), p1.hex())
2065 )
2074 )
2066 if p2missing:
2075 if p2missing:
2067 alldata['nbrevs'].append(
2076 alldata['nbrevs'].append(
2068 (data['p2.nbrevs'], b.hex(), p2.hex())
2077 (data['p2.nbrevs'], b.hex(), p2.hex())
2069 )
2078 )
2070 alldata['nbmissingfiles'].append(
2079 alldata['nbmissingfiles'].append(
2071 (data['p2.nbmissingfiles'], b.hex(), p2.hex())
2080 (data['p2.nbmissingfiles'], b.hex(), p2.hex())
2072 )
2081 )
2073 if dotiming:
2082 if dotiming:
2074 begin = util.timer()
2083 begin = util.timer()
2075 mergedata = copies.mergecopies(repo, p1, p2, b)
2084 mergedata = copies.mergecopies(repo, p1, p2, b)
2076 end = util.timer()
2085 end = util.timer()
2077 # not very stable timing since we did only one run
2086 # not very stable timing since we did only one run
2078 data['time'] = end - begin
2087 data['time'] = end - begin
2079 # mergedata contains five dicts: "copy", "movewithdir",
2088 # mergedata contains five dicts: "copy", "movewithdir",
2080 # "diverge", "renamedelete" and "dirmove".
2089 # "diverge", "renamedelete" and "dirmove".
2081 # The first 4 are about renamed file so lets count that.
2090 # The first 4 are about renamed file so lets count that.
2082 renames = len(mergedata[0])
2091 renames = len(mergedata[0])
2083 renames += len(mergedata[1])
2092 renames += len(mergedata[1])
2084 renames += len(mergedata[2])
2093 renames += len(mergedata[2])
2085 renames += len(mergedata[3])
2094 renames += len(mergedata[3])
2086 data['nbrenamedfiles'] = renames
2095 data['nbrenamedfiles'] = renames
2087 begin = util.timer()
2096 begin = util.timer()
2088 p1renames = copies.pathcopies(b, p1)
2097 p1renames = copies.pathcopies(b, p1)
2089 end = util.timer()
2098 end = util.timer()
2090 data['p1.time'] = end - begin
2099 data['p1.time'] = end - begin
2091 begin = util.timer()
2100 begin = util.timer()
2092 p2renames = copies.pathcopies(b, p2)
2101 p2renames = copies.pathcopies(b, p2)
2093 end = util.timer()
2102 end = util.timer()
2094 data['p2.time'] = end - begin
2103 data['p2.time'] = end - begin
2095 data['p1.renamedfiles'] = len(p1renames)
2104 data['p1.renamedfiles'] = len(p1renames)
2096 data['p2.renamedfiles'] = len(p2renames)
2105 data['p2.renamedfiles'] = len(p2renames)
2097
2106
2098 if dostats:
2107 if dostats:
2099 if p1missing:
2108 if p1missing:
2100 alldata['parentnbrenames'].append(
2109 alldata['parentnbrenames'].append(
2101 (data['p1.renamedfiles'], b.hex(), p1.hex())
2110 (data['p1.renamedfiles'], b.hex(), p1.hex())
2102 )
2111 )
2103 alldata['parenttime'].append(
2112 alldata['parenttime'].append(
2104 (data['p1.time'], b.hex(), p1.hex())
2113 (data['p1.time'], b.hex(), p1.hex())
2105 )
2114 )
2106 if p2missing:
2115 if p2missing:
2107 alldata['parentnbrenames'].append(
2116 alldata['parentnbrenames'].append(
2108 (data['p2.renamedfiles'], b.hex(), p2.hex())
2117 (data['p2.renamedfiles'], b.hex(), p2.hex())
2109 )
2118 )
2110 alldata['parenttime'].append(
2119 alldata['parenttime'].append(
2111 (data['p2.time'], b.hex(), p2.hex())
2120 (data['p2.time'], b.hex(), p2.hex())
2112 )
2121 )
2113 if p1missing or p2missing:
2122 if p1missing or p2missing:
2114 alldata['totalnbrenames'].append(
2123 alldata['totalnbrenames'].append(
2115 (
2124 (
2116 data['nbrenamedfiles'],
2125 data['nbrenamedfiles'],
2117 b.hex(),
2126 b.hex(),
2118 p1.hex(),
2127 p1.hex(),
2119 p2.hex(),
2128 p2.hex(),
2120 )
2129 )
2121 )
2130 )
2122 alldata['totaltime'].append(
2131 alldata['totaltime'].append(
2123 (data['time'], b.hex(), p1.hex(), p2.hex())
2132 (data['time'], b.hex(), p1.hex(), p2.hex())
2124 )
2133 )
2125 fm.startitem()
2134 fm.startitem()
2126 fm.data(**data)
2135 fm.data(**data)
2127 # make node pretty for the human output
2136 # make node pretty for the human output
2128 out = data.copy()
2137 out = data.copy()
2129 out['base'] = fm.hexfunc(b.node())
2138 out['base'] = fm.hexfunc(b.node())
2130 out['p1.node'] = fm.hexfunc(p1.node())
2139 out['p1.node'] = fm.hexfunc(p1.node())
2131 out['p2.node'] = fm.hexfunc(p2.node())
2140 out['p2.node'] = fm.hexfunc(p2.node())
2132 fm.plain(output % out)
2141 fm.plain(output % out)
2133
2142
2134 fm.end()
2143 fm.end()
2135 if dostats:
2144 if dostats:
2136 # use a second formatter because the data are quite different, not sure
2145 # use a second formatter because the data are quite different, not sure
2137 # how it flies with the templater.
2146 # how it flies with the templater.
2138 entries = [
2147 entries = [
2139 ('nbrevs', 'number of revision covered'),
2148 ('nbrevs', 'number of revision covered'),
2140 ('nbmissingfiles', 'number of missing files at head'),
2149 ('nbmissingfiles', 'number of missing files at head'),
2141 ]
2150 ]
2142 if dotiming:
2151 if dotiming:
2143 entries.append(
2152 entries.append(
2144 ('parentnbrenames', 'rename from one parent to base')
2153 ('parentnbrenames', 'rename from one parent to base')
2145 )
2154 )
2146 entries.append(('totalnbrenames', 'total number of renames'))
2155 entries.append(('totalnbrenames', 'total number of renames'))
2147 entries.append(('parenttime', 'time for one parent'))
2156 entries.append(('parenttime', 'time for one parent'))
2148 entries.append(('totaltime', 'time for both parents'))
2157 entries.append(('totaltime', 'time for both parents'))
2149 _displaystats(ui, opts, entries, alldata)
2158 _displaystats(ui, opts, entries, alldata)
2150
2159
2151
2160
2152 @command(
2161 @command(
2153 b'perf::helper-pathcopies|perfhelper-pathcopies',
2162 b'perf::helper-pathcopies|perfhelper-pathcopies',
2154 formatteropts
2163 formatteropts
2155 + [
2164 + [
2156 (b'r', b'revs', [], b'restrict search to these revisions'),
2165 (b'r', b'revs', [], b'restrict search to these revisions'),
2157 (b'', b'timing', False, b'provides extra data (costly)'),
2166 (b'', b'timing', False, b'provides extra data (costly)'),
2158 (b'', b'stats', False, b'provides statistic about the measured data'),
2167 (b'', b'stats', False, b'provides statistic about the measured data'),
2159 ],
2168 ],
2160 )
2169 )
2161 def perfhelperpathcopies(ui, repo, revs=[], **opts):
2170 def perfhelperpathcopies(ui, repo, revs=[], **opts):
2162 """find statistic about potential parameters for the `perftracecopies`
2171 """find statistic about potential parameters for the `perftracecopies`
2163
2172
2164 This command find source-destination pair relevant for copytracing testing.
2173 This command find source-destination pair relevant for copytracing testing.
2165 It report value for some of the parameters that impact copy tracing time.
2174 It report value for some of the parameters that impact copy tracing time.
2166
2175
2167 If `--timing` is set, rename detection is run and the associated timing
2176 If `--timing` is set, rename detection is run and the associated timing
2168 will be reported. The extra details comes at the cost of a slower command
2177 will be reported. The extra details comes at the cost of a slower command
2169 execution.
2178 execution.
2170
2179
2171 Since the rename detection is only run once, other factors might easily
2180 Since the rename detection is only run once, other factors might easily
2172 affect the precision of the timing. However it should give a good
2181 affect the precision of the timing. However it should give a good
2173 approximation of which revision pairs are very costly.
2182 approximation of which revision pairs are very costly.
2174 """
2183 """
2175 opts = _byteskwargs(opts)
2184 opts = _byteskwargs(opts)
2176 fm = ui.formatter(b'perf', opts)
2185 fm = ui.formatter(b'perf', opts)
2177 dotiming = opts[b'timing']
2186 dotiming = opts[b'timing']
2178 dostats = opts[b'stats']
2187 dostats = opts[b'stats']
2179
2188
2180 if dotiming:
2189 if dotiming:
2181 header = '%12s %12s %12s %12s %12s %12s\n'
2190 header = '%12s %12s %12s %12s %12s %12s\n'
2182 output = (
2191 output = (
2183 "%(source)12s %(destination)12s "
2192 "%(source)12s %(destination)12s "
2184 "%(nbrevs)12d %(nbmissingfiles)12d "
2193 "%(nbrevs)12d %(nbmissingfiles)12d "
2185 "%(nbrenamedfiles)12d %(time)18.5f\n"
2194 "%(nbrenamedfiles)12d %(time)18.5f\n"
2186 )
2195 )
2187 header_names = (
2196 header_names = (
2188 "source",
2197 "source",
2189 "destination",
2198 "destination",
2190 "nb-revs",
2199 "nb-revs",
2191 "nb-files",
2200 "nb-files",
2192 "nb-renames",
2201 "nb-renames",
2193 "time",
2202 "time",
2194 )
2203 )
2195 fm.plain(header % header_names)
2204 fm.plain(header % header_names)
2196 else:
2205 else:
2197 header = '%12s %12s %12s %12s\n'
2206 header = '%12s %12s %12s %12s\n'
2198 output = (
2207 output = (
2199 "%(source)12s %(destination)12s "
2208 "%(source)12s %(destination)12s "
2200 "%(nbrevs)12d %(nbmissingfiles)12d\n"
2209 "%(nbrevs)12d %(nbmissingfiles)12d\n"
2201 )
2210 )
2202 fm.plain(header % ("source", "destination", "nb-revs", "nb-files"))
2211 fm.plain(header % ("source", "destination", "nb-revs", "nb-files"))
2203
2212
2204 if not revs:
2213 if not revs:
2205 revs = ['all()']
2214 revs = ['all()']
2206 revs = scmutil.revrange(repo, revs)
2215 revs = scmutil.revrange(repo, revs)
2207
2216
2208 if dostats:
2217 if dostats:
2209 alldata = {
2218 alldata = {
2210 'nbrevs': [],
2219 'nbrevs': [],
2211 'nbmissingfiles': [],
2220 'nbmissingfiles': [],
2212 }
2221 }
2213 if dotiming:
2222 if dotiming:
2214 alldata['nbrenames'] = []
2223 alldata['nbrenames'] = []
2215 alldata['time'] = []
2224 alldata['time'] = []
2216
2225
2217 roi = repo.revs('merge() and %ld', revs)
2226 roi = repo.revs('merge() and %ld', revs)
2218 for r in roi:
2227 for r in roi:
2219 ctx = repo[r]
2228 ctx = repo[r]
2220 p1 = ctx.p1().rev()
2229 p1 = ctx.p1().rev()
2221 p2 = ctx.p2().rev()
2230 p2 = ctx.p2().rev()
2222 bases = repo.changelog._commonancestorsheads(p1, p2)
2231 bases = repo.changelog._commonancestorsheads(p1, p2)
2223 for p in (p1, p2):
2232 for p in (p1, p2):
2224 for b in bases:
2233 for b in bases:
2225 base = repo[b]
2234 base = repo[b]
2226 parent = repo[p]
2235 parent = repo[p]
2227 missing = copies._computeforwardmissing(base, parent)
2236 missing = copies._computeforwardmissing(base, parent)
2228 if not missing:
2237 if not missing:
2229 continue
2238 continue
2230 data = {
2239 data = {
2231 b'source': base.hex(),
2240 b'source': base.hex(),
2232 b'destination': parent.hex(),
2241 b'destination': parent.hex(),
2233 b'nbrevs': len(repo.revs('only(%d, %d)', p, b)),
2242 b'nbrevs': len(repo.revs('only(%d, %d)', p, b)),
2234 b'nbmissingfiles': len(missing),
2243 b'nbmissingfiles': len(missing),
2235 }
2244 }
2236 if dostats:
2245 if dostats:
2237 alldata['nbrevs'].append(
2246 alldata['nbrevs'].append(
2238 (
2247 (
2239 data['nbrevs'],
2248 data['nbrevs'],
2240 base.hex(),
2249 base.hex(),
2241 parent.hex(),
2250 parent.hex(),
2242 )
2251 )
2243 )
2252 )
2244 alldata['nbmissingfiles'].append(
2253 alldata['nbmissingfiles'].append(
2245 (
2254 (
2246 data['nbmissingfiles'],
2255 data['nbmissingfiles'],
2247 base.hex(),
2256 base.hex(),
2248 parent.hex(),
2257 parent.hex(),
2249 )
2258 )
2250 )
2259 )
2251 if dotiming:
2260 if dotiming:
2252 begin = util.timer()
2261 begin = util.timer()
2253 renames = copies.pathcopies(base, parent)
2262 renames = copies.pathcopies(base, parent)
2254 end = util.timer()
2263 end = util.timer()
2255 # not very stable timing since we did only one run
2264 # not very stable timing since we did only one run
2256 data['time'] = end - begin
2265 data['time'] = end - begin
2257 data['nbrenamedfiles'] = len(renames)
2266 data['nbrenamedfiles'] = len(renames)
2258 if dostats:
2267 if dostats:
2259 alldata['time'].append(
2268 alldata['time'].append(
2260 (
2269 (
2261 data['time'],
2270 data['time'],
2262 base.hex(),
2271 base.hex(),
2263 parent.hex(),
2272 parent.hex(),
2264 )
2273 )
2265 )
2274 )
2266 alldata['nbrenames'].append(
2275 alldata['nbrenames'].append(
2267 (
2276 (
2268 data['nbrenamedfiles'],
2277 data['nbrenamedfiles'],
2269 base.hex(),
2278 base.hex(),
2270 parent.hex(),
2279 parent.hex(),
2271 )
2280 )
2272 )
2281 )
2273 fm.startitem()
2282 fm.startitem()
2274 fm.data(**data)
2283 fm.data(**data)
2275 out = data.copy()
2284 out = data.copy()
2276 out['source'] = fm.hexfunc(base.node())
2285 out['source'] = fm.hexfunc(base.node())
2277 out['destination'] = fm.hexfunc(parent.node())
2286 out['destination'] = fm.hexfunc(parent.node())
2278 fm.plain(output % out)
2287 fm.plain(output % out)
2279
2288
2280 fm.end()
2289 fm.end()
2281 if dostats:
2290 if dostats:
2282 entries = [
2291 entries = [
2283 ('nbrevs', 'number of revision covered'),
2292 ('nbrevs', 'number of revision covered'),
2284 ('nbmissingfiles', 'number of missing files at head'),
2293 ('nbmissingfiles', 'number of missing files at head'),
2285 ]
2294 ]
2286 if dotiming:
2295 if dotiming:
2287 entries.append(('nbrenames', 'renamed files'))
2296 entries.append(('nbrenames', 'renamed files'))
2288 entries.append(('time', 'time'))
2297 entries.append(('time', 'time'))
2289 _displaystats(ui, opts, entries, alldata)
2298 _displaystats(ui, opts, entries, alldata)
2290
2299
2291
2300
2292 @command(b'perf::cca|perfcca', formatteropts)
2301 @command(b'perf::cca|perfcca', formatteropts)
2293 def perfcca(ui, repo, **opts):
2302 def perfcca(ui, repo, **opts):
2294 opts = _byteskwargs(opts)
2303 opts = _byteskwargs(opts)
2295 timer, fm = gettimer(ui, opts)
2304 timer, fm = gettimer(ui, opts)
2296 timer(lambda: scmutil.casecollisionauditor(ui, False, repo.dirstate))
2305 timer(lambda: scmutil.casecollisionauditor(ui, False, repo.dirstate))
2297 fm.end()
2306 fm.end()
2298
2307
2299
2308
2300 @command(b'perf::fncacheload|perffncacheload', formatteropts)
2309 @command(b'perf::fncacheload|perffncacheload', formatteropts)
2301 def perffncacheload(ui, repo, **opts):
2310 def perffncacheload(ui, repo, **opts):
2302 opts = _byteskwargs(opts)
2311 opts = _byteskwargs(opts)
2303 timer, fm = gettimer(ui, opts)
2312 timer, fm = gettimer(ui, opts)
2304 s = repo.store
2313 s = repo.store
2305
2314
2306 def d():
2315 def d():
2307 s.fncache._load()
2316 s.fncache._load()
2308
2317
2309 timer(d)
2318 timer(d)
2310 fm.end()
2319 fm.end()
2311
2320
2312
2321
2313 @command(b'perf::fncachewrite|perffncachewrite', formatteropts)
2322 @command(b'perf::fncachewrite|perffncachewrite', formatteropts)
2314 def perffncachewrite(ui, repo, **opts):
2323 def perffncachewrite(ui, repo, **opts):
2315 opts = _byteskwargs(opts)
2324 opts = _byteskwargs(opts)
2316 timer, fm = gettimer(ui, opts)
2325 timer, fm = gettimer(ui, opts)
2317 s = repo.store
2326 s = repo.store
2318 lock = repo.lock()
2327 lock = repo.lock()
2319 s.fncache._load()
2328 s.fncache._load()
2320 tr = repo.transaction(b'perffncachewrite')
2329 tr = repo.transaction(b'perffncachewrite')
2321 tr.addbackup(b'fncache')
2330 tr.addbackup(b'fncache')
2322
2331
2323 def d():
2332 def d():
2324 s.fncache._dirty = True
2333 s.fncache._dirty = True
2325 s.fncache.write(tr)
2334 s.fncache.write(tr)
2326
2335
2327 timer(d)
2336 timer(d)
2328 tr.close()
2337 tr.close()
2329 lock.release()
2338 lock.release()
2330 fm.end()
2339 fm.end()
2331
2340
2332
2341
2333 @command(b'perf::fncacheencode|perffncacheencode', formatteropts)
2342 @command(b'perf::fncacheencode|perffncacheencode', formatteropts)
2334 def perffncacheencode(ui, repo, **opts):
2343 def perffncacheencode(ui, repo, **opts):
2335 opts = _byteskwargs(opts)
2344 opts = _byteskwargs(opts)
2336 timer, fm = gettimer(ui, opts)
2345 timer, fm = gettimer(ui, opts)
2337 s = repo.store
2346 s = repo.store
2338 s.fncache._load()
2347 s.fncache._load()
2339
2348
2340 def d():
2349 def d():
2341 for p in s.fncache.entries:
2350 for p in s.fncache.entries:
2342 s.encode(p)
2351 s.encode(p)
2343
2352
2344 timer(d)
2353 timer(d)
2345 fm.end()
2354 fm.end()
2346
2355
2347
2356
2348 def _bdiffworker(q, blocks, xdiff, ready, done):
2357 def _bdiffworker(q, blocks, xdiff, ready, done):
2349 while not done.is_set():
2358 while not done.is_set():
2350 pair = q.get()
2359 pair = q.get()
2351 while pair is not None:
2360 while pair is not None:
2352 if xdiff:
2361 if xdiff:
2353 mdiff.bdiff.xdiffblocks(*pair)
2362 mdiff.bdiff.xdiffblocks(*pair)
2354 elif blocks:
2363 elif blocks:
2355 mdiff.bdiff.blocks(*pair)
2364 mdiff.bdiff.blocks(*pair)
2356 else:
2365 else:
2357 mdiff.textdiff(*pair)
2366 mdiff.textdiff(*pair)
2358 q.task_done()
2367 q.task_done()
2359 pair = q.get()
2368 pair = q.get()
2360 q.task_done() # for the None one
2369 q.task_done() # for the None one
2361 with ready:
2370 with ready:
2362 ready.wait()
2371 ready.wait()
2363
2372
2364
2373
2365 def _manifestrevision(repo, mnode):
2374 def _manifestrevision(repo, mnode):
2366 ml = repo.manifestlog
2375 ml = repo.manifestlog
2367
2376
2368 if util.safehasattr(ml, b'getstorage'):
2377 if util.safehasattr(ml, b'getstorage'):
2369 store = ml.getstorage(b'')
2378 store = ml.getstorage(b'')
2370 else:
2379 else:
2371 store = ml._revlog
2380 store = ml._revlog
2372
2381
2373 return store.revision(mnode)
2382 return store.revision(mnode)
2374
2383
2375
2384
2376 @command(
2385 @command(
2377 b'perf::bdiff|perfbdiff',
2386 b'perf::bdiff|perfbdiff',
2378 revlogopts
2387 revlogopts
2379 + formatteropts
2388 + formatteropts
2380 + [
2389 + [
2381 (
2390 (
2382 b'',
2391 b'',
2383 b'count',
2392 b'count',
2384 1,
2393 1,
2385 b'number of revisions to test (when using --startrev)',
2394 b'number of revisions to test (when using --startrev)',
2386 ),
2395 ),
2387 (b'', b'alldata', False, b'test bdiffs for all associated revisions'),
2396 (b'', b'alldata', False, b'test bdiffs for all associated revisions'),
2388 (b'', b'threads', 0, b'number of thread to use (disable with 0)'),
2397 (b'', b'threads', 0, b'number of thread to use (disable with 0)'),
2389 (b'', b'blocks', False, b'test computing diffs into blocks'),
2398 (b'', b'blocks', False, b'test computing diffs into blocks'),
2390 (b'', b'xdiff', False, b'use xdiff algorithm'),
2399 (b'', b'xdiff', False, b'use xdiff algorithm'),
2391 ],
2400 ],
2392 b'-c|-m|FILE REV',
2401 b'-c|-m|FILE REV',
2393 )
2402 )
2394 def perfbdiff(ui, repo, file_, rev=None, count=None, threads=0, **opts):
2403 def perfbdiff(ui, repo, file_, rev=None, count=None, threads=0, **opts):
2395 """benchmark a bdiff between revisions
2404 """benchmark a bdiff between revisions
2396
2405
2397 By default, benchmark a bdiff between its delta parent and itself.
2406 By default, benchmark a bdiff between its delta parent and itself.
2398
2407
2399 With ``--count``, benchmark bdiffs between delta parents and self for N
2408 With ``--count``, benchmark bdiffs between delta parents and self for N
2400 revisions starting at the specified revision.
2409 revisions starting at the specified revision.
2401
2410
2402 With ``--alldata``, assume the requested revision is a changeset and
2411 With ``--alldata``, assume the requested revision is a changeset and
2403 measure bdiffs for all changes related to that changeset (manifest
2412 measure bdiffs for all changes related to that changeset (manifest
2404 and filelogs).
2413 and filelogs).
2405 """
2414 """
2406 opts = _byteskwargs(opts)
2415 opts = _byteskwargs(opts)
2407
2416
2408 if opts[b'xdiff'] and not opts[b'blocks']:
2417 if opts[b'xdiff'] and not opts[b'blocks']:
2409 raise error.CommandError(b'perfbdiff', b'--xdiff requires --blocks')
2418 raise error.CommandError(b'perfbdiff', b'--xdiff requires --blocks')
2410
2419
2411 if opts[b'alldata']:
2420 if opts[b'alldata']:
2412 opts[b'changelog'] = True
2421 opts[b'changelog'] = True
2413
2422
2414 if opts.get(b'changelog') or opts.get(b'manifest'):
2423 if opts.get(b'changelog') or opts.get(b'manifest'):
2415 file_, rev = None, file_
2424 file_, rev = None, file_
2416 elif rev is None:
2425 elif rev is None:
2417 raise error.CommandError(b'perfbdiff', b'invalid arguments')
2426 raise error.CommandError(b'perfbdiff', b'invalid arguments')
2418
2427
2419 blocks = opts[b'blocks']
2428 blocks = opts[b'blocks']
2420 xdiff = opts[b'xdiff']
2429 xdiff = opts[b'xdiff']
2421 textpairs = []
2430 textpairs = []
2422
2431
2423 r = cmdutil.openrevlog(repo, b'perfbdiff', file_, opts)
2432 r = cmdutil.openrevlog(repo, b'perfbdiff', file_, opts)
2424
2433
2425 startrev = r.rev(r.lookup(rev))
2434 startrev = r.rev(r.lookup(rev))
2426 for rev in range(startrev, min(startrev + count, len(r) - 1)):
2435 for rev in range(startrev, min(startrev + count, len(r) - 1)):
2427 if opts[b'alldata']:
2436 if opts[b'alldata']:
2428 # Load revisions associated with changeset.
2437 # Load revisions associated with changeset.
2429 ctx = repo[rev]
2438 ctx = repo[rev]
2430 mtext = _manifestrevision(repo, ctx.manifestnode())
2439 mtext = _manifestrevision(repo, ctx.manifestnode())
2431 for pctx in ctx.parents():
2440 for pctx in ctx.parents():
2432 pman = _manifestrevision(repo, pctx.manifestnode())
2441 pman = _manifestrevision(repo, pctx.manifestnode())
2433 textpairs.append((pman, mtext))
2442 textpairs.append((pman, mtext))
2434
2443
2435 # Load filelog revisions by iterating manifest delta.
2444 # Load filelog revisions by iterating manifest delta.
2436 man = ctx.manifest()
2445 man = ctx.manifest()
2437 pman = ctx.p1().manifest()
2446 pman = ctx.p1().manifest()
2438 for filename, change in pman.diff(man).items():
2447 for filename, change in pman.diff(man).items():
2439 fctx = repo.file(filename)
2448 fctx = repo.file(filename)
2440 f1 = fctx.revision(change[0][0] or -1)
2449 f1 = fctx.revision(change[0][0] or -1)
2441 f2 = fctx.revision(change[1][0] or -1)
2450 f2 = fctx.revision(change[1][0] or -1)
2442 textpairs.append((f1, f2))
2451 textpairs.append((f1, f2))
2443 else:
2452 else:
2444 dp = r.deltaparent(rev)
2453 dp = r.deltaparent(rev)
2445 textpairs.append((r.revision(dp), r.revision(rev)))
2454 textpairs.append((r.revision(dp), r.revision(rev)))
2446
2455
2447 withthreads = threads > 0
2456 withthreads = threads > 0
2448 if not withthreads:
2457 if not withthreads:
2449
2458
2450 def d():
2459 def d():
2451 for pair in textpairs:
2460 for pair in textpairs:
2452 if xdiff:
2461 if xdiff:
2453 mdiff.bdiff.xdiffblocks(*pair)
2462 mdiff.bdiff.xdiffblocks(*pair)
2454 elif blocks:
2463 elif blocks:
2455 mdiff.bdiff.blocks(*pair)
2464 mdiff.bdiff.blocks(*pair)
2456 else:
2465 else:
2457 mdiff.textdiff(*pair)
2466 mdiff.textdiff(*pair)
2458
2467
2459 else:
2468 else:
2460 q = queue()
2469 q = queue()
2461 for i in _xrange(threads):
2470 for i in _xrange(threads):
2462 q.put(None)
2471 q.put(None)
2463 ready = threading.Condition()
2472 ready = threading.Condition()
2464 done = threading.Event()
2473 done = threading.Event()
2465 for i in _xrange(threads):
2474 for i in _xrange(threads):
2466 threading.Thread(
2475 threading.Thread(
2467 target=_bdiffworker, args=(q, blocks, xdiff, ready, done)
2476 target=_bdiffworker, args=(q, blocks, xdiff, ready, done)
2468 ).start()
2477 ).start()
2469 q.join()
2478 q.join()
2470
2479
2471 def d():
2480 def d():
2472 for pair in textpairs:
2481 for pair in textpairs:
2473 q.put(pair)
2482 q.put(pair)
2474 for i in _xrange(threads):
2483 for i in _xrange(threads):
2475 q.put(None)
2484 q.put(None)
2476 with ready:
2485 with ready:
2477 ready.notify_all()
2486 ready.notify_all()
2478 q.join()
2487 q.join()
2479
2488
2480 timer, fm = gettimer(ui, opts)
2489 timer, fm = gettimer(ui, opts)
2481 timer(d)
2490 timer(d)
2482 fm.end()
2491 fm.end()
2483
2492
2484 if withthreads:
2493 if withthreads:
2485 done.set()
2494 done.set()
2486 for i in _xrange(threads):
2495 for i in _xrange(threads):
2487 q.put(None)
2496 q.put(None)
2488 with ready:
2497 with ready:
2489 ready.notify_all()
2498 ready.notify_all()
2490
2499
2491
2500
2492 @command(
2501 @command(
2493 b'perf::unidiff|perfunidiff',
2502 b'perf::unidiff|perfunidiff',
2494 revlogopts
2503 revlogopts
2495 + formatteropts
2504 + formatteropts
2496 + [
2505 + [
2497 (
2506 (
2498 b'',
2507 b'',
2499 b'count',
2508 b'count',
2500 1,
2509 1,
2501 b'number of revisions to test (when using --startrev)',
2510 b'number of revisions to test (when using --startrev)',
2502 ),
2511 ),
2503 (b'', b'alldata', False, b'test unidiffs for all associated revisions'),
2512 (b'', b'alldata', False, b'test unidiffs for all associated revisions'),
2504 ],
2513 ],
2505 b'-c|-m|FILE REV',
2514 b'-c|-m|FILE REV',
2506 )
2515 )
2507 def perfunidiff(ui, repo, file_, rev=None, count=None, **opts):
2516 def perfunidiff(ui, repo, file_, rev=None, count=None, **opts):
2508 """benchmark a unified diff between revisions
2517 """benchmark a unified diff between revisions
2509
2518
2510 This doesn't include any copy tracing - it's just a unified diff
2519 This doesn't include any copy tracing - it's just a unified diff
2511 of the texts.
2520 of the texts.
2512
2521
2513 By default, benchmark a diff between its delta parent and itself.
2522 By default, benchmark a diff between its delta parent and itself.
2514
2523
2515 With ``--count``, benchmark diffs between delta parents and self for N
2524 With ``--count``, benchmark diffs between delta parents and self for N
2516 revisions starting at the specified revision.
2525 revisions starting at the specified revision.
2517
2526
2518 With ``--alldata``, assume the requested revision is a changeset and
2527 With ``--alldata``, assume the requested revision is a changeset and
2519 measure diffs for all changes related to that changeset (manifest
2528 measure diffs for all changes related to that changeset (manifest
2520 and filelogs).
2529 and filelogs).
2521 """
2530 """
2522 opts = _byteskwargs(opts)
2531 opts = _byteskwargs(opts)
2523 if opts[b'alldata']:
2532 if opts[b'alldata']:
2524 opts[b'changelog'] = True
2533 opts[b'changelog'] = True
2525
2534
2526 if opts.get(b'changelog') or opts.get(b'manifest'):
2535 if opts.get(b'changelog') or opts.get(b'manifest'):
2527 file_, rev = None, file_
2536 file_, rev = None, file_
2528 elif rev is None:
2537 elif rev is None:
2529 raise error.CommandError(b'perfunidiff', b'invalid arguments')
2538 raise error.CommandError(b'perfunidiff', b'invalid arguments')
2530
2539
2531 textpairs = []
2540 textpairs = []
2532
2541
2533 r = cmdutil.openrevlog(repo, b'perfunidiff', file_, opts)
2542 r = cmdutil.openrevlog(repo, b'perfunidiff', file_, opts)
2534
2543
2535 startrev = r.rev(r.lookup(rev))
2544 startrev = r.rev(r.lookup(rev))
2536 for rev in range(startrev, min(startrev + count, len(r) - 1)):
2545 for rev in range(startrev, min(startrev + count, len(r) - 1)):
2537 if opts[b'alldata']:
2546 if opts[b'alldata']:
2538 # Load revisions associated with changeset.
2547 # Load revisions associated with changeset.
2539 ctx = repo[rev]
2548 ctx = repo[rev]
2540 mtext = _manifestrevision(repo, ctx.manifestnode())
2549 mtext = _manifestrevision(repo, ctx.manifestnode())
2541 for pctx in ctx.parents():
2550 for pctx in ctx.parents():
2542 pman = _manifestrevision(repo, pctx.manifestnode())
2551 pman = _manifestrevision(repo, pctx.manifestnode())
2543 textpairs.append((pman, mtext))
2552 textpairs.append((pman, mtext))
2544
2553
2545 # Load filelog revisions by iterating manifest delta.
2554 # Load filelog revisions by iterating manifest delta.
2546 man = ctx.manifest()
2555 man = ctx.manifest()
2547 pman = ctx.p1().manifest()
2556 pman = ctx.p1().manifest()
2548 for filename, change in pman.diff(man).items():
2557 for filename, change in pman.diff(man).items():
2549 fctx = repo.file(filename)
2558 fctx = repo.file(filename)
2550 f1 = fctx.revision(change[0][0] or -1)
2559 f1 = fctx.revision(change[0][0] or -1)
2551 f2 = fctx.revision(change[1][0] or -1)
2560 f2 = fctx.revision(change[1][0] or -1)
2552 textpairs.append((f1, f2))
2561 textpairs.append((f1, f2))
2553 else:
2562 else:
2554 dp = r.deltaparent(rev)
2563 dp = r.deltaparent(rev)
2555 textpairs.append((r.revision(dp), r.revision(rev)))
2564 textpairs.append((r.revision(dp), r.revision(rev)))
2556
2565
2557 def d():
2566 def d():
2558 for left, right in textpairs:
2567 for left, right in textpairs:
2559 # The date strings don't matter, so we pass empty strings.
2568 # The date strings don't matter, so we pass empty strings.
2560 headerlines, hunks = mdiff.unidiff(
2569 headerlines, hunks = mdiff.unidiff(
2561 left, b'', right, b'', b'left', b'right', binary=False
2570 left, b'', right, b'', b'left', b'right', binary=False
2562 )
2571 )
2563 # consume iterators in roughly the way patch.py does
2572 # consume iterators in roughly the way patch.py does
2564 b'\n'.join(headerlines)
2573 b'\n'.join(headerlines)
2565 b''.join(sum((list(hlines) for hrange, hlines in hunks), []))
2574 b''.join(sum((list(hlines) for hrange, hlines in hunks), []))
2566
2575
2567 timer, fm = gettimer(ui, opts)
2576 timer, fm = gettimer(ui, opts)
2568 timer(d)
2577 timer(d)
2569 fm.end()
2578 fm.end()
2570
2579
2571
2580
2572 @command(b'perf::diffwd|perfdiffwd', formatteropts)
2581 @command(b'perf::diffwd|perfdiffwd', formatteropts)
2573 def perfdiffwd(ui, repo, **opts):
2582 def perfdiffwd(ui, repo, **opts):
2574 """Profile diff of working directory changes"""
2583 """Profile diff of working directory changes"""
2575 opts = _byteskwargs(opts)
2584 opts = _byteskwargs(opts)
2576 timer, fm = gettimer(ui, opts)
2585 timer, fm = gettimer(ui, opts)
2577 options = {
2586 options = {
2578 'w': 'ignore_all_space',
2587 'w': 'ignore_all_space',
2579 'b': 'ignore_space_change',
2588 'b': 'ignore_space_change',
2580 'B': 'ignore_blank_lines',
2589 'B': 'ignore_blank_lines',
2581 }
2590 }
2582
2591
2583 for diffopt in ('', 'w', 'b', 'B', 'wB'):
2592 for diffopt in ('', 'w', 'b', 'B', 'wB'):
2584 opts = {options[c]: b'1' for c in diffopt}
2593 opts = {options[c]: b'1' for c in diffopt}
2585
2594
2586 def d():
2595 def d():
2587 ui.pushbuffer()
2596 ui.pushbuffer()
2588 commands.diff(ui, repo, **opts)
2597 commands.diff(ui, repo, **opts)
2589 ui.popbuffer()
2598 ui.popbuffer()
2590
2599
2591 diffopt = diffopt.encode('ascii')
2600 diffopt = diffopt.encode('ascii')
2592 title = b'diffopts: %s' % (diffopt and (b'-' + diffopt) or b'none')
2601 title = b'diffopts: %s' % (diffopt and (b'-' + diffopt) or b'none')
2593 timer(d, title=title)
2602 timer(d, title=title)
2594 fm.end()
2603 fm.end()
2595
2604
2596
2605
2597 @command(
2606 @command(
2598 b'perf::revlogindex|perfrevlogindex',
2607 b'perf::revlogindex|perfrevlogindex',
2599 revlogopts + formatteropts,
2608 revlogopts + formatteropts,
2600 b'-c|-m|FILE',
2609 b'-c|-m|FILE',
2601 )
2610 )
2602 def perfrevlogindex(ui, repo, file_=None, **opts):
2611 def perfrevlogindex(ui, repo, file_=None, **opts):
2603 """Benchmark operations against a revlog index.
2612 """Benchmark operations against a revlog index.
2604
2613
2605 This tests constructing a revlog instance, reading index data,
2614 This tests constructing a revlog instance, reading index data,
2606 parsing index data, and performing various operations related to
2615 parsing index data, and performing various operations related to
2607 index data.
2616 index data.
2608 """
2617 """
2609
2618
2610 opts = _byteskwargs(opts)
2619 opts = _byteskwargs(opts)
2611
2620
2612 rl = cmdutil.openrevlog(repo, b'perfrevlogindex', file_, opts)
2621 rl = cmdutil.openrevlog(repo, b'perfrevlogindex', file_, opts)
2613
2622
2614 opener = getattr(rl, 'opener') # trick linter
2623 opener = getattr(rl, 'opener') # trick linter
2615 # compat with hg <= 5.8
2624 # compat with hg <= 5.8
2616 radix = getattr(rl, 'radix', None)
2625 radix = getattr(rl, 'radix', None)
2617 indexfile = getattr(rl, '_indexfile', None)
2626 indexfile = getattr(rl, '_indexfile', None)
2618 if indexfile is None:
2627 if indexfile is None:
2619 # compatibility with <= hg-5.8
2628 # compatibility with <= hg-5.8
2620 indexfile = getattr(rl, 'indexfile')
2629 indexfile = getattr(rl, 'indexfile')
2621 data = opener.read(indexfile)
2630 data = opener.read(indexfile)
2622
2631
2623 header = struct.unpack(b'>I', data[0:4])[0]
2632 header = struct.unpack(b'>I', data[0:4])[0]
2624 version = header & 0xFFFF
2633 version = header & 0xFFFF
2625 if version == 1:
2634 if version == 1:
2626 inline = header & (1 << 16)
2635 inline = header & (1 << 16)
2627 else:
2636 else:
2628 raise error.Abort(b'unsupported revlog version: %d' % version)
2637 raise error.Abort(b'unsupported revlog version: %d' % version)
2629
2638
2630 parse_index_v1 = getattr(mercurial.revlog, 'parse_index_v1', None)
2639 parse_index_v1 = getattr(mercurial.revlog, 'parse_index_v1', None)
2631 if parse_index_v1 is None:
2640 if parse_index_v1 is None:
2632 parse_index_v1 = mercurial.revlog.revlogio().parseindex
2641 parse_index_v1 = mercurial.revlog.revlogio().parseindex
2633
2642
2634 rllen = len(rl)
2643 rllen = len(rl)
2635
2644
2636 node0 = rl.node(0)
2645 node0 = rl.node(0)
2637 node25 = rl.node(rllen // 4)
2646 node25 = rl.node(rllen // 4)
2638 node50 = rl.node(rllen // 2)
2647 node50 = rl.node(rllen // 2)
2639 node75 = rl.node(rllen // 4 * 3)
2648 node75 = rl.node(rllen // 4 * 3)
2640 node100 = rl.node(rllen - 1)
2649 node100 = rl.node(rllen - 1)
2641
2650
2642 allrevs = range(rllen)
2651 allrevs = range(rllen)
2643 allrevsrev = list(reversed(allrevs))
2652 allrevsrev = list(reversed(allrevs))
2644 allnodes = [rl.node(rev) for rev in range(rllen)]
2653 allnodes = [rl.node(rev) for rev in range(rllen)]
2645 allnodesrev = list(reversed(allnodes))
2654 allnodesrev = list(reversed(allnodes))
2646
2655
2647 def constructor():
2656 def constructor():
2648 if radix is not None:
2657 if radix is not None:
2649 revlog(opener, radix=radix)
2658 revlog(opener, radix=radix)
2650 else:
2659 else:
2651 # hg <= 5.8
2660 # hg <= 5.8
2652 revlog(opener, indexfile=indexfile)
2661 revlog(opener, indexfile=indexfile)
2653
2662
2654 def read():
2663 def read():
2655 with opener(indexfile) as fh:
2664 with opener(indexfile) as fh:
2656 fh.read()
2665 fh.read()
2657
2666
2658 def parseindex():
2667 def parseindex():
2659 parse_index_v1(data, inline)
2668 parse_index_v1(data, inline)
2660
2669
2661 def getentry(revornode):
2670 def getentry(revornode):
2662 index = parse_index_v1(data, inline)[0]
2671 index = parse_index_v1(data, inline)[0]
2663 index[revornode]
2672 index[revornode]
2664
2673
2665 def getentries(revs, count=1):
2674 def getentries(revs, count=1):
2666 index = parse_index_v1(data, inline)[0]
2675 index = parse_index_v1(data, inline)[0]
2667
2676
2668 for i in range(count):
2677 for i in range(count):
2669 for rev in revs:
2678 for rev in revs:
2670 index[rev]
2679 index[rev]
2671
2680
2672 def resolvenode(node):
2681 def resolvenode(node):
2673 index = parse_index_v1(data, inline)[0]
2682 index = parse_index_v1(data, inline)[0]
2674 rev = getattr(index, 'rev', None)
2683 rev = getattr(index, 'rev', None)
2675 if rev is None:
2684 if rev is None:
2676 nodemap = getattr(parse_index_v1(data, inline)[0], 'nodemap', None)
2685 nodemap = getattr(parse_index_v1(data, inline)[0], 'nodemap', None)
2677 # This only works for the C code.
2686 # This only works for the C code.
2678 if nodemap is None:
2687 if nodemap is None:
2679 return
2688 return
2680 rev = nodemap.__getitem__
2689 rev = nodemap.__getitem__
2681
2690
2682 try:
2691 try:
2683 rev(node)
2692 rev(node)
2684 except error.RevlogError:
2693 except error.RevlogError:
2685 pass
2694 pass
2686
2695
2687 def resolvenodes(nodes, count=1):
2696 def resolvenodes(nodes, count=1):
2688 index = parse_index_v1(data, inline)[0]
2697 index = parse_index_v1(data, inline)[0]
2689 rev = getattr(index, 'rev', None)
2698 rev = getattr(index, 'rev', None)
2690 if rev is None:
2699 if rev is None:
2691 nodemap = getattr(parse_index_v1(data, inline)[0], 'nodemap', None)
2700 nodemap = getattr(parse_index_v1(data, inline)[0], 'nodemap', None)
2692 # This only works for the C code.
2701 # This only works for the C code.
2693 if nodemap is None:
2702 if nodemap is None:
2694 return
2703 return
2695 rev = nodemap.__getitem__
2704 rev = nodemap.__getitem__
2696
2705
2697 for i in range(count):
2706 for i in range(count):
2698 for node in nodes:
2707 for node in nodes:
2699 try:
2708 try:
2700 rev(node)
2709 rev(node)
2701 except error.RevlogError:
2710 except error.RevlogError:
2702 pass
2711 pass
2703
2712
2704 benches = [
2713 benches = [
2705 (constructor, b'revlog constructor'),
2714 (constructor, b'revlog constructor'),
2706 (read, b'read'),
2715 (read, b'read'),
2707 (parseindex, b'create index object'),
2716 (parseindex, b'create index object'),
2708 (lambda: getentry(0), b'retrieve index entry for rev 0'),
2717 (lambda: getentry(0), b'retrieve index entry for rev 0'),
2709 (lambda: resolvenode(b'a' * 20), b'look up missing node'),
2718 (lambda: resolvenode(b'a' * 20), b'look up missing node'),
2710 (lambda: resolvenode(node0), b'look up node at rev 0'),
2719 (lambda: resolvenode(node0), b'look up node at rev 0'),
2711 (lambda: resolvenode(node25), b'look up node at 1/4 len'),
2720 (lambda: resolvenode(node25), b'look up node at 1/4 len'),
2712 (lambda: resolvenode(node50), b'look up node at 1/2 len'),
2721 (lambda: resolvenode(node50), b'look up node at 1/2 len'),
2713 (lambda: resolvenode(node75), b'look up node at 3/4 len'),
2722 (lambda: resolvenode(node75), b'look up node at 3/4 len'),
2714 (lambda: resolvenode(node100), b'look up node at tip'),
2723 (lambda: resolvenode(node100), b'look up node at tip'),
2715 # 2x variation is to measure caching impact.
2724 # 2x variation is to measure caching impact.
2716 (lambda: resolvenodes(allnodes), b'look up all nodes (forward)'),
2725 (lambda: resolvenodes(allnodes), b'look up all nodes (forward)'),
2717 (lambda: resolvenodes(allnodes, 2), b'look up all nodes 2x (forward)'),
2726 (lambda: resolvenodes(allnodes, 2), b'look up all nodes 2x (forward)'),
2718 (lambda: resolvenodes(allnodesrev), b'look up all nodes (reverse)'),
2727 (lambda: resolvenodes(allnodesrev), b'look up all nodes (reverse)'),
2719 (
2728 (
2720 lambda: resolvenodes(allnodesrev, 2),
2729 lambda: resolvenodes(allnodesrev, 2),
2721 b'look up all nodes 2x (reverse)',
2730 b'look up all nodes 2x (reverse)',
2722 ),
2731 ),
2723 (lambda: getentries(allrevs), b'retrieve all index entries (forward)'),
2732 (lambda: getentries(allrevs), b'retrieve all index entries (forward)'),
2724 (
2733 (
2725 lambda: getentries(allrevs, 2),
2734 lambda: getentries(allrevs, 2),
2726 b'retrieve all index entries 2x (forward)',
2735 b'retrieve all index entries 2x (forward)',
2727 ),
2736 ),
2728 (
2737 (
2729 lambda: getentries(allrevsrev),
2738 lambda: getentries(allrevsrev),
2730 b'retrieve all index entries (reverse)',
2739 b'retrieve all index entries (reverse)',
2731 ),
2740 ),
2732 (
2741 (
2733 lambda: getentries(allrevsrev, 2),
2742 lambda: getentries(allrevsrev, 2),
2734 b'retrieve all index entries 2x (reverse)',
2743 b'retrieve all index entries 2x (reverse)',
2735 ),
2744 ),
2736 ]
2745 ]
2737
2746
2738 for fn, title in benches:
2747 for fn, title in benches:
2739 timer, fm = gettimer(ui, opts)
2748 timer, fm = gettimer(ui, opts)
2740 timer(fn, title=title)
2749 timer(fn, title=title)
2741 fm.end()
2750 fm.end()
2742
2751
2743
2752
2744 @command(
2753 @command(
2745 b'perf::revlogrevisions|perfrevlogrevisions',
2754 b'perf::revlogrevisions|perfrevlogrevisions',
2746 revlogopts
2755 revlogopts
2747 + formatteropts
2756 + formatteropts
2748 + [
2757 + [
2749 (b'd', b'dist', 100, b'distance between the revisions'),
2758 (b'd', b'dist', 100, b'distance between the revisions'),
2750 (b's', b'startrev', 0, b'revision to start reading at'),
2759 (b's', b'startrev', 0, b'revision to start reading at'),
2751 (b'', b'reverse', False, b'read in reverse'),
2760 (b'', b'reverse', False, b'read in reverse'),
2752 ],
2761 ],
2753 b'-c|-m|FILE',
2762 b'-c|-m|FILE',
2754 )
2763 )
2755 def perfrevlogrevisions(
2764 def perfrevlogrevisions(
2756 ui, repo, file_=None, startrev=0, reverse=False, **opts
2765 ui, repo, file_=None, startrev=0, reverse=False, **opts
2757 ):
2766 ):
2758 """Benchmark reading a series of revisions from a revlog.
2767 """Benchmark reading a series of revisions from a revlog.
2759
2768
2760 By default, we read every ``-d/--dist`` revision from 0 to tip of
2769 By default, we read every ``-d/--dist`` revision from 0 to tip of
2761 the specified revlog.
2770 the specified revlog.
2762
2771
2763 The start revision can be defined via ``-s/--startrev``.
2772 The start revision can be defined via ``-s/--startrev``.
2764 """
2773 """
2765 opts = _byteskwargs(opts)
2774 opts = _byteskwargs(opts)
2766
2775
2767 rl = cmdutil.openrevlog(repo, b'perfrevlogrevisions', file_, opts)
2776 rl = cmdutil.openrevlog(repo, b'perfrevlogrevisions', file_, opts)
2768 rllen = getlen(ui)(rl)
2777 rllen = getlen(ui)(rl)
2769
2778
2770 if startrev < 0:
2779 if startrev < 0:
2771 startrev = rllen + startrev
2780 startrev = rllen + startrev
2772
2781
2773 def d():
2782 def d():
2774 rl.clearcaches()
2783 rl.clearcaches()
2775
2784
2776 beginrev = startrev
2785 beginrev = startrev
2777 endrev = rllen
2786 endrev = rllen
2778 dist = opts[b'dist']
2787 dist = opts[b'dist']
2779
2788
2780 if reverse:
2789 if reverse:
2781 beginrev, endrev = endrev - 1, beginrev - 1
2790 beginrev, endrev = endrev - 1, beginrev - 1
2782 dist = -1 * dist
2791 dist = -1 * dist
2783
2792
2784 for x in _xrange(beginrev, endrev, dist):
2793 for x in _xrange(beginrev, endrev, dist):
2785 # Old revisions don't support passing int.
2794 # Old revisions don't support passing int.
2786 n = rl.node(x)
2795 n = rl.node(x)
2787 rl.revision(n)
2796 rl.revision(n)
2788
2797
2789 timer, fm = gettimer(ui, opts)
2798 timer, fm = gettimer(ui, opts)
2790 timer(d)
2799 timer(d)
2791 fm.end()
2800 fm.end()
2792
2801
2793
2802
2794 @command(
2803 @command(
2795 b'perf::revlogwrite|perfrevlogwrite',
2804 b'perf::revlogwrite|perfrevlogwrite',
2796 revlogopts
2805 revlogopts
2797 + formatteropts
2806 + formatteropts
2798 + [
2807 + [
2799 (b's', b'startrev', 1000, b'revision to start writing at'),
2808 (b's', b'startrev', 1000, b'revision to start writing at'),
2800 (b'', b'stoprev', -1, b'last revision to write'),
2809 (b'', b'stoprev', -1, b'last revision to write'),
2801 (b'', b'count', 3, b'number of passes to perform'),
2810 (b'', b'count', 3, b'number of passes to perform'),
2802 (b'', b'details', False, b'print timing for every revisions tested'),
2811 (b'', b'details', False, b'print timing for every revisions tested'),
2803 (b'', b'source', b'full', b'the kind of data feed in the revlog'),
2812 (b'', b'source', b'full', b'the kind of data feed in the revlog'),
2804 (b'', b'lazydeltabase', True, b'try the provided delta first'),
2813 (b'', b'lazydeltabase', True, b'try the provided delta first'),
2805 (b'', b'clear-caches', True, b'clear revlog cache between calls'),
2814 (b'', b'clear-caches', True, b'clear revlog cache between calls'),
2806 ],
2815 ],
2807 b'-c|-m|FILE',
2816 b'-c|-m|FILE',
2808 )
2817 )
2809 def perfrevlogwrite(ui, repo, file_=None, startrev=1000, stoprev=-1, **opts):
2818 def perfrevlogwrite(ui, repo, file_=None, startrev=1000, stoprev=-1, **opts):
2810 """Benchmark writing a series of revisions to a revlog.
2819 """Benchmark writing a series of revisions to a revlog.
2811
2820
2812 Possible source values are:
2821 Possible source values are:
2813 * `full`: add from a full text (default).
2822 * `full`: add from a full text (default).
2814 * `parent-1`: add from a delta to the first parent
2823 * `parent-1`: add from a delta to the first parent
2815 * `parent-2`: add from a delta to the second parent if it exists
2824 * `parent-2`: add from a delta to the second parent if it exists
2816 (use a delta from the first parent otherwise)
2825 (use a delta from the first parent otherwise)
2817 * `parent-smallest`: add from the smallest delta (either p1 or p2)
2826 * `parent-smallest`: add from the smallest delta (either p1 or p2)
2818 * `storage`: add from the existing precomputed deltas
2827 * `storage`: add from the existing precomputed deltas
2819
2828
2820 Note: This performance command measures performance in a custom way. As a
2829 Note: This performance command measures performance in a custom way. As a
2821 result some of the global configuration of the 'perf' command does not
2830 result some of the global configuration of the 'perf' command does not
2822 apply to it:
2831 apply to it:
2823
2832
2824 * ``pre-run``: disabled
2833 * ``pre-run``: disabled
2825
2834
2826 * ``profile-benchmark``: disabled
2835 * ``profile-benchmark``: disabled
2827
2836
2828 * ``run-limits``: disabled use --count instead
2837 * ``run-limits``: disabled use --count instead
2829 """
2838 """
2830 opts = _byteskwargs(opts)
2839 opts = _byteskwargs(opts)
2831
2840
2832 rl = cmdutil.openrevlog(repo, b'perfrevlogwrite', file_, opts)
2841 rl = cmdutil.openrevlog(repo, b'perfrevlogwrite', file_, opts)
2833 rllen = getlen(ui)(rl)
2842 rllen = getlen(ui)(rl)
2834 if startrev < 0:
2843 if startrev < 0:
2835 startrev = rllen + startrev
2844 startrev = rllen + startrev
2836 if stoprev < 0:
2845 if stoprev < 0:
2837 stoprev = rllen + stoprev
2846 stoprev = rllen + stoprev
2838
2847
2839 lazydeltabase = opts['lazydeltabase']
2848 lazydeltabase = opts['lazydeltabase']
2840 source = opts['source']
2849 source = opts['source']
2841 clearcaches = opts['clear_caches']
2850 clearcaches = opts['clear_caches']
2842 validsource = (
2851 validsource = (
2843 b'full',
2852 b'full',
2844 b'parent-1',
2853 b'parent-1',
2845 b'parent-2',
2854 b'parent-2',
2846 b'parent-smallest',
2855 b'parent-smallest',
2847 b'storage',
2856 b'storage',
2848 )
2857 )
2849 if source not in validsource:
2858 if source not in validsource:
2850 raise error.Abort('invalid source type: %s' % source)
2859 raise error.Abort('invalid source type: %s' % source)
2851
2860
2852 ### actually gather results
2861 ### actually gather results
2853 count = opts['count']
2862 count = opts['count']
2854 if count <= 0:
2863 if count <= 0:
2855 raise error.Abort('invalide run count: %d' % count)
2864 raise error.Abort('invalide run count: %d' % count)
2856 allresults = []
2865 allresults = []
2857 for c in range(count):
2866 for c in range(count):
2858 timing = _timeonewrite(
2867 timing = _timeonewrite(
2859 ui,
2868 ui,
2860 rl,
2869 rl,
2861 source,
2870 source,
2862 startrev,
2871 startrev,
2863 stoprev,
2872 stoprev,
2864 c + 1,
2873 c + 1,
2865 lazydeltabase=lazydeltabase,
2874 lazydeltabase=lazydeltabase,
2866 clearcaches=clearcaches,
2875 clearcaches=clearcaches,
2867 )
2876 )
2868 allresults.append(timing)
2877 allresults.append(timing)
2869
2878
2870 ### consolidate the results in a single list
2879 ### consolidate the results in a single list
2871 results = []
2880 results = []
2872 for idx, (rev, t) in enumerate(allresults[0]):
2881 for idx, (rev, t) in enumerate(allresults[0]):
2873 ts = [t]
2882 ts = [t]
2874 for other in allresults[1:]:
2883 for other in allresults[1:]:
2875 orev, ot = other[idx]
2884 orev, ot = other[idx]
2876 assert orev == rev
2885 assert orev == rev
2877 ts.append(ot)
2886 ts.append(ot)
2878 results.append((rev, ts))
2887 results.append((rev, ts))
2879 resultcount = len(results)
2888 resultcount = len(results)
2880
2889
2881 ### Compute and display relevant statistics
2890 ### Compute and display relevant statistics
2882
2891
2883 # get a formatter
2892 # get a formatter
2884 fm = ui.formatter(b'perf', opts)
2893 fm = ui.formatter(b'perf', opts)
2885 displayall = ui.configbool(b"perf", b"all-timing", False)
2894 displayall = ui.configbool(b"perf", b"all-timing", False)
2886
2895
2887 # print individual details if requested
2896 # print individual details if requested
2888 if opts['details']:
2897 if opts['details']:
2889 for idx, item in enumerate(results, 1):
2898 for idx, item in enumerate(results, 1):
2890 rev, data = item
2899 rev, data = item
2891 title = 'revisions #%d of %d, rev %d' % (idx, resultcount, rev)
2900 title = 'revisions #%d of %d, rev %d' % (idx, resultcount, rev)
2892 formatone(fm, data, title=title, displayall=displayall)
2901 formatone(fm, data, title=title, displayall=displayall)
2893
2902
2894 # sorts results by median time
2903 # sorts results by median time
2895 results.sort(key=lambda x: sorted(x[1])[len(x[1]) // 2])
2904 results.sort(key=lambda x: sorted(x[1])[len(x[1]) // 2])
2896 # list of (name, index) to display)
2905 # list of (name, index) to display)
2897 relevants = [
2906 relevants = [
2898 ("min", 0),
2907 ("min", 0),
2899 ("10%", resultcount * 10 // 100),
2908 ("10%", resultcount * 10 // 100),
2900 ("25%", resultcount * 25 // 100),
2909 ("25%", resultcount * 25 // 100),
2901 ("50%", resultcount * 70 // 100),
2910 ("50%", resultcount * 70 // 100),
2902 ("75%", resultcount * 75 // 100),
2911 ("75%", resultcount * 75 // 100),
2903 ("90%", resultcount * 90 // 100),
2912 ("90%", resultcount * 90 // 100),
2904 ("95%", resultcount * 95 // 100),
2913 ("95%", resultcount * 95 // 100),
2905 ("99%", resultcount * 99 // 100),
2914 ("99%", resultcount * 99 // 100),
2906 ("99.9%", resultcount * 999 // 1000),
2915 ("99.9%", resultcount * 999 // 1000),
2907 ("99.99%", resultcount * 9999 // 10000),
2916 ("99.99%", resultcount * 9999 // 10000),
2908 ("99.999%", resultcount * 99999 // 100000),
2917 ("99.999%", resultcount * 99999 // 100000),
2909 ("max", -1),
2918 ("max", -1),
2910 ]
2919 ]
2911 if not ui.quiet:
2920 if not ui.quiet:
2912 for name, idx in relevants:
2921 for name, idx in relevants:
2913 data = results[idx]
2922 data = results[idx]
2914 title = '%s of %d, rev %d' % (name, resultcount, data[0])
2923 title = '%s of %d, rev %d' % (name, resultcount, data[0])
2915 formatone(fm, data[1], title=title, displayall=displayall)
2924 formatone(fm, data[1], title=title, displayall=displayall)
2916
2925
2917 # XXX summing that many float will not be very precise, we ignore this fact
2926 # XXX summing that many float will not be very precise, we ignore this fact
2918 # for now
2927 # for now
2919 totaltime = []
2928 totaltime = []
2920 for item in allresults:
2929 for item in allresults:
2921 totaltime.append(
2930 totaltime.append(
2922 (
2931 (
2923 sum(x[1][0] for x in item),
2932 sum(x[1][0] for x in item),
2924 sum(x[1][1] for x in item),
2933 sum(x[1][1] for x in item),
2925 sum(x[1][2] for x in item),
2934 sum(x[1][2] for x in item),
2926 )
2935 )
2927 )
2936 )
2928 formatone(
2937 formatone(
2929 fm,
2938 fm,
2930 totaltime,
2939 totaltime,
2931 title="total time (%d revs)" % resultcount,
2940 title="total time (%d revs)" % resultcount,
2932 displayall=displayall,
2941 displayall=displayall,
2933 )
2942 )
2934 fm.end()
2943 fm.end()
2935
2944
2936
2945
2937 class _faketr(object):
2946 class _faketr(object):
2938 def add(s, x, y, z=None):
2947 def add(s, x, y, z=None):
2939 return None
2948 return None
2940
2949
2941
2950
2942 def _timeonewrite(
2951 def _timeonewrite(
2943 ui,
2952 ui,
2944 orig,
2953 orig,
2945 source,
2954 source,
2946 startrev,
2955 startrev,
2947 stoprev,
2956 stoprev,
2948 runidx=None,
2957 runidx=None,
2949 lazydeltabase=True,
2958 lazydeltabase=True,
2950 clearcaches=True,
2959 clearcaches=True,
2951 ):
2960 ):
2952 timings = []
2961 timings = []
2953 tr = _faketr()
2962 tr = _faketr()
2954 with _temprevlog(ui, orig, startrev) as dest:
2963 with _temprevlog(ui, orig, startrev) as dest:
2955 dest._lazydeltabase = lazydeltabase
2964 dest._lazydeltabase = lazydeltabase
2956 revs = list(orig.revs(startrev, stoprev))
2965 revs = list(orig.revs(startrev, stoprev))
2957 total = len(revs)
2966 total = len(revs)
2958 topic = 'adding'
2967 topic = 'adding'
2959 if runidx is not None:
2968 if runidx is not None:
2960 topic += ' (run #%d)' % runidx
2969 topic += ' (run #%d)' % runidx
2961 # Support both old and new progress API
2970 # Support both old and new progress API
2962 if util.safehasattr(ui, 'makeprogress'):
2971 if util.safehasattr(ui, 'makeprogress'):
2963 progress = ui.makeprogress(topic, unit='revs', total=total)
2972 progress = ui.makeprogress(topic, unit='revs', total=total)
2964
2973
2965 def updateprogress(pos):
2974 def updateprogress(pos):
2966 progress.update(pos)
2975 progress.update(pos)
2967
2976
2968 def completeprogress():
2977 def completeprogress():
2969 progress.complete()
2978 progress.complete()
2970
2979
2971 else:
2980 else:
2972
2981
2973 def updateprogress(pos):
2982 def updateprogress(pos):
2974 ui.progress(topic, pos, unit='revs', total=total)
2983 ui.progress(topic, pos, unit='revs', total=total)
2975
2984
2976 def completeprogress():
2985 def completeprogress():
2977 ui.progress(topic, None, unit='revs', total=total)
2986 ui.progress(topic, None, unit='revs', total=total)
2978
2987
2979 for idx, rev in enumerate(revs):
2988 for idx, rev in enumerate(revs):
2980 updateprogress(idx)
2989 updateprogress(idx)
2981 addargs, addkwargs = _getrevisionseed(orig, rev, tr, source)
2990 addargs, addkwargs = _getrevisionseed(orig, rev, tr, source)
2982 if clearcaches:
2991 if clearcaches:
2983 dest.index.clearcaches()
2992 dest.index.clearcaches()
2984 dest.clearcaches()
2993 dest.clearcaches()
2985 with timeone() as r:
2994 with timeone() as r:
2986 dest.addrawrevision(*addargs, **addkwargs)
2995 dest.addrawrevision(*addargs, **addkwargs)
2987 timings.append((rev, r[0]))
2996 timings.append((rev, r[0]))
2988 updateprogress(total)
2997 updateprogress(total)
2989 completeprogress()
2998 completeprogress()
2990 return timings
2999 return timings
2991
3000
2992
3001
2993 def _getrevisionseed(orig, rev, tr, source):
3002 def _getrevisionseed(orig, rev, tr, source):
2994 from mercurial.node import nullid
3003 from mercurial.node import nullid
2995
3004
2996 linkrev = orig.linkrev(rev)
3005 linkrev = orig.linkrev(rev)
2997 node = orig.node(rev)
3006 node = orig.node(rev)
2998 p1, p2 = orig.parents(node)
3007 p1, p2 = orig.parents(node)
2999 flags = orig.flags(rev)
3008 flags = orig.flags(rev)
3000 cachedelta = None
3009 cachedelta = None
3001 text = None
3010 text = None
3002
3011
3003 if source == b'full':
3012 if source == b'full':
3004 text = orig.revision(rev)
3013 text = orig.revision(rev)
3005 elif source == b'parent-1':
3014 elif source == b'parent-1':
3006 baserev = orig.rev(p1)
3015 baserev = orig.rev(p1)
3007 cachedelta = (baserev, orig.revdiff(p1, rev))
3016 cachedelta = (baserev, orig.revdiff(p1, rev))
3008 elif source == b'parent-2':
3017 elif source == b'parent-2':
3009 parent = p2
3018 parent = p2
3010 if p2 == nullid:
3019 if p2 == nullid:
3011 parent = p1
3020 parent = p1
3012 baserev = orig.rev(parent)
3021 baserev = orig.rev(parent)
3013 cachedelta = (baserev, orig.revdiff(parent, rev))
3022 cachedelta = (baserev, orig.revdiff(parent, rev))
3014 elif source == b'parent-smallest':
3023 elif source == b'parent-smallest':
3015 p1diff = orig.revdiff(p1, rev)
3024 p1diff = orig.revdiff(p1, rev)
3016 parent = p1
3025 parent = p1
3017 diff = p1diff
3026 diff = p1diff
3018 if p2 != nullid:
3027 if p2 != nullid:
3019 p2diff = orig.revdiff(p2, rev)
3028 p2diff = orig.revdiff(p2, rev)
3020 if len(p1diff) > len(p2diff):
3029 if len(p1diff) > len(p2diff):
3021 parent = p2
3030 parent = p2
3022 diff = p2diff
3031 diff = p2diff
3023 baserev = orig.rev(parent)
3032 baserev = orig.rev(parent)
3024 cachedelta = (baserev, diff)
3033 cachedelta = (baserev, diff)
3025 elif source == b'storage':
3034 elif source == b'storage':
3026 baserev = orig.deltaparent(rev)
3035 baserev = orig.deltaparent(rev)
3027 cachedelta = (baserev, orig.revdiff(orig.node(baserev), rev))
3036 cachedelta = (baserev, orig.revdiff(orig.node(baserev), rev))
3028
3037
3029 return (
3038 return (
3030 (text, tr, linkrev, p1, p2),
3039 (text, tr, linkrev, p1, p2),
3031 {'node': node, 'flags': flags, 'cachedelta': cachedelta},
3040 {'node': node, 'flags': flags, 'cachedelta': cachedelta},
3032 )
3041 )
3033
3042
3034
3043
3035 @contextlib.contextmanager
3044 @contextlib.contextmanager
3036 def _temprevlog(ui, orig, truncaterev):
3045 def _temprevlog(ui, orig, truncaterev):
3037 from mercurial import vfs as vfsmod
3046 from mercurial import vfs as vfsmod
3038
3047
3039 if orig._inline:
3048 if orig._inline:
3040 raise error.Abort('not supporting inline revlog (yet)')
3049 raise error.Abort('not supporting inline revlog (yet)')
3041 revlogkwargs = {}
3050 revlogkwargs = {}
3042 k = 'upperboundcomp'
3051 k = 'upperboundcomp'
3043 if util.safehasattr(orig, k):
3052 if util.safehasattr(orig, k):
3044 revlogkwargs[k] = getattr(orig, k)
3053 revlogkwargs[k] = getattr(orig, k)
3045
3054
3046 indexfile = getattr(orig, '_indexfile', None)
3055 indexfile = getattr(orig, '_indexfile', None)
3047 if indexfile is None:
3056 if indexfile is None:
3048 # compatibility with <= hg-5.8
3057 # compatibility with <= hg-5.8
3049 indexfile = getattr(orig, 'indexfile')
3058 indexfile = getattr(orig, 'indexfile')
3050 origindexpath = orig.opener.join(indexfile)
3059 origindexpath = orig.opener.join(indexfile)
3051
3060
3052 datafile = getattr(orig, '_datafile', getattr(orig, 'datafile'))
3061 datafile = getattr(orig, '_datafile', getattr(orig, 'datafile'))
3053 origdatapath = orig.opener.join(datafile)
3062 origdatapath = orig.opener.join(datafile)
3054 radix = b'revlog'
3063 radix = b'revlog'
3055 indexname = b'revlog.i'
3064 indexname = b'revlog.i'
3056 dataname = b'revlog.d'
3065 dataname = b'revlog.d'
3057
3066
3058 tmpdir = tempfile.mkdtemp(prefix='tmp-hgperf-')
3067 tmpdir = tempfile.mkdtemp(prefix='tmp-hgperf-')
3059 try:
3068 try:
3060 # copy the data file in a temporary directory
3069 # copy the data file in a temporary directory
3061 ui.debug('copying data in %s\n' % tmpdir)
3070 ui.debug('copying data in %s\n' % tmpdir)
3062 destindexpath = os.path.join(tmpdir, 'revlog.i')
3071 destindexpath = os.path.join(tmpdir, 'revlog.i')
3063 destdatapath = os.path.join(tmpdir, 'revlog.d')
3072 destdatapath = os.path.join(tmpdir, 'revlog.d')
3064 shutil.copyfile(origindexpath, destindexpath)
3073 shutil.copyfile(origindexpath, destindexpath)
3065 shutil.copyfile(origdatapath, destdatapath)
3074 shutil.copyfile(origdatapath, destdatapath)
3066
3075
3067 # remove the data we want to add again
3076 # remove the data we want to add again
3068 ui.debug('truncating data to be rewritten\n')
3077 ui.debug('truncating data to be rewritten\n')
3069 with open(destindexpath, 'ab') as index:
3078 with open(destindexpath, 'ab') as index:
3070 index.seek(0)
3079 index.seek(0)
3071 index.truncate(truncaterev * orig._io.size)
3080 index.truncate(truncaterev * orig._io.size)
3072 with open(destdatapath, 'ab') as data:
3081 with open(destdatapath, 'ab') as data:
3073 data.seek(0)
3082 data.seek(0)
3074 data.truncate(orig.start(truncaterev))
3083 data.truncate(orig.start(truncaterev))
3075
3084
3076 # instantiate a new revlog from the temporary copy
3085 # instantiate a new revlog from the temporary copy
3077 ui.debug('truncating adding to be rewritten\n')
3086 ui.debug('truncating adding to be rewritten\n')
3078 vfs = vfsmod.vfs(tmpdir)
3087 vfs = vfsmod.vfs(tmpdir)
3079 vfs.options = getattr(orig.opener, 'options', None)
3088 vfs.options = getattr(orig.opener, 'options', None)
3080
3089
3081 try:
3090 try:
3082 dest = revlog(vfs, radix=radix, **revlogkwargs)
3091 dest = revlog(vfs, radix=radix, **revlogkwargs)
3083 except TypeError:
3092 except TypeError:
3084 dest = revlog(
3093 dest = revlog(
3085 vfs, indexfile=indexname, datafile=dataname, **revlogkwargs
3094 vfs, indexfile=indexname, datafile=dataname, **revlogkwargs
3086 )
3095 )
3087 if dest._inline:
3096 if dest._inline:
3088 raise error.Abort('not supporting inline revlog (yet)')
3097 raise error.Abort('not supporting inline revlog (yet)')
3089 # make sure internals are initialized
3098 # make sure internals are initialized
3090 dest.revision(len(dest) - 1)
3099 dest.revision(len(dest) - 1)
3091 yield dest
3100 yield dest
3092 del dest, vfs
3101 del dest, vfs
3093 finally:
3102 finally:
3094 shutil.rmtree(tmpdir, True)
3103 shutil.rmtree(tmpdir, True)
3095
3104
3096
3105
3097 @command(
3106 @command(
3098 b'perf::revlogchunks|perfrevlogchunks',
3107 b'perf::revlogchunks|perfrevlogchunks',
3099 revlogopts
3108 revlogopts
3100 + formatteropts
3109 + formatteropts
3101 + [
3110 + [
3102 (b'e', b'engines', b'', b'compression engines to use'),
3111 (b'e', b'engines', b'', b'compression engines to use'),
3103 (b's', b'startrev', 0, b'revision to start at'),
3112 (b's', b'startrev', 0, b'revision to start at'),
3104 ],
3113 ],
3105 b'-c|-m|FILE',
3114 b'-c|-m|FILE',
3106 )
3115 )
3107 def perfrevlogchunks(ui, repo, file_=None, engines=None, startrev=0, **opts):
3116 def perfrevlogchunks(ui, repo, file_=None, engines=None, startrev=0, **opts):
3108 """Benchmark operations on revlog chunks.
3117 """Benchmark operations on revlog chunks.
3109
3118
3110 Logically, each revlog is a collection of fulltext revisions. However,
3119 Logically, each revlog is a collection of fulltext revisions. However,
3111 stored within each revlog are "chunks" of possibly compressed data. This
3120 stored within each revlog are "chunks" of possibly compressed data. This
3112 data needs to be read and decompressed or compressed and written.
3121 data needs to be read and decompressed or compressed and written.
3113
3122
3114 This command measures the time it takes to read+decompress and recompress
3123 This command measures the time it takes to read+decompress and recompress
3115 chunks in a revlog. It effectively isolates I/O and compression performance.
3124 chunks in a revlog. It effectively isolates I/O and compression performance.
3116 For measurements of higher-level operations like resolving revisions,
3125 For measurements of higher-level operations like resolving revisions,
3117 see ``perfrevlogrevisions`` and ``perfrevlogrevision``.
3126 see ``perfrevlogrevisions`` and ``perfrevlogrevision``.
3118 """
3127 """
3119 opts = _byteskwargs(opts)
3128 opts = _byteskwargs(opts)
3120
3129
3121 rl = cmdutil.openrevlog(repo, b'perfrevlogchunks', file_, opts)
3130 rl = cmdutil.openrevlog(repo, b'perfrevlogchunks', file_, opts)
3122
3131
3123 # _chunkraw was renamed to _getsegmentforrevs.
3132 # _chunkraw was renamed to _getsegmentforrevs.
3124 try:
3133 try:
3125 segmentforrevs = rl._getsegmentforrevs
3134 segmentforrevs = rl._getsegmentforrevs
3126 except AttributeError:
3135 except AttributeError:
3127 segmentforrevs = rl._chunkraw
3136 segmentforrevs = rl._chunkraw
3128
3137
3129 # Verify engines argument.
3138 # Verify engines argument.
3130 if engines:
3139 if engines:
3131 engines = {e.strip() for e in engines.split(b',')}
3140 engines = {e.strip() for e in engines.split(b',')}
3132 for engine in engines:
3141 for engine in engines:
3133 try:
3142 try:
3134 util.compressionengines[engine]
3143 util.compressionengines[engine]
3135 except KeyError:
3144 except KeyError:
3136 raise error.Abort(b'unknown compression engine: %s' % engine)
3145 raise error.Abort(b'unknown compression engine: %s' % engine)
3137 else:
3146 else:
3138 engines = []
3147 engines = []
3139 for e in util.compengines:
3148 for e in util.compengines:
3140 engine = util.compengines[e]
3149 engine = util.compengines[e]
3141 try:
3150 try:
3142 if engine.available():
3151 if engine.available():
3143 engine.revlogcompressor().compress(b'dummy')
3152 engine.revlogcompressor().compress(b'dummy')
3144 engines.append(e)
3153 engines.append(e)
3145 except NotImplementedError:
3154 except NotImplementedError:
3146 pass
3155 pass
3147
3156
3148 revs = list(rl.revs(startrev, len(rl) - 1))
3157 revs = list(rl.revs(startrev, len(rl) - 1))
3149
3158
3150 def rlfh(rl):
3159 def rlfh(rl):
3151 if rl._inline:
3160 if rl._inline:
3152 indexfile = getattr(rl, '_indexfile', None)
3161 indexfile = getattr(rl, '_indexfile', None)
3153 if indexfile is None:
3162 if indexfile is None:
3154 # compatibility with <= hg-5.8
3163 # compatibility with <= hg-5.8
3155 indexfile = getattr(rl, 'indexfile')
3164 indexfile = getattr(rl, 'indexfile')
3156 return getsvfs(repo)(indexfile)
3165 return getsvfs(repo)(indexfile)
3157 else:
3166 else:
3158 datafile = getattr(rl, 'datafile', getattr(rl, 'datafile'))
3167 datafile = getattr(rl, 'datafile', getattr(rl, 'datafile'))
3159 return getsvfs(repo)(datafile)
3168 return getsvfs(repo)(datafile)
3160
3169
3161 def doread():
3170 def doread():
3162 rl.clearcaches()
3171 rl.clearcaches()
3163 for rev in revs:
3172 for rev in revs:
3164 segmentforrevs(rev, rev)
3173 segmentforrevs(rev, rev)
3165
3174
3166 def doreadcachedfh():
3175 def doreadcachedfh():
3167 rl.clearcaches()
3176 rl.clearcaches()
3168 fh = rlfh(rl)
3177 fh = rlfh(rl)
3169 for rev in revs:
3178 for rev in revs:
3170 segmentforrevs(rev, rev, df=fh)
3179 segmentforrevs(rev, rev, df=fh)
3171
3180
3172 def doreadbatch():
3181 def doreadbatch():
3173 rl.clearcaches()
3182 rl.clearcaches()
3174 segmentforrevs(revs[0], revs[-1])
3183 segmentforrevs(revs[0], revs[-1])
3175
3184
3176 def doreadbatchcachedfh():
3185 def doreadbatchcachedfh():
3177 rl.clearcaches()
3186 rl.clearcaches()
3178 fh = rlfh(rl)
3187 fh = rlfh(rl)
3179 segmentforrevs(revs[0], revs[-1], df=fh)
3188 segmentforrevs(revs[0], revs[-1], df=fh)
3180
3189
3181 def dochunk():
3190 def dochunk():
3182 rl.clearcaches()
3191 rl.clearcaches()
3183 fh = rlfh(rl)
3192 fh = rlfh(rl)
3184 for rev in revs:
3193 for rev in revs:
3185 rl._chunk(rev, df=fh)
3194 rl._chunk(rev, df=fh)
3186
3195
3187 chunks = [None]
3196 chunks = [None]
3188
3197
3189 def dochunkbatch():
3198 def dochunkbatch():
3190 rl.clearcaches()
3199 rl.clearcaches()
3191 fh = rlfh(rl)
3200 fh = rlfh(rl)
3192 # Save chunks as a side-effect.
3201 # Save chunks as a side-effect.
3193 chunks[0] = rl._chunks(revs, df=fh)
3202 chunks[0] = rl._chunks(revs, df=fh)
3194
3203
3195 def docompress(compressor):
3204 def docompress(compressor):
3196 rl.clearcaches()
3205 rl.clearcaches()
3197
3206
3198 try:
3207 try:
3199 # Swap in the requested compression engine.
3208 # Swap in the requested compression engine.
3200 oldcompressor = rl._compressor
3209 oldcompressor = rl._compressor
3201 rl._compressor = compressor
3210 rl._compressor = compressor
3202 for chunk in chunks[0]:
3211 for chunk in chunks[0]:
3203 rl.compress(chunk)
3212 rl.compress(chunk)
3204 finally:
3213 finally:
3205 rl._compressor = oldcompressor
3214 rl._compressor = oldcompressor
3206
3215
3207 benches = [
3216 benches = [
3208 (lambda: doread(), b'read'),
3217 (lambda: doread(), b'read'),
3209 (lambda: doreadcachedfh(), b'read w/ reused fd'),
3218 (lambda: doreadcachedfh(), b'read w/ reused fd'),
3210 (lambda: doreadbatch(), b'read batch'),
3219 (lambda: doreadbatch(), b'read batch'),
3211 (lambda: doreadbatchcachedfh(), b'read batch w/ reused fd'),
3220 (lambda: doreadbatchcachedfh(), b'read batch w/ reused fd'),
3212 (lambda: dochunk(), b'chunk'),
3221 (lambda: dochunk(), b'chunk'),
3213 (lambda: dochunkbatch(), b'chunk batch'),
3222 (lambda: dochunkbatch(), b'chunk batch'),
3214 ]
3223 ]
3215
3224
3216 for engine in sorted(engines):
3225 for engine in sorted(engines):
3217 compressor = util.compengines[engine].revlogcompressor()
3226 compressor = util.compengines[engine].revlogcompressor()
3218 benches.append(
3227 benches.append(
3219 (
3228 (
3220 functools.partial(docompress, compressor),
3229 functools.partial(docompress, compressor),
3221 b'compress w/ %s' % engine,
3230 b'compress w/ %s' % engine,
3222 )
3231 )
3223 )
3232 )
3224
3233
3225 for fn, title in benches:
3234 for fn, title in benches:
3226 timer, fm = gettimer(ui, opts)
3235 timer, fm = gettimer(ui, opts)
3227 timer(fn, title=title)
3236 timer(fn, title=title)
3228 fm.end()
3237 fm.end()
3229
3238
3230
3239
3231 @command(
3240 @command(
3232 b'perf::revlogrevision|perfrevlogrevision',
3241 b'perf::revlogrevision|perfrevlogrevision',
3233 revlogopts
3242 revlogopts
3234 + formatteropts
3243 + formatteropts
3235 + [(b'', b'cache', False, b'use caches instead of clearing')],
3244 + [(b'', b'cache', False, b'use caches instead of clearing')],
3236 b'-c|-m|FILE REV',
3245 b'-c|-m|FILE REV',
3237 )
3246 )
3238 def perfrevlogrevision(ui, repo, file_, rev=None, cache=None, **opts):
3247 def perfrevlogrevision(ui, repo, file_, rev=None, cache=None, **opts):
3239 """Benchmark obtaining a revlog revision.
3248 """Benchmark obtaining a revlog revision.
3240
3249
3241 Obtaining a revlog revision consists of roughly the following steps:
3250 Obtaining a revlog revision consists of roughly the following steps:
3242
3251
3243 1. Compute the delta chain
3252 1. Compute the delta chain
3244 2. Slice the delta chain if applicable
3253 2. Slice the delta chain if applicable
3245 3. Obtain the raw chunks for that delta chain
3254 3. Obtain the raw chunks for that delta chain
3246 4. Decompress each raw chunk
3255 4. Decompress each raw chunk
3247 5. Apply binary patches to obtain fulltext
3256 5. Apply binary patches to obtain fulltext
3248 6. Verify hash of fulltext
3257 6. Verify hash of fulltext
3249
3258
3250 This command measures the time spent in each of these phases.
3259 This command measures the time spent in each of these phases.
3251 """
3260 """
3252 opts = _byteskwargs(opts)
3261 opts = _byteskwargs(opts)
3253
3262
3254 if opts.get(b'changelog') or opts.get(b'manifest'):
3263 if opts.get(b'changelog') or opts.get(b'manifest'):
3255 file_, rev = None, file_
3264 file_, rev = None, file_
3256 elif rev is None:
3265 elif rev is None:
3257 raise error.CommandError(b'perfrevlogrevision', b'invalid arguments')
3266 raise error.CommandError(b'perfrevlogrevision', b'invalid arguments')
3258
3267
3259 r = cmdutil.openrevlog(repo, b'perfrevlogrevision', file_, opts)
3268 r = cmdutil.openrevlog(repo, b'perfrevlogrevision', file_, opts)
3260
3269
3261 # _chunkraw was renamed to _getsegmentforrevs.
3270 # _chunkraw was renamed to _getsegmentforrevs.
3262 try:
3271 try:
3263 segmentforrevs = r._getsegmentforrevs
3272 segmentforrevs = r._getsegmentforrevs
3264 except AttributeError:
3273 except AttributeError:
3265 segmentforrevs = r._chunkraw
3274 segmentforrevs = r._chunkraw
3266
3275
3267 node = r.lookup(rev)
3276 node = r.lookup(rev)
3268 rev = r.rev(node)
3277 rev = r.rev(node)
3269
3278
3270 def getrawchunks(data, chain):
3279 def getrawchunks(data, chain):
3271 start = r.start
3280 start = r.start
3272 length = r.length
3281 length = r.length
3273 inline = r._inline
3282 inline = r._inline
3274 try:
3283 try:
3275 iosize = r.index.entry_size
3284 iosize = r.index.entry_size
3276 except AttributeError:
3285 except AttributeError:
3277 iosize = r._io.size
3286 iosize = r._io.size
3278 buffer = util.buffer
3287 buffer = util.buffer
3279
3288
3280 chunks = []
3289 chunks = []
3281 ladd = chunks.append
3290 ladd = chunks.append
3282 for idx, item in enumerate(chain):
3291 for idx, item in enumerate(chain):
3283 offset = start(item[0])
3292 offset = start(item[0])
3284 bits = data[idx]
3293 bits = data[idx]
3285 for rev in item:
3294 for rev in item:
3286 chunkstart = start(rev)
3295 chunkstart = start(rev)
3287 if inline:
3296 if inline:
3288 chunkstart += (rev + 1) * iosize
3297 chunkstart += (rev + 1) * iosize
3289 chunklength = length(rev)
3298 chunklength = length(rev)
3290 ladd(buffer(bits, chunkstart - offset, chunklength))
3299 ladd(buffer(bits, chunkstart - offset, chunklength))
3291
3300
3292 return chunks
3301 return chunks
3293
3302
3294 def dodeltachain(rev):
3303 def dodeltachain(rev):
3295 if not cache:
3304 if not cache:
3296 r.clearcaches()
3305 r.clearcaches()
3297 r._deltachain(rev)
3306 r._deltachain(rev)
3298
3307
3299 def doread(chain):
3308 def doread(chain):
3300 if not cache:
3309 if not cache:
3301 r.clearcaches()
3310 r.clearcaches()
3302 for item in slicedchain:
3311 for item in slicedchain:
3303 segmentforrevs(item[0], item[-1])
3312 segmentforrevs(item[0], item[-1])
3304
3313
3305 def doslice(r, chain, size):
3314 def doslice(r, chain, size):
3306 for s in slicechunk(r, chain, targetsize=size):
3315 for s in slicechunk(r, chain, targetsize=size):
3307 pass
3316 pass
3308
3317
3309 def dorawchunks(data, chain):
3318 def dorawchunks(data, chain):
3310 if not cache:
3319 if not cache:
3311 r.clearcaches()
3320 r.clearcaches()
3312 getrawchunks(data, chain)
3321 getrawchunks(data, chain)
3313
3322
3314 def dodecompress(chunks):
3323 def dodecompress(chunks):
3315 decomp = r.decompress
3324 decomp = r.decompress
3316 for chunk in chunks:
3325 for chunk in chunks:
3317 decomp(chunk)
3326 decomp(chunk)
3318
3327
3319 def dopatch(text, bins):
3328 def dopatch(text, bins):
3320 if not cache:
3329 if not cache:
3321 r.clearcaches()
3330 r.clearcaches()
3322 mdiff.patches(text, bins)
3331 mdiff.patches(text, bins)
3323
3332
3324 def dohash(text):
3333 def dohash(text):
3325 if not cache:
3334 if not cache:
3326 r.clearcaches()
3335 r.clearcaches()
3327 r.checkhash(text, node, rev=rev)
3336 r.checkhash(text, node, rev=rev)
3328
3337
3329 def dorevision():
3338 def dorevision():
3330 if not cache:
3339 if not cache:
3331 r.clearcaches()
3340 r.clearcaches()
3332 r.revision(node)
3341 r.revision(node)
3333
3342
3334 try:
3343 try:
3335 from mercurial.revlogutils.deltas import slicechunk
3344 from mercurial.revlogutils.deltas import slicechunk
3336 except ImportError:
3345 except ImportError:
3337 slicechunk = getattr(revlog, '_slicechunk', None)
3346 slicechunk = getattr(revlog, '_slicechunk', None)
3338
3347
3339 size = r.length(rev)
3348 size = r.length(rev)
3340 chain = r._deltachain(rev)[0]
3349 chain = r._deltachain(rev)[0]
3341 if not getattr(r, '_withsparseread', False):
3350 if not getattr(r, '_withsparseread', False):
3342 slicedchain = (chain,)
3351 slicedchain = (chain,)
3343 else:
3352 else:
3344 slicedchain = tuple(slicechunk(r, chain, targetsize=size))
3353 slicedchain = tuple(slicechunk(r, chain, targetsize=size))
3345 data = [segmentforrevs(seg[0], seg[-1])[1] for seg in slicedchain]
3354 data = [segmentforrevs(seg[0], seg[-1])[1] for seg in slicedchain]
3346 rawchunks = getrawchunks(data, slicedchain)
3355 rawchunks = getrawchunks(data, slicedchain)
3347 bins = r._chunks(chain)
3356 bins = r._chunks(chain)
3348 text = bytes(bins[0])
3357 text = bytes(bins[0])
3349 bins = bins[1:]
3358 bins = bins[1:]
3350 text = mdiff.patches(text, bins)
3359 text = mdiff.patches(text, bins)
3351
3360
3352 benches = [
3361 benches = [
3353 (lambda: dorevision(), b'full'),
3362 (lambda: dorevision(), b'full'),
3354 (lambda: dodeltachain(rev), b'deltachain'),
3363 (lambda: dodeltachain(rev), b'deltachain'),
3355 (lambda: doread(chain), b'read'),
3364 (lambda: doread(chain), b'read'),
3356 ]
3365 ]
3357
3366
3358 if getattr(r, '_withsparseread', False):
3367 if getattr(r, '_withsparseread', False):
3359 slicing = (lambda: doslice(r, chain, size), b'slice-sparse-chain')
3368 slicing = (lambda: doslice(r, chain, size), b'slice-sparse-chain')
3360 benches.append(slicing)
3369 benches.append(slicing)
3361
3370
3362 benches.extend(
3371 benches.extend(
3363 [
3372 [
3364 (lambda: dorawchunks(data, slicedchain), b'rawchunks'),
3373 (lambda: dorawchunks(data, slicedchain), b'rawchunks'),
3365 (lambda: dodecompress(rawchunks), b'decompress'),
3374 (lambda: dodecompress(rawchunks), b'decompress'),
3366 (lambda: dopatch(text, bins), b'patch'),
3375 (lambda: dopatch(text, bins), b'patch'),
3367 (lambda: dohash(text), b'hash'),
3376 (lambda: dohash(text), b'hash'),
3368 ]
3377 ]
3369 )
3378 )
3370
3379
3371 timer, fm = gettimer(ui, opts)
3380 timer, fm = gettimer(ui, opts)
3372 for fn, title in benches:
3381 for fn, title in benches:
3373 timer(fn, title=title)
3382 timer(fn, title=title)
3374 fm.end()
3383 fm.end()
3375
3384
3376
3385
3377 @command(
3386 @command(
3378 b'perf::revset|perfrevset',
3387 b'perf::revset|perfrevset',
3379 [
3388 [
3380 (b'C', b'clear', False, b'clear volatile cache between each call.'),
3389 (b'C', b'clear', False, b'clear volatile cache between each call.'),
3381 (b'', b'contexts', False, b'obtain changectx for each revision'),
3390 (b'', b'contexts', False, b'obtain changectx for each revision'),
3382 ]
3391 ]
3383 + formatteropts,
3392 + formatteropts,
3384 b"REVSET",
3393 b"REVSET",
3385 )
3394 )
3386 def perfrevset(ui, repo, expr, clear=False, contexts=False, **opts):
3395 def perfrevset(ui, repo, expr, clear=False, contexts=False, **opts):
3387 """benchmark the execution time of a revset
3396 """benchmark the execution time of a revset
3388
3397
3389 Use the --clean option if need to evaluate the impact of build volatile
3398 Use the --clean option if need to evaluate the impact of build volatile
3390 revisions set cache on the revset execution. Volatile cache hold filtered
3399 revisions set cache on the revset execution. Volatile cache hold filtered
3391 and obsolete related cache."""
3400 and obsolete related cache."""
3392 opts = _byteskwargs(opts)
3401 opts = _byteskwargs(opts)
3393
3402
3394 timer, fm = gettimer(ui, opts)
3403 timer, fm = gettimer(ui, opts)
3395
3404
3396 def d():
3405 def d():
3397 if clear:
3406 if clear:
3398 repo.invalidatevolatilesets()
3407 repo.invalidatevolatilesets()
3399 if contexts:
3408 if contexts:
3400 for ctx in repo.set(expr):
3409 for ctx in repo.set(expr):
3401 pass
3410 pass
3402 else:
3411 else:
3403 for r in repo.revs(expr):
3412 for r in repo.revs(expr):
3404 pass
3413 pass
3405
3414
3406 timer(d)
3415 timer(d)
3407 fm.end()
3416 fm.end()
3408
3417
3409
3418
3410 @command(
3419 @command(
3411 b'perf::volatilesets|perfvolatilesets',
3420 b'perf::volatilesets|perfvolatilesets',
3412 [
3421 [
3413 (b'', b'clear-obsstore', False, b'drop obsstore between each call.'),
3422 (b'', b'clear-obsstore', False, b'drop obsstore between each call.'),
3414 ]
3423 ]
3415 + formatteropts,
3424 + formatteropts,
3416 )
3425 )
3417 def perfvolatilesets(ui, repo, *names, **opts):
3426 def perfvolatilesets(ui, repo, *names, **opts):
3418 """benchmark the computation of various volatile set
3427 """benchmark the computation of various volatile set
3419
3428
3420 Volatile set computes element related to filtering and obsolescence."""
3429 Volatile set computes element related to filtering and obsolescence."""
3421 opts = _byteskwargs(opts)
3430 opts = _byteskwargs(opts)
3422 timer, fm = gettimer(ui, opts)
3431 timer, fm = gettimer(ui, opts)
3423 repo = repo.unfiltered()
3432 repo = repo.unfiltered()
3424
3433
3425 def getobs(name):
3434 def getobs(name):
3426 def d():
3435 def d():
3427 repo.invalidatevolatilesets()
3436 repo.invalidatevolatilesets()
3428 if opts[b'clear_obsstore']:
3437 if opts[b'clear_obsstore']:
3429 clearfilecache(repo, b'obsstore')
3438 clearfilecache(repo, b'obsstore')
3430 obsolete.getrevs(repo, name)
3439 obsolete.getrevs(repo, name)
3431
3440
3432 return d
3441 return d
3433
3442
3434 allobs = sorted(obsolete.cachefuncs)
3443 allobs = sorted(obsolete.cachefuncs)
3435 if names:
3444 if names:
3436 allobs = [n for n in allobs if n in names]
3445 allobs = [n for n in allobs if n in names]
3437
3446
3438 for name in allobs:
3447 for name in allobs:
3439 timer(getobs(name), title=name)
3448 timer(getobs(name), title=name)
3440
3449
3441 def getfiltered(name):
3450 def getfiltered(name):
3442 def d():
3451 def d():
3443 repo.invalidatevolatilesets()
3452 repo.invalidatevolatilesets()
3444 if opts[b'clear_obsstore']:
3453 if opts[b'clear_obsstore']:
3445 clearfilecache(repo, b'obsstore')
3454 clearfilecache(repo, b'obsstore')
3446 repoview.filterrevs(repo, name)
3455 repoview.filterrevs(repo, name)
3447
3456
3448 return d
3457 return d
3449
3458
3450 allfilter = sorted(repoview.filtertable)
3459 allfilter = sorted(repoview.filtertable)
3451 if names:
3460 if names:
3452 allfilter = [n for n in allfilter if n in names]
3461 allfilter = [n for n in allfilter if n in names]
3453
3462
3454 for name in allfilter:
3463 for name in allfilter:
3455 timer(getfiltered(name), title=name)
3464 timer(getfiltered(name), title=name)
3456 fm.end()
3465 fm.end()
3457
3466
3458
3467
3459 @command(
3468 @command(
3460 b'perf::branchmap|perfbranchmap',
3469 b'perf::branchmap|perfbranchmap',
3461 [
3470 [
3462 (b'f', b'full', False, b'Includes build time of subset'),
3471 (b'f', b'full', False, b'Includes build time of subset'),
3463 (
3472 (
3464 b'',
3473 b'',
3465 b'clear-revbranch',
3474 b'clear-revbranch',
3466 False,
3475 False,
3467 b'purge the revbranch cache between computation',
3476 b'purge the revbranch cache between computation',
3468 ),
3477 ),
3469 ]
3478 ]
3470 + formatteropts,
3479 + formatteropts,
3471 )
3480 )
3472 def perfbranchmap(ui, repo, *filternames, **opts):
3481 def perfbranchmap(ui, repo, *filternames, **opts):
3473 """benchmark the update of a branchmap
3482 """benchmark the update of a branchmap
3474
3483
3475 This benchmarks the full repo.branchmap() call with read and write disabled
3484 This benchmarks the full repo.branchmap() call with read and write disabled
3476 """
3485 """
3477 opts = _byteskwargs(opts)
3486 opts = _byteskwargs(opts)
3478 full = opts.get(b"full", False)
3487 full = opts.get(b"full", False)
3479 clear_revbranch = opts.get(b"clear_revbranch", False)
3488 clear_revbranch = opts.get(b"clear_revbranch", False)
3480 timer, fm = gettimer(ui, opts)
3489 timer, fm = gettimer(ui, opts)
3481
3490
3482 def getbranchmap(filtername):
3491 def getbranchmap(filtername):
3483 """generate a benchmark function for the filtername"""
3492 """generate a benchmark function for the filtername"""
3484 if filtername is None:
3493 if filtername is None:
3485 view = repo
3494 view = repo
3486 else:
3495 else:
3487 view = repo.filtered(filtername)
3496 view = repo.filtered(filtername)
3488 if util.safehasattr(view._branchcaches, '_per_filter'):
3497 if util.safehasattr(view._branchcaches, '_per_filter'):
3489 filtered = view._branchcaches._per_filter
3498 filtered = view._branchcaches._per_filter
3490 else:
3499 else:
3491 # older versions
3500 # older versions
3492 filtered = view._branchcaches
3501 filtered = view._branchcaches
3493
3502
3494 def d():
3503 def d():
3495 if clear_revbranch:
3504 if clear_revbranch:
3496 repo.revbranchcache()._clear()
3505 repo.revbranchcache()._clear()
3497 if full:
3506 if full:
3498 view._branchcaches.clear()
3507 view._branchcaches.clear()
3499 else:
3508 else:
3500 filtered.pop(filtername, None)
3509 filtered.pop(filtername, None)
3501 view.branchmap()
3510 view.branchmap()
3502
3511
3503 return d
3512 return d
3504
3513
3505 # add filter in smaller subset to bigger subset
3514 # add filter in smaller subset to bigger subset
3506 possiblefilters = set(repoview.filtertable)
3515 possiblefilters = set(repoview.filtertable)
3507 if filternames:
3516 if filternames:
3508 possiblefilters &= set(filternames)
3517 possiblefilters &= set(filternames)
3509 subsettable = getbranchmapsubsettable()
3518 subsettable = getbranchmapsubsettable()
3510 allfilters = []
3519 allfilters = []
3511 while possiblefilters:
3520 while possiblefilters:
3512 for name in possiblefilters:
3521 for name in possiblefilters:
3513 subset = subsettable.get(name)
3522 subset = subsettable.get(name)
3514 if subset not in possiblefilters:
3523 if subset not in possiblefilters:
3515 break
3524 break
3516 else:
3525 else:
3517 assert False, b'subset cycle %s!' % possiblefilters
3526 assert False, b'subset cycle %s!' % possiblefilters
3518 allfilters.append(name)
3527 allfilters.append(name)
3519 possiblefilters.remove(name)
3528 possiblefilters.remove(name)
3520
3529
3521 # warm the cache
3530 # warm the cache
3522 if not full:
3531 if not full:
3523 for name in allfilters:
3532 for name in allfilters:
3524 repo.filtered(name).branchmap()
3533 repo.filtered(name).branchmap()
3525 if not filternames or b'unfiltered' in filternames:
3534 if not filternames or b'unfiltered' in filternames:
3526 # add unfiltered
3535 # add unfiltered
3527 allfilters.append(None)
3536 allfilters.append(None)
3528
3537
3529 if util.safehasattr(branchmap.branchcache, 'fromfile'):
3538 if util.safehasattr(branchmap.branchcache, 'fromfile'):
3530 branchcacheread = safeattrsetter(branchmap.branchcache, b'fromfile')
3539 branchcacheread = safeattrsetter(branchmap.branchcache, b'fromfile')
3531 branchcacheread.set(classmethod(lambda *args: None))
3540 branchcacheread.set(classmethod(lambda *args: None))
3532 else:
3541 else:
3533 # older versions
3542 # older versions
3534 branchcacheread = safeattrsetter(branchmap, b'read')
3543 branchcacheread = safeattrsetter(branchmap, b'read')
3535 branchcacheread.set(lambda *args: None)
3544 branchcacheread.set(lambda *args: None)
3536 branchcachewrite = safeattrsetter(branchmap.branchcache, b'write')
3545 branchcachewrite = safeattrsetter(branchmap.branchcache, b'write')
3537 branchcachewrite.set(lambda *args: None)
3546 branchcachewrite.set(lambda *args: None)
3538 try:
3547 try:
3539 for name in allfilters:
3548 for name in allfilters:
3540 printname = name
3549 printname = name
3541 if name is None:
3550 if name is None:
3542 printname = b'unfiltered'
3551 printname = b'unfiltered'
3543 timer(getbranchmap(name), title=printname)
3552 timer(getbranchmap(name), title=printname)
3544 finally:
3553 finally:
3545 branchcacheread.restore()
3554 branchcacheread.restore()
3546 branchcachewrite.restore()
3555 branchcachewrite.restore()
3547 fm.end()
3556 fm.end()
3548
3557
3549
3558
3550 @command(
3559 @command(
3551 b'perf::branchmapupdate|perfbranchmapupdate',
3560 b'perf::branchmapupdate|perfbranchmapupdate',
3552 [
3561 [
3553 (b'', b'base', [], b'subset of revision to start from'),
3562 (b'', b'base', [], b'subset of revision to start from'),
3554 (b'', b'target', [], b'subset of revision to end with'),
3563 (b'', b'target', [], b'subset of revision to end with'),
3555 (b'', b'clear-caches', False, b'clear cache between each runs'),
3564 (b'', b'clear-caches', False, b'clear cache between each runs'),
3556 ]
3565 ]
3557 + formatteropts,
3566 + formatteropts,
3558 )
3567 )
3559 def perfbranchmapupdate(ui, repo, base=(), target=(), **opts):
3568 def perfbranchmapupdate(ui, repo, base=(), target=(), **opts):
3560 """benchmark branchmap update from for <base> revs to <target> revs
3569 """benchmark branchmap update from for <base> revs to <target> revs
3561
3570
3562 If `--clear-caches` is passed, the following items will be reset before
3571 If `--clear-caches` is passed, the following items will be reset before
3563 each update:
3572 each update:
3564 * the changelog instance and associated indexes
3573 * the changelog instance and associated indexes
3565 * the rev-branch-cache instance
3574 * the rev-branch-cache instance
3566
3575
3567 Examples:
3576 Examples:
3568
3577
3569 # update for the one last revision
3578 # update for the one last revision
3570 $ hg perfbranchmapupdate --base 'not tip' --target 'tip'
3579 $ hg perfbranchmapupdate --base 'not tip' --target 'tip'
3571
3580
3572 $ update for change coming with a new branch
3581 $ update for change coming with a new branch
3573 $ hg perfbranchmapupdate --base 'stable' --target 'default'
3582 $ hg perfbranchmapupdate --base 'stable' --target 'default'
3574 """
3583 """
3575 from mercurial import branchmap
3584 from mercurial import branchmap
3576 from mercurial import repoview
3585 from mercurial import repoview
3577
3586
3578 opts = _byteskwargs(opts)
3587 opts = _byteskwargs(opts)
3579 timer, fm = gettimer(ui, opts)
3588 timer, fm = gettimer(ui, opts)
3580 clearcaches = opts[b'clear_caches']
3589 clearcaches = opts[b'clear_caches']
3581 unfi = repo.unfiltered()
3590 unfi = repo.unfiltered()
3582 x = [None] # used to pass data between closure
3591 x = [None] # used to pass data between closure
3583
3592
3584 # we use a `list` here to avoid possible side effect from smartset
3593 # we use a `list` here to avoid possible side effect from smartset
3585 baserevs = list(scmutil.revrange(repo, base))
3594 baserevs = list(scmutil.revrange(repo, base))
3586 targetrevs = list(scmutil.revrange(repo, target))
3595 targetrevs = list(scmutil.revrange(repo, target))
3587 if not baserevs:
3596 if not baserevs:
3588 raise error.Abort(b'no revisions selected for --base')
3597 raise error.Abort(b'no revisions selected for --base')
3589 if not targetrevs:
3598 if not targetrevs:
3590 raise error.Abort(b'no revisions selected for --target')
3599 raise error.Abort(b'no revisions selected for --target')
3591
3600
3592 # make sure the target branchmap also contains the one in the base
3601 # make sure the target branchmap also contains the one in the base
3593 targetrevs = list(set(baserevs) | set(targetrevs))
3602 targetrevs = list(set(baserevs) | set(targetrevs))
3594 targetrevs.sort()
3603 targetrevs.sort()
3595
3604
3596 cl = repo.changelog
3605 cl = repo.changelog
3597 allbaserevs = list(cl.ancestors(baserevs, inclusive=True))
3606 allbaserevs = list(cl.ancestors(baserevs, inclusive=True))
3598 allbaserevs.sort()
3607 allbaserevs.sort()
3599 alltargetrevs = frozenset(cl.ancestors(targetrevs, inclusive=True))
3608 alltargetrevs = frozenset(cl.ancestors(targetrevs, inclusive=True))
3600
3609
3601 newrevs = list(alltargetrevs.difference(allbaserevs))
3610 newrevs = list(alltargetrevs.difference(allbaserevs))
3602 newrevs.sort()
3611 newrevs.sort()
3603
3612
3604 allrevs = frozenset(unfi.changelog.revs())
3613 allrevs = frozenset(unfi.changelog.revs())
3605 basefilterrevs = frozenset(allrevs.difference(allbaserevs))
3614 basefilterrevs = frozenset(allrevs.difference(allbaserevs))
3606 targetfilterrevs = frozenset(allrevs.difference(alltargetrevs))
3615 targetfilterrevs = frozenset(allrevs.difference(alltargetrevs))
3607
3616
3608 def basefilter(repo, visibilityexceptions=None):
3617 def basefilter(repo, visibilityexceptions=None):
3609 return basefilterrevs
3618 return basefilterrevs
3610
3619
3611 def targetfilter(repo, visibilityexceptions=None):
3620 def targetfilter(repo, visibilityexceptions=None):
3612 return targetfilterrevs
3621 return targetfilterrevs
3613
3622
3614 msg = b'benchmark of branchmap with %d revisions with %d new ones\n'
3623 msg = b'benchmark of branchmap with %d revisions with %d new ones\n'
3615 ui.status(msg % (len(allbaserevs), len(newrevs)))
3624 ui.status(msg % (len(allbaserevs), len(newrevs)))
3616 if targetfilterrevs:
3625 if targetfilterrevs:
3617 msg = b'(%d revisions still filtered)\n'
3626 msg = b'(%d revisions still filtered)\n'
3618 ui.status(msg % len(targetfilterrevs))
3627 ui.status(msg % len(targetfilterrevs))
3619
3628
3620 try:
3629 try:
3621 repoview.filtertable[b'__perf_branchmap_update_base'] = basefilter
3630 repoview.filtertable[b'__perf_branchmap_update_base'] = basefilter
3622 repoview.filtertable[b'__perf_branchmap_update_target'] = targetfilter
3631 repoview.filtertable[b'__perf_branchmap_update_target'] = targetfilter
3623
3632
3624 baserepo = repo.filtered(b'__perf_branchmap_update_base')
3633 baserepo = repo.filtered(b'__perf_branchmap_update_base')
3625 targetrepo = repo.filtered(b'__perf_branchmap_update_target')
3634 targetrepo = repo.filtered(b'__perf_branchmap_update_target')
3626
3635
3627 # try to find an existing branchmap to reuse
3636 # try to find an existing branchmap to reuse
3628 subsettable = getbranchmapsubsettable()
3637 subsettable = getbranchmapsubsettable()
3629 candidatefilter = subsettable.get(None)
3638 candidatefilter = subsettable.get(None)
3630 while candidatefilter is not None:
3639 while candidatefilter is not None:
3631 candidatebm = repo.filtered(candidatefilter).branchmap()
3640 candidatebm = repo.filtered(candidatefilter).branchmap()
3632 if candidatebm.validfor(baserepo):
3641 if candidatebm.validfor(baserepo):
3633 filtered = repoview.filterrevs(repo, candidatefilter)
3642 filtered = repoview.filterrevs(repo, candidatefilter)
3634 missing = [r for r in allbaserevs if r in filtered]
3643 missing = [r for r in allbaserevs if r in filtered]
3635 base = candidatebm.copy()
3644 base = candidatebm.copy()
3636 base.update(baserepo, missing)
3645 base.update(baserepo, missing)
3637 break
3646 break
3638 candidatefilter = subsettable.get(candidatefilter)
3647 candidatefilter = subsettable.get(candidatefilter)
3639 else:
3648 else:
3640 # no suitable subset where found
3649 # no suitable subset where found
3641 base = branchmap.branchcache()
3650 base = branchmap.branchcache()
3642 base.update(baserepo, allbaserevs)
3651 base.update(baserepo, allbaserevs)
3643
3652
3644 def setup():
3653 def setup():
3645 x[0] = base.copy()
3654 x[0] = base.copy()
3646 if clearcaches:
3655 if clearcaches:
3647 unfi._revbranchcache = None
3656 unfi._revbranchcache = None
3648 clearchangelog(repo)
3657 clearchangelog(repo)
3649
3658
3650 def bench():
3659 def bench():
3651 x[0].update(targetrepo, newrevs)
3660 x[0].update(targetrepo, newrevs)
3652
3661
3653 timer(bench, setup=setup)
3662 timer(bench, setup=setup)
3654 fm.end()
3663 fm.end()
3655 finally:
3664 finally:
3656 repoview.filtertable.pop(b'__perf_branchmap_update_base', None)
3665 repoview.filtertable.pop(b'__perf_branchmap_update_base', None)
3657 repoview.filtertable.pop(b'__perf_branchmap_update_target', None)
3666 repoview.filtertable.pop(b'__perf_branchmap_update_target', None)
3658
3667
3659
3668
3660 @command(
3669 @command(
3661 b'perf::branchmapload|perfbranchmapload',
3670 b'perf::branchmapload|perfbranchmapload',
3662 [
3671 [
3663 (b'f', b'filter', b'', b'Specify repoview filter'),
3672 (b'f', b'filter', b'', b'Specify repoview filter'),
3664 (b'', b'list', False, b'List brachmap filter caches'),
3673 (b'', b'list', False, b'List brachmap filter caches'),
3665 (b'', b'clear-revlogs', False, b'refresh changelog and manifest'),
3674 (b'', b'clear-revlogs', False, b'refresh changelog and manifest'),
3666 ]
3675 ]
3667 + formatteropts,
3676 + formatteropts,
3668 )
3677 )
3669 def perfbranchmapload(ui, repo, filter=b'', list=False, **opts):
3678 def perfbranchmapload(ui, repo, filter=b'', list=False, **opts):
3670 """benchmark reading the branchmap"""
3679 """benchmark reading the branchmap"""
3671 opts = _byteskwargs(opts)
3680 opts = _byteskwargs(opts)
3672 clearrevlogs = opts[b'clear_revlogs']
3681 clearrevlogs = opts[b'clear_revlogs']
3673
3682
3674 if list:
3683 if list:
3675 for name, kind, st in repo.cachevfs.readdir(stat=True):
3684 for name, kind, st in repo.cachevfs.readdir(stat=True):
3676 if name.startswith(b'branch2'):
3685 if name.startswith(b'branch2'):
3677 filtername = name.partition(b'-')[2] or b'unfiltered'
3686 filtername = name.partition(b'-')[2] or b'unfiltered'
3678 ui.status(
3687 ui.status(
3679 b'%s - %s\n' % (filtername, util.bytecount(st.st_size))
3688 b'%s - %s\n' % (filtername, util.bytecount(st.st_size))
3680 )
3689 )
3681 return
3690 return
3682 if not filter:
3691 if not filter:
3683 filter = None
3692 filter = None
3684 subsettable = getbranchmapsubsettable()
3693 subsettable = getbranchmapsubsettable()
3685 if filter is None:
3694 if filter is None:
3686 repo = repo.unfiltered()
3695 repo = repo.unfiltered()
3687 else:
3696 else:
3688 repo = repoview.repoview(repo, filter)
3697 repo = repoview.repoview(repo, filter)
3689
3698
3690 repo.branchmap() # make sure we have a relevant, up to date branchmap
3699 repo.branchmap() # make sure we have a relevant, up to date branchmap
3691
3700
3692 try:
3701 try:
3693 fromfile = branchmap.branchcache.fromfile
3702 fromfile = branchmap.branchcache.fromfile
3694 except AttributeError:
3703 except AttributeError:
3695 # older versions
3704 # older versions
3696 fromfile = branchmap.read
3705 fromfile = branchmap.read
3697
3706
3698 currentfilter = filter
3707 currentfilter = filter
3699 # try once without timer, the filter may not be cached
3708 # try once without timer, the filter may not be cached
3700 while fromfile(repo) is None:
3709 while fromfile(repo) is None:
3701 currentfilter = subsettable.get(currentfilter)
3710 currentfilter = subsettable.get(currentfilter)
3702 if currentfilter is None:
3711 if currentfilter is None:
3703 raise error.Abort(
3712 raise error.Abort(
3704 b'No branchmap cached for %s repo' % (filter or b'unfiltered')
3713 b'No branchmap cached for %s repo' % (filter or b'unfiltered')
3705 )
3714 )
3706 repo = repo.filtered(currentfilter)
3715 repo = repo.filtered(currentfilter)
3707 timer, fm = gettimer(ui, opts)
3716 timer, fm = gettimer(ui, opts)
3708
3717
3709 def setup():
3718 def setup():
3710 if clearrevlogs:
3719 if clearrevlogs:
3711 clearchangelog(repo)
3720 clearchangelog(repo)
3712
3721
3713 def bench():
3722 def bench():
3714 fromfile(repo)
3723 fromfile(repo)
3715
3724
3716 timer(bench, setup=setup)
3725 timer(bench, setup=setup)
3717 fm.end()
3726 fm.end()
3718
3727
3719
3728
3720 @command(b'perf::loadmarkers|perfloadmarkers')
3729 @command(b'perf::loadmarkers|perfloadmarkers')
3721 def perfloadmarkers(ui, repo):
3730 def perfloadmarkers(ui, repo):
3722 """benchmark the time to parse the on-disk markers for a repo
3731 """benchmark the time to parse the on-disk markers for a repo
3723
3732
3724 Result is the number of markers in the repo."""
3733 Result is the number of markers in the repo."""
3725 timer, fm = gettimer(ui)
3734 timer, fm = gettimer(ui)
3726 svfs = getsvfs(repo)
3735 svfs = getsvfs(repo)
3727 timer(lambda: len(obsolete.obsstore(repo, svfs)))
3736 timer(lambda: len(obsolete.obsstore(repo, svfs)))
3728 fm.end()
3737 fm.end()
3729
3738
3730
3739
3731 @command(
3740 @command(
3732 b'perf::lrucachedict|perflrucachedict',
3741 b'perf::lrucachedict|perflrucachedict',
3733 formatteropts
3742 formatteropts
3734 + [
3743 + [
3735 (b'', b'costlimit', 0, b'maximum total cost of items in cache'),
3744 (b'', b'costlimit', 0, b'maximum total cost of items in cache'),
3736 (b'', b'mincost', 0, b'smallest cost of items in cache'),
3745 (b'', b'mincost', 0, b'smallest cost of items in cache'),
3737 (b'', b'maxcost', 100, b'maximum cost of items in cache'),
3746 (b'', b'maxcost', 100, b'maximum cost of items in cache'),
3738 (b'', b'size', 4, b'size of cache'),
3747 (b'', b'size', 4, b'size of cache'),
3739 (b'', b'gets', 10000, b'number of key lookups'),
3748 (b'', b'gets', 10000, b'number of key lookups'),
3740 (b'', b'sets', 10000, b'number of key sets'),
3749 (b'', b'sets', 10000, b'number of key sets'),
3741 (b'', b'mixed', 10000, b'number of mixed mode operations'),
3750 (b'', b'mixed', 10000, b'number of mixed mode operations'),
3742 (
3751 (
3743 b'',
3752 b'',
3744 b'mixedgetfreq',
3753 b'mixedgetfreq',
3745 50,
3754 50,
3746 b'frequency of get vs set ops in mixed mode',
3755 b'frequency of get vs set ops in mixed mode',
3747 ),
3756 ),
3748 ],
3757 ],
3749 norepo=True,
3758 norepo=True,
3750 )
3759 )
3751 def perflrucache(
3760 def perflrucache(
3752 ui,
3761 ui,
3753 mincost=0,
3762 mincost=0,
3754 maxcost=100,
3763 maxcost=100,
3755 costlimit=0,
3764 costlimit=0,
3756 size=4,
3765 size=4,
3757 gets=10000,
3766 gets=10000,
3758 sets=10000,
3767 sets=10000,
3759 mixed=10000,
3768 mixed=10000,
3760 mixedgetfreq=50,
3769 mixedgetfreq=50,
3761 **opts
3770 **opts
3762 ):
3771 ):
3763 opts = _byteskwargs(opts)
3772 opts = _byteskwargs(opts)
3764
3773
3765 def doinit():
3774 def doinit():
3766 for i in _xrange(10000):
3775 for i in _xrange(10000):
3767 util.lrucachedict(size)
3776 util.lrucachedict(size)
3768
3777
3769 costrange = list(range(mincost, maxcost + 1))
3778 costrange = list(range(mincost, maxcost + 1))
3770
3779
3771 values = []
3780 values = []
3772 for i in _xrange(size):
3781 for i in _xrange(size):
3773 values.append(random.randint(0, _maxint))
3782 values.append(random.randint(0, _maxint))
3774
3783
3775 # Get mode fills the cache and tests raw lookup performance with no
3784 # Get mode fills the cache and tests raw lookup performance with no
3776 # eviction.
3785 # eviction.
3777 getseq = []
3786 getseq = []
3778 for i in _xrange(gets):
3787 for i in _xrange(gets):
3779 getseq.append(random.choice(values))
3788 getseq.append(random.choice(values))
3780
3789
3781 def dogets():
3790 def dogets():
3782 d = util.lrucachedict(size)
3791 d = util.lrucachedict(size)
3783 for v in values:
3792 for v in values:
3784 d[v] = v
3793 d[v] = v
3785 for key in getseq:
3794 for key in getseq:
3786 value = d[key]
3795 value = d[key]
3787 value # silence pyflakes warning
3796 value # silence pyflakes warning
3788
3797
3789 def dogetscost():
3798 def dogetscost():
3790 d = util.lrucachedict(size, maxcost=costlimit)
3799 d = util.lrucachedict(size, maxcost=costlimit)
3791 for i, v in enumerate(values):
3800 for i, v in enumerate(values):
3792 d.insert(v, v, cost=costs[i])
3801 d.insert(v, v, cost=costs[i])
3793 for key in getseq:
3802 for key in getseq:
3794 try:
3803 try:
3795 value = d[key]
3804 value = d[key]
3796 value # silence pyflakes warning
3805 value # silence pyflakes warning
3797 except KeyError:
3806 except KeyError:
3798 pass
3807 pass
3799
3808
3800 # Set mode tests insertion speed with cache eviction.
3809 # Set mode tests insertion speed with cache eviction.
3801 setseq = []
3810 setseq = []
3802 costs = []
3811 costs = []
3803 for i in _xrange(sets):
3812 for i in _xrange(sets):
3804 setseq.append(random.randint(0, _maxint))
3813 setseq.append(random.randint(0, _maxint))
3805 costs.append(random.choice(costrange))
3814 costs.append(random.choice(costrange))
3806
3815
3807 def doinserts():
3816 def doinserts():
3808 d = util.lrucachedict(size)
3817 d = util.lrucachedict(size)
3809 for v in setseq:
3818 for v in setseq:
3810 d.insert(v, v)
3819 d.insert(v, v)
3811
3820
3812 def doinsertscost():
3821 def doinsertscost():
3813 d = util.lrucachedict(size, maxcost=costlimit)
3822 d = util.lrucachedict(size, maxcost=costlimit)
3814 for i, v in enumerate(setseq):
3823 for i, v in enumerate(setseq):
3815 d.insert(v, v, cost=costs[i])
3824 d.insert(v, v, cost=costs[i])
3816
3825
3817 def dosets():
3826 def dosets():
3818 d = util.lrucachedict(size)
3827 d = util.lrucachedict(size)
3819 for v in setseq:
3828 for v in setseq:
3820 d[v] = v
3829 d[v] = v
3821
3830
3822 # Mixed mode randomly performs gets and sets with eviction.
3831 # Mixed mode randomly performs gets and sets with eviction.
3823 mixedops = []
3832 mixedops = []
3824 for i in _xrange(mixed):
3833 for i in _xrange(mixed):
3825 r = random.randint(0, 100)
3834 r = random.randint(0, 100)
3826 if r < mixedgetfreq:
3835 if r < mixedgetfreq:
3827 op = 0
3836 op = 0
3828 else:
3837 else:
3829 op = 1
3838 op = 1
3830
3839
3831 mixedops.append(
3840 mixedops.append(
3832 (op, random.randint(0, size * 2), random.choice(costrange))
3841 (op, random.randint(0, size * 2), random.choice(costrange))
3833 )
3842 )
3834
3843
3835 def domixed():
3844 def domixed():
3836 d = util.lrucachedict(size)
3845 d = util.lrucachedict(size)
3837
3846
3838 for op, v, cost in mixedops:
3847 for op, v, cost in mixedops:
3839 if op == 0:
3848 if op == 0:
3840 try:
3849 try:
3841 d[v]
3850 d[v]
3842 except KeyError:
3851 except KeyError:
3843 pass
3852 pass
3844 else:
3853 else:
3845 d[v] = v
3854 d[v] = v
3846
3855
3847 def domixedcost():
3856 def domixedcost():
3848 d = util.lrucachedict(size, maxcost=costlimit)
3857 d = util.lrucachedict(size, maxcost=costlimit)
3849
3858
3850 for op, v, cost in mixedops:
3859 for op, v, cost in mixedops:
3851 if op == 0:
3860 if op == 0:
3852 try:
3861 try:
3853 d[v]
3862 d[v]
3854 except KeyError:
3863 except KeyError:
3855 pass
3864 pass
3856 else:
3865 else:
3857 d.insert(v, v, cost=cost)
3866 d.insert(v, v, cost=cost)
3858
3867
3859 benches = [
3868 benches = [
3860 (doinit, b'init'),
3869 (doinit, b'init'),
3861 ]
3870 ]
3862
3871
3863 if costlimit:
3872 if costlimit:
3864 benches.extend(
3873 benches.extend(
3865 [
3874 [
3866 (dogetscost, b'gets w/ cost limit'),
3875 (dogetscost, b'gets w/ cost limit'),
3867 (doinsertscost, b'inserts w/ cost limit'),
3876 (doinsertscost, b'inserts w/ cost limit'),
3868 (domixedcost, b'mixed w/ cost limit'),
3877 (domixedcost, b'mixed w/ cost limit'),
3869 ]
3878 ]
3870 )
3879 )
3871 else:
3880 else:
3872 benches.extend(
3881 benches.extend(
3873 [
3882 [
3874 (dogets, b'gets'),
3883 (dogets, b'gets'),
3875 (doinserts, b'inserts'),
3884 (doinserts, b'inserts'),
3876 (dosets, b'sets'),
3885 (dosets, b'sets'),
3877 (domixed, b'mixed'),
3886 (domixed, b'mixed'),
3878 ]
3887 ]
3879 )
3888 )
3880
3889
3881 for fn, title in benches:
3890 for fn, title in benches:
3882 timer, fm = gettimer(ui, opts)
3891 timer, fm = gettimer(ui, opts)
3883 timer(fn, title=title)
3892 timer(fn, title=title)
3884 fm.end()
3893 fm.end()
3885
3894
3886
3895
3887 @command(
3896 @command(
3888 b'perf::write|perfwrite',
3897 b'perf::write|perfwrite',
3889 formatteropts
3898 formatteropts
3890 + [
3899 + [
3891 (b'', b'write-method', b'write', b'ui write method'),
3900 (b'', b'write-method', b'write', b'ui write method'),
3892 (b'', b'nlines', 100, b'number of lines'),
3901 (b'', b'nlines', 100, b'number of lines'),
3893 (b'', b'nitems', 100, b'number of items (per line)'),
3902 (b'', b'nitems', 100, b'number of items (per line)'),
3894 (b'', b'item', b'x', b'item that is written'),
3903 (b'', b'item', b'x', b'item that is written'),
3895 (b'', b'batch-line', None, b'pass whole line to write method at once'),
3904 (b'', b'batch-line', None, b'pass whole line to write method at once'),
3896 (b'', b'flush-line', None, b'flush after each line'),
3905 (b'', b'flush-line', None, b'flush after each line'),
3897 ],
3906 ],
3898 )
3907 )
3899 def perfwrite(ui, repo, **opts):
3908 def perfwrite(ui, repo, **opts):
3900 """microbenchmark ui.write (and others)"""
3909 """microbenchmark ui.write (and others)"""
3901 opts = _byteskwargs(opts)
3910 opts = _byteskwargs(opts)
3902
3911
3903 write = getattr(ui, _sysstr(opts[b'write_method']))
3912 write = getattr(ui, _sysstr(opts[b'write_method']))
3904 nlines = int(opts[b'nlines'])
3913 nlines = int(opts[b'nlines'])
3905 nitems = int(opts[b'nitems'])
3914 nitems = int(opts[b'nitems'])
3906 item = opts[b'item']
3915 item = opts[b'item']
3907 batch_line = opts.get(b'batch_line')
3916 batch_line = opts.get(b'batch_line')
3908 flush_line = opts.get(b'flush_line')
3917 flush_line = opts.get(b'flush_line')
3909
3918
3910 if batch_line:
3919 if batch_line:
3911 line = item * nitems + b'\n'
3920 line = item * nitems + b'\n'
3912
3921
3913 def benchmark():
3922 def benchmark():
3914 for i in pycompat.xrange(nlines):
3923 for i in pycompat.xrange(nlines):
3915 if batch_line:
3924 if batch_line:
3916 write(line)
3925 write(line)
3917 else:
3926 else:
3918 for i in pycompat.xrange(nitems):
3927 for i in pycompat.xrange(nitems):
3919 write(item)
3928 write(item)
3920 write(b'\n')
3929 write(b'\n')
3921 if flush_line:
3930 if flush_line:
3922 ui.flush()
3931 ui.flush()
3923 ui.flush()
3932 ui.flush()
3924
3933
3925 timer, fm = gettimer(ui, opts)
3934 timer, fm = gettimer(ui, opts)
3926 timer(benchmark)
3935 timer(benchmark)
3927 fm.end()
3936 fm.end()
3928
3937
3929
3938
3930 def uisetup(ui):
3939 def uisetup(ui):
3931 if util.safehasattr(cmdutil, b'openrevlog') and not util.safehasattr(
3940 if util.safehasattr(cmdutil, b'openrevlog') and not util.safehasattr(
3932 commands, b'debugrevlogopts'
3941 commands, b'debugrevlogopts'
3933 ):
3942 ):
3934 # for "historical portability":
3943 # for "historical portability":
3935 # In this case, Mercurial should be 1.9 (or a79fea6b3e77) -
3944 # In this case, Mercurial should be 1.9 (or a79fea6b3e77) -
3936 # 3.7 (or 5606f7d0d063). Therefore, '--dir' option for
3945 # 3.7 (or 5606f7d0d063). Therefore, '--dir' option for
3937 # openrevlog() should cause failure, because it has been
3946 # openrevlog() should cause failure, because it has been
3938 # available since 3.5 (or 49c583ca48c4).
3947 # available since 3.5 (or 49c583ca48c4).
3939 def openrevlog(orig, repo, cmd, file_, opts):
3948 def openrevlog(orig, repo, cmd, file_, opts):
3940 if opts.get(b'dir') and not util.safehasattr(repo, b'dirlog'):
3949 if opts.get(b'dir') and not util.safehasattr(repo, b'dirlog'):
3941 raise error.Abort(
3950 raise error.Abort(
3942 b"This version doesn't support --dir option",
3951 b"This version doesn't support --dir option",
3943 hint=b"use 3.5 or later",
3952 hint=b"use 3.5 or later",
3944 )
3953 )
3945 return orig(repo, cmd, file_, opts)
3954 return orig(repo, cmd, file_, opts)
3946
3955
3947 extensions.wrapfunction(cmdutil, b'openrevlog', openrevlog)
3956 extensions.wrapfunction(cmdutil, b'openrevlog', openrevlog)
3948
3957
3949
3958
3950 @command(
3959 @command(
3951 b'perf::progress|perfprogress',
3960 b'perf::progress|perfprogress',
3952 formatteropts
3961 formatteropts
3953 + [
3962 + [
3954 (b'', b'topic', b'topic', b'topic for progress messages'),
3963 (b'', b'topic', b'topic', b'topic for progress messages'),
3955 (b'c', b'total', 1000000, b'total value we are progressing to'),
3964 (b'c', b'total', 1000000, b'total value we are progressing to'),
3956 ],
3965 ],
3957 norepo=True,
3966 norepo=True,
3958 )
3967 )
3959 def perfprogress(ui, topic=None, total=None, **opts):
3968 def perfprogress(ui, topic=None, total=None, **opts):
3960 """printing of progress bars"""
3969 """printing of progress bars"""
3961 opts = _byteskwargs(opts)
3970 opts = _byteskwargs(opts)
3962
3971
3963 timer, fm = gettimer(ui, opts)
3972 timer, fm = gettimer(ui, opts)
3964
3973
3965 def doprogress():
3974 def doprogress():
3966 with ui.makeprogress(topic, total=total) as progress:
3975 with ui.makeprogress(topic, total=total) as progress:
3967 for i in _xrange(total):
3976 for i in _xrange(total):
3968 progress.increment()
3977 progress.increment()
3969
3978
3970 timer(doprogress)
3979 timer(doprogress)
3971 fm.end()
3980 fm.end()
@@ -1,1993 +1,1983
1 # dirstate.py - working directory tracking for mercurial
1 # dirstate.py - working directory tracking for mercurial
2 #
2 #
3 # Copyright 2005-2007 Olivia Mackall <olivia@selenic.com>
3 # Copyright 2005-2007 Olivia Mackall <olivia@selenic.com>
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 collections
10 import collections
11 import contextlib
11 import contextlib
12 import errno
12 import errno
13 import os
13 import os
14 import stat
14 import stat
15
15
16 from .i18n import _
16 from .i18n import _
17 from .pycompat import delattr
17 from .pycompat import delattr
18
18
19 from hgdemandimport import tracing
19 from hgdemandimport import tracing
20
20
21 from . import (
21 from . import (
22 encoding,
22 encoding,
23 error,
23 error,
24 match as matchmod,
24 match as matchmod,
25 pathutil,
25 pathutil,
26 policy,
26 policy,
27 pycompat,
27 pycompat,
28 scmutil,
28 scmutil,
29 sparse,
29 sparse,
30 txnutil,
30 txnutil,
31 util,
31 util,
32 )
32 )
33
33
34 from .interfaces import (
34 from .interfaces import (
35 dirstate as intdirstate,
35 dirstate as intdirstate,
36 util as interfaceutil,
36 util as interfaceutil,
37 )
37 )
38
38
39 parsers = policy.importmod('parsers')
39 parsers = policy.importmod('parsers')
40 rustmod = policy.importrust('dirstate')
40 rustmod = policy.importrust('dirstate')
41
41
42 SUPPORTS_DIRSTATE_V2 = rustmod is not None
42 SUPPORTS_DIRSTATE_V2 = rustmod is not None
43
43
44 propertycache = util.propertycache
44 propertycache = util.propertycache
45 filecache = scmutil.filecache
45 filecache = scmutil.filecache
46 _rangemask = 0x7FFFFFFF
46 _rangemask = 0x7FFFFFFF
47
47
48 dirstatetuple = parsers.dirstatetuple
48 dirstatetuple = parsers.dirstatetuple
49
49
50
50
51 class repocache(filecache):
51 class repocache(filecache):
52 """filecache for files in .hg/"""
52 """filecache for files in .hg/"""
53
53
54 def join(self, obj, fname):
54 def join(self, obj, fname):
55 return obj._opener.join(fname)
55 return obj._opener.join(fname)
56
56
57
57
58 class rootcache(filecache):
58 class rootcache(filecache):
59 """filecache for files in the repository root"""
59 """filecache for files in the repository root"""
60
60
61 def join(self, obj, fname):
61 def join(self, obj, fname):
62 return obj._join(fname)
62 return obj._join(fname)
63
63
64
64
65 def _getfsnow(vfs):
65 def _getfsnow(vfs):
66 '''Get "now" timestamp on filesystem'''
66 '''Get "now" timestamp on filesystem'''
67 tmpfd, tmpname = vfs.mkstemp()
67 tmpfd, tmpname = vfs.mkstemp()
68 try:
68 try:
69 return os.fstat(tmpfd)[stat.ST_MTIME]
69 return os.fstat(tmpfd)[stat.ST_MTIME]
70 finally:
70 finally:
71 os.close(tmpfd)
71 os.close(tmpfd)
72 vfs.unlink(tmpname)
72 vfs.unlink(tmpname)
73
73
74
74
75 @interfaceutil.implementer(intdirstate.idirstate)
75 @interfaceutil.implementer(intdirstate.idirstate)
76 class dirstate(object):
76 class dirstate(object):
77 def __init__(
77 def __init__(
78 self,
78 self,
79 opener,
79 opener,
80 ui,
80 ui,
81 root,
81 root,
82 validate,
82 validate,
83 sparsematchfn,
83 sparsematchfn,
84 nodeconstants,
84 nodeconstants,
85 use_dirstate_v2,
85 use_dirstate_v2,
86 ):
86 ):
87 """Create a new dirstate object.
87 """Create a new dirstate object.
88
88
89 opener is an open()-like callable that can be used to open the
89 opener is an open()-like callable that can be used to open the
90 dirstate file; root is the root of the directory tracked by
90 dirstate file; root is the root of the directory tracked by
91 the dirstate.
91 the dirstate.
92 """
92 """
93 self._use_dirstate_v2 = use_dirstate_v2
93 self._use_dirstate_v2 = use_dirstate_v2
94 self._nodeconstants = nodeconstants
94 self._nodeconstants = nodeconstants
95 self._opener = opener
95 self._opener = opener
96 self._validate = validate
96 self._validate = validate
97 self._root = root
97 self._root = root
98 self._sparsematchfn = sparsematchfn
98 self._sparsematchfn = sparsematchfn
99 # ntpath.join(root, '') of Python 2.7.9 does not add sep if root is
99 # ntpath.join(root, '') of Python 2.7.9 does not add sep if root is
100 # UNC path pointing to root share (issue4557)
100 # UNC path pointing to root share (issue4557)
101 self._rootdir = pathutil.normasprefix(root)
101 self._rootdir = pathutil.normasprefix(root)
102 self._dirty = False
102 self._dirty = False
103 self._lastnormaltime = 0
103 self._lastnormaltime = 0
104 self._ui = ui
104 self._ui = ui
105 self._filecache = {}
105 self._filecache = {}
106 self._parentwriters = 0
106 self._parentwriters = 0
107 self._filename = b'dirstate'
107 self._filename = b'dirstate'
108 self._pendingfilename = b'%s.pending' % self._filename
108 self._pendingfilename = b'%s.pending' % self._filename
109 self._plchangecallbacks = {}
109 self._plchangecallbacks = {}
110 self._origpl = None
110 self._origpl = None
111 self._updatedfiles = set()
111 self._updatedfiles = set()
112 self._mapcls = dirstatemap
112 self._mapcls = dirstatemap
113 # Access and cache cwd early, so we don't access it for the first time
113 # Access and cache cwd early, so we don't access it for the first time
114 # after a working-copy update caused it to not exist (accessing it then
114 # after a working-copy update caused it to not exist (accessing it then
115 # raises an exception).
115 # raises an exception).
116 self._cwd
116 self._cwd
117
117
118 def prefetch_parents(self):
118 def prefetch_parents(self):
119 """make sure the parents are loaded
119 """make sure the parents are loaded
120
120
121 Used to avoid a race condition.
121 Used to avoid a race condition.
122 """
122 """
123 self._pl
123 self._pl
124
124
125 @contextlib.contextmanager
125 @contextlib.contextmanager
126 def parentchange(self):
126 def parentchange(self):
127 """Context manager for handling dirstate parents.
127 """Context manager for handling dirstate parents.
128
128
129 If an exception occurs in the scope of the context manager,
129 If an exception occurs in the scope of the context manager,
130 the incoherent dirstate won't be written when wlock is
130 the incoherent dirstate won't be written when wlock is
131 released.
131 released.
132 """
132 """
133 self._parentwriters += 1
133 self._parentwriters += 1
134 yield
134 yield
135 # Typically we want the "undo" step of a context manager in a
135 # Typically we want the "undo" step of a context manager in a
136 # finally block so it happens even when an exception
136 # finally block so it happens even when an exception
137 # occurs. In this case, however, we only want to decrement
137 # occurs. In this case, however, we only want to decrement
138 # parentwriters if the code in the with statement exits
138 # parentwriters if the code in the with statement exits
139 # normally, so we don't have a try/finally here on purpose.
139 # normally, so we don't have a try/finally here on purpose.
140 self._parentwriters -= 1
140 self._parentwriters -= 1
141
141
142 def pendingparentchange(self):
142 def pendingparentchange(self):
143 """Returns true if the dirstate is in the middle of a set of changes
143 """Returns true if the dirstate is in the middle of a set of changes
144 that modify the dirstate parent.
144 that modify the dirstate parent.
145 """
145 """
146 return self._parentwriters > 0
146 return self._parentwriters > 0
147
147
148 @propertycache
148 @propertycache
149 def _map(self):
149 def _map(self):
150 """Return the dirstate contents (see documentation for dirstatemap)."""
150 """Return the dirstate contents (see documentation for dirstatemap)."""
151 self._map = self._mapcls(
151 self._map = self._mapcls(
152 self._ui,
152 self._ui,
153 self._opener,
153 self._opener,
154 self._root,
154 self._root,
155 self._nodeconstants,
155 self._nodeconstants,
156 self._use_dirstate_v2,
156 self._use_dirstate_v2,
157 )
157 )
158 return self._map
158 return self._map
159
159
160 @property
160 @property
161 def _sparsematcher(self):
161 def _sparsematcher(self):
162 """The matcher for the sparse checkout.
162 """The matcher for the sparse checkout.
163
163
164 The working directory may not include every file from a manifest. The
164 The working directory may not include every file from a manifest. The
165 matcher obtained by this property will match a path if it is to be
165 matcher obtained by this property will match a path if it is to be
166 included in the working directory.
166 included in the working directory.
167 """
167 """
168 # TODO there is potential to cache this property. For now, the matcher
168 # TODO there is potential to cache this property. For now, the matcher
169 # is resolved on every access. (But the called function does use a
169 # is resolved on every access. (But the called function does use a
170 # cache to keep the lookup fast.)
170 # cache to keep the lookup fast.)
171 return self._sparsematchfn()
171 return self._sparsematchfn()
172
172
173 @repocache(b'branch')
173 @repocache(b'branch')
174 def _branch(self):
174 def _branch(self):
175 try:
175 try:
176 return self._opener.read(b"branch").strip() or b"default"
176 return self._opener.read(b"branch").strip() or b"default"
177 except IOError as inst:
177 except IOError as inst:
178 if inst.errno != errno.ENOENT:
178 if inst.errno != errno.ENOENT:
179 raise
179 raise
180 return b"default"
180 return b"default"
181
181
182 @property
182 @property
183 def _pl(self):
183 def _pl(self):
184 return self._map.parents()
184 return self._map.parents()
185
185
186 def hasdir(self, d):
186 def hasdir(self, d):
187 return self._map.hastrackeddir(d)
187 return self._map.hastrackeddir(d)
188
188
189 @rootcache(b'.hgignore')
189 @rootcache(b'.hgignore')
190 def _ignore(self):
190 def _ignore(self):
191 files = self._ignorefiles()
191 files = self._ignorefiles()
192 if not files:
192 if not files:
193 return matchmod.never()
193 return matchmod.never()
194
194
195 pats = [b'include:%s' % f for f in files]
195 pats = [b'include:%s' % f for f in files]
196 return matchmod.match(self._root, b'', [], pats, warn=self._ui.warn)
196 return matchmod.match(self._root, b'', [], pats, warn=self._ui.warn)
197
197
198 @propertycache
198 @propertycache
199 def _slash(self):
199 def _slash(self):
200 return self._ui.configbool(b'ui', b'slash') and pycompat.ossep != b'/'
200 return self._ui.configbool(b'ui', b'slash') and pycompat.ossep != b'/'
201
201
202 @propertycache
202 @propertycache
203 def _checklink(self):
203 def _checklink(self):
204 return util.checklink(self._root)
204 return util.checklink(self._root)
205
205
206 @propertycache
206 @propertycache
207 def _checkexec(self):
207 def _checkexec(self):
208 return bool(util.checkexec(self._root))
208 return bool(util.checkexec(self._root))
209
209
210 @propertycache
210 @propertycache
211 def _checkcase(self):
211 def _checkcase(self):
212 return not util.fscasesensitive(self._join(b'.hg'))
212 return not util.fscasesensitive(self._join(b'.hg'))
213
213
214 def _join(self, f):
214 def _join(self, f):
215 # much faster than os.path.join()
215 # much faster than os.path.join()
216 # it's safe because f is always a relative path
216 # it's safe because f is always a relative path
217 return self._rootdir + f
217 return self._rootdir + f
218
218
219 def flagfunc(self, buildfallback):
219 def flagfunc(self, buildfallback):
220 if self._checklink and self._checkexec:
220 if self._checklink and self._checkexec:
221
221
222 def f(x):
222 def f(x):
223 try:
223 try:
224 st = os.lstat(self._join(x))
224 st = os.lstat(self._join(x))
225 if util.statislink(st):
225 if util.statislink(st):
226 return b'l'
226 return b'l'
227 if util.statisexec(st):
227 if util.statisexec(st):
228 return b'x'
228 return b'x'
229 except OSError:
229 except OSError:
230 pass
230 pass
231 return b''
231 return b''
232
232
233 return f
233 return f
234
234
235 fallback = buildfallback()
235 fallback = buildfallback()
236 if self._checklink:
236 if self._checklink:
237
237
238 def f(x):
238 def f(x):
239 if os.path.islink(self._join(x)):
239 if os.path.islink(self._join(x)):
240 return b'l'
240 return b'l'
241 if b'x' in fallback(x):
241 if b'x' in fallback(x):
242 return b'x'
242 return b'x'
243 return b''
243 return b''
244
244
245 return f
245 return f
246 if self._checkexec:
246 if self._checkexec:
247
247
248 def f(x):
248 def f(x):
249 if b'l' in fallback(x):
249 if b'l' in fallback(x):
250 return b'l'
250 return b'l'
251 if util.isexec(self._join(x)):
251 if util.isexec(self._join(x)):
252 return b'x'
252 return b'x'
253 return b''
253 return b''
254
254
255 return f
255 return f
256 else:
256 else:
257 return fallback
257 return fallback
258
258
259 @propertycache
259 @propertycache
260 def _cwd(self):
260 def _cwd(self):
261 # internal config: ui.forcecwd
261 # internal config: ui.forcecwd
262 forcecwd = self._ui.config(b'ui', b'forcecwd')
262 forcecwd = self._ui.config(b'ui', b'forcecwd')
263 if forcecwd:
263 if forcecwd:
264 return forcecwd
264 return forcecwd
265 return encoding.getcwd()
265 return encoding.getcwd()
266
266
267 def getcwd(self):
267 def getcwd(self):
268 """Return the path from which a canonical path is calculated.
268 """Return the path from which a canonical path is calculated.
269
269
270 This path should be used to resolve file patterns or to convert
270 This path should be used to resolve file patterns or to convert
271 canonical paths back to file paths for display. It shouldn't be
271 canonical paths back to file paths for display. It shouldn't be
272 used to get real file paths. Use vfs functions instead.
272 used to get real file paths. Use vfs functions instead.
273 """
273 """
274 cwd = self._cwd
274 cwd = self._cwd
275 if cwd == self._root:
275 if cwd == self._root:
276 return b''
276 return b''
277 # self._root ends with a path separator if self._root is '/' or 'C:\'
277 # self._root ends with a path separator if self._root is '/' or 'C:\'
278 rootsep = self._root
278 rootsep = self._root
279 if not util.endswithsep(rootsep):
279 if not util.endswithsep(rootsep):
280 rootsep += pycompat.ossep
280 rootsep += pycompat.ossep
281 if cwd.startswith(rootsep):
281 if cwd.startswith(rootsep):
282 return cwd[len(rootsep) :]
282 return cwd[len(rootsep) :]
283 else:
283 else:
284 # we're outside the repo. return an absolute path.
284 # we're outside the repo. return an absolute path.
285 return cwd
285 return cwd
286
286
287 def pathto(self, f, cwd=None):
287 def pathto(self, f, cwd=None):
288 if cwd is None:
288 if cwd is None:
289 cwd = self.getcwd()
289 cwd = self.getcwd()
290 path = util.pathto(self._root, cwd, f)
290 path = util.pathto(self._root, cwd, f)
291 if self._slash:
291 if self._slash:
292 return util.pconvert(path)
292 return util.pconvert(path)
293 return path
293 return path
294
294
295 def __getitem__(self, key):
295 def __getitem__(self, key):
296 """Return the current state of key (a filename) in the dirstate.
296 """Return the current state of key (a filename) in the dirstate.
297
297
298 States are:
298 States are:
299 n normal
299 n normal
300 m needs merging
300 m needs merging
301 r marked for removal
301 r marked for removal
302 a marked for addition
302 a marked for addition
303 ? not tracked
303 ? not tracked
304 """
304 """
305 return self._map.get(key, (b"?",))[0]
305 return self._map.get(key, (b"?",))[0]
306
306
307 def __contains__(self, key):
307 def __contains__(self, key):
308 return key in self._map
308 return key in self._map
309
309
310 def __iter__(self):
310 def __iter__(self):
311 return iter(sorted(self._map))
311 return iter(sorted(self._map))
312
312
313 def items(self):
313 def items(self):
314 return pycompat.iteritems(self._map)
314 return pycompat.iteritems(self._map)
315
315
316 iteritems = items
316 iteritems = items
317
317
318 def directories(self):
318 def directories(self):
319 return self._map.directories()
319 return self._map.directories()
320
320
321 def parents(self):
321 def parents(self):
322 return [self._validate(p) for p in self._pl]
322 return [self._validate(p) for p in self._pl]
323
323
324 def p1(self):
324 def p1(self):
325 return self._validate(self._pl[0])
325 return self._validate(self._pl[0])
326
326
327 def p2(self):
327 def p2(self):
328 return self._validate(self._pl[1])
328 return self._validate(self._pl[1])
329
329
330 def branch(self):
330 def branch(self):
331 return encoding.tolocal(self._branch)
331 return encoding.tolocal(self._branch)
332
332
333 def setparents(self, p1, p2=None):
333 def setparents(self, p1, p2=None):
334 """Set dirstate parents to p1 and p2.
334 """Set dirstate parents to p1 and p2.
335
335
336 When moving from two parents to one, 'm' merged entries a
336 When moving from two parents to one, 'm' merged entries a
337 adjusted to normal and previous copy records discarded and
337 adjusted to normal and previous copy records discarded and
338 returned by the call.
338 returned by the call.
339
339
340 See localrepo.setparents()
340 See localrepo.setparents()
341 """
341 """
342 if p2 is None:
342 if p2 is None:
343 p2 = self._nodeconstants.nullid
343 p2 = self._nodeconstants.nullid
344 if self._parentwriters == 0:
344 if self._parentwriters == 0:
345 raise ValueError(
345 raise ValueError(
346 b"cannot set dirstate parent outside of "
346 b"cannot set dirstate parent outside of "
347 b"dirstate.parentchange context manager"
347 b"dirstate.parentchange context manager"
348 )
348 )
349
349
350 self._dirty = True
350 self._dirty = True
351 oldp2 = self._pl[1]
351 oldp2 = self._pl[1]
352 if self._origpl is None:
352 if self._origpl is None:
353 self._origpl = self._pl
353 self._origpl = self._pl
354 self._map.setparents(p1, p2)
354 self._map.setparents(p1, p2)
355 copies = {}
355 copies = {}
356 if (
356 if (
357 oldp2 != self._nodeconstants.nullid
357 oldp2 != self._nodeconstants.nullid
358 and p2 == self._nodeconstants.nullid
358 and p2 == self._nodeconstants.nullid
359 ):
359 ):
360 candidatefiles = self._map.non_normal_or_other_parent_paths()
360 candidatefiles = self._map.non_normal_or_other_parent_paths()
361
361
362 for f in candidatefiles:
362 for f in candidatefiles:
363 s = self._map.get(f)
363 s = self._map.get(f)
364 if s is None:
364 if s is None:
365 continue
365 continue
366
366
367 # Discard 'm' markers when moving away from a merge state
367 # Discard 'm' markers when moving away from a merge state
368 if s[0] == b'm':
368 if s[0] == b'm':
369 source = self._map.copymap.get(f)
369 source = self._map.copymap.get(f)
370 if source:
370 if source:
371 copies[f] = source
371 copies[f] = source
372 self.normallookup(f)
372 self.normallookup(f)
373 # Also fix up otherparent markers
373 # Also fix up otherparent markers
374 elif s[0] == b'n' and s[2] == -2:
374 elif s[0] == b'n' and s[2] == -2:
375 source = self._map.copymap.get(f)
375 source = self._map.copymap.get(f)
376 if source:
376 if source:
377 copies[f] = source
377 copies[f] = source
378 self.add(f)
378 self.add(f)
379 return copies
379 return copies
380
380
381 def setbranch(self, branch):
381 def setbranch(self, branch):
382 self.__class__._branch.set(self, encoding.fromlocal(branch))
382 self.__class__._branch.set(self, encoding.fromlocal(branch))
383 f = self._opener(b'branch', b'w', atomictemp=True, checkambig=True)
383 f = self._opener(b'branch', b'w', atomictemp=True, checkambig=True)
384 try:
384 try:
385 f.write(self._branch + b'\n')
385 f.write(self._branch + b'\n')
386 f.close()
386 f.close()
387
387
388 # make sure filecache has the correct stat info for _branch after
388 # make sure filecache has the correct stat info for _branch after
389 # replacing the underlying file
389 # replacing the underlying file
390 ce = self._filecache[b'_branch']
390 ce = self._filecache[b'_branch']
391 if ce:
391 if ce:
392 ce.refresh()
392 ce.refresh()
393 except: # re-raises
393 except: # re-raises
394 f.discard()
394 f.discard()
395 raise
395 raise
396
396
397 def invalidate(self):
397 def invalidate(self):
398 """Causes the next access to reread the dirstate.
398 """Causes the next access to reread the dirstate.
399
399
400 This is different from localrepo.invalidatedirstate() because it always
400 This is different from localrepo.invalidatedirstate() because it always
401 rereads the dirstate. Use localrepo.invalidatedirstate() if you want to
401 rereads the dirstate. Use localrepo.invalidatedirstate() if you want to
402 check whether the dirstate has changed before rereading it."""
402 check whether the dirstate has changed before rereading it."""
403
403
404 for a in ("_map", "_branch", "_ignore"):
404 for a in ("_map", "_branch", "_ignore"):
405 if a in self.__dict__:
405 if a in self.__dict__:
406 delattr(self, a)
406 delattr(self, a)
407 self._lastnormaltime = 0
407 self._lastnormaltime = 0
408 self._dirty = False
408 self._dirty = False
409 self._updatedfiles.clear()
409 self._updatedfiles.clear()
410 self._parentwriters = 0
410 self._parentwriters = 0
411 self._origpl = None
411 self._origpl = None
412
412
413 def copy(self, source, dest):
413 def copy(self, source, dest):
414 """Mark dest as a copy of source. Unmark dest if source is None."""
414 """Mark dest as a copy of source. Unmark dest if source is None."""
415 if source == dest:
415 if source == dest:
416 return
416 return
417 self._dirty = True
417 self._dirty = True
418 if source is not None:
418 if source is not None:
419 self._map.copymap[dest] = source
419 self._map.copymap[dest] = source
420 self._updatedfiles.add(source)
420 self._updatedfiles.add(source)
421 self._updatedfiles.add(dest)
421 self._updatedfiles.add(dest)
422 elif self._map.copymap.pop(dest, None):
422 elif self._map.copymap.pop(dest, None):
423 self._updatedfiles.add(dest)
423 self._updatedfiles.add(dest)
424
424
425 def copied(self, file):
425 def copied(self, file):
426 return self._map.copymap.get(file, None)
426 return self._map.copymap.get(file, None)
427
427
428 def copies(self):
428 def copies(self):
429 return self._map.copymap
429 return self._map.copymap
430
430
431 def _addpath(self, f, state, mode, size, mtime):
431 def _addpath(self, f, state, mode, size, mtime):
432 oldstate = self[f]
432 oldstate = self[f]
433 if state == b'a' or oldstate == b'r':
433 if state == b'a' or oldstate == b'r':
434 scmutil.checkfilename(f)
434 scmutil.checkfilename(f)
435 if self._map.hastrackeddir(f):
435 if self._map.hastrackeddir(f):
436 raise error.Abort(
436 raise error.Abort(
437 _(b'directory %r already in dirstate') % pycompat.bytestr(f)
437 _(b'directory %r already in dirstate') % pycompat.bytestr(f)
438 )
438 )
439 # shadows
439 # shadows
440 for d in pathutil.finddirs(f):
440 for d in pathutil.finddirs(f):
441 if self._map.hastrackeddir(d):
441 if self._map.hastrackeddir(d):
442 break
442 break
443 entry = self._map.get(d)
443 entry = self._map.get(d)
444 if entry is not None and entry[0] != b'r':
444 if entry is not None and entry[0] != b'r':
445 raise error.Abort(
445 raise error.Abort(
446 _(b'file %r in dirstate clashes with %r')
446 _(b'file %r in dirstate clashes with %r')
447 % (pycompat.bytestr(d), pycompat.bytestr(f))
447 % (pycompat.bytestr(d), pycompat.bytestr(f))
448 )
448 )
449 self._dirty = True
449 self._dirty = True
450 self._updatedfiles.add(f)
450 self._updatedfiles.add(f)
451 self._map.addfile(f, oldstate, state, mode, size, mtime)
451 self._map.addfile(f, oldstate, state, mode, size, mtime)
452
452
453 def normal(self, f, parentfiledata=None):
453 def normal(self, f, parentfiledata=None):
454 """Mark a file normal and clean.
454 """Mark a file normal and clean.
455
455
456 parentfiledata: (mode, size, mtime) of the clean file
456 parentfiledata: (mode, size, mtime) of the clean file
457
457
458 parentfiledata should be computed from memory (for mode,
458 parentfiledata should be computed from memory (for mode,
459 size), as or close as possible from the point where we
459 size), as or close as possible from the point where we
460 determined the file was clean, to limit the risk of the
460 determined the file was clean, to limit the risk of the
461 file having been changed by an external process between the
461 file having been changed by an external process between the
462 moment where the file was determined to be clean and now."""
462 moment where the file was determined to be clean and now."""
463 if parentfiledata:
463 if parentfiledata:
464 (mode, size, mtime) = parentfiledata
464 (mode, size, mtime) = parentfiledata
465 else:
465 else:
466 s = os.lstat(self._join(f))
466 s = os.lstat(self._join(f))
467 mode = s.st_mode
467 mode = s.st_mode
468 size = s.st_size
468 size = s.st_size
469 mtime = s[stat.ST_MTIME]
469 mtime = s[stat.ST_MTIME]
470 self._addpath(f, b'n', mode, size & _rangemask, mtime & _rangemask)
470 self._addpath(f, b'n', mode, size & _rangemask, mtime & _rangemask)
471 self._map.copymap.pop(f, None)
471 self._map.copymap.pop(f, None)
472 if f in self._map.nonnormalset:
472 if f in self._map.nonnormalset:
473 self._map.nonnormalset.remove(f)
473 self._map.nonnormalset.remove(f)
474 if mtime > self._lastnormaltime:
474 if mtime > self._lastnormaltime:
475 # Remember the most recent modification timeslot for status(),
475 # Remember the most recent modification timeslot for status(),
476 # to make sure we won't miss future size-preserving file content
476 # to make sure we won't miss future size-preserving file content
477 # modifications that happen within the same timeslot.
477 # modifications that happen within the same timeslot.
478 self._lastnormaltime = mtime
478 self._lastnormaltime = mtime
479
479
480 def normallookup(self, f):
480 def normallookup(self, f):
481 '''Mark a file normal, but possibly dirty.'''
481 '''Mark a file normal, but possibly dirty.'''
482 if self._pl[1] != self._nodeconstants.nullid:
482 if self._pl[1] != self._nodeconstants.nullid:
483 # if there is a merge going on and the file was either
483 # if there is a merge going on and the file was either
484 # in state 'm' (-1) or coming from other parent (-2) before
484 # in state 'm' (-1) or coming from other parent (-2) before
485 # being removed, restore that state.
485 # being removed, restore that state.
486 entry = self._map.get(f)
486 entry = self._map.get(f)
487 if entry is not None:
487 if entry is not None:
488 if entry[0] == b'r' and entry[2] in (-1, -2):
488 if entry[0] == b'r' and entry[2] in (-1, -2):
489 source = self._map.copymap.get(f)
489 source = self._map.copymap.get(f)
490 if entry[2] == -1:
490 if entry[2] == -1:
491 self.merge(f)
491 self.merge(f)
492 elif entry[2] == -2:
492 elif entry[2] == -2:
493 self.otherparent(f)
493 self.otherparent(f)
494 if source:
494 if source:
495 self.copy(source, f)
495 self.copy(source, f)
496 return
496 return
497 if entry[0] == b'm' or entry[0] == b'n' and entry[2] == -2:
497 if entry[0] == b'm' or entry[0] == b'n' and entry[2] == -2:
498 return
498 return
499 self._addpath(f, b'n', 0, -1, -1)
499 self._addpath(f, b'n', 0, -1, -1)
500 self._map.copymap.pop(f, None)
500 self._map.copymap.pop(f, None)
501
501
502 def otherparent(self, f):
502 def otherparent(self, f):
503 '''Mark as coming from the other parent, always dirty.'''
503 '''Mark as coming from the other parent, always dirty.'''
504 if self._pl[1] == self._nodeconstants.nullid:
504 if self._pl[1] == self._nodeconstants.nullid:
505 raise error.Abort(
505 raise error.Abort(
506 _(b"setting %r to other parent only allowed in merges") % f
506 _(b"setting %r to other parent only allowed in merges") % f
507 )
507 )
508 if f in self and self[f] == b'n':
508 if f in self and self[f] == b'n':
509 # merge-like
509 # merge-like
510 self._addpath(f, b'm', 0, -2, -1)
510 self._addpath(f, b'm', 0, -2, -1)
511 else:
511 else:
512 # add-like
512 # add-like
513 self._addpath(f, b'n', 0, -2, -1)
513 self._addpath(f, b'n', 0, -2, -1)
514 self._map.copymap.pop(f, None)
514 self._map.copymap.pop(f, None)
515
515
516 def add(self, f):
516 def add(self, f):
517 '''Mark a file added.'''
517 '''Mark a file added.'''
518 self._addpath(f, b'a', 0, -1, -1)
518 self._addpath(f, b'a', 0, -1, -1)
519 self._map.copymap.pop(f, None)
519 self._map.copymap.pop(f, None)
520
520
521 def remove(self, f):
521 def remove(self, f):
522 '''Mark a file removed.'''
522 '''Mark a file removed.'''
523 self._dirty = True
523 self._dirty = True
524 oldstate = self[f]
524 oldstate = self[f]
525 size = 0
525 size = 0
526 if self._pl[1] != self._nodeconstants.nullid:
526 if self._pl[1] != self._nodeconstants.nullid:
527 entry = self._map.get(f)
527 entry = self._map.get(f)
528 if entry is not None:
528 if entry is not None:
529 # backup the previous state
529 # backup the previous state
530 if entry[0] == b'm': # merge
530 if entry[0] == b'm': # merge
531 size = -1
531 size = -1
532 elif entry[0] == b'n' and entry[2] == -2: # other parent
532 elif entry[0] == b'n' and entry[2] == -2: # other parent
533 size = -2
533 size = -2
534 self._map.otherparentset.add(f)
534 self._map.otherparentset.add(f)
535 self._updatedfiles.add(f)
535 self._updatedfiles.add(f)
536 self._map.removefile(f, oldstate, size)
536 self._map.removefile(f, oldstate, size)
537 if size == 0:
537 if size == 0:
538 self._map.copymap.pop(f, None)
538 self._map.copymap.pop(f, None)
539
539
540 def merge(self, f):
540 def merge(self, f):
541 '''Mark a file merged.'''
541 '''Mark a file merged.'''
542 if self._pl[1] == self._nodeconstants.nullid:
542 if self._pl[1] == self._nodeconstants.nullid:
543 return self.normallookup(f)
543 return self.normallookup(f)
544 return self.otherparent(f)
544 return self.otherparent(f)
545
545
546 def drop(self, f):
546 def drop(self, f):
547 '''Drop a file from the dirstate'''
547 '''Drop a file from the dirstate'''
548 oldstate = self[f]
548 oldstate = self[f]
549 if self._map.dropfile(f, oldstate):
549 if self._map.dropfile(f, oldstate):
550 self._dirty = True
550 self._dirty = True
551 self._updatedfiles.add(f)
551 self._updatedfiles.add(f)
552 self._map.copymap.pop(f, None)
552 self._map.copymap.pop(f, None)
553
553
554 def _discoverpath(self, path, normed, ignoremissing, exists, storemap):
554 def _discoverpath(self, path, normed, ignoremissing, exists, storemap):
555 if exists is None:
555 if exists is None:
556 exists = os.path.lexists(os.path.join(self._root, path))
556 exists = os.path.lexists(os.path.join(self._root, path))
557 if not exists:
557 if not exists:
558 # Maybe a path component exists
558 # Maybe a path component exists
559 if not ignoremissing and b'/' in path:
559 if not ignoremissing and b'/' in path:
560 d, f = path.rsplit(b'/', 1)
560 d, f = path.rsplit(b'/', 1)
561 d = self._normalize(d, False, ignoremissing, None)
561 d = self._normalize(d, False, ignoremissing, None)
562 folded = d + b"/" + f
562 folded = d + b"/" + f
563 else:
563 else:
564 # No path components, preserve original case
564 # No path components, preserve original case
565 folded = path
565 folded = path
566 else:
566 else:
567 # recursively normalize leading directory components
567 # recursively normalize leading directory components
568 # against dirstate
568 # against dirstate
569 if b'/' in normed:
569 if b'/' in normed:
570 d, f = normed.rsplit(b'/', 1)
570 d, f = normed.rsplit(b'/', 1)
571 d = self._normalize(d, False, ignoremissing, True)
571 d = self._normalize(d, False, ignoremissing, True)
572 r = self._root + b"/" + d
572 r = self._root + b"/" + d
573 folded = d + b"/" + util.fspath(f, r)
573 folded = d + b"/" + util.fspath(f, r)
574 else:
574 else:
575 folded = util.fspath(normed, self._root)
575 folded = util.fspath(normed, self._root)
576 storemap[normed] = folded
576 storemap[normed] = folded
577
577
578 return folded
578 return folded
579
579
580 def _normalizefile(self, path, isknown, ignoremissing=False, exists=None):
580 def _normalizefile(self, path, isknown, ignoremissing=False, exists=None):
581 normed = util.normcase(path)
581 normed = util.normcase(path)
582 folded = self._map.filefoldmap.get(normed, None)
582 folded = self._map.filefoldmap.get(normed, None)
583 if folded is None:
583 if folded is None:
584 if isknown:
584 if isknown:
585 folded = path
585 folded = path
586 else:
586 else:
587 folded = self._discoverpath(
587 folded = self._discoverpath(
588 path, normed, ignoremissing, exists, self._map.filefoldmap
588 path, normed, ignoremissing, exists, self._map.filefoldmap
589 )
589 )
590 return folded
590 return folded
591
591
592 def _normalize(self, path, isknown, ignoremissing=False, exists=None):
592 def _normalize(self, path, isknown, ignoremissing=False, exists=None):
593 normed = util.normcase(path)
593 normed = util.normcase(path)
594 folded = self._map.filefoldmap.get(normed, None)
594 folded = self._map.filefoldmap.get(normed, None)
595 if folded is None:
595 if folded is None:
596 folded = self._map.dirfoldmap.get(normed, None)
596 folded = self._map.dirfoldmap.get(normed, None)
597 if folded is None:
597 if folded is None:
598 if isknown:
598 if isknown:
599 folded = path
599 folded = path
600 else:
600 else:
601 # store discovered result in dirfoldmap so that future
601 # store discovered result in dirfoldmap so that future
602 # normalizefile calls don't start matching directories
602 # normalizefile calls don't start matching directories
603 folded = self._discoverpath(
603 folded = self._discoverpath(
604 path, normed, ignoremissing, exists, self._map.dirfoldmap
604 path, normed, ignoremissing, exists, self._map.dirfoldmap
605 )
605 )
606 return folded
606 return folded
607
607
608 def normalize(self, path, isknown=False, ignoremissing=False):
608 def normalize(self, path, isknown=False, ignoremissing=False):
609 """
609 """
610 normalize the case of a pathname when on a casefolding filesystem
610 normalize the case of a pathname when on a casefolding filesystem
611
611
612 isknown specifies whether the filename came from walking the
612 isknown specifies whether the filename came from walking the
613 disk, to avoid extra filesystem access.
613 disk, to avoid extra filesystem access.
614
614
615 If ignoremissing is True, missing path are returned
615 If ignoremissing is True, missing path are returned
616 unchanged. Otherwise, we try harder to normalize possibly
616 unchanged. Otherwise, we try harder to normalize possibly
617 existing path components.
617 existing path components.
618
618
619 The normalized case is determined based on the following precedence:
619 The normalized case is determined based on the following precedence:
620
620
621 - version of name already stored in the dirstate
621 - version of name already stored in the dirstate
622 - version of name stored on disk
622 - version of name stored on disk
623 - version provided via command arguments
623 - version provided via command arguments
624 """
624 """
625
625
626 if self._checkcase:
626 if self._checkcase:
627 return self._normalize(path, isknown, ignoremissing)
627 return self._normalize(path, isknown, ignoremissing)
628 return path
628 return path
629
629
630 def clear(self):
630 def clear(self):
631 self._map.clear()
631 self._map.clear()
632 self._lastnormaltime = 0
632 self._lastnormaltime = 0
633 self._updatedfiles.clear()
633 self._updatedfiles.clear()
634 self._dirty = True
634 self._dirty = True
635
635
636 def rebuild(self, parent, allfiles, changedfiles=None):
636 def rebuild(self, parent, allfiles, changedfiles=None):
637 if changedfiles is None:
637 if changedfiles is None:
638 # Rebuild entire dirstate
638 # Rebuild entire dirstate
639 to_lookup = allfiles
639 to_lookup = allfiles
640 to_drop = []
640 to_drop = []
641 lastnormaltime = self._lastnormaltime
641 lastnormaltime = self._lastnormaltime
642 self.clear()
642 self.clear()
643 self._lastnormaltime = lastnormaltime
643 self._lastnormaltime = lastnormaltime
644 elif len(changedfiles) < 10:
644 elif len(changedfiles) < 10:
645 # Avoid turning allfiles into a set, which can be expensive if it's
645 # Avoid turning allfiles into a set, which can be expensive if it's
646 # large.
646 # large.
647 to_lookup = []
647 to_lookup = []
648 to_drop = []
648 to_drop = []
649 for f in changedfiles:
649 for f in changedfiles:
650 if f in allfiles:
650 if f in allfiles:
651 to_lookup.append(f)
651 to_lookup.append(f)
652 else:
652 else:
653 to_drop.append(f)
653 to_drop.append(f)
654 else:
654 else:
655 changedfilesset = set(changedfiles)
655 changedfilesset = set(changedfiles)
656 to_lookup = changedfilesset & set(allfiles)
656 to_lookup = changedfilesset & set(allfiles)
657 to_drop = changedfilesset - to_lookup
657 to_drop = changedfilesset - to_lookup
658
658
659 if self._origpl is None:
659 if self._origpl is None:
660 self._origpl = self._pl
660 self._origpl = self._pl
661 self._map.setparents(parent, self._nodeconstants.nullid)
661 self._map.setparents(parent, self._nodeconstants.nullid)
662
662
663 for f in to_lookup:
663 for f in to_lookup:
664 self.normallookup(f)
664 self.normallookup(f)
665 for f in to_drop:
665 for f in to_drop:
666 self.drop(f)
666 self.drop(f)
667
667
668 self._dirty = True
668 self._dirty = True
669
669
670 def identity(self):
670 def identity(self):
671 """Return identity of dirstate itself to detect changing in storage
671 """Return identity of dirstate itself to detect changing in storage
672
672
673 If identity of previous dirstate is equal to this, writing
673 If identity of previous dirstate is equal to this, writing
674 changes based on the former dirstate out can keep consistency.
674 changes based on the former dirstate out can keep consistency.
675 """
675 """
676 return self._map.identity
676 return self._map.identity
677
677
678 def write(self, tr):
678 def write(self, tr):
679 if not self._dirty:
679 if not self._dirty:
680 return
680 return
681
681
682 filename = self._filename
682 filename = self._filename
683 if tr:
683 if tr:
684 # 'dirstate.write()' is not only for writing in-memory
684 # 'dirstate.write()' is not only for writing in-memory
685 # changes out, but also for dropping ambiguous timestamp.
685 # changes out, but also for dropping ambiguous timestamp.
686 # delayed writing re-raise "ambiguous timestamp issue".
686 # delayed writing re-raise "ambiguous timestamp issue".
687 # See also the wiki page below for detail:
687 # See also the wiki page below for detail:
688 # https://www.mercurial-scm.org/wiki/DirstateTransactionPlan
688 # https://www.mercurial-scm.org/wiki/DirstateTransactionPlan
689
689
690 # emulate dropping timestamp in 'parsers.pack_dirstate'
690 # emulate dropping timestamp in 'parsers.pack_dirstate'
691 now = _getfsnow(self._opener)
691 now = _getfsnow(self._opener)
692 self._map.clearambiguoustimes(self._updatedfiles, now)
692 self._map.clearambiguoustimes(self._updatedfiles, now)
693
693
694 # emulate that all 'dirstate.normal' results are written out
694 # emulate that all 'dirstate.normal' results are written out
695 self._lastnormaltime = 0
695 self._lastnormaltime = 0
696 self._updatedfiles.clear()
696 self._updatedfiles.clear()
697
697
698 # delay writing in-memory changes out
698 # delay writing in-memory changes out
699 tr.addfilegenerator(
699 tr.addfilegenerator(
700 b'dirstate',
700 b'dirstate',
701 (self._filename,),
701 (self._filename,),
702 self._writedirstate,
702 self._writedirstate,
703 location=b'plain',
703 location=b'plain',
704 )
704 )
705 return
705 return
706
706
707 st = self._opener(filename, b"w", atomictemp=True, checkambig=True)
707 st = self._opener(filename, b"w", atomictemp=True, checkambig=True)
708 self._writedirstate(st)
708 self._writedirstate(st)
709
709
710 def addparentchangecallback(self, category, callback):
710 def addparentchangecallback(self, category, callback):
711 """add a callback to be called when the wd parents are changed
711 """add a callback to be called when the wd parents are changed
712
712
713 Callback will be called with the following arguments:
713 Callback will be called with the following arguments:
714 dirstate, (oldp1, oldp2), (newp1, newp2)
714 dirstate, (oldp1, oldp2), (newp1, newp2)
715
715
716 Category is a unique identifier to allow overwriting an old callback
716 Category is a unique identifier to allow overwriting an old callback
717 with a newer callback.
717 with a newer callback.
718 """
718 """
719 self._plchangecallbacks[category] = callback
719 self._plchangecallbacks[category] = callback
720
720
721 def _writedirstate(self, st):
721 def _writedirstate(self, st):
722 # notify callbacks about parents change
722 # notify callbacks about parents change
723 if self._origpl is not None and self._origpl != self._pl:
723 if self._origpl is not None and self._origpl != self._pl:
724 for c, callback in sorted(
724 for c, callback in sorted(
725 pycompat.iteritems(self._plchangecallbacks)
725 pycompat.iteritems(self._plchangecallbacks)
726 ):
726 ):
727 callback(self, self._origpl, self._pl)
727 callback(self, self._origpl, self._pl)
728 self._origpl = None
728 self._origpl = None
729 # use the modification time of the newly created temporary file as the
729 # use the modification time of the newly created temporary file as the
730 # filesystem's notion of 'now'
730 # filesystem's notion of 'now'
731 now = util.fstat(st)[stat.ST_MTIME] & _rangemask
731 now = util.fstat(st)[stat.ST_MTIME] & _rangemask
732
732
733 # enough 'delaywrite' prevents 'pack_dirstate' from dropping
733 # enough 'delaywrite' prevents 'pack_dirstate' from dropping
734 # timestamp of each entries in dirstate, because of 'now > mtime'
734 # timestamp of each entries in dirstate, because of 'now > mtime'
735 delaywrite = self._ui.configint(b'debug', b'dirstate.delaywrite')
735 delaywrite = self._ui.configint(b'debug', b'dirstate.delaywrite')
736 if delaywrite > 0:
736 if delaywrite > 0:
737 # do we have any files to delay for?
737 # do we have any files to delay for?
738 for f, e in pycompat.iteritems(self._map):
738 for f, e in pycompat.iteritems(self._map):
739 if e[0] == b'n' and e[3] == now:
739 if e[0] == b'n' and e[3] == now:
740 import time # to avoid useless import
740 import time # to avoid useless import
741
741
742 # rather than sleep n seconds, sleep until the next
742 # rather than sleep n seconds, sleep until the next
743 # multiple of n seconds
743 # multiple of n seconds
744 clock = time.time()
744 clock = time.time()
745 start = int(clock) - (int(clock) % delaywrite)
745 start = int(clock) - (int(clock) % delaywrite)
746 end = start + delaywrite
746 end = start + delaywrite
747 time.sleep(end - clock)
747 time.sleep(end - clock)
748 now = end # trust our estimate that the end is near now
748 now = end # trust our estimate that the end is near now
749 break
749 break
750
750
751 self._map.write(st, now)
751 self._map.write(st, now)
752 self._lastnormaltime = 0
752 self._lastnormaltime = 0
753 self._dirty = False
753 self._dirty = False
754
754
755 def _dirignore(self, f):
755 def _dirignore(self, f):
756 if self._ignore(f):
756 if self._ignore(f):
757 return True
757 return True
758 for p in pathutil.finddirs(f):
758 for p in pathutil.finddirs(f):
759 if self._ignore(p):
759 if self._ignore(p):
760 return True
760 return True
761 return False
761 return False
762
762
763 def _ignorefiles(self):
763 def _ignorefiles(self):
764 files = []
764 files = []
765 if os.path.exists(self._join(b'.hgignore')):
765 if os.path.exists(self._join(b'.hgignore')):
766 files.append(self._join(b'.hgignore'))
766 files.append(self._join(b'.hgignore'))
767 for name, path in self._ui.configitems(b"ui"):
767 for name, path in self._ui.configitems(b"ui"):
768 if name == b'ignore' or name.startswith(b'ignore.'):
768 if name == b'ignore' or name.startswith(b'ignore.'):
769 # we need to use os.path.join here rather than self._join
769 # we need to use os.path.join here rather than self._join
770 # because path is arbitrary and user-specified
770 # because path is arbitrary and user-specified
771 files.append(os.path.join(self._rootdir, util.expandpath(path)))
771 files.append(os.path.join(self._rootdir, util.expandpath(path)))
772 return files
772 return files
773
773
774 def _ignorefileandline(self, f):
774 def _ignorefileandline(self, f):
775 files = collections.deque(self._ignorefiles())
775 files = collections.deque(self._ignorefiles())
776 visited = set()
776 visited = set()
777 while files:
777 while files:
778 i = files.popleft()
778 i = files.popleft()
779 patterns = matchmod.readpatternfile(
779 patterns = matchmod.readpatternfile(
780 i, self._ui.warn, sourceinfo=True
780 i, self._ui.warn, sourceinfo=True
781 )
781 )
782 for pattern, lineno, line in patterns:
782 for pattern, lineno, line in patterns:
783 kind, p = matchmod._patsplit(pattern, b'glob')
783 kind, p = matchmod._patsplit(pattern, b'glob')
784 if kind == b"subinclude":
784 if kind == b"subinclude":
785 if p not in visited:
785 if p not in visited:
786 files.append(p)
786 files.append(p)
787 continue
787 continue
788 m = matchmod.match(
788 m = matchmod.match(
789 self._root, b'', [], [pattern], warn=self._ui.warn
789 self._root, b'', [], [pattern], warn=self._ui.warn
790 )
790 )
791 if m(f):
791 if m(f):
792 return (i, lineno, line)
792 return (i, lineno, line)
793 visited.add(i)
793 visited.add(i)
794 return (None, -1, b"")
794 return (None, -1, b"")
795
795
796 def _walkexplicit(self, match, subrepos):
796 def _walkexplicit(self, match, subrepos):
797 """Get stat data about the files explicitly specified by match.
797 """Get stat data about the files explicitly specified by match.
798
798
799 Return a triple (results, dirsfound, dirsnotfound).
799 Return a triple (results, dirsfound, dirsnotfound).
800 - results is a mapping from filename to stat result. It also contains
800 - results is a mapping from filename to stat result. It also contains
801 listings mapping subrepos and .hg to None.
801 listings mapping subrepos and .hg to None.
802 - dirsfound is a list of files found to be directories.
802 - dirsfound is a list of files found to be directories.
803 - dirsnotfound is a list of files that the dirstate thinks are
803 - dirsnotfound is a list of files that the dirstate thinks are
804 directories and that were not found."""
804 directories and that were not found."""
805
805
806 def badtype(mode):
806 def badtype(mode):
807 kind = _(b'unknown')
807 kind = _(b'unknown')
808 if stat.S_ISCHR(mode):
808 if stat.S_ISCHR(mode):
809 kind = _(b'character device')
809 kind = _(b'character device')
810 elif stat.S_ISBLK(mode):
810 elif stat.S_ISBLK(mode):
811 kind = _(b'block device')
811 kind = _(b'block device')
812 elif stat.S_ISFIFO(mode):
812 elif stat.S_ISFIFO(mode):
813 kind = _(b'fifo')
813 kind = _(b'fifo')
814 elif stat.S_ISSOCK(mode):
814 elif stat.S_ISSOCK(mode):
815 kind = _(b'socket')
815 kind = _(b'socket')
816 elif stat.S_ISDIR(mode):
816 elif stat.S_ISDIR(mode):
817 kind = _(b'directory')
817 kind = _(b'directory')
818 return _(b'unsupported file type (type is %s)') % kind
818 return _(b'unsupported file type (type is %s)') % kind
819
819
820 badfn = match.bad
820 badfn = match.bad
821 dmap = self._map
821 dmap = self._map
822 lstat = os.lstat
822 lstat = os.lstat
823 getkind = stat.S_IFMT
823 getkind = stat.S_IFMT
824 dirkind = stat.S_IFDIR
824 dirkind = stat.S_IFDIR
825 regkind = stat.S_IFREG
825 regkind = stat.S_IFREG
826 lnkkind = stat.S_IFLNK
826 lnkkind = stat.S_IFLNK
827 join = self._join
827 join = self._join
828 dirsfound = []
828 dirsfound = []
829 foundadd = dirsfound.append
829 foundadd = dirsfound.append
830 dirsnotfound = []
830 dirsnotfound = []
831 notfoundadd = dirsnotfound.append
831 notfoundadd = dirsnotfound.append
832
832
833 if not match.isexact() and self._checkcase:
833 if not match.isexact() and self._checkcase:
834 normalize = self._normalize
834 normalize = self._normalize
835 else:
835 else:
836 normalize = None
836 normalize = None
837
837
838 files = sorted(match.files())
838 files = sorted(match.files())
839 subrepos.sort()
839 subrepos.sort()
840 i, j = 0, 0
840 i, j = 0, 0
841 while i < len(files) and j < len(subrepos):
841 while i < len(files) and j < len(subrepos):
842 subpath = subrepos[j] + b"/"
842 subpath = subrepos[j] + b"/"
843 if files[i] < subpath:
843 if files[i] < subpath:
844 i += 1
844 i += 1
845 continue
845 continue
846 while i < len(files) and files[i].startswith(subpath):
846 while i < len(files) and files[i].startswith(subpath):
847 del files[i]
847 del files[i]
848 j += 1
848 j += 1
849
849
850 if not files or b'' in files:
850 if not files or b'' in files:
851 files = [b'']
851 files = [b'']
852 # constructing the foldmap is expensive, so don't do it for the
852 # constructing the foldmap is expensive, so don't do it for the
853 # common case where files is ['']
853 # common case where files is ['']
854 normalize = None
854 normalize = None
855 results = dict.fromkeys(subrepos)
855 results = dict.fromkeys(subrepos)
856 results[b'.hg'] = None
856 results[b'.hg'] = None
857
857
858 for ff in files:
858 for ff in files:
859 if normalize:
859 if normalize:
860 nf = normalize(ff, False, True)
860 nf = normalize(ff, False, True)
861 else:
861 else:
862 nf = ff
862 nf = ff
863 if nf in results:
863 if nf in results:
864 continue
864 continue
865
865
866 try:
866 try:
867 st = lstat(join(nf))
867 st = lstat(join(nf))
868 kind = getkind(st.st_mode)
868 kind = getkind(st.st_mode)
869 if kind == dirkind:
869 if kind == dirkind:
870 if nf in dmap:
870 if nf in dmap:
871 # file replaced by dir on disk but still in dirstate
871 # file replaced by dir on disk but still in dirstate
872 results[nf] = None
872 results[nf] = None
873 foundadd((nf, ff))
873 foundadd((nf, ff))
874 elif kind == regkind or kind == lnkkind:
874 elif kind == regkind or kind == lnkkind:
875 results[nf] = st
875 results[nf] = st
876 else:
876 else:
877 badfn(ff, badtype(kind))
877 badfn(ff, badtype(kind))
878 if nf in dmap:
878 if nf in dmap:
879 results[nf] = None
879 results[nf] = None
880 except OSError as inst: # nf not found on disk - it is dirstate only
880 except OSError as inst: # nf not found on disk - it is dirstate only
881 if nf in dmap: # does it exactly match a missing file?
881 if nf in dmap: # does it exactly match a missing file?
882 results[nf] = None
882 results[nf] = None
883 else: # does it match a missing directory?
883 else: # does it match a missing directory?
884 if self._map.hasdir(nf):
884 if self._map.hasdir(nf):
885 notfoundadd(nf)
885 notfoundadd(nf)
886 else:
886 else:
887 badfn(ff, encoding.strtolocal(inst.strerror))
887 badfn(ff, encoding.strtolocal(inst.strerror))
888
888
889 # match.files() may contain explicitly-specified paths that shouldn't
889 # match.files() may contain explicitly-specified paths that shouldn't
890 # be taken; drop them from the list of files found. dirsfound/notfound
890 # be taken; drop them from the list of files found. dirsfound/notfound
891 # aren't filtered here because they will be tested later.
891 # aren't filtered here because they will be tested later.
892 if match.anypats():
892 if match.anypats():
893 for f in list(results):
893 for f in list(results):
894 if f == b'.hg' or f in subrepos:
894 if f == b'.hg' or f in subrepos:
895 # keep sentinel to disable further out-of-repo walks
895 # keep sentinel to disable further out-of-repo walks
896 continue
896 continue
897 if not match(f):
897 if not match(f):
898 del results[f]
898 del results[f]
899
899
900 # Case insensitive filesystems cannot rely on lstat() failing to detect
900 # Case insensitive filesystems cannot rely on lstat() failing to detect
901 # a case-only rename. Prune the stat object for any file that does not
901 # a case-only rename. Prune the stat object for any file that does not
902 # match the case in the filesystem, if there are multiple files that
902 # match the case in the filesystem, if there are multiple files that
903 # normalize to the same path.
903 # normalize to the same path.
904 if match.isexact() and self._checkcase:
904 if match.isexact() and self._checkcase:
905 normed = {}
905 normed = {}
906
906
907 for f, st in pycompat.iteritems(results):
907 for f, st in pycompat.iteritems(results):
908 if st is None:
908 if st is None:
909 continue
909 continue
910
910
911 nc = util.normcase(f)
911 nc = util.normcase(f)
912 paths = normed.get(nc)
912 paths = normed.get(nc)
913
913
914 if paths is None:
914 if paths is None:
915 paths = set()
915 paths = set()
916 normed[nc] = paths
916 normed[nc] = paths
917
917
918 paths.add(f)
918 paths.add(f)
919
919
920 for norm, paths in pycompat.iteritems(normed):
920 for norm, paths in pycompat.iteritems(normed):
921 if len(paths) > 1:
921 if len(paths) > 1:
922 for path in paths:
922 for path in paths:
923 folded = self._discoverpath(
923 folded = self._discoverpath(
924 path, norm, True, None, self._map.dirfoldmap
924 path, norm, True, None, self._map.dirfoldmap
925 )
925 )
926 if path != folded:
926 if path != folded:
927 results[path] = None
927 results[path] = None
928
928
929 return results, dirsfound, dirsnotfound
929 return results, dirsfound, dirsnotfound
930
930
931 def walk(self, match, subrepos, unknown, ignored, full=True):
931 def walk(self, match, subrepos, unknown, ignored, full=True):
932 """
932 """
933 Walk recursively through the directory tree, finding all files
933 Walk recursively through the directory tree, finding all files
934 matched by match.
934 matched by match.
935
935
936 If full is False, maybe skip some known-clean files.
936 If full is False, maybe skip some known-clean files.
937
937
938 Return a dict mapping filename to stat-like object (either
938 Return a dict mapping filename to stat-like object (either
939 mercurial.osutil.stat instance or return value of os.stat()).
939 mercurial.osutil.stat instance or return value of os.stat()).
940
940
941 """
941 """
942 # full is a flag that extensions that hook into walk can use -- this
942 # full is a flag that extensions that hook into walk can use -- this
943 # implementation doesn't use it at all. This satisfies the contract
943 # implementation doesn't use it at all. This satisfies the contract
944 # because we only guarantee a "maybe".
944 # because we only guarantee a "maybe".
945
945
946 if ignored:
946 if ignored:
947 ignore = util.never
947 ignore = util.never
948 dirignore = util.never
948 dirignore = util.never
949 elif unknown:
949 elif unknown:
950 ignore = self._ignore
950 ignore = self._ignore
951 dirignore = self._dirignore
951 dirignore = self._dirignore
952 else:
952 else:
953 # if not unknown and not ignored, drop dir recursion and step 2
953 # if not unknown and not ignored, drop dir recursion and step 2
954 ignore = util.always
954 ignore = util.always
955 dirignore = util.always
955 dirignore = util.always
956
956
957 matchfn = match.matchfn
957 matchfn = match.matchfn
958 matchalways = match.always()
958 matchalways = match.always()
959 matchtdir = match.traversedir
959 matchtdir = match.traversedir
960 dmap = self._map
960 dmap = self._map
961 listdir = util.listdir
961 listdir = util.listdir
962 lstat = os.lstat
962 lstat = os.lstat
963 dirkind = stat.S_IFDIR
963 dirkind = stat.S_IFDIR
964 regkind = stat.S_IFREG
964 regkind = stat.S_IFREG
965 lnkkind = stat.S_IFLNK
965 lnkkind = stat.S_IFLNK
966 join = self._join
966 join = self._join
967
967
968 exact = skipstep3 = False
968 exact = skipstep3 = False
969 if match.isexact(): # match.exact
969 if match.isexact(): # match.exact
970 exact = True
970 exact = True
971 dirignore = util.always # skip step 2
971 dirignore = util.always # skip step 2
972 elif match.prefix(): # match.match, no patterns
972 elif match.prefix(): # match.match, no patterns
973 skipstep3 = True
973 skipstep3 = True
974
974
975 if not exact and self._checkcase:
975 if not exact and self._checkcase:
976 normalize = self._normalize
976 normalize = self._normalize
977 normalizefile = self._normalizefile
977 normalizefile = self._normalizefile
978 skipstep3 = False
978 skipstep3 = False
979 else:
979 else:
980 normalize = self._normalize
980 normalize = self._normalize
981 normalizefile = None
981 normalizefile = None
982
982
983 # step 1: find all explicit files
983 # step 1: find all explicit files
984 results, work, dirsnotfound = self._walkexplicit(match, subrepos)
984 results, work, dirsnotfound = self._walkexplicit(match, subrepos)
985 if matchtdir:
985 if matchtdir:
986 for d in work:
986 for d in work:
987 matchtdir(d[0])
987 matchtdir(d[0])
988 for d in dirsnotfound:
988 for d in dirsnotfound:
989 matchtdir(d)
989 matchtdir(d)
990
990
991 skipstep3 = skipstep3 and not (work or dirsnotfound)
991 skipstep3 = skipstep3 and not (work or dirsnotfound)
992 work = [d for d in work if not dirignore(d[0])]
992 work = [d for d in work if not dirignore(d[0])]
993
993
994 # step 2: visit subdirectories
994 # step 2: visit subdirectories
995 def traverse(work, alreadynormed):
995 def traverse(work, alreadynormed):
996 wadd = work.append
996 wadd = work.append
997 while work:
997 while work:
998 tracing.counter('dirstate.walk work', len(work))
998 tracing.counter('dirstate.walk work', len(work))
999 nd = work.pop()
999 nd = work.pop()
1000 visitentries = match.visitchildrenset(nd)
1000 visitentries = match.visitchildrenset(nd)
1001 if not visitentries:
1001 if not visitentries:
1002 continue
1002 continue
1003 if visitentries == b'this' or visitentries == b'all':
1003 if visitentries == b'this' or visitentries == b'all':
1004 visitentries = None
1004 visitentries = None
1005 skip = None
1005 skip = None
1006 if nd != b'':
1006 if nd != b'':
1007 skip = b'.hg'
1007 skip = b'.hg'
1008 try:
1008 try:
1009 with tracing.log('dirstate.walk.traverse listdir %s', nd):
1009 with tracing.log('dirstate.walk.traverse listdir %s', nd):
1010 entries = listdir(join(nd), stat=True, skip=skip)
1010 entries = listdir(join(nd), stat=True, skip=skip)
1011 except OSError as inst:
1011 except OSError as inst:
1012 if inst.errno in (errno.EACCES, errno.ENOENT):
1012 if inst.errno in (errno.EACCES, errno.ENOENT):
1013 match.bad(
1013 match.bad(
1014 self.pathto(nd), encoding.strtolocal(inst.strerror)
1014 self.pathto(nd), encoding.strtolocal(inst.strerror)
1015 )
1015 )
1016 continue
1016 continue
1017 raise
1017 raise
1018 for f, kind, st in entries:
1018 for f, kind, st in entries:
1019 # Some matchers may return files in the visitentries set,
1019 # Some matchers may return files in the visitentries set,
1020 # instead of 'this', if the matcher explicitly mentions them
1020 # instead of 'this', if the matcher explicitly mentions them
1021 # and is not an exactmatcher. This is acceptable; we do not
1021 # and is not an exactmatcher. This is acceptable; we do not
1022 # make any hard assumptions about file-or-directory below
1022 # make any hard assumptions about file-or-directory below
1023 # based on the presence of `f` in visitentries. If
1023 # based on the presence of `f` in visitentries. If
1024 # visitchildrenset returned a set, we can always skip the
1024 # visitchildrenset returned a set, we can always skip the
1025 # entries *not* in the set it provided regardless of whether
1025 # entries *not* in the set it provided regardless of whether
1026 # they're actually a file or a directory.
1026 # they're actually a file or a directory.
1027 if visitentries and f not in visitentries:
1027 if visitentries and f not in visitentries:
1028 continue
1028 continue
1029 if normalizefile:
1029 if normalizefile:
1030 # even though f might be a directory, we're only
1030 # even though f might be a directory, we're only
1031 # interested in comparing it to files currently in the
1031 # interested in comparing it to files currently in the
1032 # dmap -- therefore normalizefile is enough
1032 # dmap -- therefore normalizefile is enough
1033 nf = normalizefile(
1033 nf = normalizefile(
1034 nd and (nd + b"/" + f) or f, True, True
1034 nd and (nd + b"/" + f) or f, True, True
1035 )
1035 )
1036 else:
1036 else:
1037 nf = nd and (nd + b"/" + f) or f
1037 nf = nd and (nd + b"/" + f) or f
1038 if nf not in results:
1038 if nf not in results:
1039 if kind == dirkind:
1039 if kind == dirkind:
1040 if not ignore(nf):
1040 if not ignore(nf):
1041 if matchtdir:
1041 if matchtdir:
1042 matchtdir(nf)
1042 matchtdir(nf)
1043 wadd(nf)
1043 wadd(nf)
1044 if nf in dmap and (matchalways or matchfn(nf)):
1044 if nf in dmap and (matchalways or matchfn(nf)):
1045 results[nf] = None
1045 results[nf] = None
1046 elif kind == regkind or kind == lnkkind:
1046 elif kind == regkind or kind == lnkkind:
1047 if nf in dmap:
1047 if nf in dmap:
1048 if matchalways or matchfn(nf):
1048 if matchalways or matchfn(nf):
1049 results[nf] = st
1049 results[nf] = st
1050 elif (matchalways or matchfn(nf)) and not ignore(
1050 elif (matchalways or matchfn(nf)) and not ignore(
1051 nf
1051 nf
1052 ):
1052 ):
1053 # unknown file -- normalize if necessary
1053 # unknown file -- normalize if necessary
1054 if not alreadynormed:
1054 if not alreadynormed:
1055 nf = normalize(nf, False, True)
1055 nf = normalize(nf, False, True)
1056 results[nf] = st
1056 results[nf] = st
1057 elif nf in dmap and (matchalways or matchfn(nf)):
1057 elif nf in dmap and (matchalways or matchfn(nf)):
1058 results[nf] = None
1058 results[nf] = None
1059
1059
1060 for nd, d in work:
1060 for nd, d in work:
1061 # alreadynormed means that processwork doesn't have to do any
1061 # alreadynormed means that processwork doesn't have to do any
1062 # expensive directory normalization
1062 # expensive directory normalization
1063 alreadynormed = not normalize or nd == d
1063 alreadynormed = not normalize or nd == d
1064 traverse([d], alreadynormed)
1064 traverse([d], alreadynormed)
1065
1065
1066 for s in subrepos:
1066 for s in subrepos:
1067 del results[s]
1067 del results[s]
1068 del results[b'.hg']
1068 del results[b'.hg']
1069
1069
1070 # step 3: visit remaining files from dmap
1070 # step 3: visit remaining files from dmap
1071 if not skipstep3 and not exact:
1071 if not skipstep3 and not exact:
1072 # If a dmap file is not in results yet, it was either
1072 # If a dmap file is not in results yet, it was either
1073 # a) not matching matchfn b) ignored, c) missing, or d) under a
1073 # a) not matching matchfn b) ignored, c) missing, or d) under a
1074 # symlink directory.
1074 # symlink directory.
1075 if not results and matchalways:
1075 if not results and matchalways:
1076 visit = [f for f in dmap]
1076 visit = [f for f in dmap]
1077 else:
1077 else:
1078 visit = [f for f in dmap if f not in results and matchfn(f)]
1078 visit = [f for f in dmap if f not in results and matchfn(f)]
1079 visit.sort()
1079 visit.sort()
1080
1080
1081 if unknown:
1081 if unknown:
1082 # unknown == True means we walked all dirs under the roots
1082 # unknown == True means we walked all dirs under the roots
1083 # that wasn't ignored, and everything that matched was stat'ed
1083 # that wasn't ignored, and everything that matched was stat'ed
1084 # and is already in results.
1084 # and is already in results.
1085 # The rest must thus be ignored or under a symlink.
1085 # The rest must thus be ignored or under a symlink.
1086 audit_path = pathutil.pathauditor(self._root, cached=True)
1086 audit_path = pathutil.pathauditor(self._root, cached=True)
1087
1087
1088 for nf in iter(visit):
1088 for nf in iter(visit):
1089 # If a stat for the same file was already added with a
1089 # If a stat for the same file was already added with a
1090 # different case, don't add one for this, since that would
1090 # different case, don't add one for this, since that would
1091 # make it appear as if the file exists under both names
1091 # make it appear as if the file exists under both names
1092 # on disk.
1092 # on disk.
1093 if (
1093 if (
1094 normalizefile
1094 normalizefile
1095 and normalizefile(nf, True, True) in results
1095 and normalizefile(nf, True, True) in results
1096 ):
1096 ):
1097 results[nf] = None
1097 results[nf] = None
1098 # Report ignored items in the dmap as long as they are not
1098 # Report ignored items in the dmap as long as they are not
1099 # under a symlink directory.
1099 # under a symlink directory.
1100 elif audit_path.check(nf):
1100 elif audit_path.check(nf):
1101 try:
1101 try:
1102 results[nf] = lstat(join(nf))
1102 results[nf] = lstat(join(nf))
1103 # file was just ignored, no links, and exists
1103 # file was just ignored, no links, and exists
1104 except OSError:
1104 except OSError:
1105 # file doesn't exist
1105 # file doesn't exist
1106 results[nf] = None
1106 results[nf] = None
1107 else:
1107 else:
1108 # It's either missing or under a symlink directory
1108 # It's either missing or under a symlink directory
1109 # which we in this case report as missing
1109 # which we in this case report as missing
1110 results[nf] = None
1110 results[nf] = None
1111 else:
1111 else:
1112 # We may not have walked the full directory tree above,
1112 # We may not have walked the full directory tree above,
1113 # so stat and check everything we missed.
1113 # so stat and check everything we missed.
1114 iv = iter(visit)
1114 iv = iter(visit)
1115 for st in util.statfiles([join(i) for i in visit]):
1115 for st in util.statfiles([join(i) for i in visit]):
1116 results[next(iv)] = st
1116 results[next(iv)] = st
1117 return results
1117 return results
1118
1118
1119 def _rust_status(self, matcher, list_clean, list_ignored, list_unknown):
1119 def _rust_status(self, matcher, list_clean, list_ignored, list_unknown):
1120 # Force Rayon (Rust parallelism library) to respect the number of
1120 # Force Rayon (Rust parallelism library) to respect the number of
1121 # workers. This is a temporary workaround until Rust code knows
1121 # workers. This is a temporary workaround until Rust code knows
1122 # how to read the config file.
1122 # how to read the config file.
1123 numcpus = self._ui.configint(b"worker", b"numcpus")
1123 numcpus = self._ui.configint(b"worker", b"numcpus")
1124 if numcpus is not None:
1124 if numcpus is not None:
1125 encoding.environ.setdefault(b'RAYON_NUM_THREADS', b'%d' % numcpus)
1125 encoding.environ.setdefault(b'RAYON_NUM_THREADS', b'%d' % numcpus)
1126
1126
1127 workers_enabled = self._ui.configbool(b"worker", b"enabled", True)
1127 workers_enabled = self._ui.configbool(b"worker", b"enabled", True)
1128 if not workers_enabled:
1128 if not workers_enabled:
1129 encoding.environ[b"RAYON_NUM_THREADS"] = b"1"
1129 encoding.environ[b"RAYON_NUM_THREADS"] = b"1"
1130
1130
1131 (
1131 (
1132 lookup,
1132 lookup,
1133 modified,
1133 modified,
1134 added,
1134 added,
1135 removed,
1135 removed,
1136 deleted,
1136 deleted,
1137 clean,
1137 clean,
1138 ignored,
1138 ignored,
1139 unknown,
1139 unknown,
1140 warnings,
1140 warnings,
1141 bad,
1141 bad,
1142 traversed,
1142 traversed,
1143 dirty,
1143 dirty,
1144 ) = rustmod.status(
1144 ) = rustmod.status(
1145 self._map._rustmap,
1145 self._map._rustmap,
1146 matcher,
1146 matcher,
1147 self._rootdir,
1147 self._rootdir,
1148 self._ignorefiles(),
1148 self._ignorefiles(),
1149 self._checkexec,
1149 self._checkexec,
1150 self._lastnormaltime,
1150 self._lastnormaltime,
1151 bool(list_clean),
1151 bool(list_clean),
1152 bool(list_ignored),
1152 bool(list_ignored),
1153 bool(list_unknown),
1153 bool(list_unknown),
1154 bool(matcher.traversedir),
1154 bool(matcher.traversedir),
1155 )
1155 )
1156
1156
1157 self._dirty |= dirty
1157 self._dirty |= dirty
1158
1158
1159 if matcher.traversedir:
1159 if matcher.traversedir:
1160 for dir in traversed:
1160 for dir in traversed:
1161 matcher.traversedir(dir)
1161 matcher.traversedir(dir)
1162
1162
1163 if self._ui.warn:
1163 if self._ui.warn:
1164 for item in warnings:
1164 for item in warnings:
1165 if isinstance(item, tuple):
1165 if isinstance(item, tuple):
1166 file_path, syntax = item
1166 file_path, syntax = item
1167 msg = _(b"%s: ignoring invalid syntax '%s'\n") % (
1167 msg = _(b"%s: ignoring invalid syntax '%s'\n") % (
1168 file_path,
1168 file_path,
1169 syntax,
1169 syntax,
1170 )
1170 )
1171 self._ui.warn(msg)
1171 self._ui.warn(msg)
1172 else:
1172 else:
1173 msg = _(b"skipping unreadable pattern file '%s': %s\n")
1173 msg = _(b"skipping unreadable pattern file '%s': %s\n")
1174 self._ui.warn(
1174 self._ui.warn(
1175 msg
1175 msg
1176 % (
1176 % (
1177 pathutil.canonpath(
1177 pathutil.canonpath(
1178 self._rootdir, self._rootdir, item
1178 self._rootdir, self._rootdir, item
1179 ),
1179 ),
1180 b"No such file or directory",
1180 b"No such file or directory",
1181 )
1181 )
1182 )
1182 )
1183
1183
1184 for (fn, message) in bad:
1184 for (fn, message) in bad:
1185 matcher.bad(fn, encoding.strtolocal(message))
1185 matcher.bad(fn, encoding.strtolocal(message))
1186
1186
1187 status = scmutil.status(
1187 status = scmutil.status(
1188 modified=modified,
1188 modified=modified,
1189 added=added,
1189 added=added,
1190 removed=removed,
1190 removed=removed,
1191 deleted=deleted,
1191 deleted=deleted,
1192 unknown=unknown,
1192 unknown=unknown,
1193 ignored=ignored,
1193 ignored=ignored,
1194 clean=clean,
1194 clean=clean,
1195 )
1195 )
1196 return (lookup, status)
1196 return (lookup, status)
1197
1197
1198 def status(self, match, subrepos, ignored, clean, unknown):
1198 def status(self, match, subrepos, ignored, clean, unknown):
1199 """Determine the status of the working copy relative to the
1199 """Determine the status of the working copy relative to the
1200 dirstate and return a pair of (unsure, status), where status is of type
1200 dirstate and return a pair of (unsure, status), where status is of type
1201 scmutil.status and:
1201 scmutil.status and:
1202
1202
1203 unsure:
1203 unsure:
1204 files that might have been modified since the dirstate was
1204 files that might have been modified since the dirstate was
1205 written, but need to be read to be sure (size is the same
1205 written, but need to be read to be sure (size is the same
1206 but mtime differs)
1206 but mtime differs)
1207 status.modified:
1207 status.modified:
1208 files that have definitely been modified since the dirstate
1208 files that have definitely been modified since the dirstate
1209 was written (different size or mode)
1209 was written (different size or mode)
1210 status.clean:
1210 status.clean:
1211 files that have definitely not been modified since the
1211 files that have definitely not been modified since the
1212 dirstate was written
1212 dirstate was written
1213 """
1213 """
1214 listignored, listclean, listunknown = ignored, clean, unknown
1214 listignored, listclean, listunknown = ignored, clean, unknown
1215 lookup, modified, added, unknown, ignored = [], [], [], [], []
1215 lookup, modified, added, unknown, ignored = [], [], [], [], []
1216 removed, deleted, clean = [], [], []
1216 removed, deleted, clean = [], [], []
1217
1217
1218 dmap = self._map
1218 dmap = self._map
1219 dmap.preload()
1219 dmap.preload()
1220
1220
1221 use_rust = True
1221 use_rust = True
1222
1222
1223 allowed_matchers = (
1223 allowed_matchers = (
1224 matchmod.alwaysmatcher,
1224 matchmod.alwaysmatcher,
1225 matchmod.exactmatcher,
1225 matchmod.exactmatcher,
1226 matchmod.includematcher,
1226 matchmod.includematcher,
1227 )
1227 )
1228
1228
1229 if rustmod is None:
1229 if rustmod is None:
1230 use_rust = False
1230 use_rust = False
1231 elif self._checkcase:
1231 elif self._checkcase:
1232 # Case-insensitive filesystems are not handled yet
1232 # Case-insensitive filesystems are not handled yet
1233 use_rust = False
1233 use_rust = False
1234 elif subrepos:
1234 elif subrepos:
1235 use_rust = False
1235 use_rust = False
1236 elif sparse.enabled:
1236 elif sparse.enabled:
1237 use_rust = False
1237 use_rust = False
1238 elif not isinstance(match, allowed_matchers):
1238 elif not isinstance(match, allowed_matchers):
1239 # Some matchers have yet to be implemented
1239 # Some matchers have yet to be implemented
1240 use_rust = False
1240 use_rust = False
1241
1241
1242 if use_rust:
1242 if use_rust:
1243 try:
1243 try:
1244 return self._rust_status(
1244 return self._rust_status(
1245 match, listclean, listignored, listunknown
1245 match, listclean, listignored, listunknown
1246 )
1246 )
1247 except rustmod.FallbackError:
1247 except rustmod.FallbackError:
1248 pass
1248 pass
1249
1249
1250 def noop(f):
1250 def noop(f):
1251 pass
1251 pass
1252
1252
1253 dcontains = dmap.__contains__
1253 dcontains = dmap.__contains__
1254 dget = dmap.__getitem__
1254 dget = dmap.__getitem__
1255 ladd = lookup.append # aka "unsure"
1255 ladd = lookup.append # aka "unsure"
1256 madd = modified.append
1256 madd = modified.append
1257 aadd = added.append
1257 aadd = added.append
1258 uadd = unknown.append if listunknown else noop
1258 uadd = unknown.append if listunknown else noop
1259 iadd = ignored.append if listignored else noop
1259 iadd = ignored.append if listignored else noop
1260 radd = removed.append
1260 radd = removed.append
1261 dadd = deleted.append
1261 dadd = deleted.append
1262 cadd = clean.append if listclean else noop
1262 cadd = clean.append if listclean else noop
1263 mexact = match.exact
1263 mexact = match.exact
1264 dirignore = self._dirignore
1264 dirignore = self._dirignore
1265 checkexec = self._checkexec
1265 checkexec = self._checkexec
1266 copymap = self._map.copymap
1266 copymap = self._map.copymap
1267 lastnormaltime = self._lastnormaltime
1267 lastnormaltime = self._lastnormaltime
1268
1268
1269 # We need to do full walks when either
1269 # We need to do full walks when either
1270 # - we're listing all clean files, or
1270 # - we're listing all clean files, or
1271 # - match.traversedir does something, because match.traversedir should
1271 # - match.traversedir does something, because match.traversedir should
1272 # be called for every dir in the working dir
1272 # be called for every dir in the working dir
1273 full = listclean or match.traversedir is not None
1273 full = listclean or match.traversedir is not None
1274 for fn, st in pycompat.iteritems(
1274 for fn, st in pycompat.iteritems(
1275 self.walk(match, subrepos, listunknown, listignored, full=full)
1275 self.walk(match, subrepos, listunknown, listignored, full=full)
1276 ):
1276 ):
1277 if not dcontains(fn):
1277 if not dcontains(fn):
1278 if (listignored or mexact(fn)) and dirignore(fn):
1278 if (listignored or mexact(fn)) and dirignore(fn):
1279 if listignored:
1279 if listignored:
1280 iadd(fn)
1280 iadd(fn)
1281 else:
1281 else:
1282 uadd(fn)
1282 uadd(fn)
1283 continue
1283 continue
1284
1284
1285 # This is equivalent to 'state, mode, size, time = dmap[fn]' but not
1285 # This is equivalent to 'state, mode, size, time = dmap[fn]' but not
1286 # written like that for performance reasons. dmap[fn] is not a
1286 # written like that for performance reasons. dmap[fn] is not a
1287 # Python tuple in compiled builds. The CPython UNPACK_SEQUENCE
1287 # Python tuple in compiled builds. The CPython UNPACK_SEQUENCE
1288 # opcode has fast paths when the value to be unpacked is a tuple or
1288 # opcode has fast paths when the value to be unpacked is a tuple or
1289 # a list, but falls back to creating a full-fledged iterator in
1289 # a list, but falls back to creating a full-fledged iterator in
1290 # general. That is much slower than simply accessing and storing the
1290 # general. That is much slower than simply accessing and storing the
1291 # tuple members one by one.
1291 # tuple members one by one.
1292 t = dget(fn)
1292 t = dget(fn)
1293 state = t[0]
1293 state = t[0]
1294 mode = t[1]
1294 mode = t[1]
1295 size = t[2]
1295 size = t[2]
1296 time = t[3]
1296 time = t[3]
1297
1297
1298 if not st and state in b"nma":
1298 if not st and state in b"nma":
1299 dadd(fn)
1299 dadd(fn)
1300 elif state == b'n':
1300 elif state == b'n':
1301 if (
1301 if (
1302 size >= 0
1302 size >= 0
1303 and (
1303 and (
1304 (size != st.st_size and size != st.st_size & _rangemask)
1304 (size != st.st_size and size != st.st_size & _rangemask)
1305 or ((mode ^ st.st_mode) & 0o100 and checkexec)
1305 or ((mode ^ st.st_mode) & 0o100 and checkexec)
1306 )
1306 )
1307 or size == -2 # other parent
1307 or size == -2 # other parent
1308 or fn in copymap
1308 or fn in copymap
1309 ):
1309 ):
1310 if stat.S_ISLNK(st.st_mode) and size != st.st_size:
1310 if stat.S_ISLNK(st.st_mode) and size != st.st_size:
1311 # issue6456: Size returned may be longer due to
1311 # issue6456: Size returned may be longer due to
1312 # encryption on EXT-4 fscrypt, undecided.
1312 # encryption on EXT-4 fscrypt, undecided.
1313 ladd(fn)
1313 ladd(fn)
1314 else:
1314 else:
1315 madd(fn)
1315 madd(fn)
1316 elif (
1316 elif (
1317 time != st[stat.ST_MTIME]
1317 time != st[stat.ST_MTIME]
1318 and time != st[stat.ST_MTIME] & _rangemask
1318 and time != st[stat.ST_MTIME] & _rangemask
1319 ):
1319 ):
1320 ladd(fn)
1320 ladd(fn)
1321 elif st[stat.ST_MTIME] == lastnormaltime:
1321 elif st[stat.ST_MTIME] == lastnormaltime:
1322 # fn may have just been marked as normal and it may have
1322 # fn may have just been marked as normal and it may have
1323 # changed in the same second without changing its size.
1323 # changed in the same second without changing its size.
1324 # This can happen if we quickly do multiple commits.
1324 # This can happen if we quickly do multiple commits.
1325 # Force lookup, so we don't miss such a racy file change.
1325 # Force lookup, so we don't miss such a racy file change.
1326 ladd(fn)
1326 ladd(fn)
1327 elif listclean:
1327 elif listclean:
1328 cadd(fn)
1328 cadd(fn)
1329 elif state == b'm':
1329 elif state == b'm':
1330 madd(fn)
1330 madd(fn)
1331 elif state == b'a':
1331 elif state == b'a':
1332 aadd(fn)
1332 aadd(fn)
1333 elif state == b'r':
1333 elif state == b'r':
1334 radd(fn)
1334 radd(fn)
1335 status = scmutil.status(
1335 status = scmutil.status(
1336 modified, added, removed, deleted, unknown, ignored, clean
1336 modified, added, removed, deleted, unknown, ignored, clean
1337 )
1337 )
1338 return (lookup, status)
1338 return (lookup, status)
1339
1339
1340 def matches(self, match):
1340 def matches(self, match):
1341 """
1341 """
1342 return files in the dirstate (in whatever state) filtered by match
1342 return files in the dirstate (in whatever state) filtered by match
1343 """
1343 """
1344 dmap = self._map
1344 dmap = self._map
1345 if rustmod is not None:
1345 if rustmod is not None:
1346 dmap = self._map._rustmap
1346 dmap = self._map._rustmap
1347
1347
1348 if match.always():
1348 if match.always():
1349 return dmap.keys()
1349 return dmap.keys()
1350 files = match.files()
1350 files = match.files()
1351 if match.isexact():
1351 if match.isexact():
1352 # fast path -- filter the other way around, since typically files is
1352 # fast path -- filter the other way around, since typically files is
1353 # much smaller than dmap
1353 # much smaller than dmap
1354 return [f for f in files if f in dmap]
1354 return [f for f in files if f in dmap]
1355 if match.prefix() and all(fn in dmap for fn in files):
1355 if match.prefix() and all(fn in dmap for fn in files):
1356 # fast path -- all the values are known to be files, so just return
1356 # fast path -- all the values are known to be files, so just return
1357 # that
1357 # that
1358 return list(files)
1358 return list(files)
1359 return [f for f in dmap if match(f)]
1359 return [f for f in dmap if match(f)]
1360
1360
1361 def _actualfilename(self, tr):
1361 def _actualfilename(self, tr):
1362 if tr:
1362 if tr:
1363 return self._pendingfilename
1363 return self._pendingfilename
1364 else:
1364 else:
1365 return self._filename
1365 return self._filename
1366
1366
1367 def savebackup(self, tr, backupname):
1367 def savebackup(self, tr, backupname):
1368 '''Save current dirstate into backup file'''
1368 '''Save current dirstate into backup file'''
1369 filename = self._actualfilename(tr)
1369 filename = self._actualfilename(tr)
1370 assert backupname != filename
1370 assert backupname != filename
1371
1371
1372 # use '_writedirstate' instead of 'write' to write changes certainly,
1372 # use '_writedirstate' instead of 'write' to write changes certainly,
1373 # because the latter omits writing out if transaction is running.
1373 # because the latter omits writing out if transaction is running.
1374 # output file will be used to create backup of dirstate at this point.
1374 # output file will be used to create backup of dirstate at this point.
1375 if self._dirty or not self._opener.exists(filename):
1375 if self._dirty or not self._opener.exists(filename):
1376 self._writedirstate(
1376 self._writedirstate(
1377 self._opener(filename, b"w", atomictemp=True, checkambig=True)
1377 self._opener(filename, b"w", atomictemp=True, checkambig=True)
1378 )
1378 )
1379
1379
1380 if tr:
1380 if tr:
1381 # ensure that subsequent tr.writepending returns True for
1381 # ensure that subsequent tr.writepending returns True for
1382 # changes written out above, even if dirstate is never
1382 # changes written out above, even if dirstate is never
1383 # changed after this
1383 # changed after this
1384 tr.addfilegenerator(
1384 tr.addfilegenerator(
1385 b'dirstate',
1385 b'dirstate',
1386 (self._filename,),
1386 (self._filename,),
1387 self._writedirstate,
1387 self._writedirstate,
1388 location=b'plain',
1388 location=b'plain',
1389 )
1389 )
1390
1390
1391 # ensure that pending file written above is unlinked at
1391 # ensure that pending file written above is unlinked at
1392 # failure, even if tr.writepending isn't invoked until the
1392 # failure, even if tr.writepending isn't invoked until the
1393 # end of this transaction
1393 # end of this transaction
1394 tr.registertmp(filename, location=b'plain')
1394 tr.registertmp(filename, location=b'plain')
1395
1395
1396 self._opener.tryunlink(backupname)
1396 self._opener.tryunlink(backupname)
1397 # hardlink backup is okay because _writedirstate is always called
1397 # hardlink backup is okay because _writedirstate is always called
1398 # with an "atomictemp=True" file.
1398 # with an "atomictemp=True" file.
1399 util.copyfile(
1399 util.copyfile(
1400 self._opener.join(filename),
1400 self._opener.join(filename),
1401 self._opener.join(backupname),
1401 self._opener.join(backupname),
1402 hardlink=True,
1402 hardlink=True,
1403 )
1403 )
1404
1404
1405 def restorebackup(self, tr, backupname):
1405 def restorebackup(self, tr, backupname):
1406 '''Restore dirstate by backup file'''
1406 '''Restore dirstate by backup file'''
1407 # this "invalidate()" prevents "wlock.release()" from writing
1407 # this "invalidate()" prevents "wlock.release()" from writing
1408 # changes of dirstate out after restoring from backup file
1408 # changes of dirstate out after restoring from backup file
1409 self.invalidate()
1409 self.invalidate()
1410 filename = self._actualfilename(tr)
1410 filename = self._actualfilename(tr)
1411 o = self._opener
1411 o = self._opener
1412 if util.samefile(o.join(backupname), o.join(filename)):
1412 if util.samefile(o.join(backupname), o.join(filename)):
1413 o.unlink(backupname)
1413 o.unlink(backupname)
1414 else:
1414 else:
1415 o.rename(backupname, filename, checkambig=True)
1415 o.rename(backupname, filename, checkambig=True)
1416
1416
1417 def clearbackup(self, tr, backupname):
1417 def clearbackup(self, tr, backupname):
1418 '''Clear backup file'''
1418 '''Clear backup file'''
1419 self._opener.unlink(backupname)
1419 self._opener.unlink(backupname)
1420
1420
1421
1421
1422 class dirstatemap(object):
1422 class dirstatemap(object):
1423 """Map encapsulating the dirstate's contents.
1423 """Map encapsulating the dirstate's contents.
1424
1424
1425 The dirstate contains the following state:
1425 The dirstate contains the following state:
1426
1426
1427 - `identity` is the identity of the dirstate file, which can be used to
1427 - `identity` is the identity of the dirstate file, which can be used to
1428 detect when changes have occurred to the dirstate file.
1428 detect when changes have occurred to the dirstate file.
1429
1429
1430 - `parents` is a pair containing the parents of the working copy. The
1430 - `parents` is a pair containing the parents of the working copy. The
1431 parents are updated by calling `setparents`.
1431 parents are updated by calling `setparents`.
1432
1432
1433 - the state map maps filenames to tuples of (state, mode, size, mtime),
1433 - the state map maps filenames to tuples of (state, mode, size, mtime),
1434 where state is a single character representing 'normal', 'added',
1434 where state is a single character representing 'normal', 'added',
1435 'removed', or 'merged'. It is read by treating the dirstate as a
1435 'removed', or 'merged'. It is read by treating the dirstate as a
1436 dict. File state is updated by calling the `addfile`, `removefile` and
1436 dict. File state is updated by calling the `addfile`, `removefile` and
1437 `dropfile` methods.
1437 `dropfile` methods.
1438
1438
1439 - `copymap` maps destination filenames to their source filename.
1439 - `copymap` maps destination filenames to their source filename.
1440
1440
1441 The dirstate also provides the following views onto the state:
1441 The dirstate also provides the following views onto the state:
1442
1442
1443 - `nonnormalset` is a set of the filenames that have state other
1443 - `nonnormalset` is a set of the filenames that have state other
1444 than 'normal', or are normal but have an mtime of -1 ('normallookup').
1444 than 'normal', or are normal but have an mtime of -1 ('normallookup').
1445
1445
1446 - `otherparentset` is a set of the filenames that are marked as coming
1446 - `otherparentset` is a set of the filenames that are marked as coming
1447 from the second parent when the dirstate is currently being merged.
1447 from the second parent when the dirstate is currently being merged.
1448
1448
1449 - `filefoldmap` is a dict mapping normalized filenames to the denormalized
1449 - `filefoldmap` is a dict mapping normalized filenames to the denormalized
1450 form that they appear as in the dirstate.
1450 form that they appear as in the dirstate.
1451
1451
1452 - `dirfoldmap` is a dict mapping normalized directory names to the
1452 - `dirfoldmap` is a dict mapping normalized directory names to the
1453 denormalized form that they appear as in the dirstate.
1453 denormalized form that they appear as in the dirstate.
1454 """
1454 """
1455
1455
1456 def __init__(self, ui, opener, root, nodeconstants, use_dirstate_v2):
1456 def __init__(self, ui, opener, root, nodeconstants, use_dirstate_v2):
1457 self._ui = ui
1457 self._ui = ui
1458 self._opener = opener
1458 self._opener = opener
1459 self._root = root
1459 self._root = root
1460 self._filename = b'dirstate'
1460 self._filename = b'dirstate'
1461 self._nodelen = 20
1461 self._nodelen = 20
1462 self._nodeconstants = nodeconstants
1462 self._nodeconstants = nodeconstants
1463 assert (
1463 assert (
1464 not use_dirstate_v2
1464 not use_dirstate_v2
1465 ), "should have detected unsupported requirement"
1465 ), "should have detected unsupported requirement"
1466
1466
1467 self._parents = None
1467 self._parents = None
1468 self._dirtyparents = False
1468 self._dirtyparents = False
1469
1469
1470 # for consistent view between _pl() and _read() invocations
1470 # for consistent view between _pl() and _read() invocations
1471 self._pendingmode = None
1471 self._pendingmode = None
1472
1472
1473 @propertycache
1473 @propertycache
1474 def _map(self):
1474 def _map(self):
1475 self._map = {}
1475 self._map = {}
1476 self.read()
1476 self.read()
1477 return self._map
1477 return self._map
1478
1478
1479 @propertycache
1479 @propertycache
1480 def copymap(self):
1480 def copymap(self):
1481 self.copymap = {}
1481 self.copymap = {}
1482 self._map
1482 self._map
1483 return self.copymap
1483 return self.copymap
1484
1484
1485 def directories(self):
1485 def directories(self):
1486 # Rust / dirstate-v2 only
1486 # Rust / dirstate-v2 only
1487 return []
1487 return []
1488
1488
1489 def clear(self):
1489 def clear(self):
1490 self._map.clear()
1490 self._map.clear()
1491 self.copymap.clear()
1491 self.copymap.clear()
1492 self.setparents(self._nodeconstants.nullid, self._nodeconstants.nullid)
1492 self.setparents(self._nodeconstants.nullid, self._nodeconstants.nullid)
1493 util.clearcachedproperty(self, b"_dirs")
1493 util.clearcachedproperty(self, b"_dirs")
1494 util.clearcachedproperty(self, b"_alldirs")
1494 util.clearcachedproperty(self, b"_alldirs")
1495 util.clearcachedproperty(self, b"filefoldmap")
1495 util.clearcachedproperty(self, b"filefoldmap")
1496 util.clearcachedproperty(self, b"dirfoldmap")
1496 util.clearcachedproperty(self, b"dirfoldmap")
1497 util.clearcachedproperty(self, b"nonnormalset")
1497 util.clearcachedproperty(self, b"nonnormalset")
1498 util.clearcachedproperty(self, b"otherparentset")
1498 util.clearcachedproperty(self, b"otherparentset")
1499
1499
1500 def items(self):
1500 def items(self):
1501 return pycompat.iteritems(self._map)
1501 return pycompat.iteritems(self._map)
1502
1502
1503 # forward for python2,3 compat
1503 # forward for python2,3 compat
1504 iteritems = items
1504 iteritems = items
1505
1505
1506 def __len__(self):
1506 def __len__(self):
1507 return len(self._map)
1507 return len(self._map)
1508
1508
1509 def __iter__(self):
1509 def __iter__(self):
1510 return iter(self._map)
1510 return iter(self._map)
1511
1511
1512 def get(self, key, default=None):
1512 def get(self, key, default=None):
1513 return self._map.get(key, default)
1513 return self._map.get(key, default)
1514
1514
1515 def __contains__(self, key):
1515 def __contains__(self, key):
1516 return key in self._map
1516 return key in self._map
1517
1517
1518 def __getitem__(self, key):
1518 def __getitem__(self, key):
1519 return self._map[key]
1519 return self._map[key]
1520
1520
1521 def keys(self):
1521 def keys(self):
1522 return self._map.keys()
1522 return self._map.keys()
1523
1523
1524 def preload(self):
1524 def preload(self):
1525 """Loads the underlying data, if it's not already loaded"""
1525 """Loads the underlying data, if it's not already loaded"""
1526 self._map
1526 self._map
1527
1527
1528 def addfile(self, f, oldstate, state, mode, size, mtime):
1528 def addfile(self, f, oldstate, state, mode, size, mtime):
1529 """Add a tracked file to the dirstate."""
1529 """Add a tracked file to the dirstate."""
1530 if oldstate in b"?r" and "_dirs" in self.__dict__:
1530 if oldstate in b"?r" and "_dirs" in self.__dict__:
1531 self._dirs.addpath(f)
1531 self._dirs.addpath(f)
1532 if oldstate == b"?" and "_alldirs" in self.__dict__:
1532 if oldstate == b"?" and "_alldirs" in self.__dict__:
1533 self._alldirs.addpath(f)
1533 self._alldirs.addpath(f)
1534 self._map[f] = dirstatetuple(state, mode, size, mtime)
1534 self._map[f] = dirstatetuple(state, mode, size, mtime)
1535 if state != b'n' or mtime == -1:
1535 if state != b'n' or mtime == -1:
1536 self.nonnormalset.add(f)
1536 self.nonnormalset.add(f)
1537 if size == -2:
1537 if size == -2:
1538 self.otherparentset.add(f)
1538 self.otherparentset.add(f)
1539
1539
1540 def removefile(self, f, oldstate, size):
1540 def removefile(self, f, oldstate, size):
1541 """
1541 """
1542 Mark a file as removed in the dirstate.
1542 Mark a file as removed in the dirstate.
1543
1543
1544 The `size` parameter is used to store sentinel values that indicate
1544 The `size` parameter is used to store sentinel values that indicate
1545 the file's previous state. In the future, we should refactor this
1545 the file's previous state. In the future, we should refactor this
1546 to be more explicit about what that state is.
1546 to be more explicit about what that state is.
1547 """
1547 """
1548 if oldstate not in b"?r" and "_dirs" in self.__dict__:
1548 if oldstate not in b"?r" and "_dirs" in self.__dict__:
1549 self._dirs.delpath(f)
1549 self._dirs.delpath(f)
1550 if oldstate == b"?" and "_alldirs" in self.__dict__:
1550 if oldstate == b"?" and "_alldirs" in self.__dict__:
1551 self._alldirs.addpath(f)
1551 self._alldirs.addpath(f)
1552 if "filefoldmap" in self.__dict__:
1552 if "filefoldmap" in self.__dict__:
1553 normed = util.normcase(f)
1553 normed = util.normcase(f)
1554 self.filefoldmap.pop(normed, None)
1554 self.filefoldmap.pop(normed, None)
1555 self._map[f] = dirstatetuple(b'r', 0, size, 0)
1555 self._map[f] = dirstatetuple(b'r', 0, size, 0)
1556 self.nonnormalset.add(f)
1556 self.nonnormalset.add(f)
1557
1557
1558 def dropfile(self, f, oldstate):
1558 def dropfile(self, f, oldstate):
1559 """
1559 """
1560 Remove a file from the dirstate. Returns True if the file was
1560 Remove a file from the dirstate. Returns True if the file was
1561 previously recorded.
1561 previously recorded.
1562 """
1562 """
1563 exists = self._map.pop(f, None) is not None
1563 exists = self._map.pop(f, None) is not None
1564 if exists:
1564 if exists:
1565 if oldstate != b"r" and "_dirs" in self.__dict__:
1565 if oldstate != b"r" and "_dirs" in self.__dict__:
1566 self._dirs.delpath(f)
1566 self._dirs.delpath(f)
1567 if "_alldirs" in self.__dict__:
1567 if "_alldirs" in self.__dict__:
1568 self._alldirs.delpath(f)
1568 self._alldirs.delpath(f)
1569 if "filefoldmap" in self.__dict__:
1569 if "filefoldmap" in self.__dict__:
1570 normed = util.normcase(f)
1570 normed = util.normcase(f)
1571 self.filefoldmap.pop(normed, None)
1571 self.filefoldmap.pop(normed, None)
1572 self.nonnormalset.discard(f)
1572 self.nonnormalset.discard(f)
1573 return exists
1573 return exists
1574
1574
1575 def clearambiguoustimes(self, files, now):
1575 def clearambiguoustimes(self, files, now):
1576 for f in files:
1576 for f in files:
1577 e = self.get(f)
1577 e = self.get(f)
1578 if e is not None and e[0] == b'n' and e[3] == now:
1578 if e is not None and e[0] == b'n' and e[3] == now:
1579 self._map[f] = dirstatetuple(e[0], e[1], e[2], -1)
1579 self._map[f] = dirstatetuple(e[0], e[1], e[2], -1)
1580 self.nonnormalset.add(f)
1580 self.nonnormalset.add(f)
1581
1581
1582 def nonnormalentries(self):
1582 def nonnormalentries(self):
1583 '''Compute the nonnormal dirstate entries from the dmap'''
1583 '''Compute the nonnormal dirstate entries from the dmap'''
1584 try:
1584 try:
1585 return parsers.nonnormalotherparententries(self._map)
1585 return parsers.nonnormalotherparententries(self._map)
1586 except AttributeError:
1586 except AttributeError:
1587 nonnorm = set()
1587 nonnorm = set()
1588 otherparent = set()
1588 otherparent = set()
1589 for fname, e in pycompat.iteritems(self._map):
1589 for fname, e in pycompat.iteritems(self._map):
1590 if e[0] != b'n' or e[3] == -1:
1590 if e[0] != b'n' or e[3] == -1:
1591 nonnorm.add(fname)
1591 nonnorm.add(fname)
1592 if e[0] == b'n' and e[2] == -2:
1592 if e[0] == b'n' and e[2] == -2:
1593 otherparent.add(fname)
1593 otherparent.add(fname)
1594 return nonnorm, otherparent
1594 return nonnorm, otherparent
1595
1595
1596 @propertycache
1596 @propertycache
1597 def filefoldmap(self):
1597 def filefoldmap(self):
1598 """Returns a dictionary mapping normalized case paths to their
1598 """Returns a dictionary mapping normalized case paths to their
1599 non-normalized versions.
1599 non-normalized versions.
1600 """
1600 """
1601 try:
1601 try:
1602 makefilefoldmap = parsers.make_file_foldmap
1602 makefilefoldmap = parsers.make_file_foldmap
1603 except AttributeError:
1603 except AttributeError:
1604 pass
1604 pass
1605 else:
1605 else:
1606 return makefilefoldmap(
1606 return makefilefoldmap(
1607 self._map, util.normcasespec, util.normcasefallback
1607 self._map, util.normcasespec, util.normcasefallback
1608 )
1608 )
1609
1609
1610 f = {}
1610 f = {}
1611 normcase = util.normcase
1611 normcase = util.normcase
1612 for name, s in pycompat.iteritems(self._map):
1612 for name, s in pycompat.iteritems(self._map):
1613 if s[0] != b'r':
1613 if s[0] != b'r':
1614 f[normcase(name)] = name
1614 f[normcase(name)] = name
1615 f[b'.'] = b'.' # prevents useless util.fspath() invocation
1615 f[b'.'] = b'.' # prevents useless util.fspath() invocation
1616 return f
1616 return f
1617
1617
1618 def hastrackeddir(self, d):
1618 def hastrackeddir(self, d):
1619 """
1619 """
1620 Returns True if the dirstate contains a tracked (not removed) file
1620 Returns True if the dirstate contains a tracked (not removed) file
1621 in this directory.
1621 in this directory.
1622 """
1622 """
1623 return d in self._dirs
1623 return d in self._dirs
1624
1624
1625 def hasdir(self, d):
1625 def hasdir(self, d):
1626 """
1626 """
1627 Returns True if the dirstate contains a file (tracked or removed)
1627 Returns True if the dirstate contains a file (tracked or removed)
1628 in this directory.
1628 in this directory.
1629 """
1629 """
1630 return d in self._alldirs
1630 return d in self._alldirs
1631
1631
1632 @propertycache
1632 @propertycache
1633 def _dirs(self):
1633 def _dirs(self):
1634 return pathutil.dirs(self._map, b'r')
1634 return pathutil.dirs(self._map, b'r')
1635
1635
1636 @propertycache
1636 @propertycache
1637 def _alldirs(self):
1637 def _alldirs(self):
1638 return pathutil.dirs(self._map)
1638 return pathutil.dirs(self._map)
1639
1639
1640 def _opendirstatefile(self):
1640 def _opendirstatefile(self):
1641 fp, mode = txnutil.trypending(self._root, self._opener, self._filename)
1641 fp, mode = txnutil.trypending(self._root, self._opener, self._filename)
1642 if self._pendingmode is not None and self._pendingmode != mode:
1642 if self._pendingmode is not None and self._pendingmode != mode:
1643 fp.close()
1643 fp.close()
1644 raise error.Abort(
1644 raise error.Abort(
1645 _(b'working directory state may be changed parallelly')
1645 _(b'working directory state may be changed parallelly')
1646 )
1646 )
1647 self._pendingmode = mode
1647 self._pendingmode = mode
1648 return fp
1648 return fp
1649
1649
1650 def parents(self):
1650 def parents(self):
1651 if not self._parents:
1651 if not self._parents:
1652 try:
1652 try:
1653 fp = self._opendirstatefile()
1653 fp = self._opendirstatefile()
1654 st = fp.read(2 * self._nodelen)
1654 st = fp.read(2 * self._nodelen)
1655 fp.close()
1655 fp.close()
1656 except IOError as err:
1656 except IOError as err:
1657 if err.errno != errno.ENOENT:
1657 if err.errno != errno.ENOENT:
1658 raise
1658 raise
1659 # File doesn't exist, so the current state is empty
1659 # File doesn't exist, so the current state is empty
1660 st = b''
1660 st = b''
1661
1661
1662 l = len(st)
1662 l = len(st)
1663 if l == self._nodelen * 2:
1663 if l == self._nodelen * 2:
1664 self._parents = (
1664 self._parents = (
1665 st[: self._nodelen],
1665 st[: self._nodelen],
1666 st[self._nodelen : 2 * self._nodelen],
1666 st[self._nodelen : 2 * self._nodelen],
1667 )
1667 )
1668 elif l == 0:
1668 elif l == 0:
1669 self._parents = (
1669 self._parents = (
1670 self._nodeconstants.nullid,
1670 self._nodeconstants.nullid,
1671 self._nodeconstants.nullid,
1671 self._nodeconstants.nullid,
1672 )
1672 )
1673 else:
1673 else:
1674 raise error.Abort(
1674 raise error.Abort(
1675 _(b'working directory state appears damaged!')
1675 _(b'working directory state appears damaged!')
1676 )
1676 )
1677
1677
1678 return self._parents
1678 return self._parents
1679
1679
1680 def setparents(self, p1, p2):
1680 def setparents(self, p1, p2):
1681 self._parents = (p1, p2)
1681 self._parents = (p1, p2)
1682 self._dirtyparents = True
1682 self._dirtyparents = True
1683
1683
1684 def read(self):
1684 def read(self):
1685 # ignore HG_PENDING because identity is used only for writing
1685 # ignore HG_PENDING because identity is used only for writing
1686 self.identity = util.filestat.frompath(
1686 self.identity = util.filestat.frompath(
1687 self._opener.join(self._filename)
1687 self._opener.join(self._filename)
1688 )
1688 )
1689
1689
1690 try:
1690 try:
1691 fp = self._opendirstatefile()
1691 fp = self._opendirstatefile()
1692 try:
1692 try:
1693 st = fp.read()
1693 st = fp.read()
1694 finally:
1694 finally:
1695 fp.close()
1695 fp.close()
1696 except IOError as err:
1696 except IOError as err:
1697 if err.errno != errno.ENOENT:
1697 if err.errno != errno.ENOENT:
1698 raise
1698 raise
1699 return
1699 return
1700 if not st:
1700 if not st:
1701 return
1701 return
1702
1702
1703 if util.safehasattr(parsers, b'dict_new_presized'):
1703 if util.safehasattr(parsers, b'dict_new_presized'):
1704 # Make an estimate of the number of files in the dirstate based on
1704 # Make an estimate of the number of files in the dirstate based on
1705 # its size. This trades wasting some memory for avoiding costly
1705 # its size. This trades wasting some memory for avoiding costly
1706 # resizes. Each entry have a prefix of 17 bytes followed by one or
1706 # resizes. Each entry have a prefix of 17 bytes followed by one or
1707 # two path names. Studies on various large-scale real-world repositories
1707 # two path names. Studies on various large-scale real-world repositories
1708 # found 54 bytes a reasonable upper limit for the average path names.
1708 # found 54 bytes a reasonable upper limit for the average path names.
1709 # Copy entries are ignored for the sake of this estimate.
1709 # Copy entries are ignored for the sake of this estimate.
1710 self._map = parsers.dict_new_presized(len(st) // 71)
1710 self._map = parsers.dict_new_presized(len(st) // 71)
1711
1711
1712 # Python's garbage collector triggers a GC each time a certain number
1712 # Python's garbage collector triggers a GC each time a certain number
1713 # of container objects (the number being defined by
1713 # of container objects (the number being defined by
1714 # gc.get_threshold()) are allocated. parse_dirstate creates a tuple
1714 # gc.get_threshold()) are allocated. parse_dirstate creates a tuple
1715 # for each file in the dirstate. The C version then immediately marks
1715 # for each file in the dirstate. The C version then immediately marks
1716 # them as not to be tracked by the collector. However, this has no
1716 # them as not to be tracked by the collector. However, this has no
1717 # effect on when GCs are triggered, only on what objects the GC looks
1717 # effect on when GCs are triggered, only on what objects the GC looks
1718 # into. This means that O(number of files) GCs are unavoidable.
1718 # into. This means that O(number of files) GCs are unavoidable.
1719 # Depending on when in the process's lifetime the dirstate is parsed,
1719 # Depending on when in the process's lifetime the dirstate is parsed,
1720 # this can get very expensive. As a workaround, disable GC while
1720 # this can get very expensive. As a workaround, disable GC while
1721 # parsing the dirstate.
1721 # parsing the dirstate.
1722 #
1722 #
1723 # (we cannot decorate the function directly since it is in a C module)
1723 # (we cannot decorate the function directly since it is in a C module)
1724 parse_dirstate = util.nogc(parsers.parse_dirstate)
1724 parse_dirstate = util.nogc(parsers.parse_dirstate)
1725 p = parse_dirstate(self._map, self.copymap, st)
1725 p = parse_dirstate(self._map, self.copymap, st)
1726 if not self._dirtyparents:
1726 if not self._dirtyparents:
1727 self.setparents(*p)
1727 self.setparents(*p)
1728
1728
1729 # Avoid excess attribute lookups by fast pathing certain checks
1729 # Avoid excess attribute lookups by fast pathing certain checks
1730 self.__contains__ = self._map.__contains__
1730 self.__contains__ = self._map.__contains__
1731 self.__getitem__ = self._map.__getitem__
1731 self.__getitem__ = self._map.__getitem__
1732 self.get = self._map.get
1732 self.get = self._map.get
1733
1733
1734 def write(self, st, now):
1734 def write(self, st, now):
1735 st.write(
1735 st.write(
1736 parsers.pack_dirstate(self._map, self.copymap, self.parents(), now)
1736 parsers.pack_dirstate(self._map, self.copymap, self.parents(), now)
1737 )
1737 )
1738 st.close()
1738 st.close()
1739 self._dirtyparents = False
1739 self._dirtyparents = False
1740 self.nonnormalset, self.otherparentset = self.nonnormalentries()
1740 self.nonnormalset, self.otherparentset = self.nonnormalentries()
1741
1741
1742 @propertycache
1742 @propertycache
1743 def nonnormalset(self):
1743 def nonnormalset(self):
1744 nonnorm, otherparents = self.nonnormalentries()
1744 nonnorm, otherparents = self.nonnormalentries()
1745 self.otherparentset = otherparents
1745 self.otherparentset = otherparents
1746 return nonnorm
1746 return nonnorm
1747
1747
1748 @propertycache
1748 @propertycache
1749 def otherparentset(self):
1749 def otherparentset(self):
1750 nonnorm, otherparents = self.nonnormalentries()
1750 nonnorm, otherparents = self.nonnormalentries()
1751 self.nonnormalset = nonnorm
1751 self.nonnormalset = nonnorm
1752 return otherparents
1752 return otherparents
1753
1753
1754 def non_normal_or_other_parent_paths(self):
1754 def non_normal_or_other_parent_paths(self):
1755 return self.nonnormalset.union(self.otherparentset)
1755 return self.nonnormalset.union(self.otherparentset)
1756
1756
1757 @propertycache
1757 @propertycache
1758 def identity(self):
1758 def identity(self):
1759 self._map
1759 self._map
1760 return self.identity
1760 return self.identity
1761
1761
1762 @propertycache
1762 @propertycache
1763 def dirfoldmap(self):
1763 def dirfoldmap(self):
1764 f = {}
1764 f = {}
1765 normcase = util.normcase
1765 normcase = util.normcase
1766 for name in self._dirs:
1766 for name in self._dirs:
1767 f[normcase(name)] = name
1767 f[normcase(name)] = name
1768 return f
1768 return f
1769
1769
1770
1770
1771 if rustmod is not None:
1771 if rustmod is not None:
1772
1772
1773 class dirstatemap(object):
1773 class dirstatemap(object):
1774 def __init__(self, ui, opener, root, nodeconstants, use_dirstate_v2):
1774 def __init__(self, ui, opener, root, nodeconstants, use_dirstate_v2):
1775 self._use_dirstate_v2 = use_dirstate_v2
1775 self._use_dirstate_v2 = use_dirstate_v2
1776 self._nodeconstants = nodeconstants
1776 self._nodeconstants = nodeconstants
1777 self._ui = ui
1777 self._ui = ui
1778 self._opener = opener
1778 self._opener = opener
1779 self._root = root
1779 self._root = root
1780 self._filename = b'dirstate'
1780 self._filename = b'dirstate'
1781 self._nodelen = 20 # Also update Rust code when changing this!
1781 self._nodelen = 20 # Also update Rust code when changing this!
1782 self._parents = None
1782 self._parents = None
1783 self._dirtyparents = False
1783 self._dirtyparents = False
1784
1784
1785 # for consistent view between _pl() and _read() invocations
1785 # for consistent view between _pl() and _read() invocations
1786 self._pendingmode = None
1786 self._pendingmode = None
1787
1787
1788 self._use_dirstate_tree = self._ui.configbool(
1788 self._use_dirstate_tree = self._ui.configbool(
1789 b"experimental",
1789 b"experimental",
1790 b"dirstate-tree.in-memory",
1790 b"dirstate-tree.in-memory",
1791 False,
1791 False,
1792 )
1792 )
1793
1793
1794 def addfile(self, *args, **kwargs):
1794 def addfile(self, *args, **kwargs):
1795 return self._rustmap.addfile(*args, **kwargs)
1795 return self._rustmap.addfile(*args, **kwargs)
1796
1796
1797 def removefile(self, *args, **kwargs):
1797 def removefile(self, *args, **kwargs):
1798 return self._rustmap.removefile(*args, **kwargs)
1798 return self._rustmap.removefile(*args, **kwargs)
1799
1799
1800 def dropfile(self, *args, **kwargs):
1800 def dropfile(self, *args, **kwargs):
1801 return self._rustmap.dropfile(*args, **kwargs)
1801 return self._rustmap.dropfile(*args, **kwargs)
1802
1802
1803 def clearambiguoustimes(self, *args, **kwargs):
1803 def clearambiguoustimes(self, *args, **kwargs):
1804 return self._rustmap.clearambiguoustimes(*args, **kwargs)
1804 return self._rustmap.clearambiguoustimes(*args, **kwargs)
1805
1805
1806 def nonnormalentries(self):
1806 def nonnormalentries(self):
1807 return self._rustmap.nonnormalentries()
1807 return self._rustmap.nonnormalentries()
1808
1808
1809 def get(self, *args, **kwargs):
1809 def get(self, *args, **kwargs):
1810 return self._rustmap.get(*args, **kwargs)
1810 return self._rustmap.get(*args, **kwargs)
1811
1811
1812 @property
1812 @property
1813 def copymap(self):
1813 def copymap(self):
1814 return self._rustmap.copymap()
1814 return self._rustmap.copymap()
1815
1815
1816 def directories(self):
1816 def directories(self):
1817 return self._rustmap.directories()
1817 return self._rustmap.directories()
1818
1818
1819 def preload(self):
1819 def preload(self):
1820 self._rustmap
1820 self._rustmap
1821
1821
1822 def clear(self):
1822 def clear(self):
1823 self._rustmap.clear()
1823 self._rustmap.clear()
1824 self.setparents(
1824 self.setparents(
1825 self._nodeconstants.nullid, self._nodeconstants.nullid
1825 self._nodeconstants.nullid, self._nodeconstants.nullid
1826 )
1826 )
1827 util.clearcachedproperty(self, b"_dirs")
1827 util.clearcachedproperty(self, b"_dirs")
1828 util.clearcachedproperty(self, b"_alldirs")
1828 util.clearcachedproperty(self, b"_alldirs")
1829 util.clearcachedproperty(self, b"dirfoldmap")
1829 util.clearcachedproperty(self, b"dirfoldmap")
1830
1830
1831 def items(self):
1831 def items(self):
1832 return self._rustmap.items()
1832 return self._rustmap.items()
1833
1833
1834 def keys(self):
1834 def keys(self):
1835 return iter(self._rustmap)
1835 return iter(self._rustmap)
1836
1836
1837 def __contains__(self, key):
1837 def __contains__(self, key):
1838 return key in self._rustmap
1838 return key in self._rustmap
1839
1839
1840 def __getitem__(self, item):
1840 def __getitem__(self, item):
1841 return self._rustmap[item]
1841 return self._rustmap[item]
1842
1842
1843 def __len__(self):
1843 def __len__(self):
1844 return len(self._rustmap)
1844 return len(self._rustmap)
1845
1845
1846 def __iter__(self):
1846 def __iter__(self):
1847 return iter(self._rustmap)
1847 return iter(self._rustmap)
1848
1848
1849 # forward for python2,3 compat
1849 # forward for python2,3 compat
1850 iteritems = items
1850 iteritems = items
1851
1851
1852 def _opendirstatefile(self):
1852 def _opendirstatefile(self):
1853 fp, mode = txnutil.trypending(
1853 fp, mode = txnutil.trypending(
1854 self._root, self._opener, self._filename
1854 self._root, self._opener, self._filename
1855 )
1855 )
1856 if self._pendingmode is not None and self._pendingmode != mode:
1856 if self._pendingmode is not None and self._pendingmode != mode:
1857 fp.close()
1857 fp.close()
1858 raise error.Abort(
1858 raise error.Abort(
1859 _(b'working directory state may be changed parallelly')
1859 _(b'working directory state may be changed parallelly')
1860 )
1860 )
1861 self._pendingmode = mode
1861 self._pendingmode = mode
1862 return fp
1862 return fp
1863
1863
1864 def setparents(self, p1, p2):
1864 def setparents(self, p1, p2):
1865 self._parents = (p1, p2)
1865 self._parents = (p1, p2)
1866 self._dirtyparents = True
1866 self._dirtyparents = True
1867
1867
1868 def parents(self):
1868 def parents(self):
1869 if not self._parents:
1869 if not self._parents:
1870 if self._use_dirstate_v2:
1870 if self._use_dirstate_v2:
1871 offset = len(rustmod.V2_FORMAT_MARKER)
1871 offset = len(rustmod.V2_FORMAT_MARKER)
1872 else:
1872 else:
1873 offset = 0
1873 offset = 0
1874 read_len = offset + self._nodelen * 2
1874 read_len = offset + self._nodelen * 2
1875 try:
1875 try:
1876 fp = self._opendirstatefile()
1876 fp = self._opendirstatefile()
1877 st = fp.read(read_len)
1877 st = fp.read(read_len)
1878 fp.close()
1878 fp.close()
1879 except IOError as err:
1879 except IOError as err:
1880 if err.errno != errno.ENOENT:
1880 if err.errno != errno.ENOENT:
1881 raise
1881 raise
1882 # File doesn't exist, so the current state is empty
1882 # File doesn't exist, so the current state is empty
1883 st = b''
1883 st = b''
1884
1884
1885 l = len(st)
1885 l = len(st)
1886 if l == read_len:
1886 if l == read_len:
1887 st = st[offset:]
1887 st = st[offset:]
1888 self._parents = (
1888 self._parents = (
1889 st[: self._nodelen],
1889 st[: self._nodelen],
1890 st[self._nodelen : 2 * self._nodelen],
1890 st[self._nodelen : 2 * self._nodelen],
1891 )
1891 )
1892 elif l == 0:
1892 elif l == 0:
1893 self._parents = (
1893 self._parents = (
1894 self._nodeconstants.nullid,
1894 self._nodeconstants.nullid,
1895 self._nodeconstants.nullid,
1895 self._nodeconstants.nullid,
1896 )
1896 )
1897 else:
1897 else:
1898 raise error.Abort(
1898 raise error.Abort(
1899 _(b'working directory state appears damaged!')
1899 _(b'working directory state appears damaged!')
1900 )
1900 )
1901
1901
1902 return self._parents
1902 return self._parents
1903
1903
1904 @propertycache
1904 @propertycache
1905 def _rustmap(self):
1905 def _rustmap(self):
1906 """
1906 """
1907 Fills the Dirstatemap when called.
1907 Fills the Dirstatemap when called.
1908 """
1908 """
1909 # ignore HG_PENDING because identity is used only for writing
1909 # ignore HG_PENDING because identity is used only for writing
1910 self.identity = util.filestat.frompath(
1910 self.identity = util.filestat.frompath(
1911 self._opener.join(self._filename)
1911 self._opener.join(self._filename)
1912 )
1912 )
1913
1913
1914 try:
1914 try:
1915 fp = self._opendirstatefile()
1915 fp = self._opendirstatefile()
1916 try:
1916 try:
1917 st = fp.read()
1917 st = fp.read()
1918 finally:
1918 finally:
1919 fp.close()
1919 fp.close()
1920 except IOError as err:
1920 except IOError as err:
1921 if err.errno != errno.ENOENT:
1921 if err.errno != errno.ENOENT:
1922 raise
1922 raise
1923 st = b''
1923 st = b''
1924
1924
1925 self._rustmap, parents = rustmod.DirstateMap.new(
1925 self._rustmap, parents = rustmod.DirstateMap.new(
1926 self._use_dirstate_tree, self._use_dirstate_v2, st
1926 self._use_dirstate_tree, self._use_dirstate_v2, st
1927 )
1927 )
1928
1928
1929 if parents and not self._dirtyparents:
1929 if parents and not self._dirtyparents:
1930 self.setparents(*parents)
1930 self.setparents(*parents)
1931
1931
1932 self.__contains__ = self._rustmap.__contains__
1932 self.__contains__ = self._rustmap.__contains__
1933 self.__getitem__ = self._rustmap.__getitem__
1933 self.__getitem__ = self._rustmap.__getitem__
1934 self.get = self._rustmap.get
1934 self.get = self._rustmap.get
1935 return self._rustmap
1935 return self._rustmap
1936
1936
1937 def write(self, st, now):
1937 def write(self, st, now):
1938 parents = self.parents()
1938 parents = self.parents()
1939 packed = self._rustmap.write(
1939 packed = self._rustmap.write(
1940 self._use_dirstate_v2, parents[0], parents[1], now
1940 self._use_dirstate_v2, parents[0], parents[1], now
1941 )
1941 )
1942 st.write(packed)
1942 st.write(packed)
1943 st.close()
1943 st.close()
1944 self._dirtyparents = False
1944 self._dirtyparents = False
1945
1945
1946 @propertycache
1946 @propertycache
1947 def filefoldmap(self):
1947 def filefoldmap(self):
1948 """Returns a dictionary mapping normalized case paths to their
1948 """Returns a dictionary mapping normalized case paths to their
1949 non-normalized versions.
1949 non-normalized versions.
1950 """
1950 """
1951 return self._rustmap.filefoldmapasdict()
1951 return self._rustmap.filefoldmapasdict()
1952
1952
1953 def hastrackeddir(self, d):
1953 def hastrackeddir(self, d):
1954 self._dirs # Trigger Python's propertycache
1955 return self._rustmap.hastrackeddir(d)
1954 return self._rustmap.hastrackeddir(d)
1956
1955
1957 def hasdir(self, d):
1956 def hasdir(self, d):
1958 self._dirs # Trigger Python's propertycache
1959 return self._rustmap.hasdir(d)
1957 return self._rustmap.hasdir(d)
1960
1958
1961 @propertycache
1959 @propertycache
1962 def _dirs(self):
1963 return self._rustmap.getdirs()
1964
1965 @propertycache
1966 def _alldirs(self):
1967 return self._rustmap.getalldirs()
1968
1969 @propertycache
1970 def identity(self):
1960 def identity(self):
1971 self._rustmap
1961 self._rustmap
1972 return self.identity
1962 return self.identity
1973
1963
1974 @property
1964 @property
1975 def nonnormalset(self):
1965 def nonnormalset(self):
1976 nonnorm = self._rustmap.non_normal_entries()
1966 nonnorm = self._rustmap.non_normal_entries()
1977 return nonnorm
1967 return nonnorm
1978
1968
1979 @propertycache
1969 @propertycache
1980 def otherparentset(self):
1970 def otherparentset(self):
1981 otherparents = self._rustmap.other_parent_entries()
1971 otherparents = self._rustmap.other_parent_entries()
1982 return otherparents
1972 return otherparents
1983
1973
1984 def non_normal_or_other_parent_paths(self):
1974 def non_normal_or_other_parent_paths(self):
1985 return self._rustmap.non_normal_or_other_parent_paths()
1975 return self._rustmap.non_normal_or_other_parent_paths()
1986
1976
1987 @propertycache
1977 @propertycache
1988 def dirfoldmap(self):
1978 def dirfoldmap(self):
1989 f = {}
1979 f = {}
1990 normcase = util.normcase
1980 normcase = util.normcase
1991 for name in self._dirs:
1981 for name, _pseudo_entry in self.directories():
1992 f[normcase(name)] = name
1982 f[normcase(name)] = name
1993 return f
1983 return f
@@ -1,1130 +1,1118
1 use bytes_cast::BytesCast;
1 use bytes_cast::BytesCast;
2 use micro_timer::timed;
2 use micro_timer::timed;
3 use std::borrow::Cow;
3 use std::borrow::Cow;
4 use std::convert::TryInto;
4 use std::convert::TryInto;
5 use std::path::PathBuf;
5 use std::path::PathBuf;
6
6
7 use super::on_disk;
7 use super::on_disk;
8 use super::on_disk::DirstateV2ParseError;
8 use super::on_disk::DirstateV2ParseError;
9 use super::path_with_basename::WithBasename;
9 use super::path_with_basename::WithBasename;
10 use crate::dirstate::parsers::pack_entry;
10 use crate::dirstate::parsers::pack_entry;
11 use crate::dirstate::parsers::packed_entry_size;
11 use crate::dirstate::parsers::packed_entry_size;
12 use crate::dirstate::parsers::parse_dirstate_entries;
12 use crate::dirstate::parsers::parse_dirstate_entries;
13 use crate::dirstate::parsers::Timestamp;
13 use crate::dirstate::parsers::Timestamp;
14 use crate::matchers::Matcher;
14 use crate::matchers::Matcher;
15 use crate::utils::hg_path::{HgPath, HgPathBuf};
15 use crate::utils::hg_path::{HgPath, HgPathBuf};
16 use crate::CopyMapIter;
16 use crate::CopyMapIter;
17 use crate::DirstateEntry;
17 use crate::DirstateEntry;
18 use crate::DirstateError;
18 use crate::DirstateError;
19 use crate::DirstateParents;
19 use crate::DirstateParents;
20 use crate::DirstateStatus;
20 use crate::DirstateStatus;
21 use crate::EntryState;
21 use crate::EntryState;
22 use crate::FastHashMap;
22 use crate::FastHashMap;
23 use crate::PatternFileWarning;
23 use crate::PatternFileWarning;
24 use crate::StateMapIter;
24 use crate::StateMapIter;
25 use crate::StatusError;
25 use crate::StatusError;
26 use crate::StatusOptions;
26 use crate::StatusOptions;
27
27
28 pub struct DirstateMap<'on_disk> {
28 pub struct DirstateMap<'on_disk> {
29 /// Contents of the `.hg/dirstate` file
29 /// Contents of the `.hg/dirstate` file
30 pub(super) on_disk: &'on_disk [u8],
30 pub(super) on_disk: &'on_disk [u8],
31
31
32 pub(super) root: ChildNodes<'on_disk>,
32 pub(super) root: ChildNodes<'on_disk>,
33
33
34 /// Number of nodes anywhere in the tree that have `.entry.is_some()`.
34 /// Number of nodes anywhere in the tree that have `.entry.is_some()`.
35 pub(super) nodes_with_entry_count: u32,
35 pub(super) nodes_with_entry_count: u32,
36
36
37 /// Number of nodes anywhere in the tree that have
37 /// Number of nodes anywhere in the tree that have
38 /// `.copy_source.is_some()`.
38 /// `.copy_source.is_some()`.
39 pub(super) nodes_with_copy_source_count: u32,
39 pub(super) nodes_with_copy_source_count: u32,
40
40
41 /// See on_disk::Header
41 /// See on_disk::Header
42 pub(super) ignore_patterns_hash: on_disk::IgnorePatternsHash,
42 pub(super) ignore_patterns_hash: on_disk::IgnorePatternsHash,
43 }
43 }
44
44
45 /// Using a plain `HgPathBuf` of the full path from the repository root as a
45 /// Using a plain `HgPathBuf` of the full path from the repository root as a
46 /// map key would also work: all paths in a given map have the same parent
46 /// map key would also work: all paths in a given map have the same parent
47 /// path, so comparing full paths gives the same result as comparing base
47 /// path, so comparing full paths gives the same result as comparing base
48 /// names. However `HashMap` would waste time always re-hashing the same
48 /// names. However `HashMap` would waste time always re-hashing the same
49 /// string prefix.
49 /// string prefix.
50 pub(super) type NodeKey<'on_disk> = WithBasename<Cow<'on_disk, HgPath>>;
50 pub(super) type NodeKey<'on_disk> = WithBasename<Cow<'on_disk, HgPath>>;
51
51
52 /// Similar to `&'tree Cow<'on_disk, HgPath>`, but can also be returned
52 /// Similar to `&'tree Cow<'on_disk, HgPath>`, but can also be returned
53 /// for on-disk nodes that don’t actually have a `Cow` to borrow.
53 /// for on-disk nodes that don’t actually have a `Cow` to borrow.
54 pub(super) enum BorrowedPath<'tree, 'on_disk> {
54 pub(super) enum BorrowedPath<'tree, 'on_disk> {
55 InMemory(&'tree HgPathBuf),
55 InMemory(&'tree HgPathBuf),
56 OnDisk(&'on_disk HgPath),
56 OnDisk(&'on_disk HgPath),
57 }
57 }
58
58
59 pub(super) enum ChildNodes<'on_disk> {
59 pub(super) enum ChildNodes<'on_disk> {
60 InMemory(FastHashMap<NodeKey<'on_disk>, Node<'on_disk>>),
60 InMemory(FastHashMap<NodeKey<'on_disk>, Node<'on_disk>>),
61 OnDisk(&'on_disk [on_disk::Node]),
61 OnDisk(&'on_disk [on_disk::Node]),
62 }
62 }
63
63
64 pub(super) enum ChildNodesRef<'tree, 'on_disk> {
64 pub(super) enum ChildNodesRef<'tree, 'on_disk> {
65 InMemory(&'tree FastHashMap<NodeKey<'on_disk>, Node<'on_disk>>),
65 InMemory(&'tree FastHashMap<NodeKey<'on_disk>, Node<'on_disk>>),
66 OnDisk(&'on_disk [on_disk::Node]),
66 OnDisk(&'on_disk [on_disk::Node]),
67 }
67 }
68
68
69 pub(super) enum NodeRef<'tree, 'on_disk> {
69 pub(super) enum NodeRef<'tree, 'on_disk> {
70 InMemory(&'tree NodeKey<'on_disk>, &'tree Node<'on_disk>),
70 InMemory(&'tree NodeKey<'on_disk>, &'tree Node<'on_disk>),
71 OnDisk(&'on_disk on_disk::Node),
71 OnDisk(&'on_disk on_disk::Node),
72 }
72 }
73
73
74 impl<'tree, 'on_disk> BorrowedPath<'tree, 'on_disk> {
74 impl<'tree, 'on_disk> BorrowedPath<'tree, 'on_disk> {
75 pub fn detach_from_tree(&self) -> Cow<'on_disk, HgPath> {
75 pub fn detach_from_tree(&self) -> Cow<'on_disk, HgPath> {
76 match *self {
76 match *self {
77 BorrowedPath::InMemory(in_memory) => Cow::Owned(in_memory.clone()),
77 BorrowedPath::InMemory(in_memory) => Cow::Owned(in_memory.clone()),
78 BorrowedPath::OnDisk(on_disk) => Cow::Borrowed(on_disk),
78 BorrowedPath::OnDisk(on_disk) => Cow::Borrowed(on_disk),
79 }
79 }
80 }
80 }
81 }
81 }
82
82
83 impl<'tree, 'on_disk> std::ops::Deref for BorrowedPath<'tree, 'on_disk> {
83 impl<'tree, 'on_disk> std::ops::Deref for BorrowedPath<'tree, 'on_disk> {
84 type Target = HgPath;
84 type Target = HgPath;
85
85
86 fn deref(&self) -> &HgPath {
86 fn deref(&self) -> &HgPath {
87 match *self {
87 match *self {
88 BorrowedPath::InMemory(in_memory) => in_memory,
88 BorrowedPath::InMemory(in_memory) => in_memory,
89 BorrowedPath::OnDisk(on_disk) => on_disk,
89 BorrowedPath::OnDisk(on_disk) => on_disk,
90 }
90 }
91 }
91 }
92 }
92 }
93
93
94 impl Default for ChildNodes<'_> {
94 impl Default for ChildNodes<'_> {
95 fn default() -> Self {
95 fn default() -> Self {
96 ChildNodes::InMemory(Default::default())
96 ChildNodes::InMemory(Default::default())
97 }
97 }
98 }
98 }
99
99
100 impl<'on_disk> ChildNodes<'on_disk> {
100 impl<'on_disk> ChildNodes<'on_disk> {
101 pub(super) fn as_ref<'tree>(
101 pub(super) fn as_ref<'tree>(
102 &'tree self,
102 &'tree self,
103 ) -> ChildNodesRef<'tree, 'on_disk> {
103 ) -> ChildNodesRef<'tree, 'on_disk> {
104 match self {
104 match self {
105 ChildNodes::InMemory(nodes) => ChildNodesRef::InMemory(nodes),
105 ChildNodes::InMemory(nodes) => ChildNodesRef::InMemory(nodes),
106 ChildNodes::OnDisk(nodes) => ChildNodesRef::OnDisk(nodes),
106 ChildNodes::OnDisk(nodes) => ChildNodesRef::OnDisk(nodes),
107 }
107 }
108 }
108 }
109
109
110 pub(super) fn is_empty(&self) -> bool {
110 pub(super) fn is_empty(&self) -> bool {
111 match self {
111 match self {
112 ChildNodes::InMemory(nodes) => nodes.is_empty(),
112 ChildNodes::InMemory(nodes) => nodes.is_empty(),
113 ChildNodes::OnDisk(nodes) => nodes.is_empty(),
113 ChildNodes::OnDisk(nodes) => nodes.is_empty(),
114 }
114 }
115 }
115 }
116
116
117 pub(super) fn make_mut(
117 pub(super) fn make_mut(
118 &mut self,
118 &mut self,
119 on_disk: &'on_disk [u8],
119 on_disk: &'on_disk [u8],
120 ) -> Result<
120 ) -> Result<
121 &mut FastHashMap<NodeKey<'on_disk>, Node<'on_disk>>,
121 &mut FastHashMap<NodeKey<'on_disk>, Node<'on_disk>>,
122 DirstateV2ParseError,
122 DirstateV2ParseError,
123 > {
123 > {
124 match self {
124 match self {
125 ChildNodes::InMemory(nodes) => Ok(nodes),
125 ChildNodes::InMemory(nodes) => Ok(nodes),
126 ChildNodes::OnDisk(nodes) => {
126 ChildNodes::OnDisk(nodes) => {
127 let nodes = nodes
127 let nodes = nodes
128 .iter()
128 .iter()
129 .map(|node| {
129 .map(|node| {
130 Ok((
130 Ok((
131 node.path(on_disk)?,
131 node.path(on_disk)?,
132 node.to_in_memory_node(on_disk)?,
132 node.to_in_memory_node(on_disk)?,
133 ))
133 ))
134 })
134 })
135 .collect::<Result<_, _>>()?;
135 .collect::<Result<_, _>>()?;
136 *self = ChildNodes::InMemory(nodes);
136 *self = ChildNodes::InMemory(nodes);
137 match self {
137 match self {
138 ChildNodes::InMemory(nodes) => Ok(nodes),
138 ChildNodes::InMemory(nodes) => Ok(nodes),
139 ChildNodes::OnDisk(_) => unreachable!(),
139 ChildNodes::OnDisk(_) => unreachable!(),
140 }
140 }
141 }
141 }
142 }
142 }
143 }
143 }
144 }
144 }
145
145
146 impl<'tree, 'on_disk> ChildNodesRef<'tree, 'on_disk> {
146 impl<'tree, 'on_disk> ChildNodesRef<'tree, 'on_disk> {
147 pub(super) fn get(
147 pub(super) fn get(
148 &self,
148 &self,
149 base_name: &HgPath,
149 base_name: &HgPath,
150 on_disk: &'on_disk [u8],
150 on_disk: &'on_disk [u8],
151 ) -> Result<Option<NodeRef<'tree, 'on_disk>>, DirstateV2ParseError> {
151 ) -> Result<Option<NodeRef<'tree, 'on_disk>>, DirstateV2ParseError> {
152 match self {
152 match self {
153 ChildNodesRef::InMemory(nodes) => Ok(nodes
153 ChildNodesRef::InMemory(nodes) => Ok(nodes
154 .get_key_value(base_name)
154 .get_key_value(base_name)
155 .map(|(k, v)| NodeRef::InMemory(k, v))),
155 .map(|(k, v)| NodeRef::InMemory(k, v))),
156 ChildNodesRef::OnDisk(nodes) => {
156 ChildNodesRef::OnDisk(nodes) => {
157 let mut parse_result = Ok(());
157 let mut parse_result = Ok(());
158 let search_result = nodes.binary_search_by(|node| {
158 let search_result = nodes.binary_search_by(|node| {
159 match node.base_name(on_disk) {
159 match node.base_name(on_disk) {
160 Ok(node_base_name) => node_base_name.cmp(base_name),
160 Ok(node_base_name) => node_base_name.cmp(base_name),
161 Err(e) => {
161 Err(e) => {
162 parse_result = Err(e);
162 parse_result = Err(e);
163 // Dummy comparison result, `search_result` won’t
163 // Dummy comparison result, `search_result` won’t
164 // be used since `parse_result` is an error
164 // be used since `parse_result` is an error
165 std::cmp::Ordering::Equal
165 std::cmp::Ordering::Equal
166 }
166 }
167 }
167 }
168 });
168 });
169 parse_result.map(|()| {
169 parse_result.map(|()| {
170 search_result.ok().map(|i| NodeRef::OnDisk(&nodes[i]))
170 search_result.ok().map(|i| NodeRef::OnDisk(&nodes[i]))
171 })
171 })
172 }
172 }
173 }
173 }
174 }
174 }
175
175
176 /// Iterate in undefined order
176 /// Iterate in undefined order
177 pub(super) fn iter(
177 pub(super) fn iter(
178 &self,
178 &self,
179 ) -> impl Iterator<Item = NodeRef<'tree, 'on_disk>> {
179 ) -> impl Iterator<Item = NodeRef<'tree, 'on_disk>> {
180 match self {
180 match self {
181 ChildNodesRef::InMemory(nodes) => itertools::Either::Left(
181 ChildNodesRef::InMemory(nodes) => itertools::Either::Left(
182 nodes.iter().map(|(k, v)| NodeRef::InMemory(k, v)),
182 nodes.iter().map(|(k, v)| NodeRef::InMemory(k, v)),
183 ),
183 ),
184 ChildNodesRef::OnDisk(nodes) => {
184 ChildNodesRef::OnDisk(nodes) => {
185 itertools::Either::Right(nodes.iter().map(NodeRef::OnDisk))
185 itertools::Either::Right(nodes.iter().map(NodeRef::OnDisk))
186 }
186 }
187 }
187 }
188 }
188 }
189
189
190 /// Iterate in parallel in undefined order
190 /// Iterate in parallel in undefined order
191 pub(super) fn par_iter(
191 pub(super) fn par_iter(
192 &self,
192 &self,
193 ) -> impl rayon::iter::ParallelIterator<Item = NodeRef<'tree, 'on_disk>>
193 ) -> impl rayon::iter::ParallelIterator<Item = NodeRef<'tree, 'on_disk>>
194 {
194 {
195 use rayon::prelude::*;
195 use rayon::prelude::*;
196 match self {
196 match self {
197 ChildNodesRef::InMemory(nodes) => rayon::iter::Either::Left(
197 ChildNodesRef::InMemory(nodes) => rayon::iter::Either::Left(
198 nodes.par_iter().map(|(k, v)| NodeRef::InMemory(k, v)),
198 nodes.par_iter().map(|(k, v)| NodeRef::InMemory(k, v)),
199 ),
199 ),
200 ChildNodesRef::OnDisk(nodes) => rayon::iter::Either::Right(
200 ChildNodesRef::OnDisk(nodes) => rayon::iter::Either::Right(
201 nodes.par_iter().map(NodeRef::OnDisk),
201 nodes.par_iter().map(NodeRef::OnDisk),
202 ),
202 ),
203 }
203 }
204 }
204 }
205
205
206 pub(super) fn sorted(&self) -> Vec<NodeRef<'tree, 'on_disk>> {
206 pub(super) fn sorted(&self) -> Vec<NodeRef<'tree, 'on_disk>> {
207 match self {
207 match self {
208 ChildNodesRef::InMemory(nodes) => {
208 ChildNodesRef::InMemory(nodes) => {
209 let mut vec: Vec<_> = nodes
209 let mut vec: Vec<_> = nodes
210 .iter()
210 .iter()
211 .map(|(k, v)| NodeRef::InMemory(k, v))
211 .map(|(k, v)| NodeRef::InMemory(k, v))
212 .collect();
212 .collect();
213 fn sort_key<'a>(node: &'a NodeRef) -> &'a HgPath {
213 fn sort_key<'a>(node: &'a NodeRef) -> &'a HgPath {
214 match node {
214 match node {
215 NodeRef::InMemory(path, _node) => path.base_name(),
215 NodeRef::InMemory(path, _node) => path.base_name(),
216 NodeRef::OnDisk(_) => unreachable!(),
216 NodeRef::OnDisk(_) => unreachable!(),
217 }
217 }
218 }
218 }
219 // `sort_unstable_by_key` doesn’t allow keys borrowing from the
219 // `sort_unstable_by_key` doesn’t allow keys borrowing from the
220 // value: https://github.com/rust-lang/rust/issues/34162
220 // value: https://github.com/rust-lang/rust/issues/34162
221 vec.sort_unstable_by(|a, b| sort_key(a).cmp(sort_key(b)));
221 vec.sort_unstable_by(|a, b| sort_key(a).cmp(sort_key(b)));
222 vec
222 vec
223 }
223 }
224 ChildNodesRef::OnDisk(nodes) => {
224 ChildNodesRef::OnDisk(nodes) => {
225 // Nodes on disk are already sorted
225 // Nodes on disk are already sorted
226 nodes.iter().map(NodeRef::OnDisk).collect()
226 nodes.iter().map(NodeRef::OnDisk).collect()
227 }
227 }
228 }
228 }
229 }
229 }
230 }
230 }
231
231
232 impl<'tree, 'on_disk> NodeRef<'tree, 'on_disk> {
232 impl<'tree, 'on_disk> NodeRef<'tree, 'on_disk> {
233 pub(super) fn full_path(
233 pub(super) fn full_path(
234 &self,
234 &self,
235 on_disk: &'on_disk [u8],
235 on_disk: &'on_disk [u8],
236 ) -> Result<&'tree HgPath, DirstateV2ParseError> {
236 ) -> Result<&'tree HgPath, DirstateV2ParseError> {
237 match self {
237 match self {
238 NodeRef::InMemory(path, _node) => Ok(path.full_path()),
238 NodeRef::InMemory(path, _node) => Ok(path.full_path()),
239 NodeRef::OnDisk(node) => node.full_path(on_disk),
239 NodeRef::OnDisk(node) => node.full_path(on_disk),
240 }
240 }
241 }
241 }
242
242
243 /// Returns a `BorrowedPath`, which can be turned into a `Cow<'on_disk,
243 /// Returns a `BorrowedPath`, which can be turned into a `Cow<'on_disk,
244 /// HgPath>` detached from `'tree`
244 /// HgPath>` detached from `'tree`
245 pub(super) fn full_path_borrowed(
245 pub(super) fn full_path_borrowed(
246 &self,
246 &self,
247 on_disk: &'on_disk [u8],
247 on_disk: &'on_disk [u8],
248 ) -> Result<BorrowedPath<'tree, 'on_disk>, DirstateV2ParseError> {
248 ) -> Result<BorrowedPath<'tree, 'on_disk>, DirstateV2ParseError> {
249 match self {
249 match self {
250 NodeRef::InMemory(path, _node) => match path.full_path() {
250 NodeRef::InMemory(path, _node) => match path.full_path() {
251 Cow::Borrowed(on_disk) => Ok(BorrowedPath::OnDisk(on_disk)),
251 Cow::Borrowed(on_disk) => Ok(BorrowedPath::OnDisk(on_disk)),
252 Cow::Owned(in_memory) => Ok(BorrowedPath::InMemory(in_memory)),
252 Cow::Owned(in_memory) => Ok(BorrowedPath::InMemory(in_memory)),
253 },
253 },
254 NodeRef::OnDisk(node) => {
254 NodeRef::OnDisk(node) => {
255 Ok(BorrowedPath::OnDisk(node.full_path(on_disk)?))
255 Ok(BorrowedPath::OnDisk(node.full_path(on_disk)?))
256 }
256 }
257 }
257 }
258 }
258 }
259
259
260 pub(super) fn base_name(
260 pub(super) fn base_name(
261 &self,
261 &self,
262 on_disk: &'on_disk [u8],
262 on_disk: &'on_disk [u8],
263 ) -> Result<&'tree HgPath, DirstateV2ParseError> {
263 ) -> Result<&'tree HgPath, DirstateV2ParseError> {
264 match self {
264 match self {
265 NodeRef::InMemory(path, _node) => Ok(path.base_name()),
265 NodeRef::InMemory(path, _node) => Ok(path.base_name()),
266 NodeRef::OnDisk(node) => node.base_name(on_disk),
266 NodeRef::OnDisk(node) => node.base_name(on_disk),
267 }
267 }
268 }
268 }
269
269
270 pub(super) fn children(
270 pub(super) fn children(
271 &self,
271 &self,
272 on_disk: &'on_disk [u8],
272 on_disk: &'on_disk [u8],
273 ) -> Result<ChildNodesRef<'tree, 'on_disk>, DirstateV2ParseError> {
273 ) -> Result<ChildNodesRef<'tree, 'on_disk>, DirstateV2ParseError> {
274 match self {
274 match self {
275 NodeRef::InMemory(_path, node) => Ok(node.children.as_ref()),
275 NodeRef::InMemory(_path, node) => Ok(node.children.as_ref()),
276 NodeRef::OnDisk(node) => {
276 NodeRef::OnDisk(node) => {
277 Ok(ChildNodesRef::OnDisk(node.children(on_disk)?))
277 Ok(ChildNodesRef::OnDisk(node.children(on_disk)?))
278 }
278 }
279 }
279 }
280 }
280 }
281
281
282 pub(super) fn has_copy_source(&self) -> bool {
282 pub(super) fn has_copy_source(&self) -> bool {
283 match self {
283 match self {
284 NodeRef::InMemory(_path, node) => node.copy_source.is_some(),
284 NodeRef::InMemory(_path, node) => node.copy_source.is_some(),
285 NodeRef::OnDisk(node) => node.has_copy_source(),
285 NodeRef::OnDisk(node) => node.has_copy_source(),
286 }
286 }
287 }
287 }
288
288
289 pub(super) fn copy_source(
289 pub(super) fn copy_source(
290 &self,
290 &self,
291 on_disk: &'on_disk [u8],
291 on_disk: &'on_disk [u8],
292 ) -> Result<Option<&'tree HgPath>, DirstateV2ParseError> {
292 ) -> Result<Option<&'tree HgPath>, DirstateV2ParseError> {
293 match self {
293 match self {
294 NodeRef::InMemory(_path, node) => {
294 NodeRef::InMemory(_path, node) => {
295 Ok(node.copy_source.as_ref().map(|s| &**s))
295 Ok(node.copy_source.as_ref().map(|s| &**s))
296 }
296 }
297 NodeRef::OnDisk(node) => node.copy_source(on_disk),
297 NodeRef::OnDisk(node) => node.copy_source(on_disk),
298 }
298 }
299 }
299 }
300
300
301 pub(super) fn entry(
301 pub(super) fn entry(
302 &self,
302 &self,
303 ) -> Result<Option<DirstateEntry>, DirstateV2ParseError> {
303 ) -> Result<Option<DirstateEntry>, DirstateV2ParseError> {
304 match self {
304 match self {
305 NodeRef::InMemory(_path, node) => {
305 NodeRef::InMemory(_path, node) => {
306 Ok(node.data.as_entry().copied())
306 Ok(node.data.as_entry().copied())
307 }
307 }
308 NodeRef::OnDisk(node) => node.entry(),
308 NodeRef::OnDisk(node) => node.entry(),
309 }
309 }
310 }
310 }
311
311
312 pub(super) fn state(
312 pub(super) fn state(
313 &self,
313 &self,
314 ) -> Result<Option<EntryState>, DirstateV2ParseError> {
314 ) -> Result<Option<EntryState>, DirstateV2ParseError> {
315 match self {
315 match self {
316 NodeRef::InMemory(_path, node) => {
316 NodeRef::InMemory(_path, node) => {
317 Ok(node.data.as_entry().map(|entry| entry.state))
317 Ok(node.data.as_entry().map(|entry| entry.state))
318 }
318 }
319 NodeRef::OnDisk(node) => node.state(),
319 NodeRef::OnDisk(node) => node.state(),
320 }
320 }
321 }
321 }
322
322
323 pub(super) fn cached_directory_mtime(
323 pub(super) fn cached_directory_mtime(
324 &self,
324 &self,
325 ) -> Option<&'tree on_disk::Timestamp> {
325 ) -> Option<&'tree on_disk::Timestamp> {
326 match self {
326 match self {
327 NodeRef::InMemory(_path, node) => match &node.data {
327 NodeRef::InMemory(_path, node) => match &node.data {
328 NodeData::CachedDirectory { mtime } => Some(mtime),
328 NodeData::CachedDirectory { mtime } => Some(mtime),
329 _ => None,
329 _ => None,
330 },
330 },
331 NodeRef::OnDisk(node) => node.cached_directory_mtime(),
331 NodeRef::OnDisk(node) => node.cached_directory_mtime(),
332 }
332 }
333 }
333 }
334
334
335 pub(super) fn tracked_descendants_count(&self) -> u32 {
335 pub(super) fn tracked_descendants_count(&self) -> u32 {
336 match self {
336 match self {
337 NodeRef::InMemory(_path, node) => node.tracked_descendants_count,
337 NodeRef::InMemory(_path, node) => node.tracked_descendants_count,
338 NodeRef::OnDisk(node) => node.tracked_descendants_count.get(),
338 NodeRef::OnDisk(node) => node.tracked_descendants_count.get(),
339 }
339 }
340 }
340 }
341 }
341 }
342
342
343 /// Represents a file or a directory
343 /// Represents a file or a directory
344 #[derive(Default)]
344 #[derive(Default)]
345 pub(super) struct Node<'on_disk> {
345 pub(super) struct Node<'on_disk> {
346 pub(super) data: NodeData,
346 pub(super) data: NodeData,
347
347
348 pub(super) copy_source: Option<Cow<'on_disk, HgPath>>,
348 pub(super) copy_source: Option<Cow<'on_disk, HgPath>>,
349
349
350 pub(super) children: ChildNodes<'on_disk>,
350 pub(super) children: ChildNodes<'on_disk>,
351
351
352 /// How many (non-inclusive) descendants of this node are tracked files
352 /// How many (non-inclusive) descendants of this node are tracked files
353 pub(super) tracked_descendants_count: u32,
353 pub(super) tracked_descendants_count: u32,
354 }
354 }
355
355
356 pub(super) enum NodeData {
356 pub(super) enum NodeData {
357 Entry(DirstateEntry),
357 Entry(DirstateEntry),
358 CachedDirectory { mtime: on_disk::Timestamp },
358 CachedDirectory { mtime: on_disk::Timestamp },
359 None,
359 None,
360 }
360 }
361
361
362 impl Default for NodeData {
362 impl Default for NodeData {
363 fn default() -> Self {
363 fn default() -> Self {
364 NodeData::None
364 NodeData::None
365 }
365 }
366 }
366 }
367
367
368 impl NodeData {
368 impl NodeData {
369 fn has_entry(&self) -> bool {
369 fn has_entry(&self) -> bool {
370 match self {
370 match self {
371 NodeData::Entry(_) => true,
371 NodeData::Entry(_) => true,
372 _ => false,
372 _ => false,
373 }
373 }
374 }
374 }
375
375
376 fn as_entry(&self) -> Option<&DirstateEntry> {
376 fn as_entry(&self) -> Option<&DirstateEntry> {
377 match self {
377 match self {
378 NodeData::Entry(entry) => Some(entry),
378 NodeData::Entry(entry) => Some(entry),
379 _ => None,
379 _ => None,
380 }
380 }
381 }
381 }
382 }
382 }
383
383
384 impl<'on_disk> DirstateMap<'on_disk> {
384 impl<'on_disk> DirstateMap<'on_disk> {
385 pub(super) fn empty(on_disk: &'on_disk [u8]) -> Self {
385 pub(super) fn empty(on_disk: &'on_disk [u8]) -> Self {
386 Self {
386 Self {
387 on_disk,
387 on_disk,
388 root: ChildNodes::default(),
388 root: ChildNodes::default(),
389 nodes_with_entry_count: 0,
389 nodes_with_entry_count: 0,
390 nodes_with_copy_source_count: 0,
390 nodes_with_copy_source_count: 0,
391 ignore_patterns_hash: [0; on_disk::IGNORE_PATTERNS_HASH_LEN],
391 ignore_patterns_hash: [0; on_disk::IGNORE_PATTERNS_HASH_LEN],
392 }
392 }
393 }
393 }
394
394
395 #[timed]
395 #[timed]
396 pub fn new_v2(
396 pub fn new_v2(
397 on_disk: &'on_disk [u8],
397 on_disk: &'on_disk [u8],
398 ) -> Result<(Self, Option<DirstateParents>), DirstateError> {
398 ) -> Result<(Self, Option<DirstateParents>), DirstateError> {
399 Ok(on_disk::read(on_disk)?)
399 Ok(on_disk::read(on_disk)?)
400 }
400 }
401
401
402 #[timed]
402 #[timed]
403 pub fn new_v1(
403 pub fn new_v1(
404 on_disk: &'on_disk [u8],
404 on_disk: &'on_disk [u8],
405 ) -> Result<(Self, Option<DirstateParents>), DirstateError> {
405 ) -> Result<(Self, Option<DirstateParents>), DirstateError> {
406 let mut map = Self::empty(on_disk);
406 let mut map = Self::empty(on_disk);
407 if map.on_disk.is_empty() {
407 if map.on_disk.is_empty() {
408 return Ok((map, None));
408 return Ok((map, None));
409 }
409 }
410
410
411 let parents = parse_dirstate_entries(
411 let parents = parse_dirstate_entries(
412 map.on_disk,
412 map.on_disk,
413 |path, entry, copy_source| {
413 |path, entry, copy_source| {
414 let tracked = entry.state.is_tracked();
414 let tracked = entry.state.is_tracked();
415 let node = Self::get_or_insert_node(
415 let node = Self::get_or_insert_node(
416 map.on_disk,
416 map.on_disk,
417 &mut map.root,
417 &mut map.root,
418 path,
418 path,
419 WithBasename::to_cow_borrowed,
419 WithBasename::to_cow_borrowed,
420 |ancestor| {
420 |ancestor| {
421 if tracked {
421 if tracked {
422 ancestor.tracked_descendants_count += 1
422 ancestor.tracked_descendants_count += 1
423 }
423 }
424 },
424 },
425 )?;
425 )?;
426 assert!(
426 assert!(
427 !node.data.has_entry(),
427 !node.data.has_entry(),
428 "duplicate dirstate entry in read"
428 "duplicate dirstate entry in read"
429 );
429 );
430 assert!(
430 assert!(
431 node.copy_source.is_none(),
431 node.copy_source.is_none(),
432 "duplicate dirstate entry in read"
432 "duplicate dirstate entry in read"
433 );
433 );
434 node.data = NodeData::Entry(*entry);
434 node.data = NodeData::Entry(*entry);
435 node.copy_source = copy_source.map(Cow::Borrowed);
435 node.copy_source = copy_source.map(Cow::Borrowed);
436 map.nodes_with_entry_count += 1;
436 map.nodes_with_entry_count += 1;
437 if copy_source.is_some() {
437 if copy_source.is_some() {
438 map.nodes_with_copy_source_count += 1
438 map.nodes_with_copy_source_count += 1
439 }
439 }
440 Ok(())
440 Ok(())
441 },
441 },
442 )?;
442 )?;
443 let parents = Some(parents.clone());
443 let parents = Some(parents.clone());
444
444
445 Ok((map, parents))
445 Ok((map, parents))
446 }
446 }
447
447
448 fn get_node<'tree>(
448 fn get_node<'tree>(
449 &'tree self,
449 &'tree self,
450 path: &HgPath,
450 path: &HgPath,
451 ) -> Result<Option<NodeRef<'tree, 'on_disk>>, DirstateV2ParseError> {
451 ) -> Result<Option<NodeRef<'tree, 'on_disk>>, DirstateV2ParseError> {
452 let mut children = self.root.as_ref();
452 let mut children = self.root.as_ref();
453 let mut components = path.components();
453 let mut components = path.components();
454 let mut component =
454 let mut component =
455 components.next().expect("expected at least one components");
455 components.next().expect("expected at least one components");
456 loop {
456 loop {
457 if let Some(child) = children.get(component, self.on_disk)? {
457 if let Some(child) = children.get(component, self.on_disk)? {
458 if let Some(next_component) = components.next() {
458 if let Some(next_component) = components.next() {
459 component = next_component;
459 component = next_component;
460 children = child.children(self.on_disk)?;
460 children = child.children(self.on_disk)?;
461 } else {
461 } else {
462 return Ok(Some(child));
462 return Ok(Some(child));
463 }
463 }
464 } else {
464 } else {
465 return Ok(None);
465 return Ok(None);
466 }
466 }
467 }
467 }
468 }
468 }
469
469
470 /// Returns a mutable reference to the node at `path` if it exists
470 /// Returns a mutable reference to the node at `path` if it exists
471 ///
471 ///
472 /// This takes `root` instead of `&mut self` so that callers can mutate
472 /// This takes `root` instead of `&mut self` so that callers can mutate
473 /// other fields while the returned borrow is still valid
473 /// other fields while the returned borrow is still valid
474 fn get_node_mut<'tree>(
474 fn get_node_mut<'tree>(
475 on_disk: &'on_disk [u8],
475 on_disk: &'on_disk [u8],
476 root: &'tree mut ChildNodes<'on_disk>,
476 root: &'tree mut ChildNodes<'on_disk>,
477 path: &HgPath,
477 path: &HgPath,
478 ) -> Result<Option<&'tree mut Node<'on_disk>>, DirstateV2ParseError> {
478 ) -> Result<Option<&'tree mut Node<'on_disk>>, DirstateV2ParseError> {
479 let mut children = root;
479 let mut children = root;
480 let mut components = path.components();
480 let mut components = path.components();
481 let mut component =
481 let mut component =
482 components.next().expect("expected at least one components");
482 components.next().expect("expected at least one components");
483 loop {
483 loop {
484 if let Some(child) = children.make_mut(on_disk)?.get_mut(component)
484 if let Some(child) = children.make_mut(on_disk)?.get_mut(component)
485 {
485 {
486 if let Some(next_component) = components.next() {
486 if let Some(next_component) = components.next() {
487 component = next_component;
487 component = next_component;
488 children = &mut child.children;
488 children = &mut child.children;
489 } else {
489 } else {
490 return Ok(Some(child));
490 return Ok(Some(child));
491 }
491 }
492 } else {
492 } else {
493 return Ok(None);
493 return Ok(None);
494 }
494 }
495 }
495 }
496 }
496 }
497
497
498 pub(super) fn get_or_insert<'tree, 'path>(
498 pub(super) fn get_or_insert<'tree, 'path>(
499 &'tree mut self,
499 &'tree mut self,
500 path: &HgPath,
500 path: &HgPath,
501 ) -> Result<&'tree mut Node<'on_disk>, DirstateV2ParseError> {
501 ) -> Result<&'tree mut Node<'on_disk>, DirstateV2ParseError> {
502 Self::get_or_insert_node(
502 Self::get_or_insert_node(
503 self.on_disk,
503 self.on_disk,
504 &mut self.root,
504 &mut self.root,
505 path,
505 path,
506 WithBasename::to_cow_owned,
506 WithBasename::to_cow_owned,
507 |_| {},
507 |_| {},
508 )
508 )
509 }
509 }
510
510
511 pub(super) fn get_or_insert_node<'tree, 'path>(
511 pub(super) fn get_or_insert_node<'tree, 'path>(
512 on_disk: &'on_disk [u8],
512 on_disk: &'on_disk [u8],
513 root: &'tree mut ChildNodes<'on_disk>,
513 root: &'tree mut ChildNodes<'on_disk>,
514 path: &'path HgPath,
514 path: &'path HgPath,
515 to_cow: impl Fn(
515 to_cow: impl Fn(
516 WithBasename<&'path HgPath>,
516 WithBasename<&'path HgPath>,
517 ) -> WithBasename<Cow<'on_disk, HgPath>>,
517 ) -> WithBasename<Cow<'on_disk, HgPath>>,
518 mut each_ancestor: impl FnMut(&mut Node),
518 mut each_ancestor: impl FnMut(&mut Node),
519 ) -> Result<&'tree mut Node<'on_disk>, DirstateV2ParseError> {
519 ) -> Result<&'tree mut Node<'on_disk>, DirstateV2ParseError> {
520 let mut child_nodes = root;
520 let mut child_nodes = root;
521 let mut inclusive_ancestor_paths =
521 let mut inclusive_ancestor_paths =
522 WithBasename::inclusive_ancestors_of(path);
522 WithBasename::inclusive_ancestors_of(path);
523 let mut ancestor_path = inclusive_ancestor_paths
523 let mut ancestor_path = inclusive_ancestor_paths
524 .next()
524 .next()
525 .expect("expected at least one inclusive ancestor");
525 .expect("expected at least one inclusive ancestor");
526 loop {
526 loop {
527 // TODO: can we avoid allocating an owned key in cases where the
527 // TODO: can we avoid allocating an owned key in cases where the
528 // map already contains that key, without introducing double
528 // map already contains that key, without introducing double
529 // lookup?
529 // lookup?
530 let child_node = child_nodes
530 let child_node = child_nodes
531 .make_mut(on_disk)?
531 .make_mut(on_disk)?
532 .entry(to_cow(ancestor_path))
532 .entry(to_cow(ancestor_path))
533 .or_default();
533 .or_default();
534 if let Some(next) = inclusive_ancestor_paths.next() {
534 if let Some(next) = inclusive_ancestor_paths.next() {
535 each_ancestor(child_node);
535 each_ancestor(child_node);
536 ancestor_path = next;
536 ancestor_path = next;
537 child_nodes = &mut child_node.children;
537 child_nodes = &mut child_node.children;
538 } else {
538 } else {
539 return Ok(child_node);
539 return Ok(child_node);
540 }
540 }
541 }
541 }
542 }
542 }
543
543
544 fn add_or_remove_file(
544 fn add_or_remove_file(
545 &mut self,
545 &mut self,
546 path: &HgPath,
546 path: &HgPath,
547 old_state: EntryState,
547 old_state: EntryState,
548 new_entry: DirstateEntry,
548 new_entry: DirstateEntry,
549 ) -> Result<(), DirstateV2ParseError> {
549 ) -> Result<(), DirstateV2ParseError> {
550 let tracked_count_increment =
550 let tracked_count_increment =
551 match (old_state.is_tracked(), new_entry.state.is_tracked()) {
551 match (old_state.is_tracked(), new_entry.state.is_tracked()) {
552 (false, true) => 1,
552 (false, true) => 1,
553 (true, false) => -1,
553 (true, false) => -1,
554 _ => 0,
554 _ => 0,
555 };
555 };
556
556
557 let node = Self::get_or_insert_node(
557 let node = Self::get_or_insert_node(
558 self.on_disk,
558 self.on_disk,
559 &mut self.root,
559 &mut self.root,
560 path,
560 path,
561 WithBasename::to_cow_owned,
561 WithBasename::to_cow_owned,
562 |ancestor| {
562 |ancestor| {
563 // We can’t use `+= increment` because the counter is unsigned,
563 // We can’t use `+= increment` because the counter is unsigned,
564 // and we want debug builds to detect accidental underflow
564 // and we want debug builds to detect accidental underflow
565 // through zero
565 // through zero
566 match tracked_count_increment {
566 match tracked_count_increment {
567 1 => ancestor.tracked_descendants_count += 1,
567 1 => ancestor.tracked_descendants_count += 1,
568 -1 => ancestor.tracked_descendants_count -= 1,
568 -1 => ancestor.tracked_descendants_count -= 1,
569 _ => {}
569 _ => {}
570 }
570 }
571 },
571 },
572 )?;
572 )?;
573 if !node.data.has_entry() {
573 if !node.data.has_entry() {
574 self.nodes_with_entry_count += 1
574 self.nodes_with_entry_count += 1
575 }
575 }
576 node.data = NodeData::Entry(new_entry);
576 node.data = NodeData::Entry(new_entry);
577 Ok(())
577 Ok(())
578 }
578 }
579
579
580 fn iter_nodes<'tree>(
580 fn iter_nodes<'tree>(
581 &'tree self,
581 &'tree self,
582 ) -> impl Iterator<
582 ) -> impl Iterator<
583 Item = Result<NodeRef<'tree, 'on_disk>, DirstateV2ParseError>,
583 Item = Result<NodeRef<'tree, 'on_disk>, DirstateV2ParseError>,
584 > + 'tree {
584 > + 'tree {
585 // Depth first tree traversal.
585 // Depth first tree traversal.
586 //
586 //
587 // If we could afford internal iteration and recursion,
587 // If we could afford internal iteration and recursion,
588 // this would look like:
588 // this would look like:
589 //
589 //
590 // ```
590 // ```
591 // fn traverse_children(
591 // fn traverse_children(
592 // children: &ChildNodes,
592 // children: &ChildNodes,
593 // each: &mut impl FnMut(&Node),
593 // each: &mut impl FnMut(&Node),
594 // ) {
594 // ) {
595 // for child in children.values() {
595 // for child in children.values() {
596 // traverse_children(&child.children, each);
596 // traverse_children(&child.children, each);
597 // each(child);
597 // each(child);
598 // }
598 // }
599 // }
599 // }
600 // ```
600 // ```
601 //
601 //
602 // However we want an external iterator and therefore can’t use the
602 // However we want an external iterator and therefore can’t use the
603 // call stack. Use an explicit stack instead:
603 // call stack. Use an explicit stack instead:
604 let mut stack = Vec::new();
604 let mut stack = Vec::new();
605 let mut iter = self.root.as_ref().iter();
605 let mut iter = self.root.as_ref().iter();
606 std::iter::from_fn(move || {
606 std::iter::from_fn(move || {
607 while let Some(child_node) = iter.next() {
607 while let Some(child_node) = iter.next() {
608 let children = match child_node.children(self.on_disk) {
608 let children = match child_node.children(self.on_disk) {
609 Ok(children) => children,
609 Ok(children) => children,
610 Err(error) => return Some(Err(error)),
610 Err(error) => return Some(Err(error)),
611 };
611 };
612 // Pseudo-recursion
612 // Pseudo-recursion
613 let new_iter = children.iter();
613 let new_iter = children.iter();
614 let old_iter = std::mem::replace(&mut iter, new_iter);
614 let old_iter = std::mem::replace(&mut iter, new_iter);
615 stack.push((child_node, old_iter));
615 stack.push((child_node, old_iter));
616 }
616 }
617 // Found the end of a `children.iter()` iterator.
617 // Found the end of a `children.iter()` iterator.
618 if let Some((child_node, next_iter)) = stack.pop() {
618 if let Some((child_node, next_iter)) = stack.pop() {
619 // "Return" from pseudo-recursion by restoring state from the
619 // "Return" from pseudo-recursion by restoring state from the
620 // explicit stack
620 // explicit stack
621 iter = next_iter;
621 iter = next_iter;
622
622
623 Some(Ok(child_node))
623 Some(Ok(child_node))
624 } else {
624 } else {
625 // Reached the bottom of the stack, we’re done
625 // Reached the bottom of the stack, we’re done
626 None
626 None
627 }
627 }
628 })
628 })
629 }
629 }
630
630
631 fn clear_known_ambiguous_mtimes(
631 fn clear_known_ambiguous_mtimes(
632 &mut self,
632 &mut self,
633 paths: &[impl AsRef<HgPath>],
633 paths: &[impl AsRef<HgPath>],
634 ) -> Result<(), DirstateV2ParseError> {
634 ) -> Result<(), DirstateV2ParseError> {
635 for path in paths {
635 for path in paths {
636 if let Some(node) = Self::get_node_mut(
636 if let Some(node) = Self::get_node_mut(
637 self.on_disk,
637 self.on_disk,
638 &mut self.root,
638 &mut self.root,
639 path.as_ref(),
639 path.as_ref(),
640 )? {
640 )? {
641 if let NodeData::Entry(entry) = &mut node.data {
641 if let NodeData::Entry(entry) = &mut node.data {
642 entry.clear_mtime();
642 entry.clear_mtime();
643 }
643 }
644 }
644 }
645 }
645 }
646 Ok(())
646 Ok(())
647 }
647 }
648
648
649 /// Return a faillilble iterator of full paths of nodes that have an
649 /// Return a faillilble iterator of full paths of nodes that have an
650 /// `entry` for which the given `predicate` returns true.
650 /// `entry` for which the given `predicate` returns true.
651 ///
651 ///
652 /// Fallibility means that each iterator item is a `Result`, which may
652 /// Fallibility means that each iterator item is a `Result`, which may
653 /// indicate a parse error of the on-disk dirstate-v2 format. Such errors
653 /// indicate a parse error of the on-disk dirstate-v2 format. Such errors
654 /// should only happen if Mercurial is buggy or a repository is corrupted.
654 /// should only happen if Mercurial is buggy or a repository is corrupted.
655 fn filter_full_paths<'tree>(
655 fn filter_full_paths<'tree>(
656 &'tree self,
656 &'tree self,
657 predicate: impl Fn(&DirstateEntry) -> bool + 'tree,
657 predicate: impl Fn(&DirstateEntry) -> bool + 'tree,
658 ) -> impl Iterator<Item = Result<&HgPath, DirstateV2ParseError>> + 'tree
658 ) -> impl Iterator<Item = Result<&HgPath, DirstateV2ParseError>> + 'tree
659 {
659 {
660 filter_map_results(self.iter_nodes(), move |node| {
660 filter_map_results(self.iter_nodes(), move |node| {
661 if let Some(entry) = node.entry()? {
661 if let Some(entry) = node.entry()? {
662 if predicate(&entry) {
662 if predicate(&entry) {
663 return Ok(Some(node.full_path(self.on_disk)?));
663 return Ok(Some(node.full_path(self.on_disk)?));
664 }
664 }
665 }
665 }
666 Ok(None)
666 Ok(None)
667 })
667 })
668 }
668 }
669 }
669 }
670
670
671 /// Like `Iterator::filter_map`, but over a fallible iterator of `Result`s.
671 /// Like `Iterator::filter_map`, but over a fallible iterator of `Result`s.
672 ///
672 ///
673 /// The callback is only called for incoming `Ok` values. Errors are passed
673 /// The callback is only called for incoming `Ok` values. Errors are passed
674 /// through as-is. In order to let it use the `?` operator the callback is
674 /// through as-is. In order to let it use the `?` operator the callback is
675 /// expected to return a `Result` of `Option`, instead of an `Option` of
675 /// expected to return a `Result` of `Option`, instead of an `Option` of
676 /// `Result`.
676 /// `Result`.
677 fn filter_map_results<'a, I, F, A, B, E>(
677 fn filter_map_results<'a, I, F, A, B, E>(
678 iter: I,
678 iter: I,
679 f: F,
679 f: F,
680 ) -> impl Iterator<Item = Result<B, E>> + 'a
680 ) -> impl Iterator<Item = Result<B, E>> + 'a
681 where
681 where
682 I: Iterator<Item = Result<A, E>> + 'a,
682 I: Iterator<Item = Result<A, E>> + 'a,
683 F: Fn(A) -> Result<Option<B>, E> + 'a,
683 F: Fn(A) -> Result<Option<B>, E> + 'a,
684 {
684 {
685 iter.filter_map(move |result| match result {
685 iter.filter_map(move |result| match result {
686 Ok(node) => f(node).transpose(),
686 Ok(node) => f(node).transpose(),
687 Err(e) => Some(Err(e)),
687 Err(e) => Some(Err(e)),
688 })
688 })
689 }
689 }
690
690
691 impl<'on_disk> super::dispatch::DirstateMapMethods for DirstateMap<'on_disk> {
691 impl<'on_disk> super::dispatch::DirstateMapMethods for DirstateMap<'on_disk> {
692 fn clear(&mut self) {
692 fn clear(&mut self) {
693 self.root = Default::default();
693 self.root = Default::default();
694 self.nodes_with_entry_count = 0;
694 self.nodes_with_entry_count = 0;
695 self.nodes_with_copy_source_count = 0;
695 self.nodes_with_copy_source_count = 0;
696 }
696 }
697
697
698 fn add_file(
698 fn add_file(
699 &mut self,
699 &mut self,
700 filename: &HgPath,
700 filename: &HgPath,
701 old_state: EntryState,
701 old_state: EntryState,
702 entry: DirstateEntry,
702 entry: DirstateEntry,
703 ) -> Result<(), DirstateError> {
703 ) -> Result<(), DirstateError> {
704 Ok(self.add_or_remove_file(filename, old_state, entry)?)
704 Ok(self.add_or_remove_file(filename, old_state, entry)?)
705 }
705 }
706
706
707 fn remove_file(
707 fn remove_file(
708 &mut self,
708 &mut self,
709 filename: &HgPath,
709 filename: &HgPath,
710 old_state: EntryState,
710 old_state: EntryState,
711 size: i32,
711 size: i32,
712 ) -> Result<(), DirstateError> {
712 ) -> Result<(), DirstateError> {
713 let entry = DirstateEntry {
713 let entry = DirstateEntry {
714 state: EntryState::Removed,
714 state: EntryState::Removed,
715 mode: 0,
715 mode: 0,
716 size,
716 size,
717 mtime: 0,
717 mtime: 0,
718 };
718 };
719 Ok(self.add_or_remove_file(filename, old_state, entry)?)
719 Ok(self.add_or_remove_file(filename, old_state, entry)?)
720 }
720 }
721
721
722 fn drop_file(
722 fn drop_file(
723 &mut self,
723 &mut self,
724 filename: &HgPath,
724 filename: &HgPath,
725 old_state: EntryState,
725 old_state: EntryState,
726 ) -> Result<bool, DirstateError> {
726 ) -> Result<bool, DirstateError> {
727 struct Dropped {
727 struct Dropped {
728 was_tracked: bool,
728 was_tracked: bool,
729 had_entry: bool,
729 had_entry: bool,
730 had_copy_source: bool,
730 had_copy_source: bool,
731 }
731 }
732
732
733 /// If this returns `Ok(Some((dropped, removed)))`, then
733 /// If this returns `Ok(Some((dropped, removed)))`, then
734 ///
734 ///
735 /// * `dropped` is about the leaf node that was at `filename`
735 /// * `dropped` is about the leaf node that was at `filename`
736 /// * `removed` is whether this particular level of recursion just
736 /// * `removed` is whether this particular level of recursion just
737 /// removed a node in `nodes`.
737 /// removed a node in `nodes`.
738 fn recur<'on_disk>(
738 fn recur<'on_disk>(
739 on_disk: &'on_disk [u8],
739 on_disk: &'on_disk [u8],
740 nodes: &mut ChildNodes<'on_disk>,
740 nodes: &mut ChildNodes<'on_disk>,
741 path: &HgPath,
741 path: &HgPath,
742 ) -> Result<Option<(Dropped, bool)>, DirstateV2ParseError> {
742 ) -> Result<Option<(Dropped, bool)>, DirstateV2ParseError> {
743 let (first_path_component, rest_of_path) =
743 let (first_path_component, rest_of_path) =
744 path.split_first_component();
744 path.split_first_component();
745 let node = if let Some(node) =
745 let node = if let Some(node) =
746 nodes.make_mut(on_disk)?.get_mut(first_path_component)
746 nodes.make_mut(on_disk)?.get_mut(first_path_component)
747 {
747 {
748 node
748 node
749 } else {
749 } else {
750 return Ok(None);
750 return Ok(None);
751 };
751 };
752 let dropped;
752 let dropped;
753 if let Some(rest) = rest_of_path {
753 if let Some(rest) = rest_of_path {
754 if let Some((d, removed)) =
754 if let Some((d, removed)) =
755 recur(on_disk, &mut node.children, rest)?
755 recur(on_disk, &mut node.children, rest)?
756 {
756 {
757 dropped = d;
757 dropped = d;
758 if dropped.was_tracked {
758 if dropped.was_tracked {
759 node.tracked_descendants_count -= 1;
759 node.tracked_descendants_count -= 1;
760 }
760 }
761
761
762 // Directory caches must be invalidated when removing a
762 // Directory caches must be invalidated when removing a
763 // child node
763 // child node
764 if removed {
764 if removed {
765 if let NodeData::CachedDirectory { .. } = &node.data {
765 if let NodeData::CachedDirectory { .. } = &node.data {
766 node.data = NodeData::None
766 node.data = NodeData::None
767 }
767 }
768 }
768 }
769 } else {
769 } else {
770 return Ok(None);
770 return Ok(None);
771 }
771 }
772 } else {
772 } else {
773 let had_entry = node.data.has_entry();
773 let had_entry = node.data.has_entry();
774 if had_entry {
774 if had_entry {
775 node.data = NodeData::None
775 node.data = NodeData::None
776 }
776 }
777 dropped = Dropped {
777 dropped = Dropped {
778 was_tracked: node
778 was_tracked: node
779 .data
779 .data
780 .as_entry()
780 .as_entry()
781 .map_or(false, |entry| entry.state.is_tracked()),
781 .map_or(false, |entry| entry.state.is_tracked()),
782 had_entry,
782 had_entry,
783 had_copy_source: node.copy_source.take().is_some(),
783 had_copy_source: node.copy_source.take().is_some(),
784 };
784 };
785 }
785 }
786 // After recursion, for both leaf (rest_of_path is None) nodes and
786 // After recursion, for both leaf (rest_of_path is None) nodes and
787 // parent nodes, remove a node if it just became empty.
787 // parent nodes, remove a node if it just became empty.
788 let remove = !node.data.has_entry()
788 let remove = !node.data.has_entry()
789 && node.copy_source.is_none()
789 && node.copy_source.is_none()
790 && node.children.is_empty();
790 && node.children.is_empty();
791 if remove {
791 if remove {
792 nodes.make_mut(on_disk)?.remove(first_path_component);
792 nodes.make_mut(on_disk)?.remove(first_path_component);
793 }
793 }
794 Ok(Some((dropped, remove)))
794 Ok(Some((dropped, remove)))
795 }
795 }
796
796
797 if let Some((dropped, _removed)) =
797 if let Some((dropped, _removed)) =
798 recur(self.on_disk, &mut self.root, filename)?
798 recur(self.on_disk, &mut self.root, filename)?
799 {
799 {
800 if dropped.had_entry {
800 if dropped.had_entry {
801 self.nodes_with_entry_count -= 1
801 self.nodes_with_entry_count -= 1
802 }
802 }
803 if dropped.had_copy_source {
803 if dropped.had_copy_source {
804 self.nodes_with_copy_source_count -= 1
804 self.nodes_with_copy_source_count -= 1
805 }
805 }
806 Ok(dropped.had_entry)
806 Ok(dropped.had_entry)
807 } else {
807 } else {
808 debug_assert!(!old_state.is_tracked());
808 debug_assert!(!old_state.is_tracked());
809 Ok(false)
809 Ok(false)
810 }
810 }
811 }
811 }
812
812
813 fn clear_ambiguous_times(
813 fn clear_ambiguous_times(
814 &mut self,
814 &mut self,
815 filenames: Vec<HgPathBuf>,
815 filenames: Vec<HgPathBuf>,
816 now: i32,
816 now: i32,
817 ) -> Result<(), DirstateV2ParseError> {
817 ) -> Result<(), DirstateV2ParseError> {
818 for filename in filenames {
818 for filename in filenames {
819 if let Some(node) =
819 if let Some(node) =
820 Self::get_node_mut(self.on_disk, &mut self.root, &filename)?
820 Self::get_node_mut(self.on_disk, &mut self.root, &filename)?
821 {
821 {
822 if let NodeData::Entry(entry) = &mut node.data {
822 if let NodeData::Entry(entry) = &mut node.data {
823 entry.clear_ambiguous_mtime(now);
823 entry.clear_ambiguous_mtime(now);
824 }
824 }
825 }
825 }
826 }
826 }
827 Ok(())
827 Ok(())
828 }
828 }
829
829
830 fn non_normal_entries_contains(
830 fn non_normal_entries_contains(
831 &mut self,
831 &mut self,
832 key: &HgPath,
832 key: &HgPath,
833 ) -> Result<bool, DirstateV2ParseError> {
833 ) -> Result<bool, DirstateV2ParseError> {
834 Ok(if let Some(node) = self.get_node(key)? {
834 Ok(if let Some(node) = self.get_node(key)? {
835 node.entry()?.map_or(false, |entry| entry.is_non_normal())
835 node.entry()?.map_or(false, |entry| entry.is_non_normal())
836 } else {
836 } else {
837 false
837 false
838 })
838 })
839 }
839 }
840
840
841 fn non_normal_entries_remove(&mut self, _key: &HgPath) {
841 fn non_normal_entries_remove(&mut self, _key: &HgPath) {
842 // Do nothing, this `DirstateMap` does not have a separate "non normal
842 // Do nothing, this `DirstateMap` does not have a separate "non normal
843 // entries" set that need to be kept up to date
843 // entries" set that need to be kept up to date
844 }
844 }
845
845
846 fn non_normal_or_other_parent_paths(
846 fn non_normal_or_other_parent_paths(
847 &mut self,
847 &mut self,
848 ) -> Box<dyn Iterator<Item = Result<&HgPath, DirstateV2ParseError>> + '_>
848 ) -> Box<dyn Iterator<Item = Result<&HgPath, DirstateV2ParseError>> + '_>
849 {
849 {
850 Box::new(self.filter_full_paths(|entry| {
850 Box::new(self.filter_full_paths(|entry| {
851 entry.is_non_normal() || entry.is_from_other_parent()
851 entry.is_non_normal() || entry.is_from_other_parent()
852 }))
852 }))
853 }
853 }
854
854
855 fn set_non_normal_other_parent_entries(&mut self, _force: bool) {
855 fn set_non_normal_other_parent_entries(&mut self, _force: bool) {
856 // Do nothing, this `DirstateMap` does not have a separate "non normal
856 // Do nothing, this `DirstateMap` does not have a separate "non normal
857 // entries" and "from other parent" sets that need to be recomputed
857 // entries" and "from other parent" sets that need to be recomputed
858 }
858 }
859
859
860 fn iter_non_normal_paths(
860 fn iter_non_normal_paths(
861 &mut self,
861 &mut self,
862 ) -> Box<
862 ) -> Box<
863 dyn Iterator<Item = Result<&HgPath, DirstateV2ParseError>> + Send + '_,
863 dyn Iterator<Item = Result<&HgPath, DirstateV2ParseError>> + Send + '_,
864 > {
864 > {
865 self.iter_non_normal_paths_panic()
865 self.iter_non_normal_paths_panic()
866 }
866 }
867
867
868 fn iter_non_normal_paths_panic(
868 fn iter_non_normal_paths_panic(
869 &self,
869 &self,
870 ) -> Box<
870 ) -> Box<
871 dyn Iterator<Item = Result<&HgPath, DirstateV2ParseError>> + Send + '_,
871 dyn Iterator<Item = Result<&HgPath, DirstateV2ParseError>> + Send + '_,
872 > {
872 > {
873 Box::new(self.filter_full_paths(|entry| entry.is_non_normal()))
873 Box::new(self.filter_full_paths(|entry| entry.is_non_normal()))
874 }
874 }
875
875
876 fn iter_other_parent_paths(
876 fn iter_other_parent_paths(
877 &mut self,
877 &mut self,
878 ) -> Box<
878 ) -> Box<
879 dyn Iterator<Item = Result<&HgPath, DirstateV2ParseError>> + Send + '_,
879 dyn Iterator<Item = Result<&HgPath, DirstateV2ParseError>> + Send + '_,
880 > {
880 > {
881 Box::new(self.filter_full_paths(|entry| entry.is_from_other_parent()))
881 Box::new(self.filter_full_paths(|entry| entry.is_from_other_parent()))
882 }
882 }
883
883
884 fn has_tracked_dir(
884 fn has_tracked_dir(
885 &mut self,
885 &mut self,
886 directory: &HgPath,
886 directory: &HgPath,
887 ) -> Result<bool, DirstateError> {
887 ) -> Result<bool, DirstateError> {
888 if let Some(node) = self.get_node(directory)? {
888 if let Some(node) = self.get_node(directory)? {
889 // A node without a `DirstateEntry` was created to hold child
889 // A node without a `DirstateEntry` was created to hold child
890 // nodes, and is therefore a directory.
890 // nodes, and is therefore a directory.
891 let state = node.state()?;
891 let state = node.state()?;
892 Ok(state.is_none() && node.tracked_descendants_count() > 0)
892 Ok(state.is_none() && node.tracked_descendants_count() > 0)
893 } else {
893 } else {
894 Ok(false)
894 Ok(false)
895 }
895 }
896 }
896 }
897
897
898 fn has_dir(&mut self, directory: &HgPath) -> Result<bool, DirstateError> {
898 fn has_dir(&mut self, directory: &HgPath) -> Result<bool, DirstateError> {
899 if let Some(node) = self.get_node(directory)? {
899 if let Some(node) = self.get_node(directory)? {
900 // A node without a `DirstateEntry` was created to hold child
900 // A node without a `DirstateEntry` was created to hold child
901 // nodes, and is therefore a directory.
901 // nodes, and is therefore a directory.
902 Ok(node.state()?.is_none())
902 Ok(node.state()?.is_none())
903 } else {
903 } else {
904 Ok(false)
904 Ok(false)
905 }
905 }
906 }
906 }
907
907
908 #[timed]
908 #[timed]
909 fn pack_v1(
909 fn pack_v1(
910 &mut self,
910 &mut self,
911 parents: DirstateParents,
911 parents: DirstateParents,
912 now: Timestamp,
912 now: Timestamp,
913 ) -> Result<Vec<u8>, DirstateError> {
913 ) -> Result<Vec<u8>, DirstateError> {
914 let now: i32 = now.0.try_into().expect("time overflow");
914 let now: i32 = now.0.try_into().expect("time overflow");
915 let mut ambiguous_mtimes = Vec::new();
915 let mut ambiguous_mtimes = Vec::new();
916 // Optizimation (to be measured?): pre-compute size to avoid `Vec`
916 // Optizimation (to be measured?): pre-compute size to avoid `Vec`
917 // reallocations
917 // reallocations
918 let mut size = parents.as_bytes().len();
918 let mut size = parents.as_bytes().len();
919 for node in self.iter_nodes() {
919 for node in self.iter_nodes() {
920 let node = node?;
920 let node = node?;
921 if let Some(entry) = node.entry()? {
921 if let Some(entry) = node.entry()? {
922 size += packed_entry_size(
922 size += packed_entry_size(
923 node.full_path(self.on_disk)?,
923 node.full_path(self.on_disk)?,
924 node.copy_source(self.on_disk)?,
924 node.copy_source(self.on_disk)?,
925 );
925 );
926 if entry.mtime_is_ambiguous(now) {
926 if entry.mtime_is_ambiguous(now) {
927 ambiguous_mtimes.push(
927 ambiguous_mtimes.push(
928 node.full_path_borrowed(self.on_disk)?
928 node.full_path_borrowed(self.on_disk)?
929 .detach_from_tree(),
929 .detach_from_tree(),
930 )
930 )
931 }
931 }
932 }
932 }
933 }
933 }
934 self.clear_known_ambiguous_mtimes(&ambiguous_mtimes)?;
934 self.clear_known_ambiguous_mtimes(&ambiguous_mtimes)?;
935
935
936 let mut packed = Vec::with_capacity(size);
936 let mut packed = Vec::with_capacity(size);
937 packed.extend(parents.as_bytes());
937 packed.extend(parents.as_bytes());
938
938
939 for node in self.iter_nodes() {
939 for node in self.iter_nodes() {
940 let node = node?;
940 let node = node?;
941 if let Some(entry) = node.entry()? {
941 if let Some(entry) = node.entry()? {
942 pack_entry(
942 pack_entry(
943 node.full_path(self.on_disk)?,
943 node.full_path(self.on_disk)?,
944 &entry,
944 &entry,
945 node.copy_source(self.on_disk)?,
945 node.copy_source(self.on_disk)?,
946 &mut packed,
946 &mut packed,
947 );
947 );
948 }
948 }
949 }
949 }
950 Ok(packed)
950 Ok(packed)
951 }
951 }
952
952
953 #[timed]
953 #[timed]
954 fn pack_v2(
954 fn pack_v2(
955 &mut self,
955 &mut self,
956 parents: DirstateParents,
956 parents: DirstateParents,
957 now: Timestamp,
957 now: Timestamp,
958 ) -> Result<Vec<u8>, DirstateError> {
958 ) -> Result<Vec<u8>, DirstateError> {
959 // TODO: how do we want to handle this in 2038?
959 // TODO: how do we want to handle this in 2038?
960 let now: i32 = now.0.try_into().expect("time overflow");
960 let now: i32 = now.0.try_into().expect("time overflow");
961 let mut paths = Vec::new();
961 let mut paths = Vec::new();
962 for node in self.iter_nodes() {
962 for node in self.iter_nodes() {
963 let node = node?;
963 let node = node?;
964 if let Some(entry) = node.entry()? {
964 if let Some(entry) = node.entry()? {
965 if entry.mtime_is_ambiguous(now) {
965 if entry.mtime_is_ambiguous(now) {
966 paths.push(
966 paths.push(
967 node.full_path_borrowed(self.on_disk)?
967 node.full_path_borrowed(self.on_disk)?
968 .detach_from_tree(),
968 .detach_from_tree(),
969 )
969 )
970 }
970 }
971 }
971 }
972 }
972 }
973 // Borrow of `self` ends here since we collect cloned paths
973 // Borrow of `self` ends here since we collect cloned paths
974
974
975 self.clear_known_ambiguous_mtimes(&paths)?;
975 self.clear_known_ambiguous_mtimes(&paths)?;
976
976
977 on_disk::write(self, parents)
977 on_disk::write(self, parents)
978 }
978 }
979
979
980 fn set_all_dirs(&mut self) -> Result<(), DirstateError> {
981 // Do nothing, this `DirstateMap` does not a separate `all_dirs` that
982 // needs to be recomputed
983 Ok(())
984 }
985
986 fn set_dirs(&mut self) -> Result<(), DirstateError> {
987 // Do nothing, this `DirstateMap` does not a separate `dirs` that needs
988 // to be recomputed
989 Ok(())
990 }
991
992 fn status<'a>(
980 fn status<'a>(
993 &'a mut self,
981 &'a mut self,
994 matcher: &'a (dyn Matcher + Sync),
982 matcher: &'a (dyn Matcher + Sync),
995 root_dir: PathBuf,
983 root_dir: PathBuf,
996 ignore_files: Vec<PathBuf>,
984 ignore_files: Vec<PathBuf>,
997 options: StatusOptions,
985 options: StatusOptions,
998 ) -> Result<(DirstateStatus<'a>, Vec<PatternFileWarning>), StatusError>
986 ) -> Result<(DirstateStatus<'a>, Vec<PatternFileWarning>), StatusError>
999 {
987 {
1000 super::status::status(self, matcher, root_dir, ignore_files, options)
988 super::status::status(self, matcher, root_dir, ignore_files, options)
1001 }
989 }
1002
990
1003 fn copy_map_len(&self) -> usize {
991 fn copy_map_len(&self) -> usize {
1004 self.nodes_with_copy_source_count as usize
992 self.nodes_with_copy_source_count as usize
1005 }
993 }
1006
994
1007 fn copy_map_iter(&self) -> CopyMapIter<'_> {
995 fn copy_map_iter(&self) -> CopyMapIter<'_> {
1008 Box::new(filter_map_results(self.iter_nodes(), move |node| {
996 Box::new(filter_map_results(self.iter_nodes(), move |node| {
1009 Ok(if let Some(source) = node.copy_source(self.on_disk)? {
997 Ok(if let Some(source) = node.copy_source(self.on_disk)? {
1010 Some((node.full_path(self.on_disk)?, source))
998 Some((node.full_path(self.on_disk)?, source))
1011 } else {
999 } else {
1012 None
1000 None
1013 })
1001 })
1014 }))
1002 }))
1015 }
1003 }
1016
1004
1017 fn copy_map_contains_key(
1005 fn copy_map_contains_key(
1018 &self,
1006 &self,
1019 key: &HgPath,
1007 key: &HgPath,
1020 ) -> Result<bool, DirstateV2ParseError> {
1008 ) -> Result<bool, DirstateV2ParseError> {
1021 Ok(if let Some(node) = self.get_node(key)? {
1009 Ok(if let Some(node) = self.get_node(key)? {
1022 node.has_copy_source()
1010 node.has_copy_source()
1023 } else {
1011 } else {
1024 false
1012 false
1025 })
1013 })
1026 }
1014 }
1027
1015
1028 fn copy_map_get(
1016 fn copy_map_get(
1029 &self,
1017 &self,
1030 key: &HgPath,
1018 key: &HgPath,
1031 ) -> Result<Option<&HgPath>, DirstateV2ParseError> {
1019 ) -> Result<Option<&HgPath>, DirstateV2ParseError> {
1032 if let Some(node) = self.get_node(key)? {
1020 if let Some(node) = self.get_node(key)? {
1033 if let Some(source) = node.copy_source(self.on_disk)? {
1021 if let Some(source) = node.copy_source(self.on_disk)? {
1034 return Ok(Some(source));
1022 return Ok(Some(source));
1035 }
1023 }
1036 }
1024 }
1037 Ok(None)
1025 Ok(None)
1038 }
1026 }
1039
1027
1040 fn copy_map_remove(
1028 fn copy_map_remove(
1041 &mut self,
1029 &mut self,
1042 key: &HgPath,
1030 key: &HgPath,
1043 ) -> Result<Option<HgPathBuf>, DirstateV2ParseError> {
1031 ) -> Result<Option<HgPathBuf>, DirstateV2ParseError> {
1044 let count = &mut self.nodes_with_copy_source_count;
1032 let count = &mut self.nodes_with_copy_source_count;
1045 Ok(
1033 Ok(
1046 Self::get_node_mut(self.on_disk, &mut self.root, key)?.and_then(
1034 Self::get_node_mut(self.on_disk, &mut self.root, key)?.and_then(
1047 |node| {
1035 |node| {
1048 if node.copy_source.is_some() {
1036 if node.copy_source.is_some() {
1049 *count -= 1
1037 *count -= 1
1050 }
1038 }
1051 node.copy_source.take().map(Cow::into_owned)
1039 node.copy_source.take().map(Cow::into_owned)
1052 },
1040 },
1053 ),
1041 ),
1054 )
1042 )
1055 }
1043 }
1056
1044
1057 fn copy_map_insert(
1045 fn copy_map_insert(
1058 &mut self,
1046 &mut self,
1059 key: HgPathBuf,
1047 key: HgPathBuf,
1060 value: HgPathBuf,
1048 value: HgPathBuf,
1061 ) -> Result<Option<HgPathBuf>, DirstateV2ParseError> {
1049 ) -> Result<Option<HgPathBuf>, DirstateV2ParseError> {
1062 let node = Self::get_or_insert_node(
1050 let node = Self::get_or_insert_node(
1063 self.on_disk,
1051 self.on_disk,
1064 &mut self.root,
1052 &mut self.root,
1065 &key,
1053 &key,
1066 WithBasename::to_cow_owned,
1054 WithBasename::to_cow_owned,
1067 |_ancestor| {},
1055 |_ancestor| {},
1068 )?;
1056 )?;
1069 if node.copy_source.is_none() {
1057 if node.copy_source.is_none() {
1070 self.nodes_with_copy_source_count += 1
1058 self.nodes_with_copy_source_count += 1
1071 }
1059 }
1072 Ok(node.copy_source.replace(value.into()).map(Cow::into_owned))
1060 Ok(node.copy_source.replace(value.into()).map(Cow::into_owned))
1073 }
1061 }
1074
1062
1075 fn len(&self) -> usize {
1063 fn len(&self) -> usize {
1076 self.nodes_with_entry_count as usize
1064 self.nodes_with_entry_count as usize
1077 }
1065 }
1078
1066
1079 fn contains_key(
1067 fn contains_key(
1080 &self,
1068 &self,
1081 key: &HgPath,
1069 key: &HgPath,
1082 ) -> Result<bool, DirstateV2ParseError> {
1070 ) -> Result<bool, DirstateV2ParseError> {
1083 Ok(self.get(key)?.is_some())
1071 Ok(self.get(key)?.is_some())
1084 }
1072 }
1085
1073
1086 fn get(
1074 fn get(
1087 &self,
1075 &self,
1088 key: &HgPath,
1076 key: &HgPath,
1089 ) -> Result<Option<DirstateEntry>, DirstateV2ParseError> {
1077 ) -> Result<Option<DirstateEntry>, DirstateV2ParseError> {
1090 Ok(if let Some(node) = self.get_node(key)? {
1078 Ok(if let Some(node) = self.get_node(key)? {
1091 node.entry()?
1079 node.entry()?
1092 } else {
1080 } else {
1093 None
1081 None
1094 })
1082 })
1095 }
1083 }
1096
1084
1097 fn iter(&self) -> StateMapIter<'_> {
1085 fn iter(&self) -> StateMapIter<'_> {
1098 Box::new(filter_map_results(self.iter_nodes(), move |node| {
1086 Box::new(filter_map_results(self.iter_nodes(), move |node| {
1099 Ok(if let Some(entry) = node.entry()? {
1087 Ok(if let Some(entry) = node.entry()? {
1100 Some((node.full_path(self.on_disk)?, entry))
1088 Some((node.full_path(self.on_disk)?, entry))
1101 } else {
1089 } else {
1102 None
1090 None
1103 })
1091 })
1104 }))
1092 }))
1105 }
1093 }
1106
1094
1107 fn iter_directories(
1095 fn iter_directories(
1108 &self,
1096 &self,
1109 ) -> Box<
1097 ) -> Box<
1110 dyn Iterator<
1098 dyn Iterator<
1111 Item = Result<
1099 Item = Result<
1112 (&HgPath, Option<Timestamp>),
1100 (&HgPath, Option<Timestamp>),
1113 DirstateV2ParseError,
1101 DirstateV2ParseError,
1114 >,
1102 >,
1115 > + Send
1103 > + Send
1116 + '_,
1104 + '_,
1117 > {
1105 > {
1118 Box::new(filter_map_results(self.iter_nodes(), move |node| {
1106 Box::new(filter_map_results(self.iter_nodes(), move |node| {
1119 Ok(if node.state()?.is_none() {
1107 Ok(if node.state()?.is_none() {
1120 Some((
1108 Some((
1121 node.full_path(self.on_disk)?,
1109 node.full_path(self.on_disk)?,
1122 node.cached_directory_mtime()
1110 node.cached_directory_mtime()
1123 .map(|mtime| Timestamp(mtime.seconds())),
1111 .map(|mtime| Timestamp(mtime.seconds())),
1124 ))
1112 ))
1125 } else {
1113 } else {
1126 None
1114 None
1127 })
1115 })
1128 }))
1116 }))
1129 }
1117 }
1130 }
1118 }
@@ -1,379 +1,367
1 use std::path::PathBuf;
1 use std::path::PathBuf;
2
2
3 use crate::dirstate::parsers::Timestamp;
3 use crate::dirstate::parsers::Timestamp;
4 use crate::dirstate_tree::on_disk::DirstateV2ParseError;
4 use crate::dirstate_tree::on_disk::DirstateV2ParseError;
5 use crate::matchers::Matcher;
5 use crate::matchers::Matcher;
6 use crate::utils::hg_path::{HgPath, HgPathBuf};
6 use crate::utils::hg_path::{HgPath, HgPathBuf};
7 use crate::CopyMapIter;
7 use crate::CopyMapIter;
8 use crate::DirstateEntry;
8 use crate::DirstateEntry;
9 use crate::DirstateError;
9 use crate::DirstateError;
10 use crate::DirstateMap;
10 use crate::DirstateMap;
11 use crate::DirstateParents;
11 use crate::DirstateParents;
12 use crate::DirstateStatus;
12 use crate::DirstateStatus;
13 use crate::EntryState;
13 use crate::EntryState;
14 use crate::PatternFileWarning;
14 use crate::PatternFileWarning;
15 use crate::StateMapIter;
15 use crate::StateMapIter;
16 use crate::StatusError;
16 use crate::StatusError;
17 use crate::StatusOptions;
17 use crate::StatusOptions;
18
18
19 pub trait DirstateMapMethods {
19 pub trait DirstateMapMethods {
20 fn clear(&mut self);
20 fn clear(&mut self);
21
21
22 fn add_file(
22 fn add_file(
23 &mut self,
23 &mut self,
24 filename: &HgPath,
24 filename: &HgPath,
25 old_state: EntryState,
25 old_state: EntryState,
26 entry: DirstateEntry,
26 entry: DirstateEntry,
27 ) -> Result<(), DirstateError>;
27 ) -> Result<(), DirstateError>;
28
28
29 fn remove_file(
29 fn remove_file(
30 &mut self,
30 &mut self,
31 filename: &HgPath,
31 filename: &HgPath,
32 old_state: EntryState,
32 old_state: EntryState,
33 size: i32,
33 size: i32,
34 ) -> Result<(), DirstateError>;
34 ) -> Result<(), DirstateError>;
35
35
36 fn drop_file(
36 fn drop_file(
37 &mut self,
37 &mut self,
38 filename: &HgPath,
38 filename: &HgPath,
39 old_state: EntryState,
39 old_state: EntryState,
40 ) -> Result<bool, DirstateError>;
40 ) -> Result<bool, DirstateError>;
41
41
42 fn clear_ambiguous_times(
42 fn clear_ambiguous_times(
43 &mut self,
43 &mut self,
44 filenames: Vec<HgPathBuf>,
44 filenames: Vec<HgPathBuf>,
45 now: i32,
45 now: i32,
46 ) -> Result<(), DirstateV2ParseError>;
46 ) -> Result<(), DirstateV2ParseError>;
47
47
48 fn non_normal_entries_contains(
48 fn non_normal_entries_contains(
49 &mut self,
49 &mut self,
50 key: &HgPath,
50 key: &HgPath,
51 ) -> Result<bool, DirstateV2ParseError>;
51 ) -> Result<bool, DirstateV2ParseError>;
52
52
53 fn non_normal_entries_remove(&mut self, key: &HgPath);
53 fn non_normal_entries_remove(&mut self, key: &HgPath);
54
54
55 fn non_normal_or_other_parent_paths(
55 fn non_normal_or_other_parent_paths(
56 &mut self,
56 &mut self,
57 ) -> Box<dyn Iterator<Item = Result<&HgPath, DirstateV2ParseError>> + '_>;
57 ) -> Box<dyn Iterator<Item = Result<&HgPath, DirstateV2ParseError>> + '_>;
58
58
59 fn set_non_normal_other_parent_entries(&mut self, force: bool);
59 fn set_non_normal_other_parent_entries(&mut self, force: bool);
60
60
61 fn iter_non_normal_paths(
61 fn iter_non_normal_paths(
62 &mut self,
62 &mut self,
63 ) -> Box<
63 ) -> Box<
64 dyn Iterator<Item = Result<&HgPath, DirstateV2ParseError>> + Send + '_,
64 dyn Iterator<Item = Result<&HgPath, DirstateV2ParseError>> + Send + '_,
65 >;
65 >;
66
66
67 fn iter_non_normal_paths_panic(
67 fn iter_non_normal_paths_panic(
68 &self,
68 &self,
69 ) -> Box<
69 ) -> Box<
70 dyn Iterator<Item = Result<&HgPath, DirstateV2ParseError>> + Send + '_,
70 dyn Iterator<Item = Result<&HgPath, DirstateV2ParseError>> + Send + '_,
71 >;
71 >;
72
72
73 fn iter_other_parent_paths(
73 fn iter_other_parent_paths(
74 &mut self,
74 &mut self,
75 ) -> Box<
75 ) -> Box<
76 dyn Iterator<Item = Result<&HgPath, DirstateV2ParseError>> + Send + '_,
76 dyn Iterator<Item = Result<&HgPath, DirstateV2ParseError>> + Send + '_,
77 >;
77 >;
78
78
79 fn has_tracked_dir(
79 fn has_tracked_dir(
80 &mut self,
80 &mut self,
81 directory: &HgPath,
81 directory: &HgPath,
82 ) -> Result<bool, DirstateError>;
82 ) -> Result<bool, DirstateError>;
83
83
84 fn has_dir(&mut self, directory: &HgPath) -> Result<bool, DirstateError>;
84 fn has_dir(&mut self, directory: &HgPath) -> Result<bool, DirstateError>;
85
85
86 fn pack_v1(
86 fn pack_v1(
87 &mut self,
87 &mut self,
88 parents: DirstateParents,
88 parents: DirstateParents,
89 now: Timestamp,
89 now: Timestamp,
90 ) -> Result<Vec<u8>, DirstateError>;
90 ) -> Result<Vec<u8>, DirstateError>;
91
91
92 fn pack_v2(
92 fn pack_v2(
93 &mut self,
93 &mut self,
94 parents: DirstateParents,
94 parents: DirstateParents,
95 now: Timestamp,
95 now: Timestamp,
96 ) -> Result<Vec<u8>, DirstateError>;
96 ) -> Result<Vec<u8>, DirstateError>;
97
97
98 fn set_all_dirs(&mut self) -> Result<(), DirstateError>;
99
100 fn set_dirs(&mut self) -> Result<(), DirstateError>;
101
102 fn status<'a>(
98 fn status<'a>(
103 &'a mut self,
99 &'a mut self,
104 matcher: &'a (dyn Matcher + Sync),
100 matcher: &'a (dyn Matcher + Sync),
105 root_dir: PathBuf,
101 root_dir: PathBuf,
106 ignore_files: Vec<PathBuf>,
102 ignore_files: Vec<PathBuf>,
107 options: StatusOptions,
103 options: StatusOptions,
108 ) -> Result<(DirstateStatus<'a>, Vec<PatternFileWarning>), StatusError>;
104 ) -> Result<(DirstateStatus<'a>, Vec<PatternFileWarning>), StatusError>;
109
105
110 fn copy_map_len(&self) -> usize;
106 fn copy_map_len(&self) -> usize;
111
107
112 fn copy_map_iter(&self) -> CopyMapIter<'_>;
108 fn copy_map_iter(&self) -> CopyMapIter<'_>;
113
109
114 fn copy_map_contains_key(
110 fn copy_map_contains_key(
115 &self,
111 &self,
116 key: &HgPath,
112 key: &HgPath,
117 ) -> Result<bool, DirstateV2ParseError>;
113 ) -> Result<bool, DirstateV2ParseError>;
118
114
119 fn copy_map_get(
115 fn copy_map_get(
120 &self,
116 &self,
121 key: &HgPath,
117 key: &HgPath,
122 ) -> Result<Option<&HgPath>, DirstateV2ParseError>;
118 ) -> Result<Option<&HgPath>, DirstateV2ParseError>;
123
119
124 fn copy_map_remove(
120 fn copy_map_remove(
125 &mut self,
121 &mut self,
126 key: &HgPath,
122 key: &HgPath,
127 ) -> Result<Option<HgPathBuf>, DirstateV2ParseError>;
123 ) -> Result<Option<HgPathBuf>, DirstateV2ParseError>;
128
124
129 fn copy_map_insert(
125 fn copy_map_insert(
130 &mut self,
126 &mut self,
131 key: HgPathBuf,
127 key: HgPathBuf,
132 value: HgPathBuf,
128 value: HgPathBuf,
133 ) -> Result<Option<HgPathBuf>, DirstateV2ParseError>;
129 ) -> Result<Option<HgPathBuf>, DirstateV2ParseError>;
134
130
135 fn len(&self) -> usize;
131 fn len(&self) -> usize;
136
132
137 fn contains_key(&self, key: &HgPath)
133 fn contains_key(&self, key: &HgPath)
138 -> Result<bool, DirstateV2ParseError>;
134 -> Result<bool, DirstateV2ParseError>;
139
135
140 fn get(
136 fn get(
141 &self,
137 &self,
142 key: &HgPath,
138 key: &HgPath,
143 ) -> Result<Option<DirstateEntry>, DirstateV2ParseError>;
139 ) -> Result<Option<DirstateEntry>, DirstateV2ParseError>;
144
140
145 fn iter(&self) -> StateMapIter<'_>;
141 fn iter(&self) -> StateMapIter<'_>;
146
142
147 fn iter_directories(
143 fn iter_directories(
148 &self,
144 &self,
149 ) -> Box<
145 ) -> Box<
150 dyn Iterator<
146 dyn Iterator<
151 Item = Result<
147 Item = Result<
152 (&HgPath, Option<Timestamp>),
148 (&HgPath, Option<Timestamp>),
153 DirstateV2ParseError,
149 DirstateV2ParseError,
154 >,
150 >,
155 > + Send
151 > + Send
156 + '_,
152 + '_,
157 >;
153 >;
158 }
154 }
159
155
160 impl DirstateMapMethods for DirstateMap {
156 impl DirstateMapMethods for DirstateMap {
161 fn clear(&mut self) {
157 fn clear(&mut self) {
162 self.clear()
158 self.clear()
163 }
159 }
164
160
165 fn add_file(
161 fn add_file(
166 &mut self,
162 &mut self,
167 filename: &HgPath,
163 filename: &HgPath,
168 old_state: EntryState,
164 old_state: EntryState,
169 entry: DirstateEntry,
165 entry: DirstateEntry,
170 ) -> Result<(), DirstateError> {
166 ) -> Result<(), DirstateError> {
171 self.add_file(filename, old_state, entry)
167 self.add_file(filename, old_state, entry)
172 }
168 }
173
169
174 fn remove_file(
170 fn remove_file(
175 &mut self,
171 &mut self,
176 filename: &HgPath,
172 filename: &HgPath,
177 old_state: EntryState,
173 old_state: EntryState,
178 size: i32,
174 size: i32,
179 ) -> Result<(), DirstateError> {
175 ) -> Result<(), DirstateError> {
180 self.remove_file(filename, old_state, size)
176 self.remove_file(filename, old_state, size)
181 }
177 }
182
178
183 fn drop_file(
179 fn drop_file(
184 &mut self,
180 &mut self,
185 filename: &HgPath,
181 filename: &HgPath,
186 old_state: EntryState,
182 old_state: EntryState,
187 ) -> Result<bool, DirstateError> {
183 ) -> Result<bool, DirstateError> {
188 self.drop_file(filename, old_state)
184 self.drop_file(filename, old_state)
189 }
185 }
190
186
191 fn clear_ambiguous_times(
187 fn clear_ambiguous_times(
192 &mut self,
188 &mut self,
193 filenames: Vec<HgPathBuf>,
189 filenames: Vec<HgPathBuf>,
194 now: i32,
190 now: i32,
195 ) -> Result<(), DirstateV2ParseError> {
191 ) -> Result<(), DirstateV2ParseError> {
196 Ok(self.clear_ambiguous_times(filenames, now))
192 Ok(self.clear_ambiguous_times(filenames, now))
197 }
193 }
198
194
199 fn non_normal_entries_contains(
195 fn non_normal_entries_contains(
200 &mut self,
196 &mut self,
201 key: &HgPath,
197 key: &HgPath,
202 ) -> Result<bool, DirstateV2ParseError> {
198 ) -> Result<bool, DirstateV2ParseError> {
203 let (non_normal, _other_parent) =
199 let (non_normal, _other_parent) =
204 self.get_non_normal_other_parent_entries();
200 self.get_non_normal_other_parent_entries();
205 Ok(non_normal.contains(key))
201 Ok(non_normal.contains(key))
206 }
202 }
207
203
208 fn non_normal_entries_remove(&mut self, key: &HgPath) {
204 fn non_normal_entries_remove(&mut self, key: &HgPath) {
209 self.non_normal_entries_remove(key)
205 self.non_normal_entries_remove(key)
210 }
206 }
211
207
212 fn non_normal_or_other_parent_paths(
208 fn non_normal_or_other_parent_paths(
213 &mut self,
209 &mut self,
214 ) -> Box<dyn Iterator<Item = Result<&HgPath, DirstateV2ParseError>> + '_>
210 ) -> Box<dyn Iterator<Item = Result<&HgPath, DirstateV2ParseError>> + '_>
215 {
211 {
216 let (non_normal, other_parent) =
212 let (non_normal, other_parent) =
217 self.get_non_normal_other_parent_entries();
213 self.get_non_normal_other_parent_entries();
218 Box::new(non_normal.union(other_parent).map(|p| Ok(&**p)))
214 Box::new(non_normal.union(other_parent).map(|p| Ok(&**p)))
219 }
215 }
220
216
221 fn set_non_normal_other_parent_entries(&mut self, force: bool) {
217 fn set_non_normal_other_parent_entries(&mut self, force: bool) {
222 self.set_non_normal_other_parent_entries(force)
218 self.set_non_normal_other_parent_entries(force)
223 }
219 }
224
220
225 fn iter_non_normal_paths(
221 fn iter_non_normal_paths(
226 &mut self,
222 &mut self,
227 ) -> Box<
223 ) -> Box<
228 dyn Iterator<Item = Result<&HgPath, DirstateV2ParseError>> + Send + '_,
224 dyn Iterator<Item = Result<&HgPath, DirstateV2ParseError>> + Send + '_,
229 > {
225 > {
230 let (non_normal, _other_parent) =
226 let (non_normal, _other_parent) =
231 self.get_non_normal_other_parent_entries();
227 self.get_non_normal_other_parent_entries();
232 Box::new(non_normal.iter().map(|p| Ok(&**p)))
228 Box::new(non_normal.iter().map(|p| Ok(&**p)))
233 }
229 }
234
230
235 fn iter_non_normal_paths_panic(
231 fn iter_non_normal_paths_panic(
236 &self,
232 &self,
237 ) -> Box<
233 ) -> Box<
238 dyn Iterator<Item = Result<&HgPath, DirstateV2ParseError>> + Send + '_,
234 dyn Iterator<Item = Result<&HgPath, DirstateV2ParseError>> + Send + '_,
239 > {
235 > {
240 let (non_normal, _other_parent) =
236 let (non_normal, _other_parent) =
241 self.get_non_normal_other_parent_entries_panic();
237 self.get_non_normal_other_parent_entries_panic();
242 Box::new(non_normal.iter().map(|p| Ok(&**p)))
238 Box::new(non_normal.iter().map(|p| Ok(&**p)))
243 }
239 }
244
240
245 fn iter_other_parent_paths(
241 fn iter_other_parent_paths(
246 &mut self,
242 &mut self,
247 ) -> Box<
243 ) -> Box<
248 dyn Iterator<Item = Result<&HgPath, DirstateV2ParseError>> + Send + '_,
244 dyn Iterator<Item = Result<&HgPath, DirstateV2ParseError>> + Send + '_,
249 > {
245 > {
250 let (_non_normal, other_parent) =
246 let (_non_normal, other_parent) =
251 self.get_non_normal_other_parent_entries();
247 self.get_non_normal_other_parent_entries();
252 Box::new(other_parent.iter().map(|p| Ok(&**p)))
248 Box::new(other_parent.iter().map(|p| Ok(&**p)))
253 }
249 }
254
250
255 fn has_tracked_dir(
251 fn has_tracked_dir(
256 &mut self,
252 &mut self,
257 directory: &HgPath,
253 directory: &HgPath,
258 ) -> Result<bool, DirstateError> {
254 ) -> Result<bool, DirstateError> {
259 self.has_tracked_dir(directory)
255 self.has_tracked_dir(directory)
260 }
256 }
261
257
262 fn has_dir(&mut self, directory: &HgPath) -> Result<bool, DirstateError> {
258 fn has_dir(&mut self, directory: &HgPath) -> Result<bool, DirstateError> {
263 self.has_dir(directory)
259 self.has_dir(directory)
264 }
260 }
265
261
266 fn pack_v1(
262 fn pack_v1(
267 &mut self,
263 &mut self,
268 parents: DirstateParents,
264 parents: DirstateParents,
269 now: Timestamp,
265 now: Timestamp,
270 ) -> Result<Vec<u8>, DirstateError> {
266 ) -> Result<Vec<u8>, DirstateError> {
271 self.pack(parents, now)
267 self.pack(parents, now)
272 }
268 }
273
269
274 fn pack_v2(
270 fn pack_v2(
275 &mut self,
271 &mut self,
276 _parents: DirstateParents,
272 _parents: DirstateParents,
277 _now: Timestamp,
273 _now: Timestamp,
278 ) -> Result<Vec<u8>, DirstateError> {
274 ) -> Result<Vec<u8>, DirstateError> {
279 panic!(
275 panic!(
280 "should have used dirstate_tree::DirstateMap to use the v2 format"
276 "should have used dirstate_tree::DirstateMap to use the v2 format"
281 )
277 )
282 }
278 }
283
279
284 fn set_all_dirs(&mut self) -> Result<(), DirstateError> {
285 self.set_all_dirs()
286 }
287
288 fn set_dirs(&mut self) -> Result<(), DirstateError> {
289 self.set_dirs()
290 }
291
292 fn status<'a>(
280 fn status<'a>(
293 &'a mut self,
281 &'a mut self,
294 matcher: &'a (dyn Matcher + Sync),
282 matcher: &'a (dyn Matcher + Sync),
295 root_dir: PathBuf,
283 root_dir: PathBuf,
296 ignore_files: Vec<PathBuf>,
284 ignore_files: Vec<PathBuf>,
297 options: StatusOptions,
285 options: StatusOptions,
298 ) -> Result<(DirstateStatus<'a>, Vec<PatternFileWarning>), StatusError>
286 ) -> Result<(DirstateStatus<'a>, Vec<PatternFileWarning>), StatusError>
299 {
287 {
300 crate::status(self, matcher, root_dir, ignore_files, options)
288 crate::status(self, matcher, root_dir, ignore_files, options)
301 }
289 }
302
290
303 fn copy_map_len(&self) -> usize {
291 fn copy_map_len(&self) -> usize {
304 self.copy_map.len()
292 self.copy_map.len()
305 }
293 }
306
294
307 fn copy_map_iter(&self) -> CopyMapIter<'_> {
295 fn copy_map_iter(&self) -> CopyMapIter<'_> {
308 Box::new(
296 Box::new(
309 self.copy_map
297 self.copy_map
310 .iter()
298 .iter()
311 .map(|(key, value)| Ok((&**key, &**value))),
299 .map(|(key, value)| Ok((&**key, &**value))),
312 )
300 )
313 }
301 }
314
302
315 fn copy_map_contains_key(
303 fn copy_map_contains_key(
316 &self,
304 &self,
317 key: &HgPath,
305 key: &HgPath,
318 ) -> Result<bool, DirstateV2ParseError> {
306 ) -> Result<bool, DirstateV2ParseError> {
319 Ok(self.copy_map.contains_key(key))
307 Ok(self.copy_map.contains_key(key))
320 }
308 }
321
309
322 fn copy_map_get(
310 fn copy_map_get(
323 &self,
311 &self,
324 key: &HgPath,
312 key: &HgPath,
325 ) -> Result<Option<&HgPath>, DirstateV2ParseError> {
313 ) -> Result<Option<&HgPath>, DirstateV2ParseError> {
326 Ok(self.copy_map.get(key).map(|p| &**p))
314 Ok(self.copy_map.get(key).map(|p| &**p))
327 }
315 }
328
316
329 fn copy_map_remove(
317 fn copy_map_remove(
330 &mut self,
318 &mut self,
331 key: &HgPath,
319 key: &HgPath,
332 ) -> Result<Option<HgPathBuf>, DirstateV2ParseError> {
320 ) -> Result<Option<HgPathBuf>, DirstateV2ParseError> {
333 Ok(self.copy_map.remove(key))
321 Ok(self.copy_map.remove(key))
334 }
322 }
335
323
336 fn copy_map_insert(
324 fn copy_map_insert(
337 &mut self,
325 &mut self,
338 key: HgPathBuf,
326 key: HgPathBuf,
339 value: HgPathBuf,
327 value: HgPathBuf,
340 ) -> Result<Option<HgPathBuf>, DirstateV2ParseError> {
328 ) -> Result<Option<HgPathBuf>, DirstateV2ParseError> {
341 Ok(self.copy_map.insert(key, value))
329 Ok(self.copy_map.insert(key, value))
342 }
330 }
343
331
344 fn len(&self) -> usize {
332 fn len(&self) -> usize {
345 (&**self).len()
333 (&**self).len()
346 }
334 }
347
335
348 fn contains_key(
336 fn contains_key(
349 &self,
337 &self,
350 key: &HgPath,
338 key: &HgPath,
351 ) -> Result<bool, DirstateV2ParseError> {
339 ) -> Result<bool, DirstateV2ParseError> {
352 Ok((&**self).contains_key(key))
340 Ok((&**self).contains_key(key))
353 }
341 }
354
342
355 fn get(
343 fn get(
356 &self,
344 &self,
357 key: &HgPath,
345 key: &HgPath,
358 ) -> Result<Option<DirstateEntry>, DirstateV2ParseError> {
346 ) -> Result<Option<DirstateEntry>, DirstateV2ParseError> {
359 Ok((&**self).get(key).cloned())
347 Ok((&**self).get(key).cloned())
360 }
348 }
361
349
362 fn iter(&self) -> StateMapIter<'_> {
350 fn iter(&self) -> StateMapIter<'_> {
363 Box::new((&**self).iter().map(|(key, value)| Ok((&**key, *value))))
351 Box::new((&**self).iter().map(|(key, value)| Ok((&**key, *value))))
364 }
352 }
365
353
366 fn iter_directories(
354 fn iter_directories(
367 &self,
355 &self,
368 ) -> Box<
356 ) -> Box<
369 dyn Iterator<
357 dyn Iterator<
370 Item = Result<
358 Item = Result<
371 (&HgPath, Option<Timestamp>),
359 (&HgPath, Option<Timestamp>),
372 DirstateV2ParseError,
360 DirstateV2ParseError,
373 >,
361 >,
374 > + Send
362 > + Send
375 + '_,
363 + '_,
376 > {
364 > {
377 Box::new(std::iter::empty())
365 Box::new(std::iter::empty())
378 }
366 }
379 }
367 }
@@ -1,602 +1,568
1 // dirstate_map.rs
1 // dirstate_map.rs
2 //
2 //
3 // Copyright 2019 Raphaël Gomès <rgomes@octobus.net>
3 // Copyright 2019 Raphaël Gomès <rgomes@octobus.net>
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 //! Bindings for the `hg::dirstate::dirstate_map` file provided by the
8 //! Bindings for the `hg::dirstate::dirstate_map` file provided by the
9 //! `hg-core` package.
9 //! `hg-core` package.
10
10
11 use std::cell::{RefCell, RefMut};
11 use std::cell::{RefCell, RefMut};
12 use std::convert::TryInto;
12 use std::convert::TryInto;
13
13
14 use cpython::{
14 use cpython::{
15 exc, ObjectProtocol, PyBool, PyBytes, PyClone, PyDict, PyErr, PyList,
15 exc, ObjectProtocol, PyBool, PyBytes, PyClone, PyDict, PyErr, PyList,
16 PyObject, PyResult, PySet, PyString, Python, PythonObject, ToPyObject,
16 PyObject, PyResult, PySet, PyString, Python, PythonObject, ToPyObject,
17 UnsafePyLeaked,
17 UnsafePyLeaked,
18 };
18 };
19
19
20 use crate::{
20 use crate::{
21 dirstate::copymap::{CopyMap, CopyMapItemsIterator, CopyMapKeysIterator},
21 dirstate::copymap::{CopyMap, CopyMapItemsIterator, CopyMapKeysIterator},
22 dirstate::make_dirstate_tuple,
22 dirstate::non_normal_entries::{
23 dirstate::non_normal_entries::{
23 NonNormalEntries, NonNormalEntriesIterator,
24 NonNormalEntries, NonNormalEntriesIterator,
24 },
25 },
25 dirstate::owning::OwningDirstateMap,
26 dirstate::owning::OwningDirstateMap,
26 dirstate::{dirs_multiset::Dirs, make_dirstate_tuple},
27 parsers::dirstate_parents_to_pytuple,
27 parsers::dirstate_parents_to_pytuple,
28 };
28 };
29 use hg::{
29 use hg::{
30 dirstate::parsers::Timestamp,
30 dirstate::parsers::Timestamp,
31 dirstate_tree::dispatch::DirstateMapMethods,
31 dirstate_tree::dispatch::DirstateMapMethods,
32 dirstate_tree::on_disk::DirstateV2ParseError,
32 dirstate_tree::on_disk::DirstateV2ParseError,
33 errors::HgError,
33 errors::HgError,
34 revlog::Node,
34 revlog::Node,
35 utils::files::normalize_case,
35 utils::files::normalize_case,
36 utils::hg_path::{HgPath, HgPathBuf},
36 utils::hg_path::{HgPath, HgPathBuf},
37 DirsMultiset, DirstateEntry, DirstateError,
37 DirstateEntry, DirstateError, DirstateMap as RustDirstateMap,
38 DirstateMap as RustDirstateMap, DirstateParents, EntryState, StateMapIter,
38 DirstateParents, EntryState, StateMapIter,
39 };
39 };
40
40
41 // TODO
41 // TODO
42 // This object needs to share references to multiple members of its Rust
42 // This object needs to share references to multiple members of its Rust
43 // inner struct, namely `copy_map`, `dirs` and `all_dirs`.
43 // inner struct, namely `copy_map`, `dirs` and `all_dirs`.
44 // Right now `CopyMap` is done, but it needs to have an explicit reference
44 // Right now `CopyMap` is done, but it needs to have an explicit reference
45 // to `RustDirstateMap` which itself needs to have an encapsulation for
45 // to `RustDirstateMap` which itself needs to have an encapsulation for
46 // every method in `CopyMap` (copymapcopy, etc.).
46 // every method in `CopyMap` (copymapcopy, etc.).
47 // This is ugly and hard to maintain.
47 // This is ugly and hard to maintain.
48 // The same logic applies to `dirs` and `all_dirs`, however the `Dirs`
48 // The same logic applies to `dirs` and `all_dirs`, however the `Dirs`
49 // `py_class!` is already implemented and does not mention
49 // `py_class!` is already implemented and does not mention
50 // `RustDirstateMap`, rightfully so.
50 // `RustDirstateMap`, rightfully so.
51 // All attributes also have to have a separate refcount data attribute for
51 // All attributes also have to have a separate refcount data attribute for
52 // leaks, with all methods that go along for reference sharing.
52 // leaks, with all methods that go along for reference sharing.
53 py_class!(pub class DirstateMap |py| {
53 py_class!(pub class DirstateMap |py| {
54 @shared data inner: Box<dyn DirstateMapMethods + Send>;
54 @shared data inner: Box<dyn DirstateMapMethods + Send>;
55
55
56 /// Returns a `(dirstate_map, parents)` tuple
56 /// Returns a `(dirstate_map, parents)` tuple
57 @staticmethod
57 @staticmethod
58 def new(
58 def new(
59 use_dirstate_tree: bool,
59 use_dirstate_tree: bool,
60 use_dirstate_v2: bool,
60 use_dirstate_v2: bool,
61 on_disk: PyBytes,
61 on_disk: PyBytes,
62 ) -> PyResult<PyObject> {
62 ) -> PyResult<PyObject> {
63 let dirstate_error = |e: DirstateError| {
63 let dirstate_error = |e: DirstateError| {
64 PyErr::new::<exc::OSError, _>(py, format!("Dirstate error: {:?}", e))
64 PyErr::new::<exc::OSError, _>(py, format!("Dirstate error: {:?}", e))
65 };
65 };
66 let (inner, parents) = if use_dirstate_tree || use_dirstate_v2 {
66 let (inner, parents) = if use_dirstate_tree || use_dirstate_v2 {
67 let (map, parents) =
67 let (map, parents) =
68 OwningDirstateMap::new(py, on_disk, use_dirstate_v2)
68 OwningDirstateMap::new(py, on_disk, use_dirstate_v2)
69 .map_err(dirstate_error)?;
69 .map_err(dirstate_error)?;
70 (Box::new(map) as _, parents)
70 (Box::new(map) as _, parents)
71 } else {
71 } else {
72 let bytes = on_disk.data(py);
72 let bytes = on_disk.data(py);
73 let mut map = RustDirstateMap::default();
73 let mut map = RustDirstateMap::default();
74 let parents = map.read(bytes).map_err(dirstate_error)?;
74 let parents = map.read(bytes).map_err(dirstate_error)?;
75 (Box::new(map) as _, parents)
75 (Box::new(map) as _, parents)
76 };
76 };
77 let map = Self::create_instance(py, inner)?;
77 let map = Self::create_instance(py, inner)?;
78 let parents = parents.map(|p| dirstate_parents_to_pytuple(py, &p));
78 let parents = parents.map(|p| dirstate_parents_to_pytuple(py, &p));
79 Ok((map, parents).to_py_object(py).into_object())
79 Ok((map, parents).to_py_object(py).into_object())
80 }
80 }
81
81
82 def clear(&self) -> PyResult<PyObject> {
82 def clear(&self) -> PyResult<PyObject> {
83 self.inner(py).borrow_mut().clear();
83 self.inner(py).borrow_mut().clear();
84 Ok(py.None())
84 Ok(py.None())
85 }
85 }
86
86
87 def get(
87 def get(
88 &self,
88 &self,
89 key: PyObject,
89 key: PyObject,
90 default: Option<PyObject> = None
90 default: Option<PyObject> = None
91 ) -> PyResult<Option<PyObject>> {
91 ) -> PyResult<Option<PyObject>> {
92 let key = key.extract::<PyBytes>(py)?;
92 let key = key.extract::<PyBytes>(py)?;
93 match self
93 match self
94 .inner(py)
94 .inner(py)
95 .borrow()
95 .borrow()
96 .get(HgPath::new(key.data(py)))
96 .get(HgPath::new(key.data(py)))
97 .map_err(|e| v2_error(py, e))?
97 .map_err(|e| v2_error(py, e))?
98 {
98 {
99 Some(entry) => {
99 Some(entry) => {
100 Ok(Some(make_dirstate_tuple(py, &entry)?))
100 Ok(Some(make_dirstate_tuple(py, &entry)?))
101 },
101 },
102 None => Ok(default)
102 None => Ok(default)
103 }
103 }
104 }
104 }
105
105
106 def addfile(
106 def addfile(
107 &self,
107 &self,
108 f: PyObject,
108 f: PyObject,
109 oldstate: PyObject,
109 oldstate: PyObject,
110 state: PyObject,
110 state: PyObject,
111 mode: PyObject,
111 mode: PyObject,
112 size: PyObject,
112 size: PyObject,
113 mtime: PyObject
113 mtime: PyObject
114 ) -> PyResult<PyObject> {
114 ) -> PyResult<PyObject> {
115 self.inner(py).borrow_mut().add_file(
115 self.inner(py).borrow_mut().add_file(
116 HgPath::new(f.extract::<PyBytes>(py)?.data(py)),
116 HgPath::new(f.extract::<PyBytes>(py)?.data(py)),
117 oldstate.extract::<PyBytes>(py)?.data(py)[0]
117 oldstate.extract::<PyBytes>(py)?.data(py)[0]
118 .try_into()
118 .try_into()
119 .map_err(|e: HgError| {
119 .map_err(|e: HgError| {
120 PyErr::new::<exc::ValueError, _>(py, e.to_string())
120 PyErr::new::<exc::ValueError, _>(py, e.to_string())
121 })?,
121 })?,
122 DirstateEntry {
122 DirstateEntry {
123 state: state.extract::<PyBytes>(py)?.data(py)[0]
123 state: state.extract::<PyBytes>(py)?.data(py)[0]
124 .try_into()
124 .try_into()
125 .map_err(|e: HgError| {
125 .map_err(|e: HgError| {
126 PyErr::new::<exc::ValueError, _>(py, e.to_string())
126 PyErr::new::<exc::ValueError, _>(py, e.to_string())
127 })?,
127 })?,
128 mode: mode.extract(py)?,
128 mode: mode.extract(py)?,
129 size: size.extract(py)?,
129 size: size.extract(py)?,
130 mtime: mtime.extract(py)?,
130 mtime: mtime.extract(py)?,
131 },
131 },
132 ).and(Ok(py.None())).or_else(|e: DirstateError| {
132 ).and(Ok(py.None())).or_else(|e: DirstateError| {
133 Err(PyErr::new::<exc::ValueError, _>(py, e.to_string()))
133 Err(PyErr::new::<exc::ValueError, _>(py, e.to_string()))
134 })
134 })
135 }
135 }
136
136
137 def removefile(
137 def removefile(
138 &self,
138 &self,
139 f: PyObject,
139 f: PyObject,
140 oldstate: PyObject,
140 oldstate: PyObject,
141 size: PyObject
141 size: PyObject
142 ) -> PyResult<PyObject> {
142 ) -> PyResult<PyObject> {
143 self.inner(py).borrow_mut()
143 self.inner(py).borrow_mut()
144 .remove_file(
144 .remove_file(
145 HgPath::new(f.extract::<PyBytes>(py)?.data(py)),
145 HgPath::new(f.extract::<PyBytes>(py)?.data(py)),
146 oldstate.extract::<PyBytes>(py)?.data(py)[0]
146 oldstate.extract::<PyBytes>(py)?.data(py)[0]
147 .try_into()
147 .try_into()
148 .map_err(|e: HgError| {
148 .map_err(|e: HgError| {
149 PyErr::new::<exc::ValueError, _>(py, e.to_string())
149 PyErr::new::<exc::ValueError, _>(py, e.to_string())
150 })?,
150 })?,
151 size.extract(py)?,
151 size.extract(py)?,
152 )
152 )
153 .or_else(|_| {
153 .or_else(|_| {
154 Err(PyErr::new::<exc::OSError, _>(
154 Err(PyErr::new::<exc::OSError, _>(
155 py,
155 py,
156 "Dirstate error".to_string(),
156 "Dirstate error".to_string(),
157 ))
157 ))
158 })?;
158 })?;
159 Ok(py.None())
159 Ok(py.None())
160 }
160 }
161
161
162 def dropfile(
162 def dropfile(
163 &self,
163 &self,
164 f: PyObject,
164 f: PyObject,
165 oldstate: PyObject
165 oldstate: PyObject
166 ) -> PyResult<PyBool> {
166 ) -> PyResult<PyBool> {
167 self.inner(py).borrow_mut()
167 self.inner(py).borrow_mut()
168 .drop_file(
168 .drop_file(
169 HgPath::new(f.extract::<PyBytes>(py)?.data(py)),
169 HgPath::new(f.extract::<PyBytes>(py)?.data(py)),
170 oldstate.extract::<PyBytes>(py)?.data(py)[0]
170 oldstate.extract::<PyBytes>(py)?.data(py)[0]
171 .try_into()
171 .try_into()
172 .map_err(|e: HgError| {
172 .map_err(|e: HgError| {
173 PyErr::new::<exc::ValueError, _>(py, e.to_string())
173 PyErr::new::<exc::ValueError, _>(py, e.to_string())
174 })?,
174 })?,
175 )
175 )
176 .and_then(|b| Ok(b.to_py_object(py)))
176 .and_then(|b| Ok(b.to_py_object(py)))
177 .or_else(|e| {
177 .or_else(|e| {
178 Err(PyErr::new::<exc::OSError, _>(
178 Err(PyErr::new::<exc::OSError, _>(
179 py,
179 py,
180 format!("Dirstate error: {}", e.to_string()),
180 format!("Dirstate error: {}", e.to_string()),
181 ))
181 ))
182 })
182 })
183 }
183 }
184
184
185 def clearambiguoustimes(
185 def clearambiguoustimes(
186 &self,
186 &self,
187 files: PyObject,
187 files: PyObject,
188 now: PyObject
188 now: PyObject
189 ) -> PyResult<PyObject> {
189 ) -> PyResult<PyObject> {
190 let files: PyResult<Vec<HgPathBuf>> = files
190 let files: PyResult<Vec<HgPathBuf>> = files
191 .iter(py)?
191 .iter(py)?
192 .map(|filename| {
192 .map(|filename| {
193 Ok(HgPathBuf::from_bytes(
193 Ok(HgPathBuf::from_bytes(
194 filename?.extract::<PyBytes>(py)?.data(py),
194 filename?.extract::<PyBytes>(py)?.data(py),
195 ))
195 ))
196 })
196 })
197 .collect();
197 .collect();
198 self.inner(py)
198 self.inner(py)
199 .borrow_mut()
199 .borrow_mut()
200 .clear_ambiguous_times(files?, now.extract(py)?)
200 .clear_ambiguous_times(files?, now.extract(py)?)
201 .map_err(|e| v2_error(py, e))?;
201 .map_err(|e| v2_error(py, e))?;
202 Ok(py.None())
202 Ok(py.None())
203 }
203 }
204
204
205 def other_parent_entries(&self) -> PyResult<PyObject> {
205 def other_parent_entries(&self) -> PyResult<PyObject> {
206 let mut inner_shared = self.inner(py).borrow_mut();
206 let mut inner_shared = self.inner(py).borrow_mut();
207 let set = PySet::empty(py)?;
207 let set = PySet::empty(py)?;
208 for path in inner_shared.iter_other_parent_paths() {
208 for path in inner_shared.iter_other_parent_paths() {
209 let path = path.map_err(|e| v2_error(py, e))?;
209 let path = path.map_err(|e| v2_error(py, e))?;
210 set.add(py, PyBytes::new(py, path.as_bytes()))?;
210 set.add(py, PyBytes::new(py, path.as_bytes()))?;
211 }
211 }
212 Ok(set.into_object())
212 Ok(set.into_object())
213 }
213 }
214
214
215 def non_normal_entries(&self) -> PyResult<NonNormalEntries> {
215 def non_normal_entries(&self) -> PyResult<NonNormalEntries> {
216 NonNormalEntries::from_inner(py, self.clone_ref(py))
216 NonNormalEntries::from_inner(py, self.clone_ref(py))
217 }
217 }
218
218
219 def non_normal_entries_contains(&self, key: PyObject) -> PyResult<bool> {
219 def non_normal_entries_contains(&self, key: PyObject) -> PyResult<bool> {
220 let key = key.extract::<PyBytes>(py)?;
220 let key = key.extract::<PyBytes>(py)?;
221 self.inner(py)
221 self.inner(py)
222 .borrow_mut()
222 .borrow_mut()
223 .non_normal_entries_contains(HgPath::new(key.data(py)))
223 .non_normal_entries_contains(HgPath::new(key.data(py)))
224 .map_err(|e| v2_error(py, e))
224 .map_err(|e| v2_error(py, e))
225 }
225 }
226
226
227 def non_normal_entries_display(&self) -> PyResult<PyString> {
227 def non_normal_entries_display(&self) -> PyResult<PyString> {
228 let mut inner = self.inner(py).borrow_mut();
228 let mut inner = self.inner(py).borrow_mut();
229 let paths = inner
229 let paths = inner
230 .iter_non_normal_paths()
230 .iter_non_normal_paths()
231 .collect::<Result<Vec<_>, _>>()
231 .collect::<Result<Vec<_>, _>>()
232 .map_err(|e| v2_error(py, e))?;
232 .map_err(|e| v2_error(py, e))?;
233 let formatted = format!("NonNormalEntries: {}", hg::utils::join_display(paths, ", "));
233 let formatted = format!("NonNormalEntries: {}", hg::utils::join_display(paths, ", "));
234 Ok(PyString::new(py, &formatted))
234 Ok(PyString::new(py, &formatted))
235 }
235 }
236
236
237 def non_normal_entries_remove(&self, key: PyObject) -> PyResult<PyObject> {
237 def non_normal_entries_remove(&self, key: PyObject) -> PyResult<PyObject> {
238 let key = key.extract::<PyBytes>(py)?;
238 let key = key.extract::<PyBytes>(py)?;
239 self
239 self
240 .inner(py)
240 .inner(py)
241 .borrow_mut()
241 .borrow_mut()
242 .non_normal_entries_remove(HgPath::new(key.data(py)));
242 .non_normal_entries_remove(HgPath::new(key.data(py)));
243 Ok(py.None())
243 Ok(py.None())
244 }
244 }
245
245
246 def non_normal_or_other_parent_paths(&self) -> PyResult<PyList> {
246 def non_normal_or_other_parent_paths(&self) -> PyResult<PyList> {
247 let mut inner = self.inner(py).borrow_mut();
247 let mut inner = self.inner(py).borrow_mut();
248
248
249 let ret = PyList::new(py, &[]);
249 let ret = PyList::new(py, &[]);
250 for filename in inner.non_normal_or_other_parent_paths() {
250 for filename in inner.non_normal_or_other_parent_paths() {
251 let filename = filename.map_err(|e| v2_error(py, e))?;
251 let filename = filename.map_err(|e| v2_error(py, e))?;
252 let as_pystring = PyBytes::new(py, filename.as_bytes());
252 let as_pystring = PyBytes::new(py, filename.as_bytes());
253 ret.append(py, as_pystring.into_object());
253 ret.append(py, as_pystring.into_object());
254 }
254 }
255 Ok(ret)
255 Ok(ret)
256 }
256 }
257
257
258 def non_normal_entries_iter(&self) -> PyResult<NonNormalEntriesIterator> {
258 def non_normal_entries_iter(&self) -> PyResult<NonNormalEntriesIterator> {
259 // Make sure the sets are defined before we no longer have a mutable
259 // Make sure the sets are defined before we no longer have a mutable
260 // reference to the dmap.
260 // reference to the dmap.
261 self.inner(py)
261 self.inner(py)
262 .borrow_mut()
262 .borrow_mut()
263 .set_non_normal_other_parent_entries(false);
263 .set_non_normal_other_parent_entries(false);
264
264
265 let leaked_ref = self.inner(py).leak_immutable();
265 let leaked_ref = self.inner(py).leak_immutable();
266
266
267 NonNormalEntriesIterator::from_inner(py, unsafe {
267 NonNormalEntriesIterator::from_inner(py, unsafe {
268 leaked_ref.map(py, |o| {
268 leaked_ref.map(py, |o| {
269 o.iter_non_normal_paths_panic()
269 o.iter_non_normal_paths_panic()
270 })
270 })
271 })
271 })
272 }
272 }
273
273
274 def hastrackeddir(&self, d: PyObject) -> PyResult<PyBool> {
274 def hastrackeddir(&self, d: PyObject) -> PyResult<PyBool> {
275 let d = d.extract::<PyBytes>(py)?;
275 let d = d.extract::<PyBytes>(py)?;
276 Ok(self.inner(py).borrow_mut()
276 Ok(self.inner(py).borrow_mut()
277 .has_tracked_dir(HgPath::new(d.data(py)))
277 .has_tracked_dir(HgPath::new(d.data(py)))
278 .map_err(|e| {
278 .map_err(|e| {
279 PyErr::new::<exc::ValueError, _>(py, e.to_string())
279 PyErr::new::<exc::ValueError, _>(py, e.to_string())
280 })?
280 })?
281 .to_py_object(py))
281 .to_py_object(py))
282 }
282 }
283
283
284 def hasdir(&self, d: PyObject) -> PyResult<PyBool> {
284 def hasdir(&self, d: PyObject) -> PyResult<PyBool> {
285 let d = d.extract::<PyBytes>(py)?;
285 let d = d.extract::<PyBytes>(py)?;
286 Ok(self.inner(py).borrow_mut()
286 Ok(self.inner(py).borrow_mut()
287 .has_dir(HgPath::new(d.data(py)))
287 .has_dir(HgPath::new(d.data(py)))
288 .map_err(|e| {
288 .map_err(|e| {
289 PyErr::new::<exc::ValueError, _>(py, e.to_string())
289 PyErr::new::<exc::ValueError, _>(py, e.to_string())
290 })?
290 })?
291 .to_py_object(py))
291 .to_py_object(py))
292 }
292 }
293
293
294 def write(
294 def write(
295 &self,
295 &self,
296 use_dirstate_v2: bool,
296 use_dirstate_v2: bool,
297 p1: PyObject,
297 p1: PyObject,
298 p2: PyObject,
298 p2: PyObject,
299 now: PyObject
299 now: PyObject
300 ) -> PyResult<PyBytes> {
300 ) -> PyResult<PyBytes> {
301 let now = Timestamp(now.extract(py)?);
301 let now = Timestamp(now.extract(py)?);
302 let parents = DirstateParents {
302 let parents = DirstateParents {
303 p1: extract_node_id(py, &p1)?,
303 p1: extract_node_id(py, &p1)?,
304 p2: extract_node_id(py, &p2)?,
304 p2: extract_node_id(py, &p2)?,
305 };
305 };
306
306
307 let mut inner = self.inner(py).borrow_mut();
307 let mut inner = self.inner(py).borrow_mut();
308 let result = if use_dirstate_v2 {
308 let result = if use_dirstate_v2 {
309 inner.pack_v2(parents, now)
309 inner.pack_v2(parents, now)
310 } else {
310 } else {
311 inner.pack_v1(parents, now)
311 inner.pack_v1(parents, now)
312 };
312 };
313 match result {
313 match result {
314 Ok(packed) => Ok(PyBytes::new(py, &packed)),
314 Ok(packed) => Ok(PyBytes::new(py, &packed)),
315 Err(_) => Err(PyErr::new::<exc::OSError, _>(
315 Err(_) => Err(PyErr::new::<exc::OSError, _>(
316 py,
316 py,
317 "Dirstate error".to_string(),
317 "Dirstate error".to_string(),
318 )),
318 )),
319 }
319 }
320 }
320 }
321
321
322 def filefoldmapasdict(&self) -> PyResult<PyDict> {
322 def filefoldmapasdict(&self) -> PyResult<PyDict> {
323 let dict = PyDict::new(py);
323 let dict = PyDict::new(py);
324 for item in self.inner(py).borrow_mut().iter() {
324 for item in self.inner(py).borrow_mut().iter() {
325 let (path, entry) = item.map_err(|e| v2_error(py, e))?;
325 let (path, entry) = item.map_err(|e| v2_error(py, e))?;
326 if entry.state != EntryState::Removed {
326 if entry.state != EntryState::Removed {
327 let key = normalize_case(path);
327 let key = normalize_case(path);
328 let value = path;
328 let value = path;
329 dict.set_item(
329 dict.set_item(
330 py,
330 py,
331 PyBytes::new(py, key.as_bytes()).into_object(),
331 PyBytes::new(py, key.as_bytes()).into_object(),
332 PyBytes::new(py, value.as_bytes()).into_object(),
332 PyBytes::new(py, value.as_bytes()).into_object(),
333 )?;
333 )?;
334 }
334 }
335 }
335 }
336 Ok(dict)
336 Ok(dict)
337 }
337 }
338
338
339 def __len__(&self) -> PyResult<usize> {
339 def __len__(&self) -> PyResult<usize> {
340 Ok(self.inner(py).borrow().len())
340 Ok(self.inner(py).borrow().len())
341 }
341 }
342
342
343 def __contains__(&self, key: PyObject) -> PyResult<bool> {
343 def __contains__(&self, key: PyObject) -> PyResult<bool> {
344 let key = key.extract::<PyBytes>(py)?;
344 let key = key.extract::<PyBytes>(py)?;
345 self.inner(py)
345 self.inner(py)
346 .borrow()
346 .borrow()
347 .contains_key(HgPath::new(key.data(py)))
347 .contains_key(HgPath::new(key.data(py)))
348 .map_err(|e| v2_error(py, e))
348 .map_err(|e| v2_error(py, e))
349 }
349 }
350
350
351 def __getitem__(&self, key: PyObject) -> PyResult<PyObject> {
351 def __getitem__(&self, key: PyObject) -> PyResult<PyObject> {
352 let key = key.extract::<PyBytes>(py)?;
352 let key = key.extract::<PyBytes>(py)?;
353 let key = HgPath::new(key.data(py));
353 let key = HgPath::new(key.data(py));
354 match self
354 match self
355 .inner(py)
355 .inner(py)
356 .borrow()
356 .borrow()
357 .get(key)
357 .get(key)
358 .map_err(|e| v2_error(py, e))?
358 .map_err(|e| v2_error(py, e))?
359 {
359 {
360 Some(entry) => {
360 Some(entry) => {
361 Ok(make_dirstate_tuple(py, &entry)?)
361 Ok(make_dirstate_tuple(py, &entry)?)
362 },
362 },
363 None => Err(PyErr::new::<exc::KeyError, _>(
363 None => Err(PyErr::new::<exc::KeyError, _>(
364 py,
364 py,
365 String::from_utf8_lossy(key.as_bytes()),
365 String::from_utf8_lossy(key.as_bytes()),
366 )),
366 )),
367 }
367 }
368 }
368 }
369
369
370 def keys(&self) -> PyResult<DirstateMapKeysIterator> {
370 def keys(&self) -> PyResult<DirstateMapKeysIterator> {
371 let leaked_ref = self.inner(py).leak_immutable();
371 let leaked_ref = self.inner(py).leak_immutable();
372 DirstateMapKeysIterator::from_inner(
372 DirstateMapKeysIterator::from_inner(
373 py,
373 py,
374 unsafe { leaked_ref.map(py, |o| o.iter()) },
374 unsafe { leaked_ref.map(py, |o| o.iter()) },
375 )
375 )
376 }
376 }
377
377
378 def items(&self) -> PyResult<DirstateMapItemsIterator> {
378 def items(&self) -> PyResult<DirstateMapItemsIterator> {
379 let leaked_ref = self.inner(py).leak_immutable();
379 let leaked_ref = self.inner(py).leak_immutable();
380 DirstateMapItemsIterator::from_inner(
380 DirstateMapItemsIterator::from_inner(
381 py,
381 py,
382 unsafe { leaked_ref.map(py, |o| o.iter()) },
382 unsafe { leaked_ref.map(py, |o| o.iter()) },
383 )
383 )
384 }
384 }
385
385
386 def __iter__(&self) -> PyResult<DirstateMapKeysIterator> {
386 def __iter__(&self) -> PyResult<DirstateMapKeysIterator> {
387 let leaked_ref = self.inner(py).leak_immutable();
387 let leaked_ref = self.inner(py).leak_immutable();
388 DirstateMapKeysIterator::from_inner(
388 DirstateMapKeysIterator::from_inner(
389 py,
389 py,
390 unsafe { leaked_ref.map(py, |o| o.iter()) },
390 unsafe { leaked_ref.map(py, |o| o.iter()) },
391 )
391 )
392 }
392 }
393
393
394 def getdirs(&self) -> PyResult<Dirs> {
395 // TODO don't copy, share the reference
396 self.inner(py).borrow_mut().set_dirs()
397 .map_err(|e| {
398 PyErr::new::<exc::ValueError, _>(py, e.to_string())
399 })?;
400 Dirs::from_inner(
401 py,
402 DirsMultiset::from_dirstate(
403 self.inner(py).borrow().iter(),
404 Some(EntryState::Removed),
405 )
406 .map_err(|e| {
407 PyErr::new::<exc::ValueError, _>(py, e.to_string())
408 })?,
409 )
410 }
411 def getalldirs(&self) -> PyResult<Dirs> {
412 // TODO don't copy, share the reference
413 self.inner(py).borrow_mut().set_all_dirs()
414 .map_err(|e| {
415 PyErr::new::<exc::ValueError, _>(py, e.to_string())
416 })?;
417 Dirs::from_inner(
418 py,
419 DirsMultiset::from_dirstate(
420 self.inner(py).borrow().iter(),
421 None,
422 ).map_err(|e| {
423 PyErr::new::<exc::ValueError, _>(py, e.to_string())
424 })?,
425 )
426 }
427
428 // TODO all copymap* methods, see docstring above
394 // TODO all copymap* methods, see docstring above
429 def copymapcopy(&self) -> PyResult<PyDict> {
395 def copymapcopy(&self) -> PyResult<PyDict> {
430 let dict = PyDict::new(py);
396 let dict = PyDict::new(py);
431 for item in self.inner(py).borrow().copy_map_iter() {
397 for item in self.inner(py).borrow().copy_map_iter() {
432 let (key, value) = item.map_err(|e| v2_error(py, e))?;
398 let (key, value) = item.map_err(|e| v2_error(py, e))?;
433 dict.set_item(
399 dict.set_item(
434 py,
400 py,
435 PyBytes::new(py, key.as_bytes()),
401 PyBytes::new(py, key.as_bytes()),
436 PyBytes::new(py, value.as_bytes()),
402 PyBytes::new(py, value.as_bytes()),
437 )?;
403 )?;
438 }
404 }
439 Ok(dict)
405 Ok(dict)
440 }
406 }
441
407
442 def copymapgetitem(&self, key: PyObject) -> PyResult<PyBytes> {
408 def copymapgetitem(&self, key: PyObject) -> PyResult<PyBytes> {
443 let key = key.extract::<PyBytes>(py)?;
409 let key = key.extract::<PyBytes>(py)?;
444 match self
410 match self
445 .inner(py)
411 .inner(py)
446 .borrow()
412 .borrow()
447 .copy_map_get(HgPath::new(key.data(py)))
413 .copy_map_get(HgPath::new(key.data(py)))
448 .map_err(|e| v2_error(py, e))?
414 .map_err(|e| v2_error(py, e))?
449 {
415 {
450 Some(copy) => Ok(PyBytes::new(py, copy.as_bytes())),
416 Some(copy) => Ok(PyBytes::new(py, copy.as_bytes())),
451 None => Err(PyErr::new::<exc::KeyError, _>(
417 None => Err(PyErr::new::<exc::KeyError, _>(
452 py,
418 py,
453 String::from_utf8_lossy(key.data(py)),
419 String::from_utf8_lossy(key.data(py)),
454 )),
420 )),
455 }
421 }
456 }
422 }
457 def copymap(&self) -> PyResult<CopyMap> {
423 def copymap(&self) -> PyResult<CopyMap> {
458 CopyMap::from_inner(py, self.clone_ref(py))
424 CopyMap::from_inner(py, self.clone_ref(py))
459 }
425 }
460
426
461 def copymaplen(&self) -> PyResult<usize> {
427 def copymaplen(&self) -> PyResult<usize> {
462 Ok(self.inner(py).borrow().copy_map_len())
428 Ok(self.inner(py).borrow().copy_map_len())
463 }
429 }
464 def copymapcontains(&self, key: PyObject) -> PyResult<bool> {
430 def copymapcontains(&self, key: PyObject) -> PyResult<bool> {
465 let key = key.extract::<PyBytes>(py)?;
431 let key = key.extract::<PyBytes>(py)?;
466 self.inner(py)
432 self.inner(py)
467 .borrow()
433 .borrow()
468 .copy_map_contains_key(HgPath::new(key.data(py)))
434 .copy_map_contains_key(HgPath::new(key.data(py)))
469 .map_err(|e| v2_error(py, e))
435 .map_err(|e| v2_error(py, e))
470 }
436 }
471 def copymapget(
437 def copymapget(
472 &self,
438 &self,
473 key: PyObject,
439 key: PyObject,
474 default: Option<PyObject>
440 default: Option<PyObject>
475 ) -> PyResult<Option<PyObject>> {
441 ) -> PyResult<Option<PyObject>> {
476 let key = key.extract::<PyBytes>(py)?;
442 let key = key.extract::<PyBytes>(py)?;
477 match self
443 match self
478 .inner(py)
444 .inner(py)
479 .borrow()
445 .borrow()
480 .copy_map_get(HgPath::new(key.data(py)))
446 .copy_map_get(HgPath::new(key.data(py)))
481 .map_err(|e| v2_error(py, e))?
447 .map_err(|e| v2_error(py, e))?
482 {
448 {
483 Some(copy) => Ok(Some(
449 Some(copy) => Ok(Some(
484 PyBytes::new(py, copy.as_bytes()).into_object(),
450 PyBytes::new(py, copy.as_bytes()).into_object(),
485 )),
451 )),
486 None => Ok(default),
452 None => Ok(default),
487 }
453 }
488 }
454 }
489 def copymapsetitem(
455 def copymapsetitem(
490 &self,
456 &self,
491 key: PyObject,
457 key: PyObject,
492 value: PyObject
458 value: PyObject
493 ) -> PyResult<PyObject> {
459 ) -> PyResult<PyObject> {
494 let key = key.extract::<PyBytes>(py)?;
460 let key = key.extract::<PyBytes>(py)?;
495 let value = value.extract::<PyBytes>(py)?;
461 let value = value.extract::<PyBytes>(py)?;
496 self.inner(py)
462 self.inner(py)
497 .borrow_mut()
463 .borrow_mut()
498 .copy_map_insert(
464 .copy_map_insert(
499 HgPathBuf::from_bytes(key.data(py)),
465 HgPathBuf::from_bytes(key.data(py)),
500 HgPathBuf::from_bytes(value.data(py)),
466 HgPathBuf::from_bytes(value.data(py)),
501 )
467 )
502 .map_err(|e| v2_error(py, e))?;
468 .map_err(|e| v2_error(py, e))?;
503 Ok(py.None())
469 Ok(py.None())
504 }
470 }
505 def copymappop(
471 def copymappop(
506 &self,
472 &self,
507 key: PyObject,
473 key: PyObject,
508 default: Option<PyObject>
474 default: Option<PyObject>
509 ) -> PyResult<Option<PyObject>> {
475 ) -> PyResult<Option<PyObject>> {
510 let key = key.extract::<PyBytes>(py)?;
476 let key = key.extract::<PyBytes>(py)?;
511 match self
477 match self
512 .inner(py)
478 .inner(py)
513 .borrow_mut()
479 .borrow_mut()
514 .copy_map_remove(HgPath::new(key.data(py)))
480 .copy_map_remove(HgPath::new(key.data(py)))
515 .map_err(|e| v2_error(py, e))?
481 .map_err(|e| v2_error(py, e))?
516 {
482 {
517 Some(_) => Ok(None),
483 Some(_) => Ok(None),
518 None => Ok(default),
484 None => Ok(default),
519 }
485 }
520 }
486 }
521
487
522 def copymapiter(&self) -> PyResult<CopyMapKeysIterator> {
488 def copymapiter(&self) -> PyResult<CopyMapKeysIterator> {
523 let leaked_ref = self.inner(py).leak_immutable();
489 let leaked_ref = self.inner(py).leak_immutable();
524 CopyMapKeysIterator::from_inner(
490 CopyMapKeysIterator::from_inner(
525 py,
491 py,
526 unsafe { leaked_ref.map(py, |o| o.copy_map_iter()) },
492 unsafe { leaked_ref.map(py, |o| o.copy_map_iter()) },
527 )
493 )
528 }
494 }
529
495
530 def copymapitemsiter(&self) -> PyResult<CopyMapItemsIterator> {
496 def copymapitemsiter(&self) -> PyResult<CopyMapItemsIterator> {
531 let leaked_ref = self.inner(py).leak_immutable();
497 let leaked_ref = self.inner(py).leak_immutable();
532 CopyMapItemsIterator::from_inner(
498 CopyMapItemsIterator::from_inner(
533 py,
499 py,
534 unsafe { leaked_ref.map(py, |o| o.copy_map_iter()) },
500 unsafe { leaked_ref.map(py, |o| o.copy_map_iter()) },
535 )
501 )
536 }
502 }
537
503
538 def directories(&self) -> PyResult<PyList> {
504 def directories(&self) -> PyResult<PyList> {
539 let dirs = PyList::new(py, &[]);
505 let dirs = PyList::new(py, &[]);
540 for item in self.inner(py).borrow().iter_directories() {
506 for item in self.inner(py).borrow().iter_directories() {
541 let (path, mtime) = item.map_err(|e| v2_error(py, e))?;
507 let (path, mtime) = item.map_err(|e| v2_error(py, e))?;
542 let path = PyBytes::new(py, path.as_bytes());
508 let path = PyBytes::new(py, path.as_bytes());
543 let mtime = mtime.map(|t| t.0).unwrap_or(-1);
509 let mtime = mtime.map(|t| t.0).unwrap_or(-1);
544 let tuple = (path, (b'd', 0, 0, mtime));
510 let tuple = (path, (b'd', 0, 0, mtime));
545 dirs.append(py, tuple.to_py_object(py).into_object())
511 dirs.append(py, tuple.to_py_object(py).into_object())
546 }
512 }
547 Ok(dirs)
513 Ok(dirs)
548 }
514 }
549
515
550 });
516 });
551
517
552 impl DirstateMap {
518 impl DirstateMap {
553 pub fn get_inner_mut<'a>(
519 pub fn get_inner_mut<'a>(
554 &'a self,
520 &'a self,
555 py: Python<'a>,
521 py: Python<'a>,
556 ) -> RefMut<'a, Box<dyn DirstateMapMethods + Send>> {
522 ) -> RefMut<'a, Box<dyn DirstateMapMethods + Send>> {
557 self.inner(py).borrow_mut()
523 self.inner(py).borrow_mut()
558 }
524 }
559 fn translate_key(
525 fn translate_key(
560 py: Python,
526 py: Python,
561 res: Result<(&HgPath, DirstateEntry), DirstateV2ParseError>,
527 res: Result<(&HgPath, DirstateEntry), DirstateV2ParseError>,
562 ) -> PyResult<Option<PyBytes>> {
528 ) -> PyResult<Option<PyBytes>> {
563 let (f, _entry) = res.map_err(|e| v2_error(py, e))?;
529 let (f, _entry) = res.map_err(|e| v2_error(py, e))?;
564 Ok(Some(PyBytes::new(py, f.as_bytes())))
530 Ok(Some(PyBytes::new(py, f.as_bytes())))
565 }
531 }
566 fn translate_key_value(
532 fn translate_key_value(
567 py: Python,
533 py: Python,
568 res: Result<(&HgPath, DirstateEntry), DirstateV2ParseError>,
534 res: Result<(&HgPath, DirstateEntry), DirstateV2ParseError>,
569 ) -> PyResult<Option<(PyBytes, PyObject)>> {
535 ) -> PyResult<Option<(PyBytes, PyObject)>> {
570 let (f, entry) = res.map_err(|e| v2_error(py, e))?;
536 let (f, entry) = res.map_err(|e| v2_error(py, e))?;
571 Ok(Some((
537 Ok(Some((
572 PyBytes::new(py, f.as_bytes()),
538 PyBytes::new(py, f.as_bytes()),
573 make_dirstate_tuple(py, &entry)?,
539 make_dirstate_tuple(py, &entry)?,
574 )))
540 )))
575 }
541 }
576 }
542 }
577
543
578 py_shared_iterator!(
544 py_shared_iterator!(
579 DirstateMapKeysIterator,
545 DirstateMapKeysIterator,
580 UnsafePyLeaked<StateMapIter<'static>>,
546 UnsafePyLeaked<StateMapIter<'static>>,
581 DirstateMap::translate_key,
547 DirstateMap::translate_key,
582 Option<PyBytes>
548 Option<PyBytes>
583 );
549 );
584
550
585 py_shared_iterator!(
551 py_shared_iterator!(
586 DirstateMapItemsIterator,
552 DirstateMapItemsIterator,
587 UnsafePyLeaked<StateMapIter<'static>>,
553 UnsafePyLeaked<StateMapIter<'static>>,
588 DirstateMap::translate_key_value,
554 DirstateMap::translate_key_value,
589 Option<(PyBytes, PyObject)>
555 Option<(PyBytes, PyObject)>
590 );
556 );
591
557
592 fn extract_node_id(py: Python, obj: &PyObject) -> PyResult<Node> {
558 fn extract_node_id(py: Python, obj: &PyObject) -> PyResult<Node> {
593 let bytes = obj.extract::<PyBytes>(py)?;
559 let bytes = obj.extract::<PyBytes>(py)?;
594 match bytes.data(py).try_into() {
560 match bytes.data(py).try_into() {
595 Ok(s) => Ok(s),
561 Ok(s) => Ok(s),
596 Err(e) => Err(PyErr::new::<exc::ValueError, _>(py, e.to_string())),
562 Err(e) => Err(PyErr::new::<exc::ValueError, _>(py, e.to_string())),
597 }
563 }
598 }
564 }
599
565
600 pub(super) fn v2_error(py: Python<'_>, _: DirstateV2ParseError) -> PyErr {
566 pub(super) fn v2_error(py: Python<'_>, _: DirstateV2ParseError) -> PyErr {
601 PyErr::new::<exc::ValueError, _>(py, "corrupted dirstate-v2")
567 PyErr::new::<exc::ValueError, _>(py, "corrupted dirstate-v2")
602 }
568 }
@@ -1,223 +1,215
1 use crate::dirstate::owning::OwningDirstateMap;
1 use crate::dirstate::owning::OwningDirstateMap;
2 use hg::dirstate::parsers::Timestamp;
2 use hg::dirstate::parsers::Timestamp;
3 use hg::dirstate_tree::dispatch::DirstateMapMethods;
3 use hg::dirstate_tree::dispatch::DirstateMapMethods;
4 use hg::dirstate_tree::on_disk::DirstateV2ParseError;
4 use hg::dirstate_tree::on_disk::DirstateV2ParseError;
5 use hg::matchers::Matcher;
5 use hg::matchers::Matcher;
6 use hg::utils::hg_path::{HgPath, HgPathBuf};
6 use hg::utils::hg_path::{HgPath, HgPathBuf};
7 use hg::CopyMapIter;
7 use hg::CopyMapIter;
8 use hg::DirstateEntry;
8 use hg::DirstateEntry;
9 use hg::DirstateError;
9 use hg::DirstateError;
10 use hg::DirstateParents;
10 use hg::DirstateParents;
11 use hg::DirstateStatus;
11 use hg::DirstateStatus;
12 use hg::EntryState;
12 use hg::EntryState;
13 use hg::PatternFileWarning;
13 use hg::PatternFileWarning;
14 use hg::StateMapIter;
14 use hg::StateMapIter;
15 use hg::StatusError;
15 use hg::StatusError;
16 use hg::StatusOptions;
16 use hg::StatusOptions;
17 use std::path::PathBuf;
17 use std::path::PathBuf;
18
18
19 impl DirstateMapMethods for OwningDirstateMap {
19 impl DirstateMapMethods for OwningDirstateMap {
20 fn clear(&mut self) {
20 fn clear(&mut self) {
21 self.get_mut().clear()
21 self.get_mut().clear()
22 }
22 }
23
23
24 fn add_file(
24 fn add_file(
25 &mut self,
25 &mut self,
26 filename: &HgPath,
26 filename: &HgPath,
27 old_state: EntryState,
27 old_state: EntryState,
28 entry: DirstateEntry,
28 entry: DirstateEntry,
29 ) -> Result<(), DirstateError> {
29 ) -> Result<(), DirstateError> {
30 self.get_mut().add_file(filename, old_state, entry)
30 self.get_mut().add_file(filename, old_state, entry)
31 }
31 }
32
32
33 fn remove_file(
33 fn remove_file(
34 &mut self,
34 &mut self,
35 filename: &HgPath,
35 filename: &HgPath,
36 old_state: EntryState,
36 old_state: EntryState,
37 size: i32,
37 size: i32,
38 ) -> Result<(), DirstateError> {
38 ) -> Result<(), DirstateError> {
39 self.get_mut().remove_file(filename, old_state, size)
39 self.get_mut().remove_file(filename, old_state, size)
40 }
40 }
41
41
42 fn drop_file(
42 fn drop_file(
43 &mut self,
43 &mut self,
44 filename: &HgPath,
44 filename: &HgPath,
45 old_state: EntryState,
45 old_state: EntryState,
46 ) -> Result<bool, DirstateError> {
46 ) -> Result<bool, DirstateError> {
47 self.get_mut().drop_file(filename, old_state)
47 self.get_mut().drop_file(filename, old_state)
48 }
48 }
49
49
50 fn clear_ambiguous_times(
50 fn clear_ambiguous_times(
51 &mut self,
51 &mut self,
52 filenames: Vec<HgPathBuf>,
52 filenames: Vec<HgPathBuf>,
53 now: i32,
53 now: i32,
54 ) -> Result<(), DirstateV2ParseError> {
54 ) -> Result<(), DirstateV2ParseError> {
55 self.get_mut().clear_ambiguous_times(filenames, now)
55 self.get_mut().clear_ambiguous_times(filenames, now)
56 }
56 }
57
57
58 fn non_normal_entries_contains(
58 fn non_normal_entries_contains(
59 &mut self,
59 &mut self,
60 key: &HgPath,
60 key: &HgPath,
61 ) -> Result<bool, DirstateV2ParseError> {
61 ) -> Result<bool, DirstateV2ParseError> {
62 self.get_mut().non_normal_entries_contains(key)
62 self.get_mut().non_normal_entries_contains(key)
63 }
63 }
64
64
65 fn non_normal_entries_remove(&mut self, key: &HgPath) {
65 fn non_normal_entries_remove(&mut self, key: &HgPath) {
66 self.get_mut().non_normal_entries_remove(key)
66 self.get_mut().non_normal_entries_remove(key)
67 }
67 }
68
68
69 fn non_normal_or_other_parent_paths(
69 fn non_normal_or_other_parent_paths(
70 &mut self,
70 &mut self,
71 ) -> Box<dyn Iterator<Item = Result<&HgPath, DirstateV2ParseError>> + '_>
71 ) -> Box<dyn Iterator<Item = Result<&HgPath, DirstateV2ParseError>> + '_>
72 {
72 {
73 self.get_mut().non_normal_or_other_parent_paths()
73 self.get_mut().non_normal_or_other_parent_paths()
74 }
74 }
75
75
76 fn set_non_normal_other_parent_entries(&mut self, force: bool) {
76 fn set_non_normal_other_parent_entries(&mut self, force: bool) {
77 self.get_mut().set_non_normal_other_parent_entries(force)
77 self.get_mut().set_non_normal_other_parent_entries(force)
78 }
78 }
79
79
80 fn iter_non_normal_paths(
80 fn iter_non_normal_paths(
81 &mut self,
81 &mut self,
82 ) -> Box<
82 ) -> Box<
83 dyn Iterator<Item = Result<&HgPath, DirstateV2ParseError>> + Send + '_,
83 dyn Iterator<Item = Result<&HgPath, DirstateV2ParseError>> + Send + '_,
84 > {
84 > {
85 self.get_mut().iter_non_normal_paths()
85 self.get_mut().iter_non_normal_paths()
86 }
86 }
87
87
88 fn iter_non_normal_paths_panic(
88 fn iter_non_normal_paths_panic(
89 &self,
89 &self,
90 ) -> Box<
90 ) -> Box<
91 dyn Iterator<Item = Result<&HgPath, DirstateV2ParseError>> + Send + '_,
91 dyn Iterator<Item = Result<&HgPath, DirstateV2ParseError>> + Send + '_,
92 > {
92 > {
93 self.get().iter_non_normal_paths_panic()
93 self.get().iter_non_normal_paths_panic()
94 }
94 }
95
95
96 fn iter_other_parent_paths(
96 fn iter_other_parent_paths(
97 &mut self,
97 &mut self,
98 ) -> Box<
98 ) -> Box<
99 dyn Iterator<Item = Result<&HgPath, DirstateV2ParseError>> + Send + '_,
99 dyn Iterator<Item = Result<&HgPath, DirstateV2ParseError>> + Send + '_,
100 > {
100 > {
101 self.get_mut().iter_other_parent_paths()
101 self.get_mut().iter_other_parent_paths()
102 }
102 }
103
103
104 fn has_tracked_dir(
104 fn has_tracked_dir(
105 &mut self,
105 &mut self,
106 directory: &HgPath,
106 directory: &HgPath,
107 ) -> Result<bool, DirstateError> {
107 ) -> Result<bool, DirstateError> {
108 self.get_mut().has_tracked_dir(directory)
108 self.get_mut().has_tracked_dir(directory)
109 }
109 }
110
110
111 fn has_dir(&mut self, directory: &HgPath) -> Result<bool, DirstateError> {
111 fn has_dir(&mut self, directory: &HgPath) -> Result<bool, DirstateError> {
112 self.get_mut().has_dir(directory)
112 self.get_mut().has_dir(directory)
113 }
113 }
114
114
115 fn pack_v1(
115 fn pack_v1(
116 &mut self,
116 &mut self,
117 parents: DirstateParents,
117 parents: DirstateParents,
118 now: Timestamp,
118 now: Timestamp,
119 ) -> Result<Vec<u8>, DirstateError> {
119 ) -> Result<Vec<u8>, DirstateError> {
120 self.get_mut().pack_v1(parents, now)
120 self.get_mut().pack_v1(parents, now)
121 }
121 }
122
122
123 fn pack_v2(
123 fn pack_v2(
124 &mut self,
124 &mut self,
125 parents: DirstateParents,
125 parents: DirstateParents,
126 now: Timestamp,
126 now: Timestamp,
127 ) -> Result<Vec<u8>, DirstateError> {
127 ) -> Result<Vec<u8>, DirstateError> {
128 self.get_mut().pack_v2(parents, now)
128 self.get_mut().pack_v2(parents, now)
129 }
129 }
130
130
131 fn set_all_dirs(&mut self) -> Result<(), DirstateError> {
132 self.get_mut().set_all_dirs()
133 }
134
135 fn set_dirs(&mut self) -> Result<(), DirstateError> {
136 self.get_mut().set_dirs()
137 }
138
139 fn status<'a>(
131 fn status<'a>(
140 &'a mut self,
132 &'a mut self,
141 matcher: &'a (dyn Matcher + Sync),
133 matcher: &'a (dyn Matcher + Sync),
142 root_dir: PathBuf,
134 root_dir: PathBuf,
143 ignore_files: Vec<PathBuf>,
135 ignore_files: Vec<PathBuf>,
144 options: StatusOptions,
136 options: StatusOptions,
145 ) -> Result<(DirstateStatus<'a>, Vec<PatternFileWarning>), StatusError>
137 ) -> Result<(DirstateStatus<'a>, Vec<PatternFileWarning>), StatusError>
146 {
138 {
147 self.get_mut()
139 self.get_mut()
148 .status(matcher, root_dir, ignore_files, options)
140 .status(matcher, root_dir, ignore_files, options)
149 }
141 }
150
142
151 fn copy_map_len(&self) -> usize {
143 fn copy_map_len(&self) -> usize {
152 self.get().copy_map_len()
144 self.get().copy_map_len()
153 }
145 }
154
146
155 fn copy_map_iter(&self) -> CopyMapIter<'_> {
147 fn copy_map_iter(&self) -> CopyMapIter<'_> {
156 self.get().copy_map_iter()
148 self.get().copy_map_iter()
157 }
149 }
158
150
159 fn copy_map_contains_key(
151 fn copy_map_contains_key(
160 &self,
152 &self,
161 key: &HgPath,
153 key: &HgPath,
162 ) -> Result<bool, DirstateV2ParseError> {
154 ) -> Result<bool, DirstateV2ParseError> {
163 self.get().copy_map_contains_key(key)
155 self.get().copy_map_contains_key(key)
164 }
156 }
165
157
166 fn copy_map_get(
158 fn copy_map_get(
167 &self,
159 &self,
168 key: &HgPath,
160 key: &HgPath,
169 ) -> Result<Option<&HgPath>, DirstateV2ParseError> {
161 ) -> Result<Option<&HgPath>, DirstateV2ParseError> {
170 self.get().copy_map_get(key)
162 self.get().copy_map_get(key)
171 }
163 }
172
164
173 fn copy_map_remove(
165 fn copy_map_remove(
174 &mut self,
166 &mut self,
175 key: &HgPath,
167 key: &HgPath,
176 ) -> Result<Option<HgPathBuf>, DirstateV2ParseError> {
168 ) -> Result<Option<HgPathBuf>, DirstateV2ParseError> {
177 self.get_mut().copy_map_remove(key)
169 self.get_mut().copy_map_remove(key)
178 }
170 }
179
171
180 fn copy_map_insert(
172 fn copy_map_insert(
181 &mut self,
173 &mut self,
182 key: HgPathBuf,
174 key: HgPathBuf,
183 value: HgPathBuf,
175 value: HgPathBuf,
184 ) -> Result<Option<HgPathBuf>, DirstateV2ParseError> {
176 ) -> Result<Option<HgPathBuf>, DirstateV2ParseError> {
185 self.get_mut().copy_map_insert(key, value)
177 self.get_mut().copy_map_insert(key, value)
186 }
178 }
187
179
188 fn len(&self) -> usize {
180 fn len(&self) -> usize {
189 self.get().len()
181 self.get().len()
190 }
182 }
191
183
192 fn contains_key(
184 fn contains_key(
193 &self,
185 &self,
194 key: &HgPath,
186 key: &HgPath,
195 ) -> Result<bool, DirstateV2ParseError> {
187 ) -> Result<bool, DirstateV2ParseError> {
196 self.get().contains_key(key)
188 self.get().contains_key(key)
197 }
189 }
198
190
199 fn get(
191 fn get(
200 &self,
192 &self,
201 key: &HgPath,
193 key: &HgPath,
202 ) -> Result<Option<DirstateEntry>, DirstateV2ParseError> {
194 ) -> Result<Option<DirstateEntry>, DirstateV2ParseError> {
203 self.get().get(key)
195 self.get().get(key)
204 }
196 }
205
197
206 fn iter(&self) -> StateMapIter<'_> {
198 fn iter(&self) -> StateMapIter<'_> {
207 self.get().iter()
199 self.get().iter()
208 }
200 }
209
201
210 fn iter_directories(
202 fn iter_directories(
211 &self,
203 &self,
212 ) -> Box<
204 ) -> Box<
213 dyn Iterator<
205 dyn Iterator<
214 Item = Result<
206 Item = Result<
215 (&HgPath, Option<Timestamp>),
207 (&HgPath, Option<Timestamp>),
216 DirstateV2ParseError,
208 DirstateV2ParseError,
217 >,
209 >,
218 > + Send
210 > + Send
219 + '_,
211 + '_,
220 > {
212 > {
221 self.get().iter_directories()
213 self.get().iter_directories()
222 }
214 }
223 }
215 }
General Comments 0
You need to be logged in to leave comments. Login now